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

import ch.qos.logback.classic.Logger;
import ch.qos.logback.classic.spi.ILoggingEvent;
import ch.qos.logback.core.read.ListAppender;
import com.ruoyi.ccdi.project.domain.entity.CcdiBankTagRule;
import com.ruoyi.ccdi.project.domain.entity.CcdiBankTagTask;
import com.ruoyi.ccdi.project.domain.enums.TriggerType;
import com.ruoyi.ccdi.project.domain.vo.BankTagObjectHitVO;
import com.ruoyi.ccdi.project.domain.vo.BankTagRuleExecutionConfig;
import com.ruoyi.ccdi.project.domain.vo.BankTagStatementHitVO;
import com.ruoyi.ccdi.project.mapper.CcdiBankTagAnalysisMapper;
import com.ruoyi.ccdi.project.mapper.CcdiBankTagResultMapper;
import com.ruoyi.ccdi.project.mapper.CcdiBankTagRuleMapper;
import com.ruoyi.ccdi.project.mapper.CcdiBankTagTaskMapper;
import com.ruoyi.ccdi.project.service.ICcdiProjectOverviewService;
import com.ruoyi.ccdi.project.service.ICcdiProjectService;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InOrder;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.slf4j.LoggerFactory;
import org.springframework.test.util.ReflectionTestUtils;

import java.util.List;
import java.util.Map;
import java.util.concurrent.Executor;
import java.math.BigDecimal;
import java.lang.reflect.Method;
import java.nio.file.Files;
import java.nio.file.Path;

import static org.junit.jupiter.api.Assertions.assertNotNull;
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.anyList;
import static org.mockito.ArgumentMatchers.argThat;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.inOrder;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

@ExtendWith(MockitoExtension.class)
class CcdiBankTagServiceImplTest {

    @InjectMocks
    private CcdiBankTagServiceImpl service;

    @Mock
    private CcdiBankTagRuleMapper ruleMapper;

    @Mock
    private CcdiBankTagResultMapper resultMapper;

    @Mock
    private CcdiBankTagTaskMapper taskMapper;

    @Mock
    private CcdiBankTagAnalysisMapper analysisMapper;

    @Mock
    private BankTagRuleConfigResolver configResolver;

    @Mock
    private ICcdiProjectService projectService;

    @Mock
    private ICcdiProjectOverviewService projectOverviewService;

    @Mock
    private ProjectBankTagRebuildCoordinator coordinator;

    @Test
    void submitAutoRebuild_shouldKeepAutoParamChangeTriggerType() {
        service.submitAutoRebuild(40L, TriggerType.AUTO_PARAM_CHANGE);

        verify(coordinator).submitAuto(40L, TriggerType.AUTO_PARAM_CHANGE);
    }

    @Test
    void submitAutoRebuild_shouldKeepAutoFileDeleteTriggerType() {
        service.submitAutoRebuild(40L, TriggerType.AUTO_FILE_DELETE);

        verify(coordinator).submitAuto(40L, TriggerType.AUTO_FILE_DELETE);
    }

    @Test
    void rebuildProject_shouldDeleteOldResultsBeforeSubmittingRuleTasks() {
        ReflectionTestUtils.setField(service, "tagRuleExecutor", (Executor) Runnable::run);

        CcdiBankTagRule rule = new CcdiBankTagRule();
        rule.setModelCode("LARGE_TRANSACTION");
        rule.setModelName("大额交易");
        rule.setRuleCode("HOUSE_OR_CAR_EXPENSE");
        rule.setRuleName("房车消费支出交易");
        rule.setResultType("STATEMENT");

        BankTagRuleExecutionConfig config = new BankTagRuleExecutionConfig();
        config.setProjectId(40L);
        config.setRuleMeta(rule);

        BankTagStatementHitVO hit = new BankTagStatementHitVO();
        hit.setBankStatementId(10L);
        hit.setGroupId(40);
        hit.setLogId(40001);
        hit.setReasonDetail("命中房车消费支出");

        when(ruleMapper.selectEnabledRules(null)).thenReturn(List.of(rule));
        when(configResolver.resolve(40L, rule)).thenReturn(config);
        when(analysisMapper.selectHouseOrCarExpenseStatements(40L)).thenReturn(List.of(hit));

        service.rebuildProject(40L, null, "admin", TriggerType.MANUAL);

        InOrder inOrder = inOrder(resultMapper);
        verify(resultMapper).deleteByProjectAndModel(40L, null);
        verify(resultMapper).insertBatch(anyList());
        inOrder.verify(resultMapper).deleteByProjectAndModel(40L, null);
        inOrder.verify(resultMapper).insertBatch(anyList());
        verify(taskMapper).insertTask(any(CcdiBankTagTask.class));
    }

