package com.ruoyi.asset.service.impl;

import static org.junit.jupiter.api.Assertions.assertEquals;
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.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

import com.ruoyi.asset.constant.AssetLifecycleBusinessType;
import com.ruoyi.asset.constant.AssetStatus;
import com.ruoyi.asset.domain.AmsAsset;
import com.ruoyi.asset.domain.AmsAssetLifecycleLog;
import com.ruoyi.asset.domain.AmsAssetLocation;
import com.ruoyi.asset.domain.AmsWarehouse;
import com.ruoyi.asset.domain.AssetTransitionContext;
import com.ruoyi.asset.domain.AssetTransitionResult;
import com.ruoyi.asset.mapper.AmsAssetMapper;
import com.ruoyi.asset.service.IAmsAssetLocationService;
import com.ruoyi.asset.service.IAmsWarehouseService;
import com.ruoyi.asset.service.IAssetLifecycleService;
import com.ruoyi.common.core.domain.entity.SysDept;
import com.ruoyi.common.core.domain.entity.SysUser;
import com.ruoyi.common.exception.ServiceException;
import com.ruoyi.system.service.ISysDeptService;
import com.ruoyi.system.service.ISysUserService;
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;

@ExtendWith(MockitoExtension.class)
class AssetStatusTransitionServiceImplTest
{
    @Mock
    private AmsAssetMapper amsAssetMapper;

    @Mock
    private IAmsWarehouseService amsWarehouseService;

    @Mock
    private IAmsAssetLocationService amsAssetLocationService;

    @Mock
    private ISysDeptService sysDeptService;

    @Mock
    private ISysUserService sysUserService;

    @Mock
    private IAssetLifecycleService assetLifecycleService;

    @InjectMocks
    private AssetStatusTransitionServiceImpl service;

    /** 入库流转：更新仓位、清除使用归属、记录入库履历 */
    @Test
    void confirmInboundShouldUpdateLocationClearUseOwnershipAndWriteLifecycle()
    {
        AmsAsset asset = buildAsset(AssetStatus.PENDING_INBOUND);
        asset.setUseDeptId(9L);
        asset.setUseDeptName("旧部门");
        asset.setUseUserId(8L);
        asset.setUseUserName("旧使用人");
        stubLockedAsset(asset);
        stubWarehouseLocation(2L, 20L);

        AssetTransitionResult result = service.confirmInbound(1L, 2L, 20L, buildContext());

        assertEquals(AssetStatus.IN_STOCK, result.getAfterAsset().getAssetStatus());
        assertEquals(2L, result.getAfterAsset().getWarehouseId());
        assertEquals(20L, result.getAfterAsset().getLocationId());
        assertNull(result.getAfterAsset().getUseDeptId());
        assertNull(result.getAfterAsset().getUseUserId());
        assertLifecycle(AssetLifecycleBusinessType.INBOUND, AssetStatus.PENDING_INBOUND, AssetStatus.IN_STOCK);
    }

    /** 已在库资产不得重复入库，换仓必须走调拨业务 */
    @Test
    void confirmInboundShouldRejectStockAsset()
    {
        stubLockedAssetWithoutUpdate(buildAsset(AssetStatus.IN_STOCK));

        ServiceException exception = assertThrows(ServiceException.class,
                () -> service.confirmInbound(1L, 2L, 20L, buildContext()));

        assertTrue(exception.getMessage().contains("不允许"));
        verify(amsAssetMapper, never()).updateAssetForTransition(any(AmsAsset.class));
    }

    /** 领用流转：清空仓位，写入使用部门和使用人、状态变为在用 */
    @Test
    void confirmReceiveShouldSetUseOwnershipAndWriteLifecycle()
    {
        AmsAsset asset = buildAsset(AssetStatus.IN_STOCK);
        stubLockedAsset(asset);
        stubDeptUser(3L, 4L);

        AssetTransitionResult result = service.confirmReceive(1L, 3L, 4L, buildContext());

        assertEquals(AssetStatus.IN_USE, result.getAfterAsset().getAssetStatus());
        assertEquals(3L, result.getAfterAsset().getUseDeptId());
        assertEquals("生产部", result.getAfterAsset().getUseDeptName());
        assertEquals(4L, result.getAfterAsset().getUseUserId());
        assertEquals("张三", result.getAfterAsset().getUseUserName());
        assertNull(result.getAfterAsset().getWarehouseId());
        assertNull(result.getAfterAsset().getLocationId());
        assertLifecycle(AssetLifecycleBusinessType.RECEIVE, AssetStatus.IN_STOCK, AssetStatus.IN_USE);
    }

