package com.ruoyi.ccdi.project.service.impl;

import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.ruoyi.ccdi.project.domain.CcdiModelParam;
import com.ruoyi.ccdi.project.domain.CcdiProject;
import com.ruoyi.ccdi.project.domain.dto.CcdiProjectPersonAnalysisDetailQueryDTO;
import com.ruoyi.ccdi.project.domain.dto.CcdiProjectRiskPeopleQueryDTO;
import com.ruoyi.ccdi.project.domain.dto.CcdiProjectRiskModelPeopleQueryDTO;
import com.ruoyi.ccdi.project.domain.excel.CcdiProjectAbnormalAccountExcel;
import com.ruoyi.ccdi.project.domain.excel.CcdiProjectEmployeeCreditNegativeExcel;
import com.ruoyi.ccdi.project.domain.excel.CcdiProjectRiskPeopleOverviewExcel;
import com.ruoyi.ccdi.project.domain.excel.CcdiProjectSuspiciousTransactionExcel;
import com.ruoyi.ccdi.project.domain.excel.CcdiProjectUnsubmittedAccountFlowExcel;
import com.ruoyi.ccdi.project.domain.entity.CcdiProjectOverviewEmployeeResult;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectAbnormalAccountItemVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectEmployeeCreditNegativeItemVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiBankStatementListVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectEmployeeRiskAggregateVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectOverviewDashboardVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectOverviewEmployeeHitRowVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectOverviewReportSuspiciousTransactionVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectOverviewReportVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectPersonAnalysisDetailVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectPersonAnalysisBasicInfoVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectPersonAnalysisObjectRecordVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectRiskModelCardVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectRiskModelCardsVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectRiskHitTagVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectRiskModelPeopleItemVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectRiskModelPeopleVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectRiskPeopleOverviewVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectSuspiciousTransactionItemVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectTopRiskPeopleVO;
import com.ruoyi.ccdi.project.mapper.CcdiBankStatementMapper;
import com.ruoyi.ccdi.project.mapper.CcdiModelParamMapper;
import com.ruoyi.ccdi.project.mapper.CcdiProjectMapper;
import com.ruoyi.ccdi.project.mapper.CcdiBankTagResultMapper;
import com.ruoyi.ccdi.project.mapper.CcdiProjectOverviewEmployeeResultMapper;
import com.ruoyi.ccdi.project.mapper.CcdiProjectOverviewMapper;
import com.ruoyi.common.exception.ServiceException;
import java.math.BigDecimal;
import java.util.HashMap;
import java.util.List;
import org.springframework.mock.web.MockHttpServletResponse;
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 static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.argThat;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.clearInvocations;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

@ExtendWith(MockitoExtension.class)
class CcdiProjectOverviewServiceImplTest {

    @InjectMocks
    private CcdiProjectOverviewServiceImpl service;

    @Mock
    private CcdiProjectOverviewMapper overviewMapper;

    @Mock
    private CcdiProjectMapper projectMapper;

    @Mock
    private CcdiBankStatementMapper bankStatementMapper;

    @Mock
    private CcdiModelParamMapper modelParamMapper;

    @Mock
    private CcdiProjectOverviewEmployeeResultMapper overviewEmployeeResultMapper;

    @Mock
    private CcdiBankTagResultMapper bankTagResultMapper;

    @Mock
    private CcdiProjectOverviewEmployeeResultBuilder overviewEmployeeResultBuilder;

    @Mock
    private CcdiProjectRiskDetailWorkbookExporter workbookExporter;

    @Mock
    private CcdiProjectOverviewReportPdfExporter reportPdfExporter;

    @Test
    void shouldBuildDashboardWithNoRiskCount() {
        CcdiProject project = new CcdiProject();
        project.setProjectId(40L);
        project.setTargetCount(100);
        project.setHighRiskCount(5);
        project.setMediumRiskCount(10);
        project.setLowRiskCount(15);

        when(overviewMapper.selectDashboardBaseByProjectId(40L)).thenReturn(project);

        CcdiProjectOverviewDashboardVO dashboard = service.getDashboard(40L);

        assertEquals("风险仪表盘", dashboard.getTitle());
        assertEquals(70, dashboard.getStats().get(4).getValue());
    }

