package com.xyw.console.asset.controller;

import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import static org.mockito.Mockito.verifyNoInteractions;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.multipart;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

import com.xyw.console.asset.dto.DeviceAssetPageResponse;
import com.xyw.console.asset.dto.DeviceAssetResponse;
import com.xyw.console.asset.exception.DeviceAssetValidationException;
import com.xyw.console.asset.service.DeviceAssetService;
import com.xyw.console.asset.dto.DeviceAssetPageQuery;
import com.xyw.console.asset.dto.DeviceAssetSaveRequest;
import com.xyw.console.auth.AuthPrincipal;
import com.xyw.console.auth.PagePermissionService;
import java.util.List;
import org.junit.jupiter.api.Test;
import org.springframework.core.io.ByteArrayResource;
import org.springframework.http.HttpMethod;
import org.springframework.mock.web.MockMultipartFile;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;

/** 文件用途注释：验证设备资产 HTTP 接口可正确绑定查询、multipart 表单和删除异常，不依赖真实数据库或文件目录。*/
class DeviceAssetControllerTest {
  /** 代码作用（白话）：验证列表接口将服务层的设备与人员名称数据按统一 JSON 返回。关联文件：DeviceAssetController.java、DeviceAssetService.java。关联逻辑（调用链/数据流）：GET -> Controller.page -> Service.page -> ApiResponse -> 前端表格。*/
  @Test void returnsPagedDeviceAssets() throws Exception {
    DeviceAssetService service=mock(DeviceAssetService.class);when(service.page(any())).thenReturn(new DeviceAssetPageResponse(List.of(response()),1,1,20));
    mockMvc(service).perform(get("/api/device-assets")).andExpect(status().isOk()).andExpect(jsonPath("$.code").value(200)).andExpect(jsonPath("$.data.records[0].deviceName").value("测试电脑")).andExpect(jsonPath("$.data.records[0].userPersonName").value("张三"));
  }
  /** 代码作用（白话）：验证新增接口接收 multipart 字段和图片并把保存请求交给服务层。关联文件：DeviceAssetController.java、DeviceAssetSaveRequest.java。关联逻辑（调用链/数据流）：表单/图片 -> @ModelAttribute -> Service.create -> 成功 JSON。*/
  @Test void createsDeviceAssetThroughMultipartEndpoint() throws Exception {
    DeviceAssetService service=mock(DeviceAssetService.class);when(service.create(any())).thenReturn(response());MockMultipartFile image=new MockMultipartFile("imageAttachment1","device.png","image/png",new byte[]{1,2,3});
    mockMvc(service).perform(multipart("/api/device-assets").file(image).param("deviceName","测试电脑").param("userUsageStatus","使用中").param("assetRelationStatus","未关联"))
        .andExpect(status().isOk()).andExpect(jsonPath("$.code").value(200)).andExpect(jsonPath("$.data.id").value(1));verify(service).create(any());
  }
  /** 代码作用（白话）：验证删除被关联资产阻断时，接口返回 400 和可读错误信息。关联文件：DeviceAssetController.java、DeviceAssetExceptionHandler.java。关联逻辑（调用链/数据流）：DELETE -> Service.softDelete 异常 -> Advice -> 400 JSON。*/
  @Test void returnsBadRequestWhenDeleteIsBlocked() throws Exception {
    DeviceAssetService service=mock(DeviceAssetService.class);doThrow(new DeviceAssetValidationException("设备仍被手机号码资产引用，不能删除")).when(service).softDelete(1L);
    mockMvc(service).perform(delete("/api/device-assets/1")).andExpect(status().isBadRequest()).andExpect(jsonPath("$.code").value(400));
  }
  /** Plain purpose: verify the edit endpoint binds multipart fields and delegates to the update service. Related files: DeviceAssetController.java, DeviceAssetSaveRequest.java. Flow: multipart PUT -> model binding -> service.update -> success JSON. */
  @Test void updatesDeviceAssetThroughMultipartEndpoint() throws Exception {
    DeviceAssetService service=mock(DeviceAssetService.class);when(service.update(any(),any())).thenReturn(response());MockMultipartFile image=new MockMultipartFile("imageAttachment2","edited.png","image/png",new byte[]{1,2,3});
    mockMvc(service).perform(multipart(HttpMethod.PUT,"/api/device-assets/{id}",1L).file(image).param("deviceName","\u6d4b\u8bd5\u7535\u8111").param("userUsageStatus","\u4f7f\u7528\u4e2d").param("assetRelationStatus","\u672a\u5173\u8054"))
        .andExpect(status().isOk()).andExpect(jsonPath("$.data.imageAttachment1Url").value("/api/device-assets/files/image.png"));verify(service).update(org.mockito.ArgumentMatchers.eq(1L),any());
  }
  /** Plain purpose: verify image bytes are served only through the controlled opaque identifier endpoint. Related files: DeviceAssetController.java, DeviceAssetFileStorageService.java. Flow: GET file identifier -> service resource -> response bytes. */
  @Test void readsImageThroughControlledEndpoint() throws Exception {
    DeviceAssetService service=mock(DeviceAssetService.class);when(service.findImage("opaque.png")).thenReturn(new ByteArrayResource(new byte[]{7,8}) { @Override public String getFilename(){return "opaque.png";} });
    mockMvc(service).perform(get("/api/device-assets/files/opaque.png")).andExpect(status().isOk()).andExpect(org.springframework.test.web.servlet.result.MockMvcResultMatchers.content().bytes(new byte[]{7,8}));
  }
  /** Plain purpose: assert the public list response has URLs but never deleteTime or a disk path field. Related files: DeviceAssetResponse.java, DeviceAssetController.java. Flow: service response -> ApiResponse JSON -> browser. */
  @Test void hidesDeleteTimeAndPhysicalImagePath() throws Exception {
    DeviceAssetService service=mock(DeviceAssetService.class);when(service.page(any())).thenReturn(new DeviceAssetPageResponse(List.of(response()),1,1,20));
    mockMvc(service).perform(get("/api/device-assets")).andExpect(status().isOk()).andExpect(jsonPath("$.data.records[0].deleteTime").doesNotExist()).andExpect(jsonPath("$.data.records[0].imageAttachment1Url").value("/api/device-assets/files/image.png")).andExpect(jsonPath("$.data.records[0].imageAttachment1").doesNotExist());
  }
  /** Plain purpose: prove a logged-in ordinary role cannot bypass the sidebar and call any device endpoint directly. Related files: DeviceAssetController.java, PagePermissionService.java. Flow: finance principal -> controller administrator check -> access denial before device service or image read. */
  @Test void blocksNonAdministratorsFromEveryDeviceEndpoint() {
    DeviceAssetService service = mock(DeviceAssetService.class); PagePermissionService permissions = new PagePermissionService(new com.fasterxml.jackson.databind.ObjectMapper()); DeviceAssetController controller = new DeviceAssetController(service, permissions);
    SecurityContextHolder.getContext().setAuthentication(new UsernamePasswordAuthenticationToken(new AuthPrincipal(3L, "finance", "FINANCE", 1), null));
    try {
      assertThrows(AccessDeniedException.class, () -> controller.page(new DeviceAssetPageQuery(1, 20, null, null, null, null)));
      assertThrows(AccessDeniedException.class, () -> controller.create(new DeviceAssetSaveRequest()));
      assertThrows(AccessDeniedException.class, () -> controller.update(1L, new DeviceAssetSaveRequest()));
      assertThrows(AccessDeniedException.class, () -> controller.delete(1L));
      assertThrows(AccessDeniedException.class, () -> controller.companyPersons("name"));
      assertThrows(AccessDeniedException.class, () -> controller.file("opaque.png"));
      verifyNoInteractions(service);
    } finally { SecurityContextHolder.clearContext(); }
  }
  /** 代码作用（白话）：集中创建带设备异常转换器的 MockMvc，保证控制器返回契约可被测试。关联文件：DeviceAssetController.java、DeviceAssetExceptionHandler.java。关联逻辑（调用链/数据流）：HTTP 模拟请求 -> Controller -> Advice -> JSON 响应。*/
  /** Plain purpose: create an administrator-authorized controller test harness without a live Spring Security filter chain. Related files: DeviceAssetController.java, PagePermissionService.java. Flow: mock permission check -> controller endpoint -> mocked device service -> HTTP assertion. */
  private MockMvc mockMvc(DeviceAssetService service){return MockMvcBuilders.standaloneSetup(new DeviceAssetController(service,mock(PagePermissionService.class))).setControllerAdvice(new DeviceAssetExceptionHandler()).build();}
  /** 代码作用（白话）：生成一条不含服务器文件真实路径的安全设备响应。关联文件：DeviceAssetResponse.java、DeviceAssetController.java。关联逻辑（调用链/数据流）：服务层响应 -> ApiResponse -> 页面展示。*/
  private DeviceAssetResponse response(){return new DeviceAssetResponse(1L,"测试电脑","/api/device-assets/files/image.png",null,9L,"张三","使用中","未关联",null,null);}
}
