package com.haha.admin.controller;

import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.haha.entity.Replenisher;
import com.haha.entity.dto.*;
import com.haha.service.ReplenisherService;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.http.MediaType;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;

import java.time.LocalDateTime;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;

import static org.mockito.ArgumentMatchers.*;
import static org.mockito.Mockito.*;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;

/**
 * 补货员管理控制器 - 完整单元测试
 * <p>
 * 使用 MockMvc standalone 模式覆盖 ReplenisherController 全部 12 个 API 端点，
 * 包括正常流程、边界条件和异常场景。
 */
@ExtendWith(MockitoExtension.class)
@DisplayName("补货员管理控制器")
class ReplenisherControllerTest {

    @Mock
    private ReplenisherService replenisherService;

    @InjectMocks
    private ReplenisherController controller;

    private MockMvc mockMvc;
    private ObjectMapper objectMapper;

    @BeforeEach
    void setUp() {
        objectMapper = new ObjectMapper();
        mockMvc = MockMvcBuilders.standaloneSetup(controller).build();
    }

    /**
     * 创建测试用的补货员对象
     */
    private Replenisher createTestReplenisher(Long id) {
        Replenisher r = new Replenisher();
        r.setId(id);
        r.setName("测试补货员");
        r.setPhone("13800138000");
        r.setEmployeeId("EMP001");
        r.setStatus(1);
        r.setTotalTasks(0);
        r.setBoundDeviceCount(0);
        r.setCreateTime(LocalDateTime.now());
        r.setUpdateTime(LocalDateTime.now());
        return r;
    }

    /**
     * 构建 MyBatis Plus 分页结果
     */
    private IPage<Replenisher> buildPage(List<Replenisher> records, long total, long pageSize, long current) {
        Page<Replenisher> page = new Page<>(current, pageSize);
        page.setRecords(records);
        page.setTotal(total);
        page.setPages(pageSize > 0 ? (total + pageSize - 1) / pageSize : 0);
        return page;
    }

    // ============================================================
    // 1. 分页查询补货员列表 GET /replenishers/list
    // ============================================================

    @Nested
    @DisplayName("GET /replenishers/list - 分页查询列表")
    class ListTests {

        @Test
        @DisplayName("正常查询第一页")
        void shouldListFirstPage() throws Exception {
            Replenisher r1 = createTestReplenisher(1L);
            Replenisher r2 = createTestReplenisher(2L);
            r2.setName("张三");
            IPage<Replenisher> page = buildPage(Arrays.asList(r1, r2), 20, 10, 1);

            when(replenisherService.getPage(any(ReplenisherQueryDTO.class))).thenReturn(page);

            mockMvc.perform(get("/replenishers/list")
                            .param("page", "1")
                            .param("pageSize", "10"))
                    .andExpect(status().isOk())
                    .andExpect(jsonPath("$.code").value(200))
                    .andExpect(jsonPath("$.message").value("查询成功"))
                    .andExpect(jsonPath("$.data.list.length()").value(2))
                    .andExpect(jsonPath("$.data.total").value(20))
                    .andExpect(jsonPath("$.data.list[0].name").value("测试补货员"))
                    .andExpect(jsonPath("$.data.list[1].name").value("张三"));

            verify(replenisherService).getPage(argThat(dto ->
                    dto.getPage() == 1 && dto.getPageSize() == 10));
        }

        @Test
        @DisplayName("关键词搜索")
        void shouldListWithKeyword() throws Exception {
            Replenisher r = createTestReplenisher(1L);
            r.setName("李四");
            IPage<Replenisher> page = buildPage(Collections.singletonList(r), 1, 10, 1);

            when(replenisherService.getPage(any(ReplenisherQueryDTO.class))).thenReturn(page);

            mockMvc.perform(get("/replenishers/list")
                            .param("page", "1")
                            .param("pageSize", "10")
                            .param("keyword", "李四"))
                    .andExpect(status().isOk())
                    .andExpect(jsonPath("$.data.list.length()").value(1))
                    .andExpect(jsonPath("$.data.list[0].name").value("李四"));

            verify(replenisherService).getPage(argThat(dto ->
                    "李四".equals(dto.getKeyword())));
        }

