package com.ruoyi.ccdi.project.mapper;

import org.junit.jupiter.api.Test;
import org.xml.sax.InputSource;

import javax.xml.parsers.DocumentBuilderFactory;
import java.io.StringReader;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import static org.junit.jupiter.api.Assertions.assertAll;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;

class CcdiBankTagAnalysisMapperXmlTest {

    private static final String RESOURCE = "mapper/ccdi/project/CcdiBankTagAnalysisMapper.xml";
    private static final List<String> PHASE_ONE_STATEMENT_SELECT_IDS = List.of(
        "selectGamblingSensitiveKeywordStatements",
        "selectSpecialAmountTransactionStatements",
        "selectSuspiciousIncomeKeywordStatements",
        "selectForexBuyAmtStatements",
        "selectForexSellAmtStatements",
        "selectLargePurchaseTransactionStatements",
        "selectStockTfrLargeStatements",
        "selectLargeStockTradingStatements",
        "selectExternalSingleLargeAmountStatements",
        "selectExternalNightTransactionStatements",
        "selectExternalGamblingMemoStatements",
        "selectExternalToStaffOrFamilyTransactionStatements"
    );
    private static final List<String> PHASE_TWO_OBJECT_SELECT_IDS = List.of(
        "selectLowIncomeRelativeLargeTransactionObjects",
        "selectMultiPartyGamblingTransferObjects",
        "selectExternalCumulativeTransactionAmountObjects",
        "selectExternalAnnualTurnoverObjects",
        "selectExternalMultiPartyGamblingTransferObjects",
        "selectMonthlyFixedIncomeObjects",
        "selectFixedCounterpartyTransferObjects",
        "selectSupplierConcentrationObjects",
        "selectSalaryQuickTransferObjects",
        "selectSalaryUnusedObjects"
    );
    private static final List<String> PLACEHOLDER_SELECT_IDS = List.of(
        "selectAbnormalCustomerTransactionStatements",
        "selectLowIncomeRelativeLargeTransactionObjects",
        "selectMultiPartyGamblingTransferObjects",
        "selectMonthlyFixedIncomeObjects",
        "selectFixedCounterpartyTransferObjects",
        "selectHouseRegistrationMismatchStatements",
        "selectPropertyFeeRegistrationMismatchStatements",
        "selectTaxAssetRegistrationMismatchStatements",
        "selectIncomeAssetMismatchStatements",
        "selectCrossBorderAmtStatements",
        "selectInterestPaymentByOthersObjects",
        "selectSupplierConcentrationObjects",
        "selectWithdrawCntObjects",
        "selectWithdrawAmtObjects",
        "selectSalaryQuickTransferObjects",
        "selectSalaryUnusedObjects",
        "selectProxyAccountOperationObjects"
    );

    @Test
    void statementRuleSql_shouldSelectGroupIdAndLogId() throws Exception {
        String xml = readXml(RESOURCE);
        assertTrue(xml.contains("AS groupId"));
        assertTrue(xml.contains("AS logId"));
    }

    @Test
    void dormantAccountLargeActivation_shouldRequireCompleteObservationWindow() throws Exception {
        String xml = readXml(RESOURCE);
        String selectSql = extractSelectSql(xml, "selectDormantAccountLargeActivationObjects");

        assertTrue(selectSql.contains("project_window.projectStartDate"), selectSql);
        assertTrue(selectSql.contains("project_window.projectStartDate is not null"), selectSql);
        assertTrue(selectSql.contains("project_window.projectStartDate &lt;= DATE_SUB(min(tx.txDate), INTERVAL 6 MONTH)"), selectSql);
    }

    @Test
    void suddenAccountClosure_shouldRequireLargeTransactionInClosureWindow() throws Exception {
        String xml = readXml(RESOURCE);
        String selectSql = extractSelectSql(xml, "selectSuddenAccountClosureObjects");

        assertTrue(selectSql.contains("tx.txDate >= DATE_SUB(ai.invalid_date, INTERVAL 30 DAY)"), selectSql);
        assertTrue(selectSql.contains("where t.windowTotalAmount >= 500000"), selectSql);
        assertTrue(selectSql.contains("or t.windowMaxSingleAmount >= 100000"), selectSql);
    }