    @Test
    void shouldMapRiskPeopleOverviewRows() {
        CcdiProject project = new CcdiProject();
        project.setProjectId(40L);
        when(projectMapper.selectById(40L)).thenReturn(project);
        CcdiProjectRiskPeopleQueryDTO queryDTO = buildRiskPeopleQuery(40L);

        CcdiProjectEmployeeRiskAggregateVO aggregate = new CcdiProjectEmployeeRiskAggregateVO();
        aggregate.setStaffName("李四");
        aggregate.setStaffIdCard("330000000000000001");
        aggregate.setDeptName("信息二部");
        aggregate.setRuleCount(5);
        aggregate.setHitCount(8);
        aggregate.setRiskLevelCode("HIGH");
        aggregate.setModelCount(3);
        aggregate.setRiskPoint("大额单笔收入、疑似兼职");
        Page<CcdiProjectEmployeeRiskAggregateVO> resultPage = new Page<>(1, 5);
        resultPage.setRecords(List.of(aggregate));
        resultPage.setTotal(1L);
        when(overviewMapper.selectRiskPeopleOverviewPage(any(Page.class), any(CcdiProjectRiskPeopleQueryDTO.class)))
            .thenReturn(resultPage);
        when(overviewMapper.selectRiskHitTagsByScope(40L, "330000000000000001", null)).thenReturn(List.of(
            buildHitTag("LARGE_TRANSACTION", "大额交易模型", "RULE_A", "大额单笔收入", "HIGH"),
            buildHitTag("PART_TIME", "兼职取酬模型", "RULE_B", "疑似兼职", "MEDIUM")
        ));

        CcdiProjectRiskPeopleOverviewVO overview = service.getRiskPeopleOverview(queryDTO);

        assertEquals(1, overview.getRows().size());
        assertEquals(1L, overview.getTotal());
        assertEquals(1L, overview.getPageNum());
        assertEquals(5L, overview.getPageSize());
        assertEquals(8, overview.getRows().getFirst().getRiskCount());
        assertEquals("高风险", overview.getRows().getFirst().getRiskLevel());
        assertEquals("danger", overview.getRows().getFirst().getRiskLevelType());
        assertEquals(3, overview.getRows().getFirst().getModelCount());
        assertEquals(2, overview.getRows().getFirst().getRiskPointTagList().size());
        assertEquals("LARGE_TRANSACTION", overview.getRows().getFirst().getRiskPointTagList().getFirst().getModelCode());
        assertEquals("大额交易模型", overview.getRows().getFirst().getRiskPointTagList().getFirst().getModelName());
        assertEquals("大额单笔收入、疑似兼职", overview.getRows().getFirst().getRiskPoint());
        assertEquals("查看详情", overview.getRows().getFirst().getActionLabel());
        verify(overviewMapper).selectRiskPeopleOverviewPage(
            argThat(page -> page.getCurrent() == 1L && page.getSize() == 5L),
            argThat(query -> query.getProjectId().equals(40L))
        );
    }

    @Test
    void shouldDefaultRiskPeoplePageNumAndPageSizeToOneAndFive() {
        CcdiProject project = new CcdiProject();
        project.setProjectId(40L);
        when(projectMapper.selectById(40L)).thenReturn(project);

        Page<CcdiProjectEmployeeRiskAggregateVO> emptyPage = new Page<>(1, 5);
        emptyPage.setRecords(List.of());
        emptyPage.setTotal(0L);
        when(overviewMapper.selectRiskPeopleOverviewPage(any(Page.class), any(CcdiProjectRiskPeopleQueryDTO.class)))
            .thenReturn(emptyPage);

        CcdiProjectRiskPeopleQueryDTO queryDTO = new CcdiProjectRiskPeopleQueryDTO();
        queryDTO.setProjectId(40L);
        service.getRiskPeopleOverview(queryDTO);

        verify(overviewMapper).selectRiskPeopleOverviewPage(
            argThat(page -> page.getCurrent() == 1L && page.getSize() == 5L),
            any(CcdiProjectRiskPeopleQueryDTO.class)
        );

        clearInvocations(overviewMapper);

        CcdiProjectRiskPeopleQueryDTO invalidQuery = new CcdiProjectRiskPeopleQueryDTO();
        invalidQuery.setProjectId(40L);
        invalidQuery.setPageNum(0);
        invalidQuery.setPageSize(-1);
        service.getRiskPeopleOverview(invalidQuery);

        verify(overviewMapper, times(1)).selectRiskPeopleOverviewPage(
            argThat(page -> page.getCurrent() == 1L && page.getSize() == 5L),
            any(CcdiProjectRiskPeopleQueryDTO.class)
        );
    }