    @Test
    void rebuildProject_shouldLogTaskLifecycleAndRuleSummary() {
        ReflectionTestUtils.setField(service, "tagRuleExecutor", (Executor) Runnable::run);

        CcdiBankTagRule rule = new CcdiBankTagRule();
        rule.setModelCode("LARGE_TRANSACTION");
        rule.setModelName("大额交易");
        rule.setRuleCode("HOUSE_OR_CAR_EXPENSE");
        rule.setRuleName("房车消费支出交易");
        rule.setResultType("STATEMENT");

        BankTagRuleExecutionConfig config = new BankTagRuleExecutionConfig();
        config.setProjectId(40L);
        config.setRuleMeta(rule);

        BankTagStatementHitVO hit = new BankTagStatementHitVO();
        hit.setBankStatementId(10L);
        hit.setGroupId(40);
        hit.setLogId(40001);
        hit.setReasonDetail("命中房车消费支出");

        doAnswer(invocation -> {
            CcdiBankTagTask task = invocation.getArgument(0);
            task.setId(88L);
            return 1;
        }).when(taskMapper).insertTask(any(CcdiBankTagTask.class));

        when(ruleMapper.selectEnabledRules(null)).thenReturn(List.of(rule));
        when(configResolver.resolve(40L, rule)).thenReturn(config);
        when(analysisMapper.selectHouseOrCarExpenseStatements(40L)).thenReturn(List.of(hit));

        Logger logger = (Logger) LoggerFactory.getLogger(CcdiBankTagServiceImpl.class);
        ListAppender<ILoggingEvent> logAppender = new ListAppender<>();
        logAppender.start();
        logger.addAppender(logAppender);

        try {
            service.rebuildProject(40L, null, "admin", TriggerType.MANUAL);

            assertTrue(logAppender.list.stream().map(ILoggingEvent::getFormattedMessage)
                .anyMatch(message -> message.contains("任务创建成功")
                    && message.contains("taskId=88")
                    && message.contains("projectId=40")));
            assertTrue(logAppender.list.stream().map(ILoggingEvent::getFormattedMessage)
                .anyMatch(message -> message.contains("规则执行完成")
                    && message.contains("ruleCode=HOUSE_OR_CAR_EXPENSE")
                    && message.contains("hitCount=1")));
        } finally {
            logger.detachAppender(logAppender);
        }
    }

    @Test
    void rebuildProject_shouldLogFailureSummaryWhenRuleExecutionFails() {
        ReflectionTestUtils.setField(service, "tagRuleExecutor", (Executor) Runnable::run);

        CcdiBankTagRule rule = new CcdiBankTagRule();
        rule.setModelCode("LARGE_TRANSACTION");
        rule.setRuleCode("HOUSE_OR_CAR_EXPENSE");
        rule.setResultType("STATEMENT");

        doAnswer(invocation -> {
            CcdiBankTagTask task = invocation.getArgument(0);
            task.setId(89L);
            return 1;
        }).when(taskMapper).insertTask(any(CcdiBankTagTask.class));

        when(ruleMapper.selectEnabledRules(null)).thenReturn(List.of(rule));
        when(configResolver.resolve(40L, rule)).thenThrow(new RuntimeException("threshold missing"));

        Logger logger = (Logger) LoggerFactory.getLogger(CcdiBankTagServiceImpl.class);
        ListAppender<ILoggingEvent> logAppender = new ListAppender<>();
        logAppender.start();
        logger.addAppender(logAppender);

        try {
            assertThrows(RuntimeException.class,
                () -> service.rebuildProject(40L, null, "admin", TriggerType.MANUAL));
            assertTrue(logAppender.list.stream().map(ILoggingEvent::getFormattedMessage)
                .anyMatch(message -> message.contains("任务执行失败")
                    && message.contains("taskId=89")
                    && message.contains("threshold missing")));
        } finally {
            logger.detachAppender(logAppender);
        }
    }

    @Test
    void rebuildProject_shouldDispatchPlaceholderStatementRuleAndFinishWithoutResults() {
        ReflectionTestUtils.setField(service, "tagRuleExecutor", (Executor) Runnable::run);

        CcdiBankTagRule rule = buildRule("ABNORMAL_TRANSACTION", "异常交易",
            "ABNORMAL_CUSTOMER_TRANSACTION", "与客户之间非正常资金往来", "STATEMENT");
        BankTagRuleExecutionConfig config = buildConfig(40L, rule);

        when(ruleMapper.selectEnabledRules("ABNORMAL_TRANSACTION")).thenReturn(List.of(rule));
        when(configResolver.resolve(40L, rule)).thenReturn(config);
        when(analysisMapper.selectAbnormalCustomerTransactionStatements(40L)).thenReturn(List.of());

        service.rebuildProject(40L, "ABNORMAL_TRANSACTION", "admin", TriggerType.MANUAL);

        verify(resultMapper).deleteByProjectAndModel(40L, "ABNORMAL_TRANSACTION");
        verify(analysisMapper).selectAbnormalCustomerTransactionStatements(40L);
        verify(resultMapper, never()).insertBatch(anyList());
        verify(taskMapper).updateTask(argThat(task -> "SUCCESS".equals(task.getStatus()) && task.getHitCount() == 0));
    }