    @Test
    void houseOrCarExpenseRule_shouldJoinBankStatementAndReturnStatementHitFields() throws Exception {
        String xml = readXml(RESOURCE);
        assertTrue(xml.contains("selectHouseOrCarExpenseStatements"));
        assertTrue(xml.contains("bs.bank_statement_id AS bankStatementId"));
        assertTrue(xml.contains("bs.group_id AS groupId"));
        assertTrue(xml.contains("bs.batch_id AS logId"));
    }

    @Test
    void allLargeTransactionRules_shouldExistInAnalysisMapperXml() throws Exception {
        String xml = readXml(RESOURCE);
        assertTrue(xml.contains("selectTaxExpenseStatements"));
        assertTrue(xml.contains("selectSingleLargeIncomeStatements"));
        assertTrue(xml.contains("selectCumulativeIncomeObjects"));
        assertTrue(xml.contains("selectAnnualTurnoverObjects"));
        assertTrue(xml.contains("selectLargeCashDepositStatements"));
        assertTrue(xml.contains("selectFrequentCashDepositObjects"));
        assertTrue(xml.contains("selectLargeTransferStatements"));
    }

    @Test
    void allPlaceholderRules_shouldExistInAnalysisMapperXml() throws Exception {
        String xml = readXml(RESOURCE);
        for (String selectId : PHASE_ONE_STATEMENT_SELECT_IDS) {
            assertTrue(xml.contains(selectId), () -> "缺少第一期规则 SQL: " + selectId);
        }
        for (String selectId : PLACEHOLDER_SELECT_IDS) {
            assertTrue(xml.contains(selectId), () -> "缺少占位规则 SQL: " + selectId);
        }
    }

    @Test
    void placeholderRules_shouldUseEmptyResultSqlTemplate() throws Exception {
        String xml = readXml(RESOURCE);
        assertTrue(xml.contains("占位SQL，待补充真实规则"));
        assertEquals(
            countMatches(xml, "占位SQL，待补充真实规则"),
            countMatches(xml, "where 1 = 0")
        );
    }

    @Test
    void phaseOneStatementRules_shouldUseRealSqlAndKeepHitFields() throws Exception {
        String xml = readXml(RESOURCE);
        for (String selectId : PHASE_ONE_STATEMENT_SELECT_IDS) {
            String selectSql = extractSelectSql(xml, selectId);
            assertTrue(selectSql.contains("AS bankStatementId"), () -> selectId + " 缺少 bankStatementId");
            assertTrue(selectSql.contains("AS groupId"), () -> selectId + " 缺少 groupId");
            assertTrue(selectSql.contains("AS logId"), () -> selectId + " 缺少 logId");
            assertTrue(selectSql.contains("reasonDetail"), () -> selectId + " 缺少 reasonDetail");
            assertTrue(!selectSql.contains("where 1 = 0"), () -> selectId + " 仍是占位 SQL");
        }
    }

    @Test
    void lowIncomeRelativeRule_shouldIgnoreNullAnnualIncome() throws Exception {
        String xml = readXml(RESOURCE);
        String selectSql = extractSelectSql(xml, "selectLowIncomeRelativeLargeTransactionObjects");
        assertTrue(selectSql.contains("relation.annual_income is not null"));
        assertTrue(!selectSql.contains("relation.annual_income is null"));
    }

    @Test
    void abnormalCustomerTransactionRule_shouldUseCreditCustomerAndIntermediaryAccountRules() throws Exception {
        String xml = readXml(RESOURCE);
        String selectSql = extractSelectSql(xml, "selectAbnormalCustomerTransactionStatements");

        assertTrue(selectSql.contains("account.owner_type = 'CREDIT_CUSTOMER'"));
        assertTrue(selectSql.contains("account.owner_type = 'INTERMEDIARY'"));
        assertTrue(selectSql.contains("account.account_no = bs.customer_account_no"));
        assertTrue(selectSql.contains("enterprise.ent_source = 'INTERMEDIARY'"));
        assertTrue(selectSql.contains("intermediary.name = bs.CUSTOMER_ACCOUNT_NAME"));
        assertTrue(selectSql.contains("bs.CUSTOMER_ACCOUNT_NAME like concat('%', intermediary.name, '%')"));
        assertTrue(selectSql.contains("bs.bank in ('ALIPAY', 'WECHAT')"));
        assertEquals(5, countMatches(selectSql, "GREATEST(IFNULL(bs.AMOUNT_DR, 0), IFNULL(bs.AMOUNT_CR, 0)) > 1000"));
        assertTrue(!selectSql.contains("customer_cert_no"));
        assertTrue(!selectSql.contains("social_credit_code = bs"));
    }

