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.ArgumentMatchers.anyString;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

import java.util.Date;
import com.ruoyi.asset.constant.AssetLifecycleBusinessType;
import com.ruoyi.asset.constant.AssetStatus;
import com.ruoyi.asset.constant.RfidBindStatus;
import com.ruoyi.asset.constant.RfidTagStatus;
import com.ruoyi.asset.domain.AmsAsset;
import com.ruoyi.asset.domain.AmsAssetLifecycleLog;
import com.ruoyi.asset.domain.AmsRfidTag;
import com.ruoyi.asset.domain.RfidBindingContext;
import com.ruoyi.asset.domain.RfidBindingResult;
import com.ruoyi.asset.mapper.AmsAssetMapper;
import com.ruoyi.asset.mapper.AmsRfidTagMapper;
import com.ruoyi.asset.service.IAssetLifecycleService;
import com.ruoyi.common.exception.ServiceException;
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 RfidBindingServiceImplTest
{
    @Mock
    private AmsRfidTagMapper amsRfidTagMapper;

    @Mock
    private AmsAssetMapper amsAssetMapper;

    @Mock
    private IAssetLifecycleService assetLifecycleService;

    @InjectMocks
    private RfidBindingServiceImpl service;

    /** 正常绑定应同步更新标签、资产并写入标签来源履历 */
    @Test
    void bindShouldUpdateBothSidesAndWriteLifecycle()
    {
        AmsRfidTag tag = buildUnboundTag();
        AmsAsset asset = buildAsset(AssetStatus.PENDING_INBOUND);
        stubBind(tag, asset);

        RfidBindingResult result = service.bind(" ASSET-001 ", " EPC-001 ", buildContext());

        assertEquals(RfidBindStatus.BOUND, result.getAfterTag().getBindStatus());
        assertEquals(asset.getAssetId(), result.getAfterTag().getAssetId());
        assertEquals(tag.getTagId(), result.getAfterAsset().getTagId());
        assertEquals(tag.getEpcCode(), result.getAfterAsset().getEpcCode());
        ArgumentCaptor<AmsAssetLifecycleLog> lifecycleCaptor = ArgumentCaptor.forClass(AmsAssetLifecycleLog.class);
        verify(assetLifecycleService).recordLifecycle(any(), any(), lifecycleCaptor.capture());
        assertEquals(AssetLifecycleBusinessType.RFID_BIND, lifecycleCaptor.getValue().getBusinessType());
        assertEquals(tag.getTagId(), lifecycleCaptor.getValue().getSourceOrderId());
        assertEquals(tag.getTagCode(), lifecycleCaptor.getValue().getSourceOrderNo());
    }

    /** 已作废标签不能绑定 */
    @Test
    void bindShouldRejectVoidTag()
    {
        AmsRfidTag tag = buildUnboundTag();
        tag.setTagStatus(RfidTagStatus.VOID);
        when(amsRfidTagMapper.selectAmsRfidTagByEpcForUpdate("EPC-001")).thenReturn(tag);

        ServiceException exception = assertThrows(ServiceException.class,
                () -> service.bind("ASSET-001", "EPC-001", buildContext()));

        assertTrue(exception.getMessage().contains("作废"));
        verify(amsAssetMapper, never()).selectAmsAssetByAssetCodeForUpdate(anyString());
    }

    /** 已绑定标签不能通过绑定接口覆盖当前关系 */
    @Test
    void bindShouldRejectBoundTag()
    {
        AmsRfidTag tag = buildBoundTag();
        when(amsRfidTagMapper.selectAmsRfidTagByEpcForUpdate("EPC-001")).thenReturn(tag);

        ServiceException exception = assertThrows(ServiceException.class,
                () -> service.bind("ASSET-001", "EPC-001", buildContext()));

        assertTrue(exception.getMessage().contains("重复绑定"));
        verify(amsAssetMapper, never()).selectAmsAssetByAssetCodeForUpdate(anyString());
    }

    /** 未绑定状态下残留绑定数据应阻断绑定 */
    @Test
    void bindShouldRejectTagWithResidualBindingData()
    {
        AmsRfidTag tag = buildUnboundTag();
        tag.setBindTime(new Date());
        when(amsRfidTagMapper.selectAmsRfidTagByEpcForUpdate("EPC-001")).thenReturn(tag);

        ServiceException exception = assertThrows(ServiceException.class,
                () -> service.bind("ASSET-001", "EPC-001", buildContext()));

        assertTrue(exception.getMessage().contains("不一致"));
        verify(amsAssetMapper, never()).selectAmsAssetByAssetCodeForUpdate(anyString());
    }

    /** 已报废资产不能绑定 */
    @Test
    void bindShouldRejectDisposedAsset()
    {
        AmsRfidTag tag = buildUnboundTag();
        AmsAsset asset = buildAsset(AssetStatus.DISPOSED);
        when(amsRfidTagMapper.selectAmsRfidTagByEpcForUpdate("EPC-001")).thenReturn(tag);
        when(amsAssetMapper.selectAmsAssetByAssetCodeForUpdate("ASSET-001")).thenReturn(asset);

        ServiceException exception = assertThrows(ServiceException.class,
                () -> service.bind("ASSET-001", "EPC-001", buildContext()));

        assertTrue(exception.getMessage().contains("报废"));
        verify(amsRfidTagMapper, never()).bindAmsRfidTag(any(AmsRfidTag.class));
    }

    /** 资产不存在时不能更新标签 */
    @Test
    void bindShouldRejectMissingAsset()
    {
        AmsRfidTag tag = buildUnboundTag();
        when(amsRfidTagMapper.selectAmsRfidTagByEpcForUpdate("EPC-001")).thenReturn(tag);

        ServiceException exception = assertThrows(ServiceException.class,
                () -> service.bind("ASSET-001", "EPC-001", buildContext()));

        assertTrue(exception.getMessage().contains("资产不存在"));
        verify(amsRfidTagMapper, never()).bindAmsRfidTag(any(AmsRfidTag.class));
    }

    /** 资产残留任一RFID字段时不能覆盖绑定 */
    @Test
    void bindShouldRejectAssetWithPartialBindingData()
    {
        AmsRfidTag tag = buildUnboundTag();
        AmsAsset asset = buildAsset(AssetStatus.IN_STOCK);
        asset.setEpcCode("OLD-EPC");
        when(amsRfidTagMapper.selectAmsRfidTagByEpcForUpdate("EPC-001")).thenReturn(tag);
        when(amsAssetMapper.selectAmsAssetByAssetCodeForUpdate("ASSET-001")).thenReturn(asset);

        ServiceException exception = assertThrows(ServiceException.class,
                () -> service.bind("ASSET-001", "EPC-001", buildContext()));

        assertTrue(exception.getMessage().contains("不能重复绑定"));
        verify(amsRfidTagMapper, never()).bindAmsRfidTag(any(AmsRfidTag.class));
    }

    /** 正常解绑允许处理已报废资产，并清空双方全部当前绑定字段 */
    @Test
    void unbindShouldClearBothSidesIncludingBindingMetadataForDisposedAsset()
    {
        AmsRfidTag tag = buildBoundTag();
        AmsAsset asset = buildAsset(AssetStatus.DISPOSED);
        fillAssetBinding(asset, tag);
        stubUnbind(tag, asset);

        RfidBindingResult result = service.unbind(10L, buildContext());

        assertEquals(RfidBindStatus.UNBOUND, result.getAfterTag().getBindStatus());
        assertNull(result.getAfterTag().getAssetId());
        assertNull(result.getAfterTag().getBindTime());
        assertNull(result.getAfterTag().getBindUserId());
        assertNull(result.getAfterTag().getBindUserName());
        assertNull(result.getAfterAsset().getTagId());
        assertNull(result.getAfterAsset().getTagCode());
        assertNull(result.getAfterAsset().getEpcCode());
        ArgumentCaptor<AmsAssetLifecycleLog> lifecycleCaptor = ArgumentCaptor.forClass(AmsAssetLifecycleLog.class);
        verify(assetLifecycleService).recordLifecycle(any(), any(), lifecycleCaptor.capture());
        assertEquals(AssetLifecycleBusinessType.RFID_UNBIND, lifecycleCaptor.getValue().getBusinessType());
        assertEquals(tag.getTagId(), lifecycleCaptor.getValue().getSourceOrderId());
    }

    /** 标签和资产当前关系不一致时禁止解绑 */
    @Test
    void unbindShouldRejectMismatchedBinding()
    {
        AmsRfidTag tag = buildBoundTag();
        AmsAsset asset = buildAsset(AssetStatus.IN_STOCK);
        fillAssetBinding(asset, tag);
        asset.setTagId(99L);
        when(amsRfidTagMapper.selectAmsRfidTagByTagIdForUpdate(10L)).thenReturn(tag);
        when(amsAssetMapper.selectAmsAssetByAssetIdForUpdate(1L)).thenReturn(asset);

        ServiceException exception = assertThrows(ServiceException.class,
                () -> service.unbind(10L, buildContext()));

        assertTrue(exception.getMessage().contains("不一致"));
        verify(amsRfidTagMapper, never()).unbindAmsRfidTagByTagId(any(), any(), any(), any());
    }

    /** 标签保存的资产快照与资产主表不一致时禁止解绑 */
    @Test
    void unbindShouldRejectMismatchedAssetSnapshot()
    {
        AmsRfidTag tag = buildBoundTag();
        AmsAsset asset = buildAsset(AssetStatus.IN_STOCK);
        fillAssetBinding(asset, tag);
        tag.setAssetCode("OLD-ASSET-CODE");
        when(amsRfidTagMapper.selectAmsRfidTagByTagIdForUpdate(10L)).thenReturn(tag);
        when(amsAssetMapper.selectAmsAssetByAssetIdForUpdate(1L)).thenReturn(asset);

        ServiceException exception = assertThrows(ServiceException.class,
                () -> service.unbind(10L, buildContext()));

        assertTrue(exception.getMessage().contains("不一致"));
        verify(amsRfidTagMapper, never()).unbindAmsRfidTagByTagId(any(), any(), any(), any());
    }

    /** 已绑定标签缺少绑定元数据时不能继续解绑 */
    @Test
    void unbindShouldRejectIncompleteTagBinding()
    {
        AmsRfidTag tag = buildBoundTag();
        tag.setBindUserName(null);
        when(amsRfidTagMapper.selectAmsRfidTagByTagIdForUpdate(10L)).thenReturn(tag);

        ServiceException exception = assertThrows(ServiceException.class,
                () -> service.unbind(10L, buildContext()));

        assertTrue(exception.getMessage().contains("不完整"));
        verify(amsAssetMapper, never()).selectAmsAssetByAssetIdForUpdate(any());
    }

    /** 标签专用更新失败时不得继续更新资产 */
    @Test
    void bindShouldStopWhenTagUpdateFails()
    {
        AmsRfidTag tag = buildUnboundTag();
        AmsAsset asset = buildAsset(AssetStatus.IN_STOCK);
        when(amsRfidTagMapper.selectAmsRfidTagByEpcForUpdate("EPC-001")).thenReturn(tag);
        when(amsAssetMapper.selectAmsAssetByAssetCodeForUpdate("ASSET-001")).thenReturn(asset);
        when(amsRfidTagMapper.bindAmsRfidTag(any(AmsRfidTag.class))).thenReturn(0);

        ServiceException exception = assertThrows(ServiceException.class,
                () -> service.bind("ASSET-001", "EPC-001", buildContext()));

        assertTrue(exception.getMessage().contains("绑定失败"));
        verify(amsAssetMapper, never()).bindAssetRfid(any(), any(), any(), any(), any(), any(), any());
        verify(assetLifecycleService, never()).recordLifecycle(any(), any(), any());
    }

    /** 资产专用更新失败时不得写入履历，事务负责回滚标签更新 */
    @Test
    void bindShouldStopBeforeLifecycleWhenAssetUpdateFails()
    {
        AmsRfidTag tag = buildUnboundTag();
        AmsAsset asset = buildAsset(AssetStatus.IN_STOCK);
        when(amsRfidTagMapper.selectAmsRfidTagByEpcForUpdate("EPC-001")).thenReturn(tag);
        when(amsAssetMapper.selectAmsAssetByAssetCodeForUpdate("ASSET-001")).thenReturn(asset);
        when(amsRfidTagMapper.bindAmsRfidTag(any(AmsRfidTag.class))).thenReturn(1);
        when(amsAssetMapper.bindAssetRfid(any(), any(), any(), any(), any(), any(), any())).thenReturn(0);

        ServiceException exception = assertThrows(ServiceException.class,
                () -> service.bind("ASSET-001", "EPC-001", buildContext()));

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

    /** 履历失败异常必须向上传播以触发绑定事务回滚 */
    @Test
    void bindShouldPropagateLifecycleFailure()
    {
        AmsRfidTag tag = buildUnboundTag();
        AmsAsset asset = buildAsset(AssetStatus.IN_STOCK);
        stubBind(tag, asset);
        when(assetLifecycleService.recordLifecycle(any(), any(), any()))
                .thenThrow(new ServiceException("履历写入失败"));

        ServiceException exception = assertThrows(ServiceException.class,
                () -> service.bind("ASSET-001", "EPC-001", buildContext()));

        assertTrue(exception.getMessage().contains("履历写入失败"));
    }

    /** 解绑标签更新失败时不得继续清空资产 */
    @Test
    void unbindShouldStopWhenTagUpdateFails()
    {
        AmsRfidTag tag = buildBoundTag();
        AmsAsset asset = buildAsset(AssetStatus.IN_STOCK);
        fillAssetBinding(asset, tag);
        when(amsRfidTagMapper.selectAmsRfidTagByTagIdForUpdate(10L)).thenReturn(tag);
        when(amsAssetMapper.selectAmsAssetByAssetIdForUpdate(1L)).thenReturn(asset);
        when(amsRfidTagMapper.unbindAmsRfidTagByTagId(any(), any(), any(), any())).thenReturn(0);

        ServiceException exception = assertThrows(ServiceException.class,
                () -> service.unbind(10L, buildContext()));

        assertTrue(exception.getMessage().contains("解绑失败"));
        verify(amsAssetMapper, never()).clearAssetRfidBinding(any(), any(), any(), any(), any(), any());
        verify(assetLifecycleService, never()).recordLifecycle(any(), any(), any());
    }

    /** 缺少操作上下文时不能加锁或写数据 */
    @Test
    void bindShouldRejectMissingContext()
    {
        assertThrows(ServiceException.class, () -> service.bind("ASSET-001", "EPC-001", null));

        verify(amsRfidTagMapper, never()).selectAmsRfidTagByEpcForUpdate(anyString());
    }

    private void stubBind(AmsRfidTag tag, AmsAsset asset)
    {
        when(amsRfidTagMapper.selectAmsRfidTagByEpcForUpdate("EPC-001")).thenReturn(tag);
        when(amsAssetMapper.selectAmsAssetByAssetCodeForUpdate("ASSET-001")).thenReturn(asset);
        when(amsRfidTagMapper.bindAmsRfidTag(any(AmsRfidTag.class))).thenReturn(1);
        when(amsAssetMapper.bindAssetRfid(any(), any(), any(), any(), any(), any(), any())).thenReturn(1);
        when(assetLifecycleService.recordLifecycle(any(), any(), any())).thenReturn(1);
    }

    private void stubUnbind(AmsRfidTag tag, AmsAsset asset)
    {
        when(amsRfidTagMapper.selectAmsRfidTagByTagIdForUpdate(10L)).thenReturn(tag);
        when(amsAssetMapper.selectAmsAssetByAssetIdForUpdate(1L)).thenReturn(asset);
        when(amsRfidTagMapper.unbindAmsRfidTagByTagId(any(), any(), any(), any())).thenReturn(1);
        when(amsAssetMapper.clearAssetRfidBinding(any(), any(), any(), any(), any(), any())).thenReturn(1);
        when(assetLifecycleService.recordLifecycle(any(), any(), any())).thenReturn(1);
    }

    private RfidBindingContext buildContext()
    {
        RfidBindingContext context = new RfidBindingContext();
        context.setOperateUserId(1L);
        context.setOperateUserName("管理员");
        context.setOperateLoginName("admin");
        context.setRemark("测试");
        return context;
    }

    private AmsRfidTag buildUnboundTag()
    {
        AmsRfidTag tag = new AmsRfidTag();
        tag.setTagId(10L);
        tag.setTagCode("TAG-001");
        tag.setEpcCode("EPC-001");
        tag.setTagStatus(RfidTagStatus.NORMAL);
        tag.setBindStatus(RfidBindStatus.UNBOUND);
        return tag;
    }

    private AmsRfidTag buildBoundTag()
    {
        AmsRfidTag tag = buildUnboundTag();
        tag.setBindStatus(RfidBindStatus.BOUND);
        tag.setAssetId(1L);
        tag.setAssetCode("ASSET-001");
        tag.setAssetName("测试资产");
        tag.setBindTime(new Date());
        tag.setBindUserId(1L);
        tag.setBindUserName("管理员");
        return tag;
    }

    private AmsAsset buildAsset(String status)
    {
        AmsAsset asset = new AmsAsset();
        asset.setAssetId(1L);
        asset.setAssetCode("ASSET-001");
        asset.setAssetName("测试资产");
        asset.setAssetStatus(status);
        return asset;
    }

    private void fillAssetBinding(AmsAsset asset, AmsRfidTag tag)
    {
        asset.setTagId(tag.getTagId());
        asset.setTagCode(tag.getTagCode());
        asset.setEpcCode(tag.getEpcCode());
    }
}