        @Test
        @DisplayName("按状态筛选 - 仅启用")
        void shouldFilterByStatusActive() throws Exception {
            IPage<Replenisher> page = buildPage(Collections.emptyList(), 0, 10, 1);
            when(replenisherService.getPage(any(ReplenisherQueryDTO.class))).thenReturn(page);

            mockMvc.perform(get("/replenishers/list")
                            .param("page", "1")
                            .param("pageSize", "10")
                            .param("status", "1"))
                    .andExpect(status().isOk());

            verify(replenisherService).getPage(argThat(dto ->
                    dto.getStatus() != null && dto.getStatus() == 1));
        }

        @Test
        @DisplayName("按状态筛选 - 仅禁用")
        void shouldFilterByStatusDisabled() throws Exception {
            IPage<Replenisher> page = buildPage(Collections.emptyList(), 0, 10, 1);
            when(replenisherService.getPage(any(ReplenisherQueryDTO.class))).thenReturn(page);

            mockMvc.perform(get("/replenishers/list")
                            .param("page", "1")
                            .param("pageSize", "10")
                            .param("status", "0"))
                    .andExpect(status().isOk());

            verify(replenisherService).getPage(argThat(dto ->
                    dto.getStatus() != null && dto.getStatus() == 0));
        }

        @Test
        @DisplayName("空列表")
        void shouldReturnEmptyList() throws Exception {
            IPage<Replenisher> page = buildPage(Collections.emptyList(), 0, 10, 1);
            when(replenisherService.getPage(any(ReplenisherQueryDTO.class))).thenReturn(page);

            mockMvc.perform(get("/replenishers/list")
                            .param("page", "1")
                            .param("pageSize", "10"))
                    .andExpect(status().isOk())
                    .andExpect(jsonPath("$.data.list.length()").value(0))
                    .andExpect(jsonPath("$.data.total").value(0));
        }
    }

    // ============================================================
    // 2. 获取补货员详情 GET /replenishers/{id}
    // ============================================================

    @Nested
    @DisplayName("GET /replenishers/{id} - 获取补货员详情")
    class GetByIdTests {

        @Test
        @DisplayName("查询存在的补货员")
        void shouldGetById() throws Exception {
            Replenisher r = createTestReplenisher(1L);
            r.setBoundDeviceCount(5);

            when(replenisherService.getReplenisherDetail(1L)).thenReturn(r);

            mockMvc.perform(get("/replenishers/1"))
                    .andExpect(status().isOk())
                    .andExpect(jsonPath("$.code").value(200))
                    .andExpect(jsonPath("$.data.id").value(1))
                    .andExpect(jsonPath("$.data.name").value("测试补货员"))
                    .andExpect(jsonPath("$.data.phone").value("13800138000"))
                    .andExpect(jsonPath("$.data.employeeId").value("EMP001"))
                    .andExpect(jsonPath("$.data.boundDeviceCount").value(5));

            verify(replenisherService).getReplenisherDetail(1L);
        }

        @Test
        @DisplayName("查询不存在的补货员返回404")
        void shouldReturn404WhenNotFound() throws Exception {
            when(replenisherService.getReplenisherDetail(999L)).thenReturn(null);

            mockMvc.perform(get("/replenishers/999"))
                    .andExpect(status().isOk())
                    .andExpect(jsonPath("$.code").value(404))
                    .andExpect(jsonPath("$.message").value("补货员不存在"));
        }

        @Test
        @DisplayName("已禁用的补货员")
        void shouldGetDisabledReplenisher() throws Exception {
            Replenisher r = createTestReplenisher(2L);
            r.setStatus(0);

            when(replenisherService.getReplenisherDetail(2L)).thenReturn(r);

            mockMvc.perform(get("/replenishers/2"))
                    .andExpect(status().isOk())
                    .andExpect(jsonPath("$.data.status").value(0));
        }
    }