    @Test
    void gamblingSensitiveKeywordRules_shouldIgnoreSmallAmountTransactions() throws Exception {
        String xml = readXml(RESOURCE);
        String staffSelectSql = extractSelectSql(xml, "selectGamblingSensitiveKeywordStatements");
        String externalSelectSql = extractSelectSql(xml, "selectExternalGamblingMemoStatements");

        assertTrue(staffSelectSql.contains("IFNULL(bs.AMOUNT_DR, 0) >= 200"));
        assertTrue(staffSelectSql.contains("达到敏感交易最低金额 200 元"));
        assertTrue(externalSelectSql.contains(
            "GREATEST(IFNULL(bs.AMOUNT_DR, 0), IFNULL(bs.AMOUNT_CR, 0)) >= 200"
        ));
        assertTrue(externalSelectSql.contains("达到敏感交易最低金额 200 元"));
    }

    @Test
    void annualTurnoverRules_shouldUseProjectStatementDateAsOneYearAnchor() throws Exception {
        String xml = readXml(RESOURCE);
        String staffSelectSql = extractSelectSql(xml, "selectAnnualTurnoverObjects");
        String externalSelectSql = extractSelectSql(xml, "selectExternalAnnualTurnoverObjects");

        assertTrue(staffSelectSql.contains("project_anchor.anchorDate"));
        assertTrue(staffSelectSql.contains("DATE_SUB(project_anchor.anchorDate, INTERVAL 12 MONTH)"));
        assertFalse(staffSelectSql.contains("CURDATE()"));
        assertTrue(externalSelectSql.contains("project_anchor.anchorDate"));
        assertTrue(externalSelectSql.contains("DATE_SUB(project_anchor.anchorDate, INTERVAL 12 MONTH)"));
        assertFalse(externalSelectSql.contains("CURDATE()"));
    }

    @Test
    void propertyFeeMismatchRule_shouldUseReasonablePropertyKeywordsAndAssetNameMatching() throws Exception {
        String xml = readXml(RESOURCE);
        String selectSql = extractSelectSql(xml, "selectPropertyFeeRegistrationMismatchStatements");

        assertTrue(selectSql.contains("物业|物管|业委会|业主委员会|维修基金|住宅专项维修资金|房屋维修资金"));
        assertFalse(selectSql.contains("中心"));
        assertFalse(selectSql.contains("社区"));
        assertFalse(selectSql.contains("大厦"));
        assertTrue(selectSql.contains("asset.asset_name AS assetName"));
        assertTrue(selectSql.contains("trade.customerAccountName"));
        assertTrue(selectSql.contains("trade.userMemo"));
        assertTrue(selectSql.contains("名下无匹配房产登记"));
    }

    @Test
    void withdrawCntObjectRule_shouldUseRealSqlAndKeepObjectHitFields() throws Exception {
        String xml = readXml(RESOURCE);
        String selectSql = extractSelectSql(xml, "selectWithdrawCntObjects");
        assertTrue(selectSql.contains("'STAFF_ID_CARD' AS objectType"));
        assertTrue(selectSql.contains("AS objectKey"));
        assertTrue(selectSql.contains("reasonDetail"));
        assertTrue(!selectSql.contains("where 1 = 0"));
    }