    @Test
    void rebuildProject_shouldDispatchPlaceholderObjectRuleAndFinishWithoutResults() {
        ReflectionTestUtils.setField(service, "tagRuleExecutor", (Executor) Runnable::run);

        CcdiBankTagRule rule = buildRule("SUSPICIOUS_PURCHASE", "可疑采购",
            "SUPPLIER_CONCENTRATION", "可疑采购", "OBJECT");
        BankTagRuleExecutionConfig config = buildConfig(40L, rule);

        when(ruleMapper.selectEnabledRules("SUSPICIOUS_PURCHASE")).thenReturn(List.of(rule));
        when(configResolver.resolve(40L, rule)).thenReturn(config);
        when(analysisMapper.selectSupplierConcentrationObjects(40L)).thenReturn(List.of());

        service.rebuildProject(40L, "SUSPICIOUS_PURCHASE", "admin", TriggerType.MANUAL);

        verify(analysisMapper).selectSupplierConcentrationObjects(40L);
        verify(resultMapper).deleteByProjectAndModel(40L, "SUSPICIOUS_PURCHASE");
        verify(resultMapper, never()).insertBatch(anyList());
        verify(taskMapper).updateTask(argThat(task -> "SUCCESS".equals(task.getStatus()) && task.getFailedRuleCount() == 0));
    }

    @Test
    void rebuildProject_shouldDispatchWithdrawCntObjectRuleWithResolvedThreshold() {
        ReflectionTestUtils.setField(service, "tagRuleExecutor", (Executor) Runnable::run);

        CcdiBankTagRule rule = buildRule("ABNORMAL_BEHAVIOR", "异常行为",
            "WITHDRAW_CNT", "微信支付宝频繁提现", "OBJECT");
        BankTagRuleExecutionConfig config = buildConfig(40L, rule);
        config.setThresholdValues(Map.of("WITHDRAW_CNT", "3"));

        BankTagObjectHitVO hit = new BankTagObjectHitVO();
        hit.setObjectType("STAFF_ID_CARD");
        hit.setObjectKey("330101199001011234");
        hit.setReasonDetail("单日微信提现 4 次，超过阈值 3 次");

        when(ruleMapper.selectEnabledRules("ABNORMAL_BEHAVIOR")).thenReturn(List.of(rule));
        when(configResolver.resolve(40L, rule)).thenReturn(config);
        when(analysisMapper.selectWithdrawCntObjects(40L, 3)).thenReturn(List.of(hit));

        service.rebuildProject(40L, "ABNORMAL_BEHAVIOR", "admin", TriggerType.MANUAL);

        verify(analysisMapper).selectWithdrawCntObjects(40L, 3);
        verify(resultMapper).insertBatch(argThat(results -> results.size() == 1
            && "STAFF_ID_CARD".equals(results.get(0).getObjectType())
            && "330101199001011234".equals(results.get(0).getObjectKey())));
    }

    @Test
    void rebuildProject_shouldTreatEmptyWithdrawCntHitsAsSuccess() {
        ReflectionTestUtils.setField(service, "tagRuleExecutor", (Executor) Runnable::run);

        CcdiBankTagRule rule = buildRule("ABNORMAL_BEHAVIOR", "异常行为",
            "WITHDRAW_CNT", "微信支付宝频繁提现", "OBJECT");
        BankTagRuleExecutionConfig config = buildConfig(40L, rule);
        config.setThresholdValues(Map.of("WITHDRAW_CNT", "3"));

        when(ruleMapper.selectEnabledRules("ABNORMAL_BEHAVIOR")).thenReturn(List.of(rule));
        when(configResolver.resolve(40L, rule)).thenReturn(config);
        when(analysisMapper.selectWithdrawCntObjects(40L, 3)).thenReturn(List.of());

        service.rebuildProject(40L, "ABNORMAL_BEHAVIOR", "admin", TriggerType.MANUAL);

        verify(analysisMapper).selectWithdrawCntObjects(40L, 3);
        verify(resultMapper, never()).insertBatch(anyList());
        verify(taskMapper).updateTask(argThat(task -> "SUCCESS".equals(task.getStatus()) && task.getHitCount() == 0));
    }