    @Test
    void shouldMapTopRiskPeopleRows() {
        CcdiProject project = new CcdiProject();
        project.setProjectId(40L);
        when(projectMapper.selectById(40L)).thenReturn(project);

        CcdiProjectEmployeeRiskAggregateVO aggregate = new CcdiProjectEmployeeRiskAggregateVO();
        aggregate.setStaffName("张三");
        aggregate.setStaffIdCard("330000000000000002");
        aggregate.setDeptName("信贷部");
        aggregate.setRiskLevelCode("HIGH");
        aggregate.setModelCount(8);
        when(overviewMapper.selectTopRiskPeopleByProjectId(40L)).thenReturn(List.of(aggregate));

        CcdiProjectTopRiskPeopleVO topRiskPeople = service.getTopRiskPeople(40L);

        assertEquals(1, topRiskPeople.getTopRiskList().size());
        assertEquals("高风险", topRiskPeople.getTopRiskList().getFirst().getRiskLevel());
        assertEquals("danger", topRiskPeople.getTopRiskList().getFirst().getRiskLevelType());
        assertEquals("查看详情", topRiskPeople.getTopRiskList().getFirst().getActionLabel());
    }

    @Test
    void shouldExportRiskPeopleOverviewRowsWithSameFieldsAsPage() {
        CcdiProject project = new CcdiProject();
        project.setProjectId(40L);
        when(projectMapper.selectById(40L)).thenReturn(project);

        CcdiProjectEmployeeRiskAggregateVO aggregate = new CcdiProjectEmployeeRiskAggregateVO();
        aggregate.setStaffName("李四");
        aggregate.setStaffIdCard("330000000000000001");
        aggregate.setDeptName("信息二部");
        aggregate.setRuleCount(5);
        aggregate.setHitCount(8);
        aggregate.setRiskLevelCode("HIGH");
        aggregate.setModelCount(3);
        aggregate.setRiskPoint("大额单笔收入、疑似兼职");
        when(overviewMapper.selectRiskPeopleOverviewList(40L)).thenReturn(List.of(aggregate));
        when(overviewMapper.selectRiskHitTagsByScope(40L, "330000000000000001", null)).thenReturn(List.of(
            buildHitTag("LARGE_TRANSACTION", "大额交易模型", "RULE_A", "大额单笔收入", "HIGH"),
            buildHitTag("PART_TIME", "兼职取酬模型", "RULE_B", "疑似兼职", "MEDIUM")
        ));

        List<CcdiProjectRiskPeopleOverviewExcel> rows = service.exportRiskPeopleOverview(40L);

        assertEquals(1, rows.size());
        assertEquals("李四", rows.getFirst().getName());
        assertEquals("330000000000000001", rows.getFirst().getIdNo());
        assertEquals("信息二部", rows.getFirst().getDepartment());
        assertEquals(8, rows.getFirst().getRiskCount());
        assertEquals("高风险", rows.getFirst().getRiskLevel());
        assertEquals(3, rows.getFirst().getModelCount());
        assertEquals("大额单笔收入、疑似兼职", rows.getFirst().getRiskPoint());
        verify(overviewMapper).selectRiskPeopleOverviewList(40L);
    }