    @Test
    void phaseTwoObjectRules_shouldUseRealSqlAndKeepObjectHitFields() throws Exception {
        String xml = readXml(RESOURCE);
        for (String selectId : PHASE_TWO_OBJECT_SELECT_IDS) {
            String selectSql = extractSelectSql(xml, selectId);
            assertTrue(
                selectSql.contains("'STAFF_ID_CARD' AS objectType")
                    || selectSql.contains("'EXTERNAL_CERT_NO' AS objectType"),
                () -> selectId + " 缺少 objectType"
            );
            assertTrue(selectSql.contains("AS objectKey"), () -> selectId + " 缺少 objectKey");
            assertTrue(selectSql.contains("reasonDetail"), () -> selectId + " 缺少 reasonDetail");
            assertTrue(!selectSql.contains("where 1 = 0"), () -> selectId + " 仍是占位 SQL");
        }
    }

    @Test
    void purchaseRules_shouldBeScopedToCurrentProjectStaff() throws Exception {
        String xml = readXml(RESOURCE);

        assertAll(
            () -> assertPurchaseRuleScopedByProject(xml, "selectLargePurchaseTransactionStatements"),
            () -> assertPurchaseRuleScopedByProject(xml, "selectSupplierConcentrationObjects")
        );
    }

    @Test
    void largePurchaseTransactionRule_shouldAlignCollationForJoinFields() throws Exception {
        String xml = readXml(RESOURCE);
        String purchaseSelectSql = extractSelectSql(xml, "selectLargePurchaseTransactionStatements");
        String supplierSelectSql = extractSelectSql(xml, "selectSupplierConcentrationObjects");

        assertTrue(
            purchaseSelectSql.contains("project_staff.staffId COLLATE utf8mb4_general_ci = pt.applicant_id COLLATE utf8mb4_general_ci")
        );
        assertTrue(
            purchaseSelectSql.contains("project_staff.staffId COLLATE utf8mb4_general_ci = pt.purchase_leader_id COLLATE utf8mb4_general_ci")
        );
        assertTrue(
            supplierSelectSql.contains("project_staff.staffId COLLATE utf8mb4_general_ci = pt.applicant_id COLLATE utf8mb4_general_ci")
        );
        assertTrue(
            supplierSelectSql.contains("project_staff.staffId COLLATE utf8mb4_general_ci = pt.purchase_leader_id COLLATE utf8mb4_general_ci")
        );
        assertTrue(
            !xml.contains("project_staff.staffId = pt.applicant_id"),
            "采购交易相关 join 不应再使用未声明 COLLATE 的 applicant_id 比较"
        );
        assertTrue(
            !xml.contains("project_staff.staffId = pt.purchase_leader_id"),
            "采购交易相关 join 不应再使用未声明 COLLATE 的 purchase_leader_id 比较"
        );
    }

    @Test
    void assetRegistrationMismatchRules_shouldUseRealSqlAndAssetTable() throws Exception {
        String xml = readXml(RESOURCE);

        assertAll(
            () -> assertStatementRuleUsesAssetTable(xml, "selectHouseRegistrationMismatchStatements"),
            () -> assertStatementRuleUsesAssetTable(xml, "selectPropertyFeeRegistrationMismatchStatements"),
            () -> assertStatementRuleUsesAssetTable(xml, "selectTaxAssetRegistrationMismatchStatements")
        );
    }

    @Test
    void analysisMapperXml_shouldBeWellFormed() throws Exception {
        String xml = readXml(RESOURCE);
        DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
        factory.setNamespaceAware(false);
        factory.setValidating(false);
        factory.newDocumentBuilder().parse(new InputSource(new StringReader(xml)));
    }