    @Test
    void shouldMarkProjectTaggingBeforeExecutingAndCompletedAfterSuccess() {
        ReflectionTestUtils.setField(service, "tagRuleExecutor", (Executor) Runnable::run);

        CcdiBankTagRule rule = buildRule("LARGE_TRANSACTION", "大额交易",
            "HOUSE_OR_CAR_EXPENSE", "房车消费支出交易", "STATEMENT");
        BankTagRuleExecutionConfig config = buildConfig(40L, rule);

        when(ruleMapper.selectEnabledRules(null)).thenReturn(List.of(rule));
        when(configResolver.resolve(40L, rule)).thenReturn(config);
        when(analysisMapper.selectHouseOrCarExpenseStatements(40L)).thenReturn(List.of());

        service.rebuildProject(40L, null, "tester", TriggerType.MANUAL);

        InOrder inOrder = inOrder(projectService, taskMapper);
        inOrder.verify(projectService).updateProjectStatus(40L, "3", "tester");
        inOrder.verify(taskMapper).updateTask(argThat(task -> "SUCCESS".equals(task.getStatus())));
        inOrder.verify(projectService).updateProjectStatus(40L, "1", "tester");
    }

    @Test
    void shouldMarkProjectTagFailedWhenRebuildFails() {
        ReflectionTestUtils.setField(service, "tagRuleExecutor", (Executor) Runnable::run);

        CcdiBankTagRule rule = buildRule("LARGE_TRANSACTION", "大额交易",
            "HOUSE_OR_CAR_EXPENSE", "房车消费支出交易", "STATEMENT");

        when(ruleMapper.selectEnabledRules(null)).thenReturn(List.of(rule));
        when(configResolver.resolve(40L, rule)).thenThrow(new RuntimeException("threshold missing"));

        assertThrows(RuntimeException.class,
            () -> service.rebuildProject(40L, null, "tester", TriggerType.MANUAL));

        verify(projectService).updateProjectStatus(40L, "4", "tester");
    }

    @Test
    void rebuildProject_shouldDispatchPhaseTwoThresholdObjectRulesWithResolvedThresholds() {
        ReflectionTestUtils.setField(service, "tagRuleExecutor", (Executor) Runnable::run);

        CcdiBankTagRule gamblingRule = buildRule("SUSPICIOUS_GAMBLING", "疑似赌博",
            "MULTI_PARTY_GAMBLING_TRANSFER", "疑似赌博交易", "OBJECT");
        CcdiBankTagRule monthlyRule = buildRule("SUSPICIOUS_PART_TIME", "可疑兼职",
            "MONTHLY_FIXED_INCOME", "疑似兼职", "OBJECT");
        CcdiBankTagRule fixedCounterpartyRule = buildRule("SUSPICIOUS_PART_TIME", "可疑兼职",
            "FIXED_COUNTERPARTY_TRANSFER", "疑似兼职", "OBJECT");

        BankTagRuleExecutionConfig gamblingConfig = buildConfig(40L, gamblingRule);
        gamblingConfig.setThresholdValues(Map.of("MULTI_PARTY_AMT_MIN", "500", "MULTI_PARTY_AMT_MAX", "5000"));
        BankTagRuleExecutionConfig monthlyConfig = buildConfig(40L, monthlyRule);
        monthlyConfig.setThresholdValues(Map.of("MONTHLY_FIXED_INCOME", "5000"));
        BankTagRuleExecutionConfig fixedCounterpartyConfig = buildConfig(40L, fixedCounterpartyRule);
        fixedCounterpartyConfig.setThresholdValues(Map.of(
            "FIXED_COUNTERPARTY_TRANSFER_MIN", "3000",
            "FIXED_COUNTERPARTY_TRANSFER_MAX", "15000"
        ));

        when(ruleMapper.selectEnabledRules("SUSPICIOUS_GAMBLING")).thenReturn(List.of(gamblingRule));
        when(configResolver.resolve(40L, gamblingRule)).thenReturn(gamblingConfig);
        when(analysisMapper.selectMultiPartyGamblingTransferObjects(40L, new BigDecimal("500"), new BigDecimal("5000")))
            .thenReturn(List.of());

        when(ruleMapper.selectEnabledRules("SUSPICIOUS_PART_TIME")).thenReturn(List.of(monthlyRule, fixedCounterpartyRule));
        when(configResolver.resolve(40L, monthlyRule)).thenReturn(monthlyConfig);
        when(configResolver.resolve(40L, fixedCounterpartyRule)).thenReturn(fixedCounterpartyConfig);
        when(analysisMapper.selectMonthlyFixedIncomeObjects(40L, new BigDecimal("5000"))).thenReturn(List.of());
        when(analysisMapper.selectFixedCounterpartyTransferObjects(40L, new BigDecimal("3000"), new BigDecimal("15000")))
            .thenReturn(List.of());

        service.rebuildProject(40L, "SUSPICIOUS_GAMBLING", "admin", TriggerType.MANUAL);
        service.rebuildProject(40L, "SUSPICIOUS_PART_TIME", "admin", TriggerType.MANUAL);

        verify(analysisMapper).selectMultiPartyGamblingTransferObjects(40L, new BigDecimal("500"), new BigDecimal("5000"));
        verify(analysisMapper).selectMonthlyFixedIncomeObjects(40L, new BigDecimal("5000"));
        verify(analysisMapper).selectFixedCounterpartyTransferObjects(40L, new BigDecimal("3000"), new BigDecimal("15000"));
    }

