package com.ruoyi.asset.service.impl;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyList;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

import java.math.BigDecimal;
import java.util.Calendar;
import java.util.Date;

import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.ArgumentCaptor;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;

import com.ruoyi.asset.domain.AmsAsset;
import com.ruoyi.asset.domain.AmsRepairOperateContext;
import com.ruoyi.asset.domain.AmsRepairOrder;
import com.ruoyi.asset.domain.AssetTransitionContext;
import com.ruoyi.asset.domain.AssetTransitionResult;
import com.ruoyi.asset.mapper.AmsAssetMapper;
import com.ruoyi.asset.mapper.AmsRepairOrderMapper;
import com.ruoyi.asset.service.IAssetStatusTransitionService;
import com.ruoyi.common.exception.ServiceException;
import com.ruoyi.system.service.ISysCodeRuleService;

/**
 * 报修维修管理服务层单元测试类
 *
 * 重点校验报修单的新增、修改、占用拦截、驳回、开始维修、完成维修状态联动及恢复等全生命周期流转。
 *
 * @author Yangk
 * @date 2026-06-16
 */
@ExtendWith(MockitoExtension.class)
class AmsRepairOrderServiceImplTest
{
    @Mock
    private AmsRepairOrderMapper amsRepairOrderMapper;

    @Mock
    private AmsAssetMapper amsAssetMapper;

    @Mock
    private ISysCodeRuleService sysCodeRuleService;

    @Mock
    private IAssetStatusTransitionService assetStatusTransitionService;

    @InjectMocks
    private AmsRepairOrderServiceImpl service;

    /**
     * 测试：新增报修单草稿并回填快照
     * 断言：
     * 1. 自动生成以 BX 为前缀的报修单号。
     * 2. 状态扭转为 DRAFT。
     * 3. 能够自动快照资产的编码、名称、品牌等属性。
     * 4. 维修受控字段被安全初始化。
     */
    @Test
    void insertShouldGenerateCodeAndFillSnapshots()
    {
        AmsRepairOrder order = buildRequest();
        order.setReportUserId(999L);
        order.setReportUserName("前端篡改报修人");
        stubAsset("IN_STOCK");

        when(sysCodeRuleService.nextCode("REPAIR_ORDER")).thenReturn("BX202606160001");
        when(amsRepairOrderMapper.countOtherActiveRepairOrderByAssetId(eq(1L), eq(null), anyList()))
                .thenReturn(0);

        doAnswer(invocation -> {
            AmsRepairOrder inserted = invocation.getArgument(0);
            inserted.setRepairId(100L);
            return 1;
        }).when(amsRepairOrderMapper).insertAmsRepairOrder(any(AmsRepairOrder.class));

        assertEquals(1, service.insertAmsRepairOrder(order,
                operateContext(2L, "真实报修人", "admin")));

        assertEquals("BX202606160001", order.getRepairNo());
        assertEquals("DRAFT", order.getOrderStatus());
        assertEquals(2L, order.getReportUserId());
        assertEquals("真实报修人", order.getReportUserName());
        assertNotNull(order.getReportTime());
        assertEquals("admin", order.getCreateBy());
        assertEquals("A001", order.getAssetCode());
        assertEquals("一号资产", order.getAssetName());
        assertEquals("CAT-001", order.getCategoryCode());
        assertEquals("品牌A", order.getBrand());
        assertEquals("型号X", order.getSpecModel());
        assertEquals(BigDecimal.ZERO, order.getRepairCost());
        assertNull(order.getRepairStartTime());
        assertNull(order.getRepairFinishTime());
        assertNull(order.getRejectReason());
    }

    /**
     * 测试：新增草稿时资产已被其他报修单占用
     * 断言：抛出 ServiceException 异常，不写入数据库。
     */
    @Test
    void insertShouldRejectIfAssetOccupied()
    {
        AmsRepairOrder order = buildRequest();
        stubAsset("IN_STOCK");

        // 模拟资产已被其他报修单占用
        when(amsRepairOrderMapper.countOtherActiveRepairOrderByAssetId(eq(1L), eq(null), anyList()))
                .thenReturn(1);

        ServiceException exception = assertThrows(ServiceException.class,
                () -> service.insertAmsRepairOrder(order, operateContext(2L, "真实报修人", "admin")));

        assertTrue(exception.getMessage().contains("已被其他未完结报修单占用"));
        verify(amsRepairOrderMapper, never()).insertAmsRepairOrder(any(AmsRepairOrder.class));
    }

