package com.ruoyi.ccdi.project.mapper;

import com.ruoyi.ccdi.project.domain.dto.CcdiBankStatementQueryDTO;
import org.apache.ibatis.builder.xml.XMLMapperBuilder;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.Environment;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.scripting.xmltags.XMLLanguageDriver;
import org.apache.ibatis.session.Configuration;
import org.apache.ibatis.transaction.jdbc.JdbcTransactionFactory;
import org.apache.ibatis.type.TypeAliasRegistry;
import org.junit.jupiter.api.Test;

import javax.sql.DataSource;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

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

class CcdiBankStatementMapperXmlTest {

    private static final String RESOURCE = "mapper/ccdi/project/CcdiBankStatementMapper.xml";

    @Test
    void selectStatementPage_shouldKeepWhitespaceBeforeDynamicAndConditions() throws Exception {
        Configuration configuration = new Configuration();
        configuration.setEnvironment(new Environment("test", new JdbcTransactionFactory(), new NoOpDataSource()));
        registerTypeAliases(configuration.getTypeAliasRegistry());
        configuration.getLanguageRegistry().register(XMLLanguageDriver.class);
        configuration.addMapper(CcdiBankStatementMapper.class);

        try (InputStream inputStream = getClass().getClassLoader().getResourceAsStream(RESOURCE)) {
            XMLMapperBuilder xmlMapperBuilder =
                    new XMLMapperBuilder(inputStream, configuration, RESOURCE, configuration.getSqlFragments());
            xmlMapperBuilder.parse();
        }

        MappedStatement mappedStatement = configuration.getMappedStatement(
                "com.ruoyi.ccdi.project.mapper.CcdiBankStatementMapper.selectStatementPage");
        CcdiBankStatementQueryDTO queryDTO = new CcdiBankStatementQueryDTO();
        queryDTO.setProjectId(33L);
        queryDTO.setTabType("in");

        Map<String, Object> params = new HashMap<>();
        params.put("query", queryDTO);

        BoundSql boundSql = mappedStatement.getBoundSql(params);
        String sql = boundSql.getSql().replaceAll("\\s+", " ").trim();

        assertFalse(sql.contains("?AND"), sql);
        assertTrue(sql.contains("(bs.project_id = ?) AND IFNULL"), sql);
    }

    @Test
    void statementFilterWhere_shouldWrapBaseConditionToAvoidParameterAndCollision() throws Exception {
        try (InputStream inputStream = getClass().getClassLoader().getResourceAsStream(RESOURCE)) {
            String xml = new String(inputStream.readAllBytes(), StandardCharsets.UTF_8);
            String projectIdLine = Arrays.stream(xml.split("\\R"))
                    .filter(line -> line.contains("bs.project_id = #{query.projectId}"))
                    .findFirst()
                    .orElse("");

            assertTrue(xml.contains("AND (bs.project_id = #{query.projectId})"), xml);
            assertTrue(projectIdLine.contains("AND (bs.project_id = #{query.projectId})"), projectIdLine);
        }
    }

    @Test
    void statementFilterWhere_shouldWrapIncludedCaseExpressionsToAvoidAndCaseCollision() throws Exception {
        try (InputStream inputStream = getClass().getClassLoader().getResourceAsStream(RESOURCE)) {
            String xml = new String(inputStream.readAllBytes(), StandardCharsets.UTF_8);

            assertTrue(xml.contains("AND (<include refid=\"parsedTrxDateExpr\"/>) <![CDATA[ >= ]]>"), xml);
            assertTrue(xml.contains("AND (<include refid=\"parsedTrxDateExpr\"/>) <![CDATA[ <= ]]>"), xml);
            assertTrue(xml.contains("AND (<include refid=\"absoluteAmountExpr\"/>) <![CDATA[ >= ]]>"), xml);
            assertTrue(xml.contains("AND (<include refid=\"absoluteAmountExpr\"/>) <![CDATA[ <= ]]>"), xml);
        }
    }