    @Test
    void rebuildProject_shouldDispatchPhaseTwoObjectRulesWithoutExtraThresholds() {
        ReflectionTestUtils.setField(service, "tagRuleExecutor", (Executor) Runnable::run);

        CcdiBankTagRule lowIncomeRule = buildRule("ABNORMAL_TRANSACTION", "异常交易",
            "LOW_INCOME_RELATIVE_LARGE_TRANSACTION", "低收入亲属大额交易", "OBJECT");
        CcdiBankTagRule supplierRule = buildRule("SUSPICIOUS_PURCHASE", "可疑采购",
            "SUPPLIER_CONCENTRATION", "可疑采购", "OBJECT");
        CcdiBankTagRule salaryQuickRule = buildRule("ABNORMAL_BEHAVIOR", "异常行为",
            "SALARY_QUICK_TRANSFER", "工资快速转出", "OBJECT");
        CcdiBankTagRule salaryUnusedRule = buildRule("ABNORMAL_BEHAVIOR", "异常行为",
            "SALARY_UNUSED", "工资无使用记录", "OBJECT");

        when(ruleMapper.selectEnabledRules("ABNORMAL_TRANSACTION")).thenReturn(List.of(lowIncomeRule));
        when(configResolver.resolve(40L, lowIncomeRule)).thenReturn(buildConfig(40L, lowIncomeRule));
        when(analysisMapper.selectLowIncomeRelativeLargeTransactionObjects(40L)).thenReturn(List.of());

        when(ruleMapper.selectEnabledRules("SUSPICIOUS_PURCHASE")).thenReturn(List.of(supplierRule));
        when(configResolver.resolve(40L, supplierRule)).thenReturn(buildConfig(40L, supplierRule));
        when(analysisMapper.selectSupplierConcentrationObjects(40L)).thenReturn(List.of());

        when(ruleMapper.selectEnabledRules("ABNORMAL_BEHAVIOR")).thenReturn(List.of(salaryQuickRule, salaryUnusedRule));
        when(configResolver.resolve(40L, salaryQuickRule)).thenReturn(buildConfig(40L, salaryQuickRule));
        when(configResolver.resolve(40L, salaryUnusedRule)).thenReturn(buildConfig(40L, salaryUnusedRule));
        when(analysisMapper.selectSalaryQuickTransferObjects(40L)).thenReturn(List.of());
        when(analysisMapper.selectSalaryUnusedObjects(40L)).thenReturn(List.of());

        service.rebuildProject(40L, "ABNORMAL_TRANSACTION", "admin", TriggerType.MANUAL);
        service.rebuildProject(40L, "SUSPICIOUS_PURCHASE", "admin", TriggerType.MANUAL);
        service.rebuildProject(40L, "ABNORMAL_BEHAVIOR", "admin", TriggerType.MANUAL);

        verify(analysisMapper).selectLowIncomeRelativeLargeTransactionObjects(40L);
        verify(analysisMapper).selectSupplierConcentrationObjects(40L);
        verify(analysisMapper).selectSalaryQuickTransferObjects(40L);
        verify(analysisMapper).selectSalaryUnusedObjects(40L);
    }

    @Test
    void rebuildProject_shouldDispatchSuddenAccountClosureObjectRule() {
        ReflectionTestUtils.setField(service, "tagRuleExecutor", (Executor) Runnable::run);

        CcdiBankTagRule rule = buildRule("ABNORMAL_ACCOUNT", "异常账户",
            "SUDDEN_ACCOUNT_CLOSURE", "突然销户", "OBJECT");

        when(ruleMapper.selectEnabledRules("ABNORMAL_ACCOUNT")).thenReturn(List.of(rule));
        when(configResolver.resolve(40L, rule)).thenReturn(buildConfig(40L, rule));
        when(analysisMapper.selectSuddenAccountClosureObjects(40L)).thenReturn(List.of());

        service.rebuildProject(40L, "ABNORMAL_ACCOUNT", "admin", TriggerType.MANUAL);

        verify(analysisMapper).selectSuddenAccountClosureObjects(40L);
    }