    @Test
    void shouldThrowWhenProjectDoesNotExist() {
        when(projectMapper.selectById(99L)).thenReturn(null);

        assertThrows(ServiceException.class, () -> service.getRiskPeopleOverview(buildRiskPeopleQuery(99L)));
        assertThrows(ServiceException.class, () -> service.exportRiskPeopleOverview(99L));
        assertThrows(ServiceException.class, () -> service.getTopRiskPeople(99L));
        assertThrows(ServiceException.class, () -> service.getRiskModelCards(99L));
        assertThrows(ServiceException.class, () -> service.getRiskModelPeople(buildRiskModelPeopleQuery(99L)));
        assertThrows(ServiceException.class, () -> service.getPersonAnalysisDetail(buildPersonAnalysisDetailQuery(99L)));
    }

    @Test
    void shouldExportRiskDetailsWorkbook() throws Exception {
        CcdiProject project = new CcdiProject();
        project.setProjectId(40L);
        when(projectMapper.selectById(40L)).thenReturn(project);

        CcdiProjectOverviewReportSuspiciousTransactionVO suspiciousItem =
            new CcdiProjectOverviewReportSuspiciousTransactionVO();
        suspiciousItem.setTrxDate("2026-03-20 10:00:00");
        suspiciousItem.setLeAccountNo("6222000000000000");
        suspiciousItem.setLeAccountName("张三");
        suspiciousItem.setCustomerAccountName("测试对手方");
        suspiciousItem.setCustomerAccountNo("6222000000000001");
        suspiciousItem.setRelatedStaffName("张三");
        suspiciousItem.setRelatedStaffCode("1001");
        suspiciousItem.setUserMemo("转账");
        suspiciousItem.setCashType("转账");
        suspiciousItem.setHitTags("异常标签");
        suspiciousItem.setDisplayAmount(new BigDecimal("100.00"));
        when(overviewMapper.selectReportSuspiciousTransactionList(any())).thenReturn(List.of(suspiciousItem));

        CcdiProjectEmployeeCreditNegativeItemVO creditItem = new CcdiProjectEmployeeCreditNegativeItemVO();
        creditItem.setPersonName("李四");
        creditItem.setPersonId("330000000000000001");
        creditItem.setQueryDate("2026-03-20");
        creditItem.setCivilCnt(1);
        creditItem.setCivilLmt(new BigDecimal("20000.00"));
        when(overviewMapper.selectEmployeeCreditNegativeList(40L)).thenReturn(List.of(creditItem));

        CcdiProjectAbnormalAccountItemVO abnormalItem = new CcdiProjectAbnormalAccountItemVO();
        abnormalItem.setAccountNo("6222000000000001");
        abnormalItem.setAccountName("李四");
        abnormalItem.setBankName("中国农业银行");
        abnormalItem.setAbnormalType("突然销户");
        abnormalItem.setAbnormalTime("2026-03-20");
        abnormalItem.setStatus("已销户");
        abnormalItem.setReasonDetail("账户6222000000000001销户前存在异常交易");
        abnormalItem.setInvolvedAmount(new BigDecimal("120000.00"));
        when(overviewMapper.selectAbnormalAccountList(40L)).thenReturn(List.of(abnormalItem));

        MockHttpServletResponse response = new MockHttpServletResponse();
        service.exportRiskDetails(response, 40L);

        verify(overviewMapper).selectReportSuspiciousTransactionList(argThat(query ->
            query.getProjectId().equals(40L) && "ALL".equals(query.getSuspiciousType())
        ));
        verify(workbookExporter).export(
            eq(response),
            eq(40L),
            argThat((List<CcdiProjectSuspiciousTransactionExcel> rows) ->
                rows.size() == 1
                    && "张三".equals(rows.getFirst().getLeAccountName())
                    && "测试对手方".equals(rows.getFirst().getCustomerAccountName())
                    && "异常标签".equals(rows.getFirst().getHitTags())
            ),
            argThat((List<CcdiProjectEmployeeCreditNegativeExcel> rows) ->
                rows.size() == 1 && "李四".equals(rows.getFirst().getPersonName())
            ),
            argThat((List<CcdiProjectAbnormalAccountExcel> rows) ->
                rows.size() == 1
                    && "6222000000000001".equals(rows.getFirst().getAccountNo())
                    && "账户6222000000000001销户前存在异常交易".equals(rows.getFirst().getReasonDetail())
                    && new BigDecimal("120000.00").equals(rows.getFirst().getInvolvedAmount())
            ),
            argThat((List<CcdiProjectUnsubmittedAccountFlowExcel> rows) ->
                rows != null && rows.isEmpty()
            )
        );
    }