    @Test
    void selectStatementPage_shouldKeepWhitespaceBeforeDateRangeExpressions() throws Exception {
        MappedStatement mappedStatement = loadMappedStatement(
                "com.ruoyi.ccdi.project.mapper.CcdiBankStatementMapper.selectStatementPage");
        CcdiBankStatementQueryDTO queryDTO = new CcdiBankStatementQueryDTO();
        queryDTO.setProjectId(33L);
        queryDTO.setTransactionStartTime("2024-01-01 00:00:00");
        queryDTO.setTransactionEndTime("2024-01-31 23:59:59");

        String sql = renderSql(mappedStatement, queryDTO);

        assertFalse(sql.contains("ANDCASE"), sql);
        assertTrue(sql.contains("AND ( CASE WHEN bs.TRX_DATE"), sql);
        assertTrue(sql.contains("END ) >="), sql);
        assertTrue(sql.contains("END ) <="), sql);
    }

    @Test
    void selectStatementPage_shouldKeepWhitespaceBeforeAmountRangeExpressions() throws Exception {
        MappedStatement mappedStatement = loadMappedStatement(
                "com.ruoyi.ccdi.project.mapper.CcdiBankStatementMapper.selectStatementPage");
        CcdiBankStatementQueryDTO queryDTO = new CcdiBankStatementQueryDTO();
        queryDTO.setProjectId(33L);
        queryDTO.setAmountMin(java.math.BigDecimal.ONE);
        queryDTO.setAmountMax(new java.math.BigDecimal("100"));

        String sql = renderSql(mappedStatement, queryDTO);

        assertFalse(sql.contains("ANDCASE"), sql);
        assertTrue(sql.contains("AND ( CASE WHEN IFNULL(bs.AMOUNT_CR, 0) > 0"), sql);
        assertTrue(sql.contains("END ) >= ?"), sql);
        assertTrue(sql.contains("END ) <= ?"), sql);
    }

    @Test
    void selectCounterpartyPage_shouldGroupByTrimmedCounterpartyNameAndReuseFilters() throws Exception {
        MappedStatement mappedStatement = loadMappedStatement(
                "com.ruoyi.ccdi.project.mapper.CcdiBankStatementMapper.selectCounterpartyPage");
        CcdiBankStatementQueryDTO queryDTO = new CcdiBankStatementQueryDTO();
        queryDTO.setProjectId(33L);
        queryDTO.setTabType("out");
        queryDTO.setCounterpartyName("杭州");
        queryDTO.setOrderBy("totalAmount");
        queryDTO.setOrderDirection("desc");

        String sql = renderSql(mappedStatement, queryDTO);

        assertTrue(sql.contains("TRIM(IFNULL(bs.CUSTOMER_ACCOUNT_NAME, '')) AS customerAccountName"), sql);
        assertTrue(sql.contains("GROUP BY TRIM(IFNULL(bs.CUSTOMER_ACCOUNT_NAME, ''))"), sql);
        assertTrue(sql.contains("(bs.project_id = ?)"), sql);
        assertTrue(sql.contains("IFNULL(bs.AMOUNT_DR, 0) > 0"), sql);
        assertTrue(sql.contains("bs.CUSTOMER_ACCOUNT_NAME LIKE CONCAT('%', TRIM(?), '%')"), sql);
        assertTrue(sql.contains("ORDER BY totalAmount DESC, totalCount DESC, customerAccountName ASC"), sql);
    }

    @Test
    void selectStatementDetailById_shouldJoinUploadRecordForOriginalFileMetadata() throws Exception {
        try (InputStream inputStream = getClass().getClassLoader().getResourceAsStream(RESOURCE)) {
            String xml = new String(inputStream.readAllBytes(), StandardCharsets.UTF_8);

            assertTrue(
                    xml.contains("LEFT JOIN (")
                            && xml.contains("SELECT latest_record.project_id, latest_record.log_id, latest_record.file_name, latest_record.upload_time")
                            && xml.contains("MAX(id) AS max_id")
                            && xml.contains("fur.log_id = bs.batch_id AND fur.project_id = bs.project_id"),
                    xml
            );
            assertTrue(xml.contains("fur.file_name AS originalFileName"), xml);
            assertTrue(xml.contains("fur.upload_time AS uploadTime"), xml);
        }
    }