    // ============================================================
    // 3. 关键词搜索补货员 GET /replenishers/search
    // ============================================================

    @Nested
    @DisplayName("GET /replenishers/search - 关键词搜索")
    class SearchTests {

        @Test
        @DisplayName("按姓名搜索")
        void shouldSearchByName() throws Exception {
            Replenisher r = createTestReplenisher(1L);
            r.setName("王五");

            when(replenisherService.searchByKeyword("王五"))
                    .thenReturn(Collections.singletonList(r));

            mockMvc.perform(get("/replenishers/search")
                            .param("keyword", "王五"))
                    .andExpect(status().isOk())
                    .andExpect(jsonPath("$.data.length()").value(1))
                    .andExpect(jsonPath("$.data[0].name").value("王五"));

            verify(replenisherService).searchByKeyword("王五");
        }

        @Test
        @DisplayName("无关键词搜索（返回全部启用补货员）")
        void shouldSearchWithEmptyKeyword() throws Exception {
            when(replenisherService.searchByKeyword(isNull()))
                    .thenReturn(Collections.emptyList());

            mockMvc.perform(get("/replenishers/search"))
                    .andExpect(status().isOk())
                    .andExpect(jsonPath("$.data.length()").value(0));

            verify(replenisherService).searchByKeyword(isNull());
        }

        @Test
        @DisplayName("搜索无结果")
        void shouldReturnEmptyWhenNoMatch() throws Exception {
            when(replenisherService.searchByKeyword("不存在"))
                    .thenReturn(Collections.emptyList());

            mockMvc.perform(get("/replenishers/search")
                            .param("keyword", "不存在"))
                    .andExpect(status().isOk())
                    .andExpect(jsonPath("$.data.length()").value(0));

            verify(replenisherService).searchByKeyword("不存在");
        }
    }

    // ============================================================
    // 4. 创建补货员 POST /replenishers
    // ============================================================

    @Nested
    @DisplayName("POST /replenishers - 创建补货员")
    class CreateTests {

        @Test
        @DisplayName("创建成功")
        void shouldCreateReplenisher() throws Exception {
            ReplenisherCreateDTO dto = new ReplenisherCreateDTO();
            dto.setName("新补货员");
            dto.setPhone("13900139000");
            dto.setEmployeeId("EMP002");

            Replenisher saved = createTestReplenisher(10L);
            saved.setName("新补货员");
            saved.setPhone("13900139000");
            saved.setEmployeeId("EMP002");

            when(replenisherService.createReplenisher(any(ReplenisherCreateDTO.class)))
                    .thenReturn(saved);

            mockMvc.perform(post("/replenishers")
                            .contentType(MediaType.APPLICATION_JSON)
                            .content(objectMapper.writeValueAsString(dto)))
                    .andExpect(status().isOk())
                    .andExpect(jsonPath("$.code").value(200))
                    .andExpect(jsonPath("$.message").value("创建成功"))
                    .andExpect(jsonPath("$.data.id").value(10))
                    .andExpect(jsonPath("$.data.name").value("新补货员"));

            verify(replenisherService).createReplenisher(argThat(d ->
                    "新补货员".equals(d.getName())));
        }

        @Test
        @DisplayName("仅填姓名创建")
        void shouldCreateWithNameOnly() throws Exception {
            ReplenisherCreateDTO dto = new ReplenisherCreateDTO();
            dto.setName("最简补货员");

            Replenisher saved = createTestReplenisher(11L);
            saved.setName("最简补货员");
            saved.setPhone(null);
            saved.setEmployeeId(null);

            when(replenisherService.createReplenisher(any(ReplenisherCreateDTO.class)))
                    .thenReturn(saved);

            mockMvc.perform(post("/replenishers")
                            .contentType(MediaType.APPLICATION_JSON)
                            .content(objectMapper.writeValueAsString(dto)))
                    .andExpect(status().isOk())
                    .andExpect(jsonPath("$.code").value(200))
                    .andExpect(jsonPath("$.data.name").value("最简补货员"));

            verify(replenisherService).createReplenisher(any(ReplenisherCreateDTO.class));
        }
    }