    /** 退库流转：回到在库状态并清除使用归属 */
    @Test
    void confirmReturnShouldRestoreStockAndClearUseOwnership()
    {
        AmsAsset asset = buildAsset(AssetStatus.IN_USE);
        asset.setUseDeptId(3L);
        asset.setUseDeptName("生产部");
        asset.setUseUserId(4L);
        asset.setUseUserName("张三");
        stubLockedAsset(asset);
        stubWarehouseLocation(2L, 20L);

        AssetTransitionResult result = service.confirmReturn(1L, 2L, 20L, buildContext());

        assertEquals(AssetStatus.IN_STOCK, result.getAfterAsset().getAssetStatus());
        assertNull(result.getAfterAsset().getUseDeptId());
        assertNull(result.getAfterAsset().getUseUserId());
        assertLifecycle(AssetLifecycleBusinessType.RETURN, AssetStatus.IN_USE, AssetStatus.IN_STOCK);
    }

    /** 借出校验：在库资产如果残留使用归属数据应拒绝借出 */
    @Test
    void confirmBorrowShouldRejectStockAssetWithUseOwnership()
    {
        AmsAsset asset = buildAsset(AssetStatus.IN_STOCK);
        asset.setUseUserId(4L);
        asset.setUseUserName("张三");
        stubLockedAssetWithoutUpdate(asset);

        ServiceException exception = assertThrows(ServiceException.class,
                () -> service.confirmBorrow(1L, buildContext()));

        assertTrue(exception.getMessage().contains("使用归属"));
        verify(amsAssetMapper, never()).updateAssetForTransition(any(AmsAsset.class));
        verify(assetLifecycleService, never()).recordLifecycle(any(), any(), any());
    }

    /** 借出正常流转：在库→借用中 */
    @Test
    void confirmBorrowShouldEnterBorrowed()
    {
        stubLockedAsset(buildAsset(AssetStatus.IN_STOCK));

        AssetTransitionResult result = service.confirmBorrow(1L, buildContext());

        assertEquals(AssetStatus.BORROWED, result.getAfterAsset().getAssetStatus());
        assertNull(result.getAfterAsset().getWarehouseId());
        assertNull(result.getAfterAsset().getLocationId());
        assertLifecycle(AssetLifecycleBusinessType.BORROW, AssetStatus.IN_STOCK, AssetStatus.BORROWED);
    }

    /** 借用归还：借用中→在库 */
    @Test
    void confirmBorrowReturnShouldRestoreStock()
    {
        AmsAsset asset = buildAsset(AssetStatus.BORROWED);
        stubLockedAsset(asset);
        stubWarehouseLocation(2L, 20L);

        AssetTransitionResult result = service.confirmBorrowReturn(1L, 2L, 20L, buildContext());

        assertEquals(AssetStatus.IN_STOCK, result.getAfterAsset().getAssetStatus());
        assertLifecycle(AssetLifecycleBusinessType.BORROW_RETURN, AssetStatus.BORROWED, AssetStatus.IN_STOCK);
    }

    /** 在用资产不能直接调拨，必须先退库。 */
    @Test
    void confirmTransferShouldRejectInUseAsset()
    {
        AmsAsset asset = buildAsset(AssetStatus.IN_USE);
        asset.setUseDeptId(7L);
        asset.setUseDeptName("旧部门");
        asset.setUseUserId(8L);
        asset.setUseUserName("旧使用人");
        stubLockedAssetWithoutUpdate(asset);

        ServiceException exception = assertThrows(ServiceException.class,
                () -> service.confirmTransfer(1L, 2L, 20L, buildContext()));

        assertTrue(exception.getMessage().contains("不允许"));
        verify(amsAssetMapper, never()).updateAssetForTransition(any(AmsAsset.class));
    }

