package com.xyw.console.asset.service;

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

import com.baomidou.mybatisplus.core.conditions.Wrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.xyw.console.asset.dto.DeviceAssetPageQuery;
import com.xyw.console.asset.dto.DeviceAssetSaveRequest;
import com.xyw.console.asset.entity.AssetDeviceEntity;
import com.xyw.console.asset.entity.CompanyPersonEntity;
import com.xyw.console.asset.exception.DeviceAssetValidationException;
import com.xyw.console.asset.mapper.*;
import java.awt.image.BufferedImage;
import java.io.ByteArrayOutputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import javax.imageio.ImageIO;
import org.mockito.ArgumentCaptor;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import org.springframework.mock.web.MockMultipartFile;

/** 文件用途（白话）：以 Mock Mapper 验证设备列表名称解析和关键校验规则，不访问真实数据库。 */
class DeviceAssetServiceTest {
  /** 代码作用（白话）：验证分页行会批量解析有效使用人姓名。关联文件：DeviceAssetService.java、DeviceAssetResponse.java。关联逻辑（调用链/数据流）：设备 Mapper -> page -> 人员 Mapper -> 响应行。 */
  @Test void resolvesUserPersonNameForPageRecord() {
    AssetDeviceMapper devices=mock(AssetDeviceMapper.class); CompanyPersonMapper people=mock(CompanyPersonMapper.class); DeviceAssetService service=service(devices,people);
    AssetDeviceEntity device=new AssetDeviceEntity();device.setId(1L);device.setDeviceName("测试机");device.setUserPersonId(9L);device.setUserUsageStatus("使用中");device.setAssetRelationStatus("已关联");Page<AssetDeviceEntity> page=new Page<>(1,20);page.setRecords(List.of(device));page.setTotal(1);
    CompanyPersonEntity person=new CompanyPersonEntity();person.setId(9L);person.setPersonName("张三");when(devices.selectPage(any(Page.class),any(Wrapper.class))).thenReturn(page);when(people.selectList(any(Wrapper.class))).thenReturn(List.of(person));
    assertEquals("张三",service.page(new DeviceAssetPageQuery(null,null,null,null,null,null)).records().get(0).userPersonName());
  }
  /** 代码作用（白话）：验证服务端不接受前端伪造的状态值。关联文件：DeviceAssetService.java、DeviceAssetSaveRequest.java。关联逻辑（调用链/数据流）：保存表单 -> validateStatus -> 400 业务异常。 */
  @Test void rejectsUnsupportedUsageStatus() {
    DeviceAssetService service=service(mock(AssetDeviceMapper.class),mock(CompanyPersonMapper.class));DeviceAssetSaveRequest request=new DeviceAssetSaveRequest();request.setDeviceName("测试机");request.setUserUsageStatus("未知");request.setAssetRelationStatus("已关联");
    assertThrows(DeviceAssetValidationException.class,()->service.create(request));
  }
  /** 代码作用（白话）：验证重名的有效设备不能再次创建，防止列表出现无法区分的资产。关联文件：DeviceAssetService.java、AssetDeviceMapper.java。关联逻辑（调用链/数据流）：POST 表单 -> validateSaveRequest -> selectCount -> 400 业务异常。*/
  @Test void rejectsDuplicateActiveDeviceName() {
    AssetDeviceMapper devices=mock(AssetDeviceMapper.class);when(devices.selectCount(any(Wrapper.class))).thenReturn(1L);
    assertThrows(DeviceAssetValidationException.class,()->service(devices,mock(CompanyPersonMapper.class)).create(validRequest("重复设备")));
  }
  /** 代码作用（白话）：验证手机号码资产仍关联设备时，删除请求会被阻断，且不更新设备删除时间。关联文件：DeviceAssetService.java、PhoneAssetMapper.java。关联逻辑（调用链/数据流）：DELETE -> 查询设备 -> 手机号码引用计数 -> 400 业务异常。*/
  @Test void blocksDeleteWhenPhoneAssetStillReferencesDevice() {
    AssetDeviceMapper devices=mock(AssetDeviceMapper.class);PhoneAssetMapper phones=mock(PhoneAssetMapper.class);when(devices.selectOne(any(Wrapper.class))).thenReturn(activeDevice(1L));when(phones.selectCount(any(Wrapper.class))).thenReturn(1L);
    assertThrows(DeviceAssetValidationException.class,()->service(devices,mock(CompanyPersonMapper.class),phones,new DeviceAssetFileStorageService(System.getProperty("java.io.tmpdir")+"/device-test-images")).softDelete(1L));
  }
  /** 代码作用（白话）：验证软删除只隐藏数据库记录，不物理删除原图，保留审计和后续恢复的可能。关联文件：DeviceAssetService.java、DeviceAssetFileStorageService.java。关联逻辑（调用链/数据流）：DELETE -> 关联检查 -> updateById(deleteTime)；图片文件继续留在受控目录。*/
  @Test void softDeleteKeepsExistingImageFile(@TempDir Path directory) throws Exception {
    DeviceAssetFileStorageService storage=new DeviceAssetFileStorageService(directory.toString());String identifier=storage.store(new MockMultipartFile("image","retained.png","image/png",png()));AssetDeviceMapper devices=mock(AssetDeviceMapper.class);AssetDeviceEntity device=activeDevice(2L);device.setImageAttachment1(identifier);when(devices.selectOne(any(Wrapper.class))).thenReturn(device);when(devices.updateById(any(AssetDeviceEntity.class))).thenReturn(1);
    service(devices,mock(CompanyPersonMapper.class),mock(PhoneAssetMapper.class),storage).softDelete(2L);
    assertTrue(Files.exists(directory.resolve(identifier)));
  }
  /** 代码作用（白话）：验证图片先保存但数据库新增失败时，刚上传的新图片会立即清理，避免留下孤儿文件。关联文件：DeviceAssetService.java、DeviceAssetFileStorageService.java。关联逻辑（调用链/数据流）：POST 图片 -> store -> Mapper.insert 失败 -> cleanupNewFile。*/
  @Test void createFailureCleansNewImageFile(@TempDir Path directory) {
    AssetDeviceMapper devices=mock(AssetDeviceMapper.class);DeviceAssetSaveRequest request=validRequest("保存失败设备");request.setImageAttachment1(new MockMultipartFile("image","failed.png","image/png",png()));
    assertThrows(IllegalStateException.class,()->service(devices,mock(CompanyPersonMapper.class),mock(PhoneAssetMapper.class),new DeviceAssetFileStorageService(directory.toString())).create(request));
    try(var files=Files.list(directory)){assertEquals(0,files.count());}catch(Exception exception){throw new IllegalStateException(exception);}
  }
  /** 代码作用（白话）：构造只含当前测试所需 Mock 的设备服务。关联文件：DeviceAssetService.java、DeviceAssetFileStorageService.java。关联逻辑（调用链/数据流）：测试 -> Mock Mapper/文件服务 -> Service 规则。 */
  /** Plain purpose: prove a failed update removes only the new replacement file and retains the pre-existing original. Related files: DeviceAssetService.java, DeviceAssetFileStorageService.java. Flow: PUT image -> store replacement -> mapper failure -> cleanup replacement; original stays. */
  @Test void updateFailureCleansReplacementButRetainsOriginal(@TempDir Path directory) throws Exception {
    DeviceAssetFileStorageService storage=new DeviceAssetFileStorageService(directory.toString());String original=storage.store(new MockMultipartFile("image","original.png","image/png",png()));AssetDeviceMapper devices=mock(AssetDeviceMapper.class);AssetDeviceEntity device=activeDevice(3L);device.setImageAttachment1(original);when(devices.selectOne(any(Wrapper.class))).thenReturn(device);
    DeviceAssetSaveRequest request=validRequest("update-failure");request.setImageAttachment1(new MockMultipartFile("image","replacement.png","image/png",png()));
    assertThrows(com.xyw.console.asset.exception.DeviceAssetNotFoundException.class,()->service(devices,mock(CompanyPersonMapper.class),mock(PhoneAssetMapper.class),storage).update(3L,request));
    try(var files=Files.list(directory)){assertEquals(1,files.count());}assertTrue(Files.exists(directory.resolve(original)));
  }
  /** Plain purpose: reject a supplied company-person ID when that person is no longer active. Related files: DeviceAssetService.java, CompanyPersonMapper.java. Flow: POST userPersonId -> active-person count -> validation error before insert. */
  @Test void rejectsDeletedUserPersonBeforeCreate() {
    CompanyPersonMapper people=mock(CompanyPersonMapper.class);when(people.selectCount(any(Wrapper.class))).thenReturn(0L);DeviceAssetSaveRequest request=validRequest("inactive-person");request.setUserPersonId(9L);
    assertThrows(DeviceAssetValidationException.class,()->service(mock(AssetDeviceMapper.class),people).create(request));
  }
  /** Plain purpose: reject deletion when an enterprise-WeChat record references the device. Related files: DeviceAssetService.java, WecomAccountMapper.java. Flow: DELETE -> Wecom reference count -> validation error. */
  @Test void blocksDeleteWhenWecomAccountReferencesDevice() { assertReferenceBlocksDelete(ReferenceType.WECOM); }
  /** Plain purpose: reject deletion when a WeChat record references the device. Related files: DeviceAssetService.java, WechatAccountMapper.java. Flow: DELETE -> WeChat reference count -> validation error. */
  @Test void blocksDeleteWhenWechatAccountReferencesDevice() { assertReferenceBlocksDelete(ReferenceType.WECHAT); }
  /** Plain purpose: reject deletion when a Douyin record references the device. Related files: DeviceAssetService.java, DouyinAccountMapper.java. Flow: DELETE -> Douyin reference count -> validation error. */
  @Test void blocksDeleteWhenDouyinAccountReferencesDevice() { assertReferenceBlocksDelete(ReferenceType.DOUYIN); }
  /** Plain purpose: verify omitted page values become page 1 with twenty rows. Related files: DeviceAssetPageQuery.java, DeviceAssetService.java. Flow: GET without page/size -> resolved defaults -> MyBatis page. */
  @Test void usesDefaultPaginationForDevicePage() {
    AssetDeviceMapper devices=mock(AssetDeviceMapper.class);Page<AssetDeviceEntity> result=new Page<>(1,20);result.setRecords(List.of());when(devices.selectPage(any(Page.class),any(Wrapper.class))).thenReturn(result);
    service(devices,mock(CompanyPersonMapper.class)).page(new DeviceAssetPageQuery(null,null,null,null,null,null));ArgumentCaptor<Page<AssetDeviceEntity>> page=ArgumentCaptor.forClass(Page.class);verify(devices).selectPage(page.capture(),any(Wrapper.class));assertEquals(1,page.getValue().getCurrent());assertEquals(20,page.getValue().getSize());
  }
  /** Plain purpose: verify every supported list filter contributes a database predicate. Related files: DeviceAssetPageQuery.java, DeviceAssetService.java. Flow: GET filter values -> LambdaQueryWrapper -> Mapper page query. */
  @Test void appliesEverySupportedDevicePageFilter() {
    AssetDeviceMapper devices=mock(AssetDeviceMapper.class);Page<AssetDeviceEntity> result=new Page<>(2,5);result.setRecords(List.of());when(devices.selectPage(any(Page.class),any(Wrapper.class))).thenReturn(result);
    service(devices,mock(CompanyPersonMapper.class)).page(new DeviceAssetPageQuery(2,5,"filter-name",9L,"\u4f7f\u7528\u4e2d","\u672a\u5173\u8054"));ArgumentCaptor<Page<AssetDeviceEntity>> page=ArgumentCaptor.forClass(Page.class);ArgumentCaptor<Wrapper<AssetDeviceEntity>> filter=ArgumentCaptor.forClass(Wrapper.class);verify(devices).selectPage(page.capture(),filter.capture());
    assertEquals(2,page.getValue().getCurrent());assertEquals(5,page.getValue().getSize());assertTrue(filter.getValue()!=null);
  }
  /** Plain purpose: verify a successful replacement changes the stored identifier but retains the historical original file. Related files: DeviceAssetService.java, DeviceAssetFileStorageService.java. Flow: PUT replacement -> new store -> mapper update -> new URL; old file remains. */
  @Test void updateReplacesImageWithoutDeletingOriginal(@TempDir Path directory) throws Exception {
    DeviceAssetFileStorageService storage=new DeviceAssetFileStorageService(directory.toString());String original=storage.store(new MockMultipartFile("image","original.png","image/png",png()));AssetDeviceMapper devices=mock(AssetDeviceMapper.class);AssetDeviceEntity device=activeDevice(5L);device.setImageAttachment1(original);when(devices.selectOne(any(Wrapper.class))).thenReturn(device);when(devices.updateById(any(AssetDeviceEntity.class))).thenReturn(1);
    DeviceAssetSaveRequest request=validRequest("replace-image");request.setImageAttachment1(new MockMultipartFile("image","replacement.png","image/png",png()));String responseUrl=service(devices,mock(CompanyPersonMapper.class),mock(PhoneAssetMapper.class),storage).update(5L,request).imageAttachment1Url();
    assertTrue(Files.exists(directory.resolve(original)));assertTrue(!responseUrl.endsWith(original));try(var files=Files.list(directory)){assertEquals(2,files.count());}
  }
  /** Plain purpose: verify explicit removal clears the database-facing URL but retains the original physical file for recovery. Related files: DeviceAssetService.java, DeviceAssetFileStorageService.java. Flow: PUT remove flag -> null reference -> mapper update; source file stays. */
  @Test void updateRemovalClearsImageReferenceButRetainsFile(@TempDir Path directory) throws Exception {
    DeviceAssetFileStorageService storage=new DeviceAssetFileStorageService(directory.toString());String original=storage.store(new MockMultipartFile("image","original.png","image/png",png()));AssetDeviceMapper devices=mock(AssetDeviceMapper.class);AssetDeviceEntity device=activeDevice(6L);device.setImageAttachment1(original);when(devices.selectOne(any(Wrapper.class))).thenReturn(device);when(devices.updateById(any(AssetDeviceEntity.class))).thenReturn(1);
    DeviceAssetSaveRequest request=validRequest("remove-image");request.setRemoveImageAttachment1(true);assertEquals(null,service(devices,mock(CompanyPersonMapper.class),mock(PhoneAssetMapper.class),storage).update(6L,request).imageAttachment1Url());assertTrue(Files.exists(directory.resolve(original)));
  }
  /** Plain purpose: exercise the three account-reference variants through their actual mapper count boundary. Related files: DeviceAssetService.java, account mappers. Flow: selected mapper count=1 -> softDelete -> validation error. */
  private void assertReferenceBlocksDelete(ReferenceType type) {
    AssetDeviceMapper devices=mock(AssetDeviceMapper.class);PhoneAssetMapper phones=mock(PhoneAssetMapper.class);WecomAccountMapper wecom=mock(WecomAccountMapper.class);WechatAccountMapper wechat=mock(WechatAccountMapper.class);DouyinAccountMapper douyin=mock(DouyinAccountMapper.class);when(devices.selectOne(any(Wrapper.class))).thenReturn(activeDevice(4L));
    if(type==ReferenceType.WECOM)when(wecom.selectCount(any(Wrapper.class))).thenReturn(1L);if(type==ReferenceType.WECHAT)when(wechat.selectCount(any(Wrapper.class))).thenReturn(1L);if(type==ReferenceType.DOUYIN)when(douyin.selectCount(any(Wrapper.class))).thenReturn(1L);
    assertThrows(DeviceAssetValidationException.class,()->new DeviceAssetService(devices,mock(CompanyPersonMapper.class),phones,wecom,wechat,douyin,new DeviceAssetFileStorageService(System.getProperty("java.io.tmpdir")+"/device-test-images")).softDelete(4L));
  }
  /** Plain purpose: name the account-mapper variant used by the shared reference-protection test. Related files: DeviceAssetService.java, account mappers. Flow: test case -> one mapper count -> delete protection. */
  private enum ReferenceType { WECOM, WECHAT, DOUYIN }
  private DeviceAssetService service(AssetDeviceMapper devices,CompanyPersonMapper people){return new DeviceAssetService(devices,people,mock(PhoneAssetMapper.class),mock(WecomAccountMapper.class),mock(WechatAccountMapper.class),mock(DouyinAccountMapper.class),new DeviceAssetFileStorageService(System.getProperty("java.io.tmpdir")+"/device-test-images"));}
  /** 代码作用（白话）：构造可替换手机引用和图片目录的设备服务，供删除与失败清理场景使用。关联文件：DeviceAssetService.java、PhoneAssetMapper.java。关联逻辑（调用链/数据流）：测试用例 -> 特定 Mock Mapper/文件服务 -> Service 业务规则。*/
  private DeviceAssetService service(AssetDeviceMapper devices,CompanyPersonMapper people,PhoneAssetMapper phones,DeviceAssetFileStorageService storage){return new DeviceAssetService(devices,people,phones,mock(WecomAccountMapper.class),mock(WechatAccountMapper.class),mock(DouyinAccountMapper.class),storage);}
  /** 代码作用（白话）：生成符合下拉选项约束的最小新增表单。关联文件：DeviceAssetSaveRequest.java、DeviceAssetService.java。关联逻辑（调用链/数据流）：测试输入 -> 服务端状态校验 -> 创建或校验异常。*/
  private DeviceAssetSaveRequest validRequest(String name){DeviceAssetSaveRequest request=new DeviceAssetSaveRequest();request.setDeviceName(name);request.setUserUsageStatus("使用中");request.setAssetRelationStatus("未关联");return request;}
  /** 代码作用（白话）：生成一条仍有效的设备记录，供删除前读取流程使用。关联文件：AssetDeviceEntity.java、DeviceAssetService.java。关联逻辑（调用链/数据流）：Mapper.selectOne -> requireActiveDevice -> 删除关联检查。*/
  private AssetDeviceEntity activeDevice(Long id){AssetDeviceEntity device=new AssetDeviceEntity();device.setId(id);device.setDeviceName("删除测试设备");device.setUserUsageStatus("使用中");device.setAssetRelationStatus("未关联");device.setDeleteTime(0L);return device;}
  /** 代码作用（白话）：生成 ImageIO 可解码的 PNG 字节，确保失败清理测试验证的是业务流程。关联文件：DeviceAssetFileStorageService.java。关联逻辑（调用链/数据流）：BufferedImage -> MultipartFile -> 文件保存 -> 异常清理。*/
  private byte[] png(){try{ByteArrayOutputStream output=new ByteArrayOutputStream();ImageIO.write(new BufferedImage(1,1,BufferedImage.TYPE_INT_ARGB),"png",output);return output.toByteArray();}catch(Exception exception){throw new IllegalStateException(exception);}}
}