    // ============================================================
    // 5. 更新补货员 PUT /replenishers/{id}
    // ============================================================

    @Nested
    @DisplayName("PUT /replenishers/{id} - 更新补货员信息")
    class UpdateTests {

        @Test
        @DisplayName("全字段更新")
        void shouldUpdateAllFields() throws Exception {
            ReplenisherUpdateDTO dto = new ReplenisherUpdateDTO();
            dto.setName("更新后的名字");
            dto.setPhone("13700137000");
            dto.setEmployeeId("EMP003");
            dto.setStatus(0);

            Replenisher updated = createTestReplenisher(1L);
            updated.setName("更新后的名字");
            updated.setPhone("13700137000");
            updated.setEmployeeId("EMP003");
            updated.setStatus(0);

            when(replenisherService.updateReplenisher(eq(1L), any(ReplenisherUpdateDTO.class)))
                    .thenReturn(updated);

            mockMvc.perform(put("/replenishers/1")
                            .contentType(MediaType.APPLICATION_JSON)
                            .content(objectMapper.writeValueAsString(dto)))
                    .andExpect(status().isOk())
                    .andExpect(jsonPath("$.code").value(200))
                    .andExpect(jsonPath("$.message").value("更新成功"))
                    .andExpect(jsonPath("$.data.name").value("更新后的名字"))
                    .andExpect(jsonPath("$.data.status").value(0));

            verify(replenisherService).updateReplenisher(eq(1L), argThat(d ->
                    "更新后的名字".equals(d.getName()) && d.getStatus() == 0));
        }

        @Test
        @DisplayName("部分字段更新")
        void shouldPartialUpdate() throws Exception {
            ReplenisherUpdateDTO dto = new ReplenisherUpdateDTO();
            dto.setPhone("13600136000");

            Replenisher updated = createTestReplenisher(1L);
            updated.setPhone("13600136000");

            when(replenisherService.updateReplenisher(eq(1L), any(ReplenisherUpdateDTO.class)))
                    .thenReturn(updated);

            mockMvc.perform(put("/replenishers/1")
                            .contentType(MediaType.APPLICATION_JSON)
                            .content(objectMapper.writeValueAsString(dto)))
                    .andExpect(status().isOk())
                    .andExpect(jsonPath("$.data.phone").value("13600136000"));

            verify(replenisherService).updateReplenisher(eq(1L), any(ReplenisherUpdateDTO.class));
        }
    }

    // ============================================================
    // 6. 更新补货员状态 PUT /replenishers/{id}/status
    // ============================================================

    @Nested
    @DisplayName("PUT /replenishers/{id}/status - 启用/禁用")
    class UpdateStatusTests {

        @Test
        @DisplayName("禁用补货员")
        void shouldDisableReplenisher() throws Exception {
            StatusDTO dto = new StatusDTO();
            dto.setStatus(0);

            doNothing().when(replenisherService).updateStatus(1L, 0);

            mockMvc.perform(put("/replenishers/1/status")
                            .contentType(MediaType.APPLICATION_JSON)
                            .content(objectMapper.writeValueAsString(dto)))
                    .andExpect(status().isOk())
                    .andExpect(jsonPath("$.code").value(200))
                    .andExpect(jsonPath("$.message").value("状态更新成功"));

            verify(replenisherService).updateStatus(1L, 0);
        }

        @Test
        @DisplayName("启用补货员")
        void shouldEnableReplenisher() throws Exception {
            StatusDTO dto = new StatusDTO();
            dto.setStatus(1);

            doNothing().when(replenisherService).updateStatus(2L, 1);

            mockMvc.perform(put("/replenishers/2/status")
                            .contentType(MediaType.APPLICATION_JSON)
                            .content(objectMapper.writeValueAsString(dto)))
                    .andExpect(status().isOk())
                    .andExpect(jsonPath("$.code").value(200));

            verify(replenisherService).updateStatus(2L, 1);
        }
    }