    /** 在库资产调拨：保持在库状态并清除使用归属 */
    @Test
    void confirmTransferShouldKeepStockStatusAndClearOwnership()
    {
        AmsAsset asset = buildAsset(AssetStatus.IN_STOCK);
        asset.setUseDeptId(7L);
        asset.setUseDeptName("旧部门");
        asset.setUseUserId(8L);
        asset.setUseUserName("旧使用人");
        stubLockedAsset(asset);
        stubWarehouseLocation(2L, 20L);

        AssetTransitionResult result = service.confirmTransfer(1L, 2L, 20L, buildContext());

        assertEquals(AssetStatus.IN_STOCK, result.getAfterAsset().getAssetStatus());
        assertNull(result.getAfterAsset().getUseDeptId());
        assertNull(result.getAfterAsset().getUseUserId());
        assertLifecycle(AssetLifecycleBusinessType.TRANSFER, AssetStatus.IN_STOCK, AssetStatus.IN_STOCK);
    }

    /** 开始维修：记录维修前状态，状态变为维修中 */
    @Test
    void startRepairShouldReturnBeforeStatusAndEnterRepairing()
    {
        AmsAsset asset = buildAsset(AssetStatus.IN_USE);
        asset.setUseDeptId(3L);
        asset.setUseDeptName("生产部");
        asset.setUseUserId(4L);
        asset.setUseUserName("张三");
        clearWarehouseLocation(asset);
        stubLockedAsset(asset);

        AssetTransitionResult result = service.startRepair(1L, buildContext());

        assertEquals(AssetStatus.IN_USE, result.getBeforeAsset().getAssetStatus());
        assertEquals(AssetStatus.REPAIRING, result.getAfterAsset().getAssetStatus());
        assertLifecycle(AssetLifecycleBusinessType.REPAIR, AssetStatus.IN_USE, AssetStatus.REPAIRING);
    }

    /** 完成维修（未传维修前状态）：默认恢复为在库并清除使用归属 */
    @Test
    void finishRepairWithoutBeforeStatusShouldRestoreStock()
    {
        AmsAsset asset = buildAsset(AssetStatus.REPAIRING);
        stubLockedAsset(asset);

        AssetTransitionResult result = service.finishRepair(1L, null, buildContext());

        assertEquals(AssetStatus.IN_STOCK, result.getAfterAsset().getAssetStatus());
        assertNull(result.getAfterAsset().getUseDeptId());
        assertNull(result.getAfterAsset().getUseUserId());
        assertLifecycle(AssetLifecycleBusinessType.REPAIR, AssetStatus.REPAIRING, AssetStatus.IN_STOCK);
    }

    /** 完成维修（维修前在用）：恢复为在用状态并保留使用归属 */
    @Test
    void finishRepairShouldRestoreInUse()
    {
        AmsAsset asset = buildAsset(AssetStatus.REPAIRING);
        asset.setUseDeptId(3L);
        asset.setUseDeptName("生产部");
        asset.setUseUserId(4L);
        asset.setUseUserName("张三");
        clearWarehouseLocation(asset);
        stubLockedAsset(asset);

        AssetTransitionResult result = service.finishRepair(1L, AssetStatus.IN_USE, buildContext());

        assertEquals(AssetStatus.IN_USE, result.getAfterAsset().getAssetStatus());
        assertEquals(3L, result.getAfterAsset().getUseDeptId());
        assertEquals(4L, result.getAfterAsset().getUseUserId());
        assertLifecycle(AssetLifecycleBusinessType.REPAIR, AssetStatus.REPAIRING, AssetStatus.IN_USE);
    }

    /** 完成维修传入非法状态应被拒绝（如传入 BORROWED） */
    @Test
    void finishRepairShouldRejectIllegalRestoreStatus()
    {
        ServiceException exception = assertThrows(ServiceException.class,
                () -> service.finishRepair(1L, AssetStatus.BORROWED, buildContext()));

        assertTrue(exception.getMessage().contains("只能恢复"));
        verify(amsAssetMapper, never()).selectAmsAssetByAssetIdForUpdate(any());
    }