    @Test
    void shouldExportOverviewReportParamsFromDefaultProjectConfig() throws Exception {
        CcdiProject project = new CcdiProject();
        project.setProjectId(40L);
        project.setProjectName("测试项目");
        project.setConfigType("default");
        when(projectMapper.selectById(40L)).thenReturn(project);

        CcdiProject dashboardProject = new CcdiProject();
        dashboardProject.setProjectId(40L);
        dashboardProject.setTargetCount(10);
        dashboardProject.setHighRiskCount(1);
        dashboardProject.setMediumRiskCount(2);
        dashboardProject.setLowRiskCount(3);
        when(overviewMapper.selectDashboardBaseByProjectId(40L)).thenReturn(dashboardProject);
        when(modelParamMapper.selectByProjectId(0L)).thenReturn(List.of(
            buildModelParam("LARGE_TRANSACTION", "大额交易模型", "单笔金额", "1000", "元", "单笔金额阈值")
        ));

        MockHttpServletResponse response = new MockHttpServletResponse();
        service.exportOverviewReport(response, 40L);

        ArgumentCaptor<CcdiProjectOverviewReportVO> captor =
            ArgumentCaptor.forClass(CcdiProjectOverviewReportVO.class);
        verify(modelParamMapper).selectByProjectId(0L);
        verify(reportPdfExporter).export(eq(response), captor.capture());
        assertEquals(1, captor.getValue().getParams().size());
        assertEquals("大额交易模型", captor.getValue().getParams().getFirst().getModelName());
        assertEquals("单笔金额", captor.getValue().getParams().getFirst().getParamName());
    }