    /**
     * 测试：修改报修单草稿
     * 断言：只能在 DRAFT 状态修改，覆盖前端提交的快照，强制保留 DRAFT 且清除流转字段。
     */
    @Test
    void updateShouldKeepDraftAndOverwriteSnapshots()
    {
        AmsRepairOrder order = buildPersistedOrder("DRAFT");
        order.setFaultDesc("更新后的故障描述");
        order.setRemark("更新后的备注");
        order.setReportUserId(999L);
        order.setReportUserName("前端篡改报修人");

        AmsRepairOrder current = buildPersistedOrder("DRAFT");
        when(amsRepairOrderMapper.selectAmsRepairOrderByRepairIdForUpdate(100L)).thenReturn(current);
        stubAsset("IN_STOCK");
        when(amsRepairOrderMapper.countOtherActiveRepairOrderByAssetId(eq(1L), eq(100L), anyList()))
                .thenReturn(0);
        when(amsRepairOrderMapper.updateAmsRepairOrder(any(AmsRepairOrder.class))).thenReturn(1);

        assertEquals(1, service.updateAmsRepairOrder(order, adminContext()));

        assertEquals("DRAFT", order.getOrderStatus());
        assertEquals("admin", order.getUpdateBy());
        assertEquals("DRAFT", order.getParams().get("expectedOrderStatus"));
        assertEquals("更新后的故障描述", order.getFaultDesc());
        assertEquals(2L, order.getReportUserId());
        assertEquals("张三报修人", order.getReportUserName());
        assertEquals("A001", order.getAssetCode()); // 强制回填覆盖
        assertNull(order.getRepairStartTime());
        assertNull(order.getRepairFinishTime());
        assertEquals(BigDecimal.ZERO, order.getRepairCost());
    }

    /**
     * 测试：在非 DRAFT 状态下修改报修单
     * 断言：抛出 ServiceException，阻断修改。
     */
    @Test
    void updateShouldRejectNonDraft()
    {
        AmsRepairOrder order = buildPersistedOrder("PENDING_CONFIRM");

        AmsRepairOrder current = buildPersistedOrder("PENDING_CONFIRM");
        when(amsRepairOrderMapper.selectAmsRepairOrderByRepairIdForUpdate(100L)).thenReturn(current);

        ServiceException exception = assertThrows(ServiceException.class,
                () -> service.updateAmsRepairOrder(order, adminContext()));

        assertTrue(exception.getMessage().contains("仅草稿状态的报修单允许修改"));
        verify(amsRepairOrderMapper, never()).updateAmsRepairOrder(any(AmsRepairOrder.class));
    }

    /**
     * 测试：提交报修申请
     * 断言：单据从 DRAFT 流转至 PENDING_CONFIRM，且校验资产未被占用。
     */
    @Test
    void submitShouldMoveDraftToPendingConfirm()
    {
        AmsRepairOrder order = buildPersistedOrder("DRAFT");
        when(amsRepairOrderMapper.selectAmsRepairOrderByRepairIdForUpdate(100L)).thenReturn(order);
        stubAsset("IN_STOCK");
        when(amsRepairOrderMapper.countOtherActiveRepairOrderByAssetId(eq(1L), eq(100L), anyList()))
                .thenReturn(0);
        when(amsRepairOrderMapper.updateAmsRepairOrder(order)).thenReturn(1);

        assertEquals(1, service.submitRepair(100L, adminContext()));
        assertEquals("PENDING_CONFIRM", order.getOrderStatus());
        assertEquals("DRAFT", order.getParams().get("expectedOrderStatus"));
        verify(amsRepairOrderMapper).updateAmsRepairOrder(order);
    }

    /**
     * 测试：确认受理报修
     * 断言：单据从 PENDING_CONFIRM 进入 WAIT_REPAIR 状态。
     */
    @Test
    void confirmShouldMovePendingConfirmToWaitRepair()
    {
        AmsRepairOrder order = buildPersistedOrder("PENDING_CONFIRM");
        order.setRejectReason("上次驳回的原因");
        when(amsRepairOrderMapper.selectAmsRepairOrderByRepairIdForUpdate(100L)).thenReturn(order);
        stubAsset("IN_STOCK");
        when(amsRepairOrderMapper.countOtherActiveRepairOrderByAssetId(eq(1L), eq(100L), anyList()))
                .thenReturn(0);
        when(amsRepairOrderMapper.updateAmsRepairOrder(order)).thenReturn(1);

        assertEquals(1, service.confirmRepair(100L, adminContext()));
        assertEquals("WAIT_REPAIR", order.getOrderStatus());
        assertEquals("PENDING_CONFIRM", order.getParams().get("expectedOrderStatus"));
        assertNull(order.getRejectReason()); // 清空驳回记录
        verify(amsRepairOrderMapper).updateAmsRepairOrder(order);
    }