    @Test
    void rebuildProject_shouldDispatchDormantAccountLargeActivationObjectRule() {
        ReflectionTestUtils.setField(service, "tagRuleExecutor", (Executor) Runnable::run);

        CcdiBankTagRule rule = buildRule("ABNORMAL_ACCOUNT", "异常账户",
            "DORMANT_ACCOUNT_LARGE_ACTIVATION", "休眠账户大额启用", "OBJECT");

        when(ruleMapper.selectEnabledRules("ABNORMAL_ACCOUNT")).thenReturn(List.of(rule));
        when(configResolver.resolve(40L, rule)).thenReturn(buildConfig(40L, rule));
        when(analysisMapper.selectDormantAccountLargeActivationObjects(40L)).thenReturn(List.of());

        service.rebuildProject(40L, "ABNORMAL_ACCOUNT", "admin", TriggerType.MANUAL);

        verify(analysisMapper).selectDormantAccountLargeActivationObjects(40L);
    }

    @Test
    void rebuildProject_shouldInsertSuddenAccountClosureObjectResults() {
        ReflectionTestUtils.setField(service, "tagRuleExecutor", (Executor) Runnable::run);

        CcdiBankTagRule rule = buildRule("ABNORMAL_ACCOUNT", "异常账户",
            "SUDDEN_ACCOUNT_CLOSURE", "突然销户", "OBJECT");
        BankTagRuleExecutionConfig config = buildConfig(40L, rule);

        BankTagObjectHitVO hit = new BankTagObjectHitVO();
        hit.setObjectType("STAFF_ID_CARD");
        hit.setObjectKey("330101199001011234");
        hit.setReasonDetail("账户62220001于2026-03-15销户，销户前30天内最后交易日2026-03-10，累计交易金额120000元，单笔最大金额80000元");

        when(ruleMapper.selectEnabledRules("ABNORMAL_ACCOUNT")).thenReturn(List.of(rule));
        when(configResolver.resolve(40L, rule)).thenReturn(config);
        when(analysisMapper.selectSuddenAccountClosureObjects(40L)).thenReturn(List.of(hit));

        service.rebuildProject(40L, "ABNORMAL_ACCOUNT", "admin", TriggerType.MANUAL);

        verify(resultMapper).insertBatch(argThat(results -> results.stream().anyMatch(item ->
            "ABNORMAL_ACCOUNT".equals(item.getModelCode())
                && "SUDDEN_ACCOUNT_CLOSURE".equals(item.getRuleCode())
                && "OBJECT".equals(item.getResultType())
                && "STAFF_ID_CARD".equals(item.getObjectType())
        )));
    }

    @Test
    void rebuildProject_shouldInsertDormantAccountLargeActivationObjectResults() {
        ReflectionTestUtils.setField(service, "tagRuleExecutor", (Executor) Runnable::run);

        CcdiBankTagRule rule = buildRule("ABNORMAL_ACCOUNT", "异常账户",
            "DORMANT_ACCOUNT_LARGE_ACTIVATION", "休眠账户大额启用", "OBJECT");
        BankTagRuleExecutionConfig config = buildConfig(40L, rule);

        BankTagObjectHitVO hit = new BankTagObjectHitVO();
        hit.setObjectType("STAFF_ID_CARD");
        hit.setObjectKey("330101199001011235");
        hit.setReasonDetail("账户62220002开户于2025-01-01，首次交易日期2025-08-01，沉睡时长7个月，启用后累计交易金额500000元，单笔最大金额120000元");

        when(ruleMapper.selectEnabledRules("ABNORMAL_ACCOUNT")).thenReturn(List.of(rule));
        when(configResolver.resolve(40L, rule)).thenReturn(config);
        when(analysisMapper.selectDormantAccountLargeActivationObjects(40L)).thenReturn(List.of(hit));

        service.rebuildProject(40L, "ABNORMAL_ACCOUNT", "admin", TriggerType.MANUAL);

        verify(resultMapper).insertBatch(argThat(results -> results.stream().anyMatch(item ->
            "ABNORMAL_ACCOUNT".equals(item.getModelCode())
                && "DORMANT_ACCOUNT_LARGE_ACTIVATION".equals(item.getRuleCode())
                && "OBJECT".equals(item.getResultType())
                && "STAFF_ID_CARD".equals(item.getObjectType())
        )));
    }