    /** 已报废资产不允许再执行任何流转 */
    @Test
    void disposedAssetShouldRejectFurtherTransition()
    {
        stubLockedAssetWithoutUpdate(buildAsset(AssetStatus.DISPOSED));

        ServiceException exception = assertThrows(ServiceException.class,
                () -> service.confirmInbound(1L, 2L, 20L, buildContext()));

        assertTrue(exception.getMessage().contains("不允许"));
        verify(amsAssetMapper, never()).updateAssetForTransition(any(AmsAsset.class));
    }

    /** 借用中资产不允许调拨 */
    @Test
    void borrowedAssetShouldRejectTransfer()
    {
        stubLockedAssetWithoutUpdate(buildAsset(AssetStatus.BORROWED));

        ServiceException exception = assertThrows(ServiceException.class,
                () -> service.confirmTransfer(1L, 2L, 20L, buildContext()));

        assertTrue(exception.getMessage().contains("不允许"));
        verify(amsAssetMapper, never()).updateAssetForTransition(any(AmsAsset.class));
    }

    /** 维修中资产不允许报废 */
    @Test
    void repairingAssetShouldRejectDisposal()
    {
        stubLockedAssetWithoutUpdate(buildAsset(AssetStatus.REPAIRING));

        ServiceException exception = assertThrows(ServiceException.class,
                () -> service.confirmDisposal(1L, buildContext()));

        assertTrue(exception.getMessage().contains("不允许"));
        verify(amsAssetMapper, never()).updateAssetForTransition(any(AmsAsset.class));
    }

    /** 报废不清除 RFID 绑定，保留标签溯源能力 */
    @Test
    void confirmDisposalShouldEnterDisposedAndKeepRfid()
    {
        AmsAsset asset = buildAsset(AssetStatus.IN_USE);
        asset.setTagId(9L);
        asset.setTagCode("TAG-001");
        asset.setEpcCode("EPC-001");
        stubLockedAsset(asset);

        AssetTransitionResult result = service.confirmDisposal(1L, buildContext());

        assertEquals(AssetStatus.DISPOSED, result.getAfterAsset().getAssetStatus());
        assertEquals(9L, result.getAfterAsset().getTagId());
        assertEquals("EPC-001", result.getAfterAsset().getEpcCode());
        assertLifecycle(AssetLifecycleBusinessType.DISPOSAL, AssetStatus.IN_USE, AssetStatus.DISPOSED);
    }

    /** 仓位归属校验：位置不属于所选仓库应拒绝流转 */
    @Test
    void transitionShouldRejectLocationOutsideWarehouse()
    {
        stubLockedAssetWithoutUpdate(buildAsset(AssetStatus.PENDING_INBOUND));
        AmsWarehouse warehouse = buildWarehouse(2L);
        AmsAssetLocation location = buildLocation(20L, 3L);
        when(amsWarehouseService.selectAmsWarehouseByWarehouseId(2L)).thenReturn(warehouse);
        when(amsAssetLocationService.selectAmsAssetLocationByLocationId(20L)).thenReturn(location);

        ServiceException exception = assertThrows(ServiceException.class,
                () -> service.confirmInbound(1L, 2L, 20L, buildContext()));

        assertTrue(exception.getMessage().contains("不属于"));
        verify(amsAssetMapper, never()).updateAssetForTransition(any(AmsAsset.class));
    }

    /** 主数据状态校验：停用仓库应拒绝流转 */
    @Test
    void transitionShouldRejectInactiveWarehouse()
    {
        stubLockedAssetWithoutUpdate(buildAsset(AssetStatus.PENDING_INBOUND));
        AmsWarehouse warehouse = buildWarehouse(2L);
        warehouse.setEnabled("N");
        when(amsWarehouseService.selectAmsWarehouseByWarehouseId(2L)).thenReturn(warehouse);

        ServiceException exception = assertThrows(ServiceException.class,
                () -> service.confirmInbound(1L, 2L, 20L, buildContext()));

        assertTrue(exception.getMessage().contains("仓库"));
        verify(amsAssetMapper, never()).updateAssetForTransition(any(AmsAsset.class));
    }