    @Test
    void fileUploadRecordMapper_shouldContainHistoryImportSourceFields() throws Exception {
        try (InputStream inputStream = getClass().getClassLoader()
                .getResourceAsStream("mapper/ccdi/project/CcdiFileUploadRecordMapper.xml")) {
            String xml = new String(inputStream.readAllBytes(), StandardCharsets.UTF_8);

            assertTrue(xml.contains("source_type"), xml);
            assertTrue(xml.contains("source_project_id"), xml);
            assertTrue(xml.contains("source_project_name"), xml);
        }

        try (InputStream inputStream = getClass().getClassLoader().getResourceAsStream(RESOURCE)) {
            String xml = new String(inputStream.readAllBytes(), StandardCharsets.UTF_8);

            assertTrue(xml.contains("fur.source_project_name"), xml);
        }
    }

    @Test
    void historyImportQueries_shouldFilterSuccessfulSourceBatchesAndDateRange() throws Exception {
        try (InputStream inputStream = getClass().getClassLoader()
                .getResourceAsStream("mapper/ccdi/project/CcdiFileUploadRecordMapper.xml")) {
            String xml = new String(inputStream.readAllBytes(), StandardCharsets.UTF_8);

            assertTrue(xml.contains("selectSuccessfulRecordsByProjectIds"), xml);
            assertTrue(xml.contains("file_status = 'parsed_success'"), xml);
            assertTrue(xml.contains("log_id is not null"), xml);
        }

        try (InputStream inputStream = getClass().getClassLoader().getResourceAsStream(RESOURCE)) {
            String xml = new String(inputStream.readAllBytes(), StandardCharsets.UTF_8);

            assertTrue(xml.contains("selectStatementsForHistoryImport"), xml);
            assertTrue(xml.contains("bs.project_id = #{projectId}"), xml);
            assertTrue(xml.contains("bs.batch_id = #{batchId}"), xml);
            assertTrue(xml.contains("#{startDate}"), xml);
            assertTrue(xml.contains("#{endDate}"), xml);
        }
    }

    @Test
    void selectStatementsForHistoryImport_shouldNotGenerateDuplicatedSelectKeyword() throws Exception {
        MappedStatement mappedStatement = loadMappedStatement(
                "com.ruoyi.ccdi.project.mapper.CcdiBankStatementMapper.selectStatementsForHistoryImport");

        Map<String, Object> params = new HashMap<>();
        params.put("projectId", 48L);
        params.put("batchId", 17094);
        params.put("startDate", "2026-03-17");
        params.put("endDate", "2026-03-18");

        BoundSql boundSql = mappedStatement.getBoundSql(params);
        String sql = boundSql.getSql().replaceAll("\\s+", " ").trim();

        assertFalse(sql.startsWith("SELECT select"), sql);
        assertTrue(sql.startsWith("SELECT bank_statement_id"), sql);
        assertFalse(sql.contains("FROM ccdi_bank_statement FROM ccdi_bank_statement"), sql);
        assertFalse(sql.contains("?AND"), sql);
        assertTrue(sql.contains("WHERE (bs.project_id = ?) AND (bs.batch_id = ?) AND ( CASE"), sql);
    }

    @Test
    void insertBatch_shouldAvoidUpdatingAutoIncrementPrimaryKeyInDuplicateBranch() throws Exception {
        try (InputStream inputStream = getClass().getClassLoader().getResourceAsStream(RESOURCE)) {
            String xml = new String(inputStream.readAllBytes(), StandardCharsets.UTF_8);

            assertTrue(xml.contains("on duplicate key update"), xml);
            assertTrue(xml.contains("batch_id = batch_id"), xml);
            assertFalse(xml.contains("bank_statement_id = bank_statement_id"), xml);
        }
    }