    // ============================================================
    // 7. 删除补货员 DELETE /replenishers/{id}
    // ============================================================

    @Nested
    @DisplayName("DELETE /replenishers/{id} - 删除补货员")
    class DeleteTests {

        @Test
        @DisplayName("删除成功")
        void shouldDeleteReplenisher() throws Exception {
            doNothing().when(replenisherService).deleteReplenisher(1L);

            mockMvc.perform(delete("/replenishers/1"))
                    .andExpect(status().isOk())
                    .andExpect(jsonPath("$.code").value(200))
                    .andExpect(jsonPath("$.message").value("删除成功"));

            verify(replenisherService).deleteReplenisher(1L);
        }
    }

    // ============================================================
    // 8. 获取已绑定设备 GET /replenishers/{id}/devices
    // ============================================================

    @Nested
    @DisplayName("GET /replenishers/{id}/devices - 获取已绑定设备")
    class GetBoundDevicesTests {

        @Test
        @DisplayName("有绑定设备")
        void shouldReturnBoundDevices() throws Exception {
            when(replenisherService.getBoundDeviceIds(1L))
                    .thenReturn(Arrays.asList("DEV001", "DEV002", "DEV003"));

            mockMvc.perform(get("/replenishers/1/devices"))
                    .andExpect(status().isOk())
                    .andExpect(jsonPath("$.code").value(200))
                    .andExpect(jsonPath("$.data.length()").value(3))
                    .andExpect(jsonPath("$.data[0]").value("DEV001"))
                    .andExpect(jsonPath("$.data[2]").value("DEV003"));

            verify(replenisherService).getBoundDeviceIds(1L);
        }

        @Test
        @DisplayName("无绑定设备")
        void shouldReturnEmptyDeviceList() throws Exception {
            when(replenisherService.getBoundDeviceIds(1L))
                    .thenReturn(Collections.emptyList());

            mockMvc.perform(get("/replenishers/1/devices"))
                    .andExpect(status().isOk())
                    .andExpect(jsonPath("$.data.length()").value(0));

            verify(replenisherService).getBoundDeviceIds(1L);
        }
    }

    // ============================================================
    // 9. 绑定设备 POST /replenishers/{id}/devices
    // ============================================================

    @Nested
    @DisplayName("POST /replenishers/{id}/devices - 绑定设备")
    class BindDevicesTests {

        @Test
        @DisplayName("绑定成功")
        void shouldBindDevices() throws Exception {
            BindDeviceDTO dto = new BindDeviceDTO();
            dto.setDeviceIds(Arrays.asList("DEV001", "DEV002"));

            when(replenisherService.bindDevices(eq(1L), anyList())).thenReturn(2);

            mockMvc.perform(post("/replenishers/1/devices")
                            .contentType(MediaType.APPLICATION_JSON)
                            .content(objectMapper.writeValueAsString(dto)))
                    .andExpect(status().isOk())
                    .andExpect(jsonPath("$.code").value(200))
                    .andExpect(jsonPath("$.message").value("绑定完成"))
                    .andExpect(jsonPath("$.data.success").value(2))
                    .andExpect(jsonPath("$.data.total").value(2));

            verify(replenisherService).bindDevices(eq(1L), eq(Arrays.asList("DEV001", "DEV002")));
        }

        @Test
        @DisplayName("部分绑定成功")
        void shouldPartialBind() throws Exception {
            BindDeviceDTO dto = new BindDeviceDTO();
            dto.setDeviceIds(Arrays.asList("DEV001", "DEV002", "DEV003", "DEV004"));

            when(replenisherService.bindDevices(eq(1L), anyList())).thenReturn(3);

            mockMvc.perform(post("/replenishers/1/devices")
                            .contentType(MediaType.APPLICATION_JSON)
                            .content(objectMapper.writeValueAsString(dto)))
                    .andExpect(status().isOk())
                    .andExpect(jsonPath("$.data.success").value(3))
                    .andExpect(jsonPath("$.data.total").value(4));
        }
    }

    // ============================================================
    // 10. 解绑设备 DELETE /replenishers/{id}/devices/{deviceId}
    // ============================================================