    /** 主数据状态校验：停用用户应拒绝领用 */
    @Test
    void transitionShouldRejectInactiveUser()
    {
        stubLockedAssetWithoutUpdate(buildAsset(AssetStatus.IN_STOCK));
        SysDept dept = new SysDept();
        dept.setDeptId(3L);
        dept.setDeptName("生产部");
        dept.setStatus("0");
        dept.setDelFlag("0");
        SysUser user = new SysUser();
        user.setUserId(4L);
        user.setUserName("张三");
        user.setStatus("1");
        user.setDelFlag("0");
        when(sysDeptService.selectDeptById(3L)).thenReturn(dept);
        when(sysUserService.selectUserById(4L)).thenReturn(user);

        ServiceException exception = assertThrows(ServiceException.class,
                () -> service.confirmReceive(1L, 3L, 4L, buildContext()));

        assertTrue(exception.getMessage().contains("使用人"));
        verify(amsAssetMapper, never()).updateAssetForTransition(any(AmsAsset.class));
    }

    /** 上下文校验：缺少流转上下文应拒绝 */
    @Test
    void transitionShouldRejectMissingContext()
    {
        ServiceException exception = assertThrows(ServiceException.class,
                () -> service.confirmDisposal(1L, null));

        assertTrue(exception.getMessage().contains("上下文"));
        verify(amsAssetMapper, never()).selectAmsAssetByAssetIdForUpdate(any());
    }

    /** 资产更新失败应阻断履历写入（不能写了履历但资产状态没变） */
    @Test
    void transitionShouldStopBeforeLifecycleWhenAssetUpdateFails()
    {
        AmsAsset asset = buildAsset(AssetStatus.IN_STOCK);
        when(amsAssetMapper.selectAmsAssetByAssetIdForUpdate(1L)).thenReturn(asset);
        when(amsAssetMapper.updateAssetForTransition(any(AmsAsset.class))).thenReturn(0);

        ServiceException exception = assertThrows(ServiceException.class,
                () -> service.confirmDisposal(1L, buildContext()));

        assertTrue(exception.getMessage().contains("更新失败"));
        verify(assetLifecycleService, never()).recordLifecycle(any(), any(), any());
    }

    /** 履历写入失败应向上传播异常以触发事务回滚 */
    @Test
    void transitionShouldPropagateLifecycleFailureForTransactionRollback()
    {
        AmsAsset asset = buildAsset(AssetStatus.IN_STOCK);
        when(amsAssetMapper.selectAmsAssetByAssetIdForUpdate(1L)).thenReturn(asset);
        when(amsAssetMapper.updateAssetForTransition(any(AmsAsset.class))).thenReturn(1);
        when(assetLifecycleService.recordLifecycle(any(), any(), any()))
                .thenThrow(new ServiceException("履历写入失败"));

        ServiceException exception = assertThrows(ServiceException.class,
                () -> service.confirmDisposal(1L, buildContext()));

        assertTrue(exception.getMessage().contains("履历写入失败"));
        verify(amsAssetMapper).updateAssetForTransition(any(AmsAsset.class));
    }

    /** 桩方法：锁定资产 + 更新成功 + 履历写入成功 */
    private void stubLockedAsset(AmsAsset asset)
    {
        when(amsAssetMapper.selectAmsAssetByAssetIdForUpdate(1L)).thenReturn(asset);
        when(amsAssetMapper.updateAssetForTransition(any(AmsAsset.class))).thenReturn(1);
        when(assetLifecycleService.recordLifecycle(any(), any(), any())).thenReturn(1);
    }

    /** 桩方法：仅锁定资产，不设置更新成功（用于验证拒绝场景） */
    private void stubLockedAssetWithoutUpdate(AmsAsset asset)
    {
        when(amsAssetMapper.selectAmsAssetByAssetIdForUpdate(1L)).thenReturn(asset);
    }

    private void stubWarehouseLocation(Long warehouseId, Long locationId)
    {
        when(amsWarehouseService.selectAmsWarehouseByWarehouseId(warehouseId))
                .thenReturn(buildWarehouse(warehouseId));
        when(amsAssetLocationService.selectAmsAssetLocationByLocationId(locationId))
                .thenReturn(buildLocation(locationId, warehouseId));
    }