    @Test
    void mapperXml_shouldContainCounterpartyIdentityColumns() throws Exception {
        try (InputStream inputStream = getClass().getClassLoader().getResourceAsStream(RESOURCE)) {
            String xml = new String(inputStream.readAllBytes(), StandardCharsets.UTF_8);

            assertTrue(xml.contains("<result property=\"customerCertNo\" column=\"customer_cert_no\" />"), xml);
            assertTrue(
                    xml.contains("<result property=\"customerSocialCreditCode\" column=\"customer_social_credit_code\" />"),
                    xml
            );
            assertTrue(xml.contains("customer_bank, customer_reference, customer_cert_no, customer_social_credit_code,"), xml);
            assertTrue(xml.contains("#{item.customerBank}, #{item.customerReference}, #{item.customerCertNo}, #{item.customerSocialCreditCode},"), xml);
        }
    }

    @Test
    void targetCount_shouldOnlyUseStatementCretNoMatchedStaff() throws Exception {
        try (InputStream inputStream = getClass().getClassLoader().getResourceAsStream(RESOURCE)) {
            String xml = new String(inputStream.readAllBytes(), StandardCharsets.UTF_8);
            String selectSql = extractSelect(xml, "countMatchedStaffCountByProjectId");

            assertTrue(selectSql.contains("select count(distinct trim(bs.cret_no))"), selectSql);
            assertTrue(selectSql.contains("inner join ccdi_base_staff staff on staff.id_card = trim(bs.cret_no)"), selectSql);
            assertFalse(selectSql.contains("ccdi_staff_fmy_relation"), selectSql);
            assertFalse(selectSql.contains("ccdi_account_info"), selectSql);
            assertFalse(selectSql.contains("LE_ACCOUNT_NO"), selectSql);
        }
    }

    private MappedStatement loadMappedStatement(String statementId) throws Exception {
        Configuration configuration = new Configuration();
        configuration.setEnvironment(new Environment("test", new JdbcTransactionFactory(), new NoOpDataSource()));
        registerTypeAliases(configuration.getTypeAliasRegistry());
        configuration.getLanguageRegistry().register(XMLLanguageDriver.class);
        configuration.addMapper(CcdiBankStatementMapper.class);

        try (InputStream inputStream = getClass().getClassLoader().getResourceAsStream(RESOURCE)) {
            XMLMapperBuilder xmlMapperBuilder =
                    new XMLMapperBuilder(inputStream, configuration, RESOURCE, configuration.getSqlFragments());
            xmlMapperBuilder.parse();
        }
        return configuration.getMappedStatement(statementId);
    }

    private String renderSql(MappedStatement mappedStatement, CcdiBankStatementQueryDTO queryDTO) {
        Map<String, Object> params = new HashMap<>();
        params.put("query", queryDTO);
        BoundSql boundSql = mappedStatement.getBoundSql(params);
        return boundSql.getSql().replaceAll("\\s+", " ").trim();
    }

    private String extractSelect(String xml, String selectId) {
        String start = "<select id=\"" + selectId + "\"";
        int startIndex = xml.indexOf(start);
        assertTrue(startIndex >= 0, "missing select: " + selectId);
        int endIndex = xml.indexOf("</select>", startIndex);
        assertTrue(endIndex >= 0, "missing closing select tag: " + selectId);
        return xml.substring(startIndex, endIndex);
    }

    private void registerTypeAliases(TypeAliasRegistry typeAliasRegistry) {
        typeAliasRegistry.registerAlias("map", Map.class);
    }

    private static class NoOpDataSource implements DataSource {

        @Override
        public java.sql.Connection getConnection() {
            throw new UnsupportedOperationException("Not required for SQL rendering tests");
        }

        @Override
        public java.sql.Connection getConnection(String username, String password) {
            throw new UnsupportedOperationException("Not required for SQL rendering tests");
        }

        @Override
        public java.io.PrintWriter getLogWriter() {
            return null;
        }

        @Override
        public void setLogWriter(java.io.PrintWriter out) {
        }

        @Override
        public void setLoginTimeout(int seconds) {
        }

        @Override
        public int getLoginTimeout() {
            return 0;
        }

        @Override
        public java.util.logging.Logger getParentLogger() {
            return java.util.logging.Logger.getGlobal();
        }

        @Override
        public <T> T unwrap(Class<T> iface) {
            throw new UnsupportedOperationException("Not supported");
        }

        @Override
        public boolean isWrapperFor(Class<?> iface) {
            return false;
        }
    }
}