    @Test
    void shouldReturnPersonAnalysisDetailWithBasicInfoAndGroupedAbnormalDetail() {
        CcdiProject project = new CcdiProject();
        project.setProjectId(40L);
        when(projectMapper.selectById(40L)).thenReturn(project);

        CcdiProjectPersonAnalysisBasicInfoVO basicInfo = new CcdiProjectPersonAnalysisBasicInfoVO();
        basicInfo.setName("李四");
        basicInfo.setIdNo("330000000000000001");
        basicInfo.setStaffCode("10001");
        basicInfo.setDepartment("信息二部");
        basicInfo.setPhone("13800000000");
        basicInfo.setRiskLevel("高风险");
        basicInfo.setProjectName("测试项目");
        when(overviewMapper.selectPersonAnalysisBasicInfo(40L, "330000000000000001")).thenReturn(basicInfo);

        CcdiBankStatementListVO statementRow = new CcdiBankStatementListVO();
        statementRow.setBankStatementId(1L);
        statementRow.setTrxDate("2026-03-01 10:00:00");
        statementRow.setLeAccountNo("62220001");
        statementRow.setLeAccountName("李四");
        statementRow.setCustomerAccountName("张三");
        statementRow.setCustomerAccountNo("62220002");
        statementRow.setUserMemo("转账");
        statementRow.setCashType("转账汇款");
        statementRow.setDisplayAmount(new BigDecimal("1000.00"));
        when(overviewMapper.selectPersonAnalysisStatementRows(40L, "330000000000000001"))
            .thenReturn(List.of(statementRow));

        com.ruoyi.ccdi.project.domain.vo.CcdiBankStatementHitTagVO hitTag =
            new com.ruoyi.ccdi.project.domain.vo.CcdiBankStatementHitTagVO();
        hitTag.setBankStatementId(1L);
        hitTag.setRuleCode("RULE_A");
        hitTag.setRuleName("大额转账");
        hitTag.setRiskLevel("HIGH");
        when(bankTagResultMapper.selectStatementTagsByProjectAndStatementIds(
            40L, List.of(1L), null, null
        ))
            .thenReturn(List.of(hitTag));

        CcdiProjectPersonAnalysisObjectRecordVO objectRow = new CcdiProjectPersonAnalysisObjectRecordVO();
        objectRow.setTitle("张三");
        objectRow.setSubtitle("关联人员");
        objectRow.setRiskTags(List.of("频繁往来"));
        objectRow.setReasonDetail("命中近30日高频往来规则，存在多笔短周期回流");
        objectRow.setSummary("高频往来");
        CcdiProjectPersonAnalysisObjectRecordVO objectRowTwo = new CcdiProjectPersonAnalysisObjectRecordVO();
        objectRowTwo.setTitle("张三");
        objectRowTwo.setSubtitle("关联人员");
        objectRowTwo.setRiskTags(List.of("异常关联"));
        objectRowTwo.setReasonDetail("命中跨主体异常关联规则，存在关键时间点往来");
        objectRowTwo.setSummary("跨主体关联");
        when(overviewMapper.selectPersonAnalysisObjectRows(40L, "330000000000000001"))
            .thenReturn(List.of(objectRow, objectRowTwo));

        CcdiProjectPersonAnalysisDetailVO result = service.getPersonAnalysisDetail(buildPersonAnalysisDetailQuery(40L));

        assertNotNull(result.getBasicInfo());
        assertEquals("李四", result.getBasicInfo().getName());
        assertEquals("高风险", result.getBasicInfo().getRiskLevel());
        assertNotNull(result.getAbnormalDetail());
        assertNotNull(result.getAbnormalDetail().getGroups());
        assertEquals(2, result.getAbnormalDetail().getGroups().size());
        assertEquals("BANK_STATEMENT", result.getAbnormalDetail().getGroups().get(0).getGroupType());
        assertEquals("OBJECT", result.getAbnormalDetail().getGroups().get(1).getGroupType());
        List<?> statementRecords = result.getAbnormalDetail().getGroups().get(0).getRecords();
        assertEquals(1, ((CcdiBankStatementListVO) statementRecords.getFirst()).getHitTags().size());
        List<?> objectRecords = result.getAbnormalDetail().getGroups().get(1).getRecords();
        assertEquals(2, objectRecords.size());
        assertEquals(
            "命中近30日高频往来规则，存在多笔短周期回流",
            ((CcdiProjectPersonAnalysisObjectRecordVO) objectRecords.getFirst()).getReasonDetail()
        );
        assertNotNull(((CcdiProjectPersonAnalysisObjectRecordVO) objectRecords.getFirst()).getExtraFields());
    }

    @Test
    @SuppressWarnings({"rawtypes", "unchecked"})
    void shouldConvertBigDecimalRiskCountsWhenRefreshingProjectRiskCounts() {
        CcdiProject project = new CcdiProject();
        project.setProjectId(43L);
        when(projectMapper.selectById(43L)).thenReturn(project);

        HashMap summary = new HashMap();
        summary.put("highRiskCount", new BigDecimal("2"));
        summary.put("mediumRiskCount", new BigDecimal("1"));
        summary.put("lowRiskCount", new BigDecimal("3"));
        when(overviewMapper.selectRiskCountSummaryByProjectId(43L)).thenReturn(summary);
        when(bankStatementMapper.countMatchedStaffCountByProjectId(43L)).thenReturn(6);

        service.refreshProjectRiskCounts(43L, "tester");

        verify(projectMapper).updateRiskCountsByProjectId(eq(43L), eq(6), eq(2), eq(1), eq(3), eq("tester"));
    }