    @Nested
    @DisplayName("DELETE /replenishers/{id}/devices/{deviceId} - 解绑设备")
    class UnbindDeviceTests {

        @Test
        @DisplayName("解绑成功")
        void shouldUnbindDevice() throws Exception {
            doNothing().when(replenisherService).unbindDevice(1L, "DEV001");

            mockMvc.perform(delete("/replenishers/1/devices/DEV001"))
                    .andExpect(status().isOk())
                    .andExpect(jsonPath("$.code").value(200))
                    .andExpect(jsonPath("$.message").value("解绑成功"));

            verify(replenisherService).unbindDevice(1L, "DEV001");
        }
    }

    // ============================================================
    // 11. 批量绑定 POST /replenishers/batch-bind
    // ============================================================

    @Nested
    @DisplayName("POST /replenishers/batch-bind - 批量绑定")
    class BatchBindTests {

        @Test
        @DisplayName("批量绑定成功")
        void shouldBatchBind() throws Exception {
            BatchBindDevicesDTO dto = new BatchBindDevicesDTO();
            dto.setReplenisherIds(Arrays.asList(1L, 2L, 3L));
            dto.setDeviceIds(Arrays.asList("DEV001", "DEV002"));

            when(replenisherService.batchBindDevices(anyList(), anyList())).thenReturn(6);

            mockMvc.perform(post("/replenishers/batch-bind")
                            .contentType(MediaType.APPLICATION_JSON)
                            .content(objectMapper.writeValueAsString(dto)))
                    .andExpect(status().isOk())
                    .andExpect(jsonPath("$.code").value(200))
                    .andExpect(jsonPath("$.message").value("批量绑定完成"))
                    .andExpect(jsonPath("$.data.success").value(6))
                    .andExpect(jsonPath("$.data.total").value(6));

            verify(replenisherService).batchBindDevices(
                    eq(Arrays.asList(1L, 2L, 3L)),
                    eq(Arrays.asList("DEV001", "DEV002")));
        }

        @Test
        @DisplayName("批量绑定部分成功")
        void shouldPartialBatchBind() throws Exception {
            BatchBindDevicesDTO dto = new BatchBindDevicesDTO();
            dto.setReplenisherIds(Arrays.asList(1L, 2L));
            dto.setDeviceIds(Collections.singletonList("DEV001"));

            when(replenisherService.batchBindDevices(anyList(), anyList())).thenReturn(1);

            mockMvc.perform(post("/replenishers/batch-bind")
                            .contentType(MediaType.APPLICATION_JSON)
                            .content(objectMapper.writeValueAsString(dto)))
                    .andExpect(status().isOk())
                    .andExpect(jsonPath("$.data.success").value(1))
                    .andExpect(jsonPath("$.data.total").value(2));
        }

        @Test
        @DisplayName("空列表批量绑定")
        void shouldHandleEmptyBatchBind() throws Exception {
            BatchBindDevicesDTO dto = new BatchBindDevicesDTO();
            dto.setReplenisherIds(Collections.emptyList());
            dto.setDeviceIds(Collections.emptyList());

            when(replenisherService.batchBindDevices(anyList(), anyList())).thenReturn(0);

            mockMvc.perform(post("/replenishers/batch-bind")
                            .contentType(MediaType.APPLICATION_JSON)
                            .content(objectMapper.writeValueAsString(dto)))
                    .andExpect(status().isOk())
                    .andExpect(jsonPath("$.data.success").value(0))
                    .andExpect(jsonPath("$.data.total").value(0));
        }
    }

    // ============================================================
    // 12. 生成绑定码 POST /replenishers/{id}/binding-code
    // ============================================================

    @Nested
    @DisplayName("POST /replenishers/{id}/binding-code - 生成绑定码")
    class GenerateBindingCodeTests {