    /**
     * 测试：状态流转更新行数为 0 时快速失败
     * 断言：Service 抛出异常，避免 Controller 将 0 行更新误当作业务成功。
     */
    @Test
    void confirmShouldThrowIfUpdateRowsZero()
    {
        AmsRepairOrder order = buildPersistedOrder("PENDING_CONFIRM");
        when(amsRepairOrderMapper.selectAmsRepairOrderByRepairIdForUpdate(100L)).thenReturn(order);
        stubAsset("IN_STOCK");
        when(amsRepairOrderMapper.countOtherActiveRepairOrderByAssetId(eq(1L), eq(100L), anyList()))
                .thenReturn(0);
        when(amsRepairOrderMapper.updateAmsRepairOrder(order)).thenReturn(0);

        ServiceException exception = assertThrows(ServiceException.class,
                () -> service.confirmRepair(100L, adminContext()));

        assertTrue(exception.getMessage().contains("受理失败"));
    }

    /**
     * 测试：驳回报修申请
     * 断言：单据由 PENDING_CONFIRM 变为 REJECTED，并必须带有非空的驳回原因。
     */
    @Test
    void rejectShouldSetStatusAndReason()
    {
        AmsRepairOrder order = buildPersistedOrder("PENDING_CONFIRM");
        when(amsRepairOrderMapper.selectAmsRepairOrderByRepairIdForUpdate(100L)).thenReturn(order);
        when(amsRepairOrderMapper.updateAmsRepairOrder(order)).thenReturn(1);

        assertEquals(1, service.rejectRepair(100L, "资产情况描述不清", adminContext()));
        assertEquals("REJECTED", order.getOrderStatus());
        assertEquals("PENDING_CONFIRM", order.getParams().get("expectedOrderStatus"));
        assertEquals("资产情况描述不清", order.getRejectReason());

        // 测试空驳回原因拦截
        assertThrows(ServiceException.class, () -> service.rejectRepair(100L, "", adminContext()));
    }

    /**
     * 测试：开始维修 (内修模式)
     * 断言：
     * 1. 状态由 WAIT_REPAIR 变为 REPAIRING。
     * 2. 调用公共流转服务 `startRepair`，锁定并转移资产到维修中。
     * 3. 写入维修方信息（类型、人员姓名等）以及开始时间。
     * 4. 记录资产维修前状态快照 `beforeAssetStatus`。
     */
    @Test
    void startRepairInternalShouldTransitionAssetAndSetStartTime()
    {
        AmsRepairOrder order = buildPersistedOrder("WAIT_REPAIR");
        when(amsRepairOrderMapper.selectAmsRepairOrderByRepairIdForUpdate(100L)).thenReturn(order);
        AmsAsset beforeAsset = stubAsset("IN_STOCK"); // 原本在库
        when(assetStatusTransitionService.startRepair(eq(1L), any(AssetTransitionContext.class)))
                .thenReturn(new AssetTransitionResult(beforeAsset, buildAsset("REPAIRING")));
        when(amsRepairOrderMapper.updateAmsRepairOrder(order)).thenReturn(1);

        AmsRepairOrder startParams = new AmsRepairOrder();
        startParams.setRepairId(100L);
        startParams.setRepairerType("INTERNAL");
        startParams.setRepairUserId(5L);
        startParams.setRepairUserName("张三");
        startParams.setRepairContactPhone("13888888888");

        assertEquals(1, service.startRepair(startParams, adminContext()));

        assertEquals("REPAIRING", order.getOrderStatus());
        assertEquals("INTERNAL", order.getRepairerType());
        assertEquals(5L, order.getRepairUserId());
        assertEquals("张三", order.getRepairUserName());
        assertNull(order.getRepairOrgName());
        assertEquals("13888888888", order.getRepairContactPhone());
        assertEquals("IN_STOCK", order.getBeforeAssetStatus()); // 快照原本在库状态
        assertEquals("WAIT_REPAIR", order.getParams().get("expectedOrderStatus"));
        assertNotNull(order.getRepairStartTime());

        ArgumentCaptor<AssetTransitionContext> contextCaptor = ArgumentCaptor.forClass(AssetTransitionContext.class);
        verify(assetStatusTransitionService).startRepair(eq(1L), contextCaptor.capture());
        assertEquals(100L, contextCaptor.getValue().getSourceOrderId());
        assertEquals("BX202606160001", contextCaptor.getValue().getSourceOrderNo());
        assertEquals(9L, contextCaptor.getValue().getOperateUserId());
        assertEquals("管理员", contextCaptor.getValue().getOperateUserName());
    }