    @Test
    void shouldWrapRiskModelCardsIntoCardList() {
        CcdiProject project = new CcdiProject();
        project.setProjectId(40L);
        when(projectMapper.selectById(40L)).thenReturn(project);

        CcdiProjectRiskModelCardVO card = new CcdiProjectRiskModelCardVO();
        card.setModelCode("MODEL_A");
        card.setWarningCount(3);
        when(overviewMapper.selectRiskModelCardsByProjectId(40L)).thenReturn(List.of(card));

        CcdiProjectRiskModelCardsVO result = service.getRiskModelCards(40L);

        assertNotNull(result.getCardList());
        assertEquals(1, result.getCardList().size());
        assertEquals("MODEL_A", result.getCardList().getFirst().getModelCode());
        assertEquals(3, result.getCardList().getFirst().getWarningCount());
    }

    @Test
    void shouldWrapRiskModelPeopleRowsTotalAndActionLabel() {
        CcdiProject project = new CcdiProject();
        project.setProjectId(40L);
        when(projectMapper.selectById(40L)).thenReturn(project);

        CcdiProjectRiskModelPeopleItemVO item = new CcdiProjectRiskModelPeopleItemVO();
        item.setStaffName("王五");
        item.setStaffCode("10001");

        Page<CcdiProjectRiskModelPeopleItemVO> page = new Page<>(2, 5);
        page.setRecords(List.of(item));
        page.setTotal(11);
        when(overviewMapper.selectRiskModelPeoplePage(any(Page.class), any(CcdiProjectRiskModelPeopleQueryDTO.class)))
            .thenReturn(page);

        CcdiProjectRiskModelPeopleVO result = service.getRiskModelPeople(buildRiskModelPeopleQuery(40L));

        assertNotNull(result.getRows());
        assertEquals(1, result.getRows().size());
        assertEquals(11L, result.getTotal());
        assertEquals("查看详情", result.getRows().getFirst().getActionLabel());
    }

    @Test
    void shouldRefreshOverviewEmployeeResultsAndSyncRiskCounts() {
        CcdiProject project = new CcdiProject();
        project.setProjectId(43L);
        when(projectMapper.selectById(43L)).thenReturn(project);

        List<CcdiProjectOverviewEmployeeHitRowVO> hitRows = List.of(new CcdiProjectOverviewEmployeeHitRowVO());
        List<CcdiProjectOverviewEmployeeResult> results = List.of(
            buildEmployeeResult("HIGH"),
            buildEmployeeResult("MEDIUM"),
            buildEmployeeResult("LOW")
        );
        when(overviewEmployeeResultMapper.selectEmployeeHitRowsByProjectId(43L)).thenReturn(hitRows);
        when(overviewEmployeeResultBuilder.build(43L, hitRows, "tester")).thenReturn(results);
        when(bankStatementMapper.countMatchedStaffCountByProjectId(43L)).thenReturn(3);

        service.refreshOverviewEmployeeResults(43L, "tester");

        org.mockito.InOrder inOrder = org.mockito.Mockito.inOrder(
            projectMapper,
            overviewEmployeeResultMapper,
            overviewEmployeeResultBuilder
        );
        inOrder.verify(projectMapper).selectById(43L);
        inOrder.verify(overviewEmployeeResultMapper).deleteByProjectId(43L);
        inOrder.verify(overviewEmployeeResultMapper).selectEmployeeHitRowsByProjectId(43L);
        inOrder.verify(overviewEmployeeResultBuilder).build(43L, hitRows, "tester");
        inOrder.verify(overviewEmployeeResultMapper).insertBatch(results);
        inOrder.verify(projectMapper).updateRiskCountsByProjectId(43L, 3, 1, 1, 1, "tester");
    }

    @Test
    void shouldReturnEmptyCollectionsForRiskModelCardsAndPeople() {
        CcdiProject project = new CcdiProject();
        project.setProjectId(40L);
        when(projectMapper.selectById(40L)).thenReturn(project);
        when(overviewMapper.selectRiskModelCardsByProjectId(40L)).thenReturn(List.of());

        Page<CcdiProjectRiskModelPeopleItemVO> emptyPage = new Page<>(1, 10);
        emptyPage.setRecords(List.of());
        emptyPage.setTotal(0);
        when(overviewMapper.selectRiskModelPeoplePage(any(Page.class), any(CcdiProjectRiskModelPeopleQueryDTO.class)))
            .thenReturn(emptyPage);

        CcdiProjectRiskModelCardsVO cards = service.getRiskModelCards(40L);
        CcdiProjectRiskModelPeopleVO people = service.getRiskModelPeople(buildRiskModelPeopleQuery(40L));

        assertNotNull(cards.getCardList());
        assertEquals(0, cards.getCardList().size());
        assertNotNull(people.getRows());
        assertEquals(0, people.getRows().size());
        assertEquals(0L, people.getTotal());
    }