        @Test
        @DisplayName("生成成功")
        void shouldGenerateBindingCode() throws Exception {
            String expectedCode = "A1B2-C3D4-E5F6";
            when(replenisherService.generateBindingCode(1L)).thenReturn(expectedCode);

            mockMvc.perform(post("/replenishers/1/binding-code"))
                    .andExpect(status().isOk())
                    .andExpect(jsonPath("$.code").value(200))
                    .andExpect(jsonPath("$.message").value("生成绑定码成功"))
                    .andExpect(jsonPath("$.data.bindingCode").value(expectedCode))
                    .andExpect(jsonPath("$.data.replenisherId").value("1"));

            verify(replenisherService).generateBindingCode(1L);
        }

        @Test
        @DisplayName("为不同补货员生成不同绑定码")
        void shouldGenerateDifferentCodesForDifferentReplenishers() throws Exception {
            when(replenisherService.generateBindingCode(1L)).thenReturn("CODE-AAAA");
            when(replenisherService.generateBindingCode(2L)).thenReturn("CODE-BBBB");

            mockMvc.perform(post("/replenishers/1/binding-code"))
                    .andExpect(status().isOk())
                    .andExpect(jsonPath("$.data.bindingCode").value("CODE-AAAA"));

            mockMvc.perform(post("/replenishers/2/binding-code"))
                    .andExpect(status().isOk())
                    .andExpect(jsonPath("$.data.bindingCode").value("CODE-BBBB"));

            verify(replenisherService).generateBindingCode(1L);
            verify(replenisherService).generateBindingCode(2L);
        }
    }

    // ============================================================
    // 综合场景测试
    // ============================================================

    @Nested
    @DisplayName("综合场景 - 补货员完整生命周期")
    class LifecycleTests {

        @Test
        @DisplayName("创建 → 查询 → 更新 → 禁用 → 删除")
        void shouldCompleteLifecycle() throws Exception {
            // 1. 创建
            ReplenisherCreateDTO createDto = new ReplenisherCreateDTO();
            createDto.setName("生命周期测试");
            createDto.setPhone("13500135000");

            Replenisher created = createTestReplenisher(100L);
            created.setName("生命周期测试");
            created.setPhone("13500135000");

            when(replenisherService.createReplenisher(any(ReplenisherCreateDTO.class)))
                    .thenReturn(created);

            mockMvc.perform(post("/replenishers")
                            .contentType(MediaType.APPLICATION_JSON)
                            .content(objectMapper.writeValueAsString(createDto)))
                    .andExpect(status().isOk())
                    .andExpect(jsonPath("$.data.id").value(100));

            // 2. 查询
            when(replenisherService.getReplenisherDetail(100L)).thenReturn(created);

            mockMvc.perform(get("/replenishers/100"))
                    .andExpect(status().isOk())
                    .andExpect(jsonPath("$.data.name").value("生命周期测试"));

            // 3. 更新
            ReplenisherUpdateDTO updateDto = new ReplenisherUpdateDTO();
            updateDto.setName("已更新");

            Replenisher updated = createTestReplenisher(100L);
            updated.setName("已更新");

            when(replenisherService.updateReplenisher(eq(100L), any(ReplenisherUpdateDTO.class)))
                    .thenReturn(updated);

            mockMvc.perform(put("/replenishers/100")
                            .contentType(MediaType.APPLICATION_JSON)
                            .content(objectMapper.writeValueAsString(updateDto)))
                    .andExpect(status().isOk())
                    .andExpect(jsonPath("$.data.name").value("已更新"));

            // 4. 禁用
            StatusDTO statusDto = new StatusDTO();
            statusDto.setStatus(0);

            doNothing().when(replenisherService).updateStatus(100L, 0);

            mockMvc.perform(put("/replenishers/100/status")
                            .contentType(MediaType.APPLICATION_JSON)
                            .content(objectMapper.writeValueAsString(statusDto)))
                    .andExpect(status().isOk());

            // 5. 删除
            doNothing().when(replenisherService).deleteReplenisher(100L);

            mockMvc.perform(delete("/replenishers/100"))
                    .andExpect(status().isOk())
                    .andExpect(jsonPath("$.message").value("删除成功"));
        }
    }
}