    @Test
    void rebuildProject_shouldDispatchExternalPersonStatementRules() {
        ReflectionTestUtils.setField(service, "tagRuleExecutor", (Executor) Runnable::run);

        CcdiBankTagRule largeRule = buildRule("EXTERNAL_LARGE_TRANSACTION", "外部人员大额交易",
            "EXTERNAL_SINGLE_LARGE_AMOUNT", "外部人员单笔大额交易", "STATEMENT");
        CcdiBankTagRule nightRule = buildRule("EXTERNAL_ABNORMAL_TRANSACTION", "外部人员异常交易",
            "EXTERNAL_NIGHT_TRANSACTION", "外部人员夜间集中交易", "STATEMENT");
        CcdiBankTagRule gamblingRule = buildRule("EXTERNAL_SUSPICIOUS_GAMBLING", "外部人员可疑赌博",
            "EXTERNAL_GAMBLING_MEMO", "外部人员疑似赌博摘要", "STATEMENT");
        CcdiBankTagRule relationRule = buildRule("EXTERNAL_SUSPICIOUS_RELATION", "外部人员可疑关系",
            "EXTERNAL_TO_STAFF_FAMILY_TRANSACTION", "外部人员与员工或员工亲属交易", "STATEMENT");
        BankTagRuleExecutionConfig largeConfig = buildConfig(40L, largeRule);
        largeConfig.setThresholdValues(Map.of("FREQUENT_TRANSFER", "100000"));

        BankTagStatementHitVO hit = new BankTagStatementHitVO();
        hit.setBankStatementId(100L);
        hit.setGroupId(40);
        hit.setLogId(40001);
        hit.setReasonDetail("外部人员命中");

        when(ruleMapper.selectEnabledRules(null)).thenReturn(List.of(largeRule, nightRule, gamblingRule, relationRule));
        when(configResolver.resolve(40L, largeRule)).thenReturn(largeConfig);
        when(configResolver.resolve(40L, nightRule)).thenReturn(buildConfig(40L, nightRule));
        when(configResolver.resolve(40L, gamblingRule)).thenReturn(buildConfig(40L, gamblingRule));
        when(configResolver.resolve(40L, relationRule)).thenReturn(buildConfig(40L, relationRule));
        when(analysisMapper.selectExternalSingleLargeAmountStatements(40L, new BigDecimal("100000"))).thenReturn(List.of(hit));
        when(analysisMapper.selectExternalNightTransactionStatements(40L)).thenReturn(List.of());
        when(analysisMapper.selectExternalGamblingMemoStatements(40L)).thenReturn(List.of());
        when(analysisMapper.selectExternalToStaffOrFamilyTransactionStatements(40L)).thenReturn(List.of());

        service.rebuildProject(40L, null, "admin", TriggerType.MANUAL);

        verify(analysisMapper).selectExternalSingleLargeAmountStatements(40L, new BigDecimal("100000"));
        verify(analysisMapper).selectExternalNightTransactionStatements(40L);
        verify(analysisMapper).selectExternalGamblingMemoStatements(40L);
        verify(analysisMapper).selectExternalToStaffOrFamilyTransactionStatements(40L);
        verify(resultMapper).insertBatch(argThat(results -> results.stream().anyMatch(item ->
            "EXTERNAL_LARGE_TRANSACTION".equals(item.getModelCode())
                && "EXTERNAL_SINGLE_LARGE_AMOUNT".equals(item.getRuleCode())
                && "STATEMENT".equals(item.getResultType())
                && Long.valueOf(100L).equals(item.getBankStatementId())
        )));
    }