    private void stubDeptUser(Long deptId, Long userId)
    {
        SysDept dept = new SysDept();
        dept.setDeptId(deptId);
        dept.setDeptName("生产部");
        dept.setStatus("0");
        dept.setDelFlag("0");
        SysUser user = new SysUser();
        user.setUserId(userId);
        user.setUserName("张三");
        user.setStatus("0");
        user.setDelFlag("0");
        when(sysDeptService.selectDeptById(deptId)).thenReturn(dept);
        when(sysUserService.selectUserById(userId)).thenReturn(user);
    }

    private void clearWarehouseLocation(AmsAsset asset)
    {
        asset.setWarehouseId(null);
        asset.setWarehouseCode(null);
        asset.setWarehouseName(null);
        asset.setLocationId(null);
        asset.setLocationCode(null);
        asset.setLocationName(null);
    }

    /** 断言履历写入：验证业务类型、前后状态和来源单据信息 */
    private void assertLifecycle(String businessType, String beforeStatus, String afterStatus)
    {
        ArgumentCaptor<AmsAsset> beforeCaptor = ArgumentCaptor.forClass(AmsAsset.class);
        ArgumentCaptor<AmsAsset> afterCaptor = ArgumentCaptor.forClass(AmsAsset.class);
        ArgumentCaptor<AmsAssetLifecycleLog> logCaptor = ArgumentCaptor.forClass(AmsAssetLifecycleLog.class);
        verify(assetLifecycleService).recordLifecycle(beforeCaptor.capture(), afterCaptor.capture(), logCaptor.capture());
        assertEquals(beforeStatus, beforeCaptor.getValue().getAssetStatus());
        assertEquals(afterStatus, afterCaptor.getValue().getAssetStatus());
        assertEquals(businessType, logCaptor.getValue().getBusinessType());
        assertEquals(100L, logCaptor.getValue().getSourceOrderId());
        assertEquals("ORDER-001", logCaptor.getValue().getSourceOrderNo());
        assertEquals(1L, logCaptor.getValue().getOperateUserId());
        assertEquals("管理员", logCaptor.getValue().getOperateUserName());
        assertEquals("admin", logCaptor.getValue().getCreateBy());
    }

    /** 构建标准的流转上下文测试数据 */
    private AssetTransitionContext buildContext()
    {
        AssetTransitionContext context = new AssetTransitionContext();
        context.setSourceOrderId(100L);
        context.setSourceOrderNo("ORDER-001");
        context.setSourceItemId(101L);
        context.setOperateUserId(1L);
        context.setOperateUserName("管理员");
        context.setOperateLoginName("admin");
        return context;
    }

    /** 构建指定状态的测试资产 */
    private AmsAsset buildAsset(String status)
    {
        AmsAsset asset = new AmsAsset();
        asset.setAssetId(1L);
        asset.setAssetCode("ASSET-001");
        asset.setAssetName("测试资产");
        asset.setAssetStatus(status);
        asset.setWarehouseId(1L);
        asset.setWarehouseCode("WH-001");
        asset.setWarehouseName("一号仓");
        asset.setLocationId(10L);
        asset.setLocationCode("LOC-001");
        asset.setLocationName("A区");
        return asset;
    }

    private AmsWarehouse buildWarehouse(Long warehouseId)
    {
        AmsWarehouse warehouse = new AmsWarehouse();
        warehouse.setWarehouseId(warehouseId);
        warehouse.setWarehouseCode("WH-" + warehouseId);
        warehouse.setWarehouseName("目标仓");
        warehouse.setEnabled("Y");
        return warehouse;
    }

    private AmsAssetLocation buildLocation(Long locationId, Long warehouseId)
    {
        AmsAssetLocation location = new AmsAssetLocation();
        location.setLocationId(locationId);
        location.setLocationCode("LOC-" + locationId);
        location.setLocationName("目标位置");
        location.setWarehouseId(warehouseId);
        location.setEnabled("Y");
        return location;
    }
}