    /**
     * 测试：开始维修 (外修模式)
     * 断言：校验外部维修单位不为空，正确写入外部单位名称。
     */
    @Test
    void startRepairExternalShouldRequireOrgName()
    {
        AmsRepairOrder order = buildPersistedOrder("WAIT_REPAIR");
        when(amsRepairOrderMapper.selectAmsRepairOrderByRepairIdForUpdate(100L)).thenReturn(order);
        AmsAsset beforeAsset = stubAsset("IN_USE"); // 原本在用
        when(assetStatusTransitionService.startRepair(eq(1L), any(AssetTransitionContext.class)))
                .thenReturn(new AssetTransitionResult(beforeAsset, buildAsset("REPAIRING")));
        when(amsRepairOrderMapper.updateAmsRepairOrder(order)).thenReturn(1);

        AmsRepairOrder startParams = new AmsRepairOrder();
        startParams.setRepairId(100L);
        startParams.setRepairerType("EXTERNAL");
        startParams.setRepairOrgName("外部维修公司");
        startParams.setRepairUserName("李外部人");
        startParams.setRepairContactPhone("13999999999");

        assertEquals(1, service.startRepair(startParams, adminContext()));

        assertEquals("REPAIRING", order.getOrderStatus());
        assertEquals("EXTERNAL", order.getRepairerType());
        assertNull(order.getRepairUserId());
        assertEquals("李外部人", order.getRepairUserName());
        assertEquals("外部维修公司", order.getRepairOrgName());
        assertEquals("IN_USE", order.getBeforeAssetStatus()); // 快照原本在用状态
    }

    /**
     * 测试：完成维修
     * 断言：
     * 1. 状态进入 REPAIR_DONE。
     * 2. 调用公共流转服务 `finishRepair`，利用 `beforeAssetStatus` 将资产恢复到原状态。
     * 3. 填入完成时间、非负费用和维修结果。
     */
    @Test
    void finishRepairShouldCompleteOrderAndRestoreAssetStatus()
    {
        AmsRepairOrder order = buildPersistedOrder("REPAIRING");
        // 模拟原本是在用状态
        order.setBeforeAssetStatus("IN_USE");
        Calendar cal = Calendar.getInstance();
        cal.add(Calendar.HOUR_OF_DAY, -2);
        order.setRepairStartTime(cal.getTime()); // 开始时间设为两小时前

        when(amsRepairOrderMapper.selectAmsRepairOrderByRepairIdForUpdate(100L)).thenReturn(order);
        AmsAsset beforeAsset = stubAsset("REPAIRING"); // 实物处于维修中
        when(assetStatusTransitionService.finishRepair(eq(1L), eq("IN_USE"), any(AssetTransitionContext.class)))
                .thenReturn(new AssetTransitionResult(beforeAsset, buildAsset("IN_USE")));
        when(amsRepairOrderMapper.updateAmsRepairOrder(order)).thenReturn(1);

        AmsRepairOrder finishParams = new AmsRepairOrder();
        finishParams.setRepairId(100L);
        finishParams.setRepairFinishTime(new Date());
        finishParams.setRepairCost(new BigDecimal("150.00"));
        finishParams.setRepairResult("更换了电源，目前通电测试正常");

        assertEquals(1, service.finishRepair(finishParams, adminContext()));

        assertEquals("REPAIR_DONE", order.getOrderStatus());
        assertEquals("REPAIRING", order.getParams().get("expectedOrderStatus"));
        assertEquals(new BigDecimal("150.00"), order.getRepairCost());
        assertEquals("更换了电源，目前通电测试正常", order.getRepairResult());
        assertNotNull(order.getRepairFinishTime());

        ArgumentCaptor<AssetTransitionContext> contextCaptor = ArgumentCaptor.forClass(AssetTransitionContext.class);
        // 验证流转服务被调用，且传入了维修前原状态 IN_USE
        verify(assetStatusTransitionService).finishRepair(eq(1L), eq("IN_USE"), contextCaptor.capture());
        assertEquals(100L, contextCaptor.getValue().getSourceOrderId());
        assertEquals(9L, contextCaptor.getValue().getOperateUserId());
        assertEquals("管理员", contextCaptor.getValue().getOperateUserName());
    }