    @Test
    void rebuildProject_shouldDispatchExternalPersonObjectRules() {
        ReflectionTestUtils.setField(service, "tagRuleExecutor", (Executor) Runnable::run);

        CcdiBankTagRule cumulativeRule = buildRule("EXTERNAL_LARGE_TRANSACTION", "外部人员大额交易",
            "EXTERNAL_CUMULATIVE_TRANSACTION_AMOUNT", "外部人员累计交易超限", "OBJECT");
        CcdiBankTagRule annualRule = buildRule("EXTERNAL_LARGE_TRANSACTION", "外部人员大额交易",
            "EXTERNAL_ANNUAL_TURNOVER", "外部人员年流水交易额超限", "OBJECT");
        CcdiBankTagRule gamblingRule = buildRule("EXTERNAL_SUSPICIOUS_GAMBLING", "外部人员可疑赌博",
            "EXTERNAL_MULTI_PARTY_GAMBLING_TRANSFER", "外部人员同日多对手方疑似赌博交易", "OBJECT");
        BankTagRuleExecutionConfig cumulativeConfig = buildConfig(40L, cumulativeRule);
        cumulativeConfig.setThresholdValues(Map.of("CUMULATIVE_TRANSACTION_AMOUNT", "500000"));
        BankTagRuleExecutionConfig annualConfig = buildConfig(40L, annualRule);
        annualConfig.setThresholdValues(Map.of("ANNUAL_TURNOVER", "800000"));
        BankTagRuleExecutionConfig gamblingConfig = buildConfig(40L, gamblingRule);
        gamblingConfig.setThresholdValues(Map.of("MULTI_PARTY_AMT_MIN", "500", "MULTI_PARTY_AMT_MAX", "5000"));

        BankTagObjectHitVO hit = new BankTagObjectHitVO();
        hit.setObjectType("EXTERNAL_CERT_NO");
        hit.setObjectKey("330100198801010033");
        hit.setReasonDetail("外部人员累计交易超限");

        when(ruleMapper.selectEnabledRules(null)).thenReturn(List.of(cumulativeRule, annualRule, gamblingRule));
        when(configResolver.resolve(40L, cumulativeRule)).thenReturn(cumulativeConfig);
        when(configResolver.resolve(40L, annualRule)).thenReturn(annualConfig);
        when(configResolver.resolve(40L, gamblingRule)).thenReturn(gamblingConfig);
        when(analysisMapper.selectExternalCumulativeTransactionAmountObjects(40L, new BigDecimal("500000"))).thenReturn(List.of(hit));
        when(analysisMapper.selectExternalAnnualTurnoverObjects(40L, new BigDecimal("800000"))).thenReturn(List.of());
        when(analysisMapper.selectExternalMultiPartyGamblingTransferObjects(40L, new BigDecimal("500"), new BigDecimal("5000"))).thenReturn(List.of());

        service.rebuildProject(40L, null, "admin", TriggerType.MANUAL);

        verify(analysisMapper).selectExternalCumulativeTransactionAmountObjects(40L, new BigDecimal("500000"));
        verify(analysisMapper).selectExternalAnnualTurnoverObjects(40L, new BigDecimal("800000"));
        verify(analysisMapper).selectExternalMultiPartyGamblingTransferObjects(40L, new BigDecimal("500"), new BigDecimal("5000"));
        verify(resultMapper).insertBatch(argThat(results -> results.stream().anyMatch(item ->
            "EXTERNAL_LARGE_TRANSACTION".equals(item.getModelCode())
                && "EXTERNAL_CUMULATIVE_TRANSACTION_AMOUNT".equals(item.getRuleCode())
                && "OBJECT".equals(item.getResultType())
                && "EXTERNAL_CERT_NO".equals(item.getObjectType())
                && "330100198801010033".equals(item.getObjectKey())
        )));
    }

    @Test
    void buildSafeTaskErrorMessage_shouldKeepLongMessageForLongTextColumn() throws Exception {
        Method method = CcdiBankTagServiceImpl.class.getDeclaredMethod(
            "buildSafeTaskErrorMessage", Throwable.class
        );
        method.setAccessible(true);

        String longMessage = "X".repeat(5000);
        RuntimeException throwable = new RuntimeException("root-cause:" + longMessage);

        String result = (String) method.invoke(null, throwable);

        assertNotNull(result);
        assertTrue(result.length() > 2000, "LONGTEXT 方案下不应继续把错误信息截断到 2000");
        assertTrue(result.contains("root-cause"), "错误信息应保留根因关键字");
    }

    @Test
    void abnormalAccountMapperXml_shouldDeclareObjectSelects() throws Exception {
        String xml = Files.readString(Path.of("src/main/resources/mapper/ccdi/project/CcdiBankTagAnalysisMapper.xml"));

        assertTrue(xml.contains("select id=\"selectSuddenAccountClosureObjects\""));
        assertTrue(xml.contains("select id=\"selectDormantAccountLargeActivationObjects\""));
    }

    @Test
    void dormantAccountLargeActivationMapperXml_shouldContainDormantAccountConditions() throws Exception {
        String xml = Files.readString(Path.of("src/main/resources/mapper/ccdi/project/CcdiBankTagAnalysisMapper.xml"));

        assertTrue(xml.contains("select id=\"selectDormantAccountLargeActivationObjects\""));
        assertTrue(xml.contains("ai.owner_type = 'EMPLOYEE'"));
        assertTrue(xml.contains("ai.status = 1"));
        assertTrue(xml.contains("ai.effective_date is not null"));
        assertTrue(xml.contains("DATE_ADD(ai.effective_date, INTERVAL 6 MONTH)"));
        assertTrue(xml.contains("windowTotalAmount >= 500000") || xml.contains("windowMaxSingleAmount >= 100000"));
    }

    private CcdiBankTagRule buildRule(String modelCode, String modelName, String ruleCode, String ruleName, String resultType) {
        CcdiBankTagRule rule = new CcdiBankTagRule();
        rule.setModelCode(modelCode);
        rule.setModelName(modelName);
        rule.setRuleCode(ruleCode);
        rule.setRuleName(ruleName);
        rule.setResultType(resultType);
        return rule;
    }

    private BankTagRuleExecutionConfig buildConfig(Long projectId, CcdiBankTagRule rule) {
        BankTagRuleExecutionConfig config = new BankTagRuleExecutionConfig();
        config.setProjectId(projectId);
        config.setRuleMeta(rule);
        return config;
    }
}