    @Test
    void shouldDefaultRiskModelPeopleMatchModeToAny() {
        CcdiProject project = new CcdiProject();
        project.setProjectId(40L);
        when(projectMapper.selectById(40L)).thenReturn(project);

        Page<CcdiProjectRiskModelPeopleItemVO> emptyPage = new Page<>(1, 10);
        emptyPage.setRecords(List.of());
        emptyPage.setTotal(0);
        when(overviewMapper.selectRiskModelPeoplePage(any(Page.class), any(CcdiProjectRiskModelPeopleQueryDTO.class)))
            .thenReturn(emptyPage);

        CcdiProjectRiskModelPeopleQueryDTO queryDTO = buildRiskModelPeopleQuery(40L);
        queryDTO.setMatchMode(null);
        service.getRiskModelPeople(queryDTO);

        verify(overviewMapper).selectRiskModelPeoplePage(
            any(Page.class),
            argThat(query -> "ANY".equals(query.getMatchMode()))
        );
    }

    private CcdiProjectRiskPeopleQueryDTO buildRiskPeopleQuery(Long projectId) {
        CcdiProjectRiskPeopleQueryDTO queryDTO = new CcdiProjectRiskPeopleQueryDTO();
        queryDTO.setProjectId(projectId);
        queryDTO.setPageNum(1);
        queryDTO.setPageSize(5);
        return queryDTO;
    }

    private CcdiProjectRiskModelPeopleQueryDTO buildRiskModelPeopleQuery(Long projectId) {
        CcdiProjectRiskModelPeopleQueryDTO queryDTO = new CcdiProjectRiskModelPeopleQueryDTO();
        queryDTO.setProjectId(projectId);
        queryDTO.setPageNum(1);
        queryDTO.setPageSize(10);
        return queryDTO;
    }

    private CcdiProjectPersonAnalysisDetailQueryDTO buildPersonAnalysisDetailQuery(Long projectId) {
        CcdiProjectPersonAnalysisDetailQueryDTO queryDTO = new CcdiProjectPersonAnalysisDetailQueryDTO();
        queryDTO.setProjectId(projectId);
        queryDTO.setStaffIdCard("330000000000000001");
        return queryDTO;
    }

    private CcdiProjectOverviewEmployeeResult buildEmployeeResult(String riskLevelCode) {
        CcdiProjectOverviewEmployeeResult result = new CcdiProjectOverviewEmployeeResult();
        result.setRiskLevelCode(riskLevelCode);
        return result;
    }

    private CcdiModelParam buildModelParam(
        String modelCode,
        String modelName,
        String paramName,
        String paramValue,
        String paramUnit,
        String paramDesc
    ) {
        CcdiModelParam param = new CcdiModelParam();
        param.setModelCode(modelCode);
        param.setModelName(modelName);
        param.setParamName(paramName);
        param.setParamValue(paramValue);
        param.setParamUnit(paramUnit);
        param.setParamDesc(paramDesc);
        return param;
    }

    private CcdiProjectRiskHitTagVO buildHitTag(
        String modelCode,
        String modelName,
        String ruleCode,
        String ruleName,
        String riskLevel
    ) {
        CcdiProjectRiskHitTagVO hitTag = new CcdiProjectRiskHitTagVO();
        hitTag.setModelCode(modelCode);
        hitTag.setModelName(modelName);
        hitTag.setRuleCode(ruleCode);
        hitTag.setRuleName(ruleName);
        hitTag.setRiskLevel(riskLevel);
        return hitTag;
    }
}