    /**
     * 测试：完成时间早于开始时间拦截
     */
    @Test
    void finishRepairShouldRejectEarlyFinishTime()
    {
        AmsRepairOrder order = buildPersistedOrder("REPAIRING");
        order.setRepairStartTime(new Date());

        when(amsRepairOrderMapper.selectAmsRepairOrderByRepairIdForUpdate(100L)).thenReturn(order);

        AmsRepairOrder finishParams = new AmsRepairOrder();
        finishParams.setRepairId(100L);
        // 完成时间设为昨天（早于开始时间）
        Calendar cal = Calendar.getInstance();
        cal.add(Calendar.DAY_OF_YEAR, -1);
        finishParams.setRepairFinishTime(cal.getTime());
        finishParams.setRepairCost(new BigDecimal("50.00"));
        finishParams.setRepairResult("测试");

        assertThrows(ServiceException.class, () -> service.finishRepair(finishParams, adminContext()));
    }

    /**
     * 测试：批量/单个逻辑删除报修单草稿
     * 断言：仅限 DRAFT 状态删除，否则抛出异常。
     */
    @Test
    void deleteShouldOnlyAllowDraft()
    {
        // 1. 删除草稿单，验证成功
        AmsRepairOrder order1 = buildPersistedOrder("DRAFT");
        when(amsRepairOrderMapper.selectAmsRepairOrderByRepairIdForUpdate(100L)).thenReturn(order1);
        when(amsRepairOrderMapper.deleteAmsRepairOrderByRepairId(100L)).thenReturn(1);

        assertEquals(1, service.deleteAmsRepairOrderByRepairId(100L));

        // 2. 删除非草稿单，验证抛出异常被拦截
        AmsRepairOrder order2 = buildPersistedOrder("REPAIRING");
        when(amsRepairOrderMapper.selectAmsRepairOrderByRepairIdForUpdate(200L)).thenReturn(order2);

        assertThrows(ServiceException.class, () -> service.deleteAmsRepairOrderByRepairId(200L));
        verify(amsRepairOrderMapper, never()).deleteAmsRepairOrderByRepairId(200L);
    }

    // ========================================== 辅助方法 ==========================================

    private AmsRepairOrder buildRequest()
    {
        AmsRepairOrder order = new AmsRepairOrder();
        order.setAssetId(1L);
        order.setFaultDesc("资产电源无法开机");
        order.setExpectedFinishTime(new Date());
        order.setRemark("急需使用");
        return order;
    }

    private AmsRepairOperateContext adminContext()
    {
        return operateContext(9L, "管理员", "admin");
    }

    private AmsRepairOperateContext operateContext(Long operateUserId, String operateUserName, String operateLoginName)
    {
        return new AmsRepairOperateContext(operateUserId, operateUserName, operateLoginName);
    }

    private AmsRepairOrder buildPersistedOrder(String status)
    {
        AmsRepairOrder order = buildRequest();
        order.setRepairId(100L);
        order.setRepairNo("BX202606160001");
        order.setOrderStatus(status);
        order.setRepairerType("INTERNAL");
        order.setReportUserId(2L);
        order.setReportUserName("张三报修人");
        order.setReportTime(new Date());
        order.setCreateBy("admin");
        order.setCreateTime(new Date());
        order.setDelFlag("0");
        return order;
    }

    private AmsAsset stubAsset(String status)
    {
        AmsAsset asset = buildAsset(status);
        when(amsAssetMapper.selectAmsAssetByAssetIdForUpdate(1L)).thenReturn(asset);
        return asset;
    }

    private AmsAsset buildAsset(String status)
    {
        AmsAsset asset = new AmsAsset();
        asset.setAssetId(1L);
        asset.setAssetCode("A001");
        asset.setAssetName("一号资产");
        asset.setCategoryId(10L);
        asset.setCategoryCode("CAT-001");
        asset.setCategoryName("类别一");
        asset.setBrand("品牌A");
        asset.setSpecModel("型号X");
        asset.setAssetStatus(status);
        asset.setDelFlag("0");
        return asset;
    }
}