    @Test
    void externalPersonRules_shouldUseExternalSubjectScopeAndCounterpartyEmployeeMatching() throws Exception {
        String xml = readXml(RESOURCE);
        String scopeSql = extractSqlFragment(xml, "externalPersonPredicateSql");
        String relationSql = extractSelectSql(xml, "selectExternalToStaffOrFamilyTransactionStatements");
        String annualSql = extractSelectSql(xml, "selectExternalAnnualTurnoverObjects");
        String gamblingSql = extractSelectSql(xml, "selectExternalMultiPartyGamblingTransferObjects");

        assertTrue(scopeSql.contains("bs.cret_no is not null"));
        assertTrue(scopeSql.contains("staff.id_card = bs.cret_no"));
        assertTrue(scopeSql.contains("relation.relation_cert_no = bs.cret_no"));
        assertTrue(scopeSql.contains("trim(IFNULL(bs.LE_ACCOUNT_NAME, '')) &lt;&gt; trim(IFNULL(bs.CUSTOMER_ACCOUNT_NAME, ''))"));
        assertFalse(scopeSql.contains("LE_ACCOUNT_NO"));
        assertTrue(annualSql.contains("'EXTERNAL_CERT_NO' AS objectType"));
        assertTrue(annualSql.contains("bs.cret_no AS certNo"));
        assertTrue(gamblingSql.contains("'EXTERNAL_CERT_NO' AS objectType"));
        assertTrue(gamblingSql.contains("having COUNT(1) > 2"));
        assertTrue(relationSql.contains("counter_account.owner_type in ('EMPLOYEE', 'RELATION', 'INTERMEDIARY', 'CREDIT_CUSTOMER')"));
        assertTrue(relationSql.contains("on counter_account.account_no is null"));
        assertTrue(relationSql.contains("counter_staff.name = trim(bs.CUSTOMER_ACCOUNT_NAME)"));
        assertTrue(relationSql.contains("counter_relation.relation_name = trim(bs.CUSTOMER_ACCOUNT_NAME)"));
        assertTrue(relationSql.contains("counter_account.owner_type in ('EMPLOYEE', 'RELATION')"));
        assertFalse(relationSql.contains("customer_cert_no"));
    }

    private String readXml(String resource) throws Exception {
        try (InputStream inputStream = getClass().getClassLoader().getResourceAsStream(resource)) {
            return new String(inputStream.readAllBytes(), StandardCharsets.UTF_8);
        }
    }

    private int countMatches(String text, String target) {
        int count = 0;
        int index = 0;
        while ((index = text.indexOf(target, index)) >= 0) {
            count++;
            index += target.length();
        }
        return count;
    }

    private String extractSelectSql(String xml, String selectId) {
        Pattern pattern = Pattern.compile(
            "<select\\s+id=\"" + selectId + "\"[\\s\\S]*?</select>"
        );
        Matcher matcher = pattern.matcher(xml);
        assertTrue(matcher.find(), () -> "未找到 select: " + selectId);
        return matcher.group();
    }

    private void assertStatementRuleUsesAssetTable(String xml, String selectId) {
        String selectSql = extractSelectSql(xml, selectId);
        assertTrue(selectSql.contains("ccdi_asset_info"), () -> selectId + " 缺少 ccdi_asset_info");
        assertTrue(selectSql.contains("AS bankStatementId"), () -> selectId + " 缺少 bankStatementId");
        assertTrue(selectSql.contains("AS groupId"), () -> selectId + " 缺少 groupId");
        assertTrue(selectSql.contains("AS logId"), () -> selectId + " 缺少 logId");
        assertTrue(selectSql.contains("reasonDetail"), () -> selectId + " 缺少 reasonDetail");
        assertTrue(!selectSql.contains("where 1 = 0"), () -> selectId + " 仍是占位 SQL");
    }

    private void assertPurchaseRuleScopedByProject(String xml, String selectId) {
        String selectSql = extractSelectSql(xml, selectId);
        String scopeSql = extractSqlFragment(xml, "projectScopedDirectStaffSql");
        assertTrue(selectSql.contains("projectScopedDirectStaffSql"), () -> selectId + " 未引用项目员工范围片段");
        assertTrue(scopeSql.contains("ccdi_bank_statement"), () -> selectId + " 缺少项目流水范围约束");
        assertTrue(scopeSql.contains("#{projectId}"), () -> selectId + " 缺少 projectId 过滤条件");
    }

    private String extractSqlFragment(String xml, String sqlId) {
        Pattern pattern = Pattern.compile(
            "<sql\\s+id=\"" + sqlId + "\"[\\s\\S]*?</sql>"
        );
        Matcher matcher = pattern.matcher(xml);
        assertTrue(matcher.find(), () -> "未找到 sql 片段: " + sqlId);
        return matcher.group();
    }
}
