package com.ga.batch.settlement.scenario;

import com.ga.batch.settlement.calc.CommissionCalculator;
import com.ga.batch.settlement.domain.BatchChargebackCalculator;
import com.ga.batch.settlement.domain.BatchInstallmentPlanGenerator;
import com.ga.batch.settlement.domain.BatchRegulatoryLimitChecker;
import com.ga.batch.settlement.domain.BatchTaxCalculator;
import com.ga.batch.settlement.domain.ChargebackResult;
import com.ga.batch.settlement.domain.TaxBreakdown;
import com.ga.common.system.SystemConfigService;
import com.ga.core.mapper.chargeback.ChargebackGradeMapper;
import com.ga.core.mapper.installment.InstallmentRatioMapper;
import com.ga.core.mapper.ledger.MaintainLedgerMapper;
import com.ga.core.mapper.ledger.RecruitLedgerMapper;
import com.ga.core.mapper.org.AgentMapper;
import com.ga.core.mapper.regulatory.RegulatoryLimitMapper;
import com.ga.core.mapper.rule.RuleMapper;
import com.ga.core.vo.chargeback.ChargebackGradeVO;
import com.ga.core.vo.installment.InstallmentPlanVO;
import com.ga.core.vo.installment.InstallmentRatioVO;
import com.ga.core.vo.ledger.LedgerVO;
import com.ga.core.vo.org.AgentVO;
import com.ga.core.vo.org.TaxType;
import com.ga.core.vo.org.VatType;
import com.ga.core.vo.product.ContractResp;
import com.ga.core.vo.regulatory.RegulatoryLimitVO;
import com.ga.batch.settlement.domain.ViolationResult;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.mockito.junit.jupiter.MockitoSettings;
import org.mockito.quality.Strictness;

import java.math.BigDecimal;
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.List;

import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.lenient;
import static org.mockito.Mockito.when;

/**
 * E2E 시나리오 테스트 — "보험이 어떻게 들어와서 수수료가 어떻게 나가는가".
 *
 * 계약 1건(POL-0001, 연납 1,000,000원, 김설계 등급5 모집)의 생애주기를
 * 실제 계산 코드로 단계별로 따라가며 검증한다.
 * 시나리오 문서: docs/시나리오_E2E_보험인입_수수료정산.md
 *
 * 등장인물·룰은 Mock 으로 주입(=테스트 데이터)하며 숫자는 DOMAIN_KNOWLEDGE 7장 예시와 일치한다.
 */
@ExtendWith(MockitoExtension.class)
@MockitoSettings(strictness = Strictness.LENIENT)
class SettlementScenarioTest {

    // ── 계산기가 의존하는 Mapper/Service (테스트 데이터 주입 지점) ──
    @Mock RuleMapper ruleMapper;
    @Mock AgentMapper agentMapper;
    @Mock ChargebackGradeMapper chargebackGradeMapper;
    @Mock InstallmentRatioMapper installmentRatioMapper;
    @Mock RegulatoryLimitMapper regulatoryLimitMapper;
    @Mock RecruitLedgerMapper recruitLedgerMapper;
    @Mock MaintainLedgerMapper maintainLedgerMapper;
    @Mock com.ga.core.mapper.product.ContractMapper contractMapper;
    @Mock SystemConfigService configService;

    // ── 실제 계산 코드 (테스트 대상) ──
    CommissionCalculator commissionCalculator;
    BatchRegulatoryLimitChecker regulatoryChecker;
    BatchInstallmentPlanGenerator installmentGenerator;
    BatchChargebackCalculator chargebackCalculator;
    BatchTaxCalculator taxCalculator;

    // ── 시나리오 상수 ──
    static final Long CONTRACT_ID = 100L;
    static final Long KIM = 10L;          // 원 모집설계사 김설계 (등급5)
    static final Long PARK = 20L;         // 후임 승계설계사 박승계 (등급3)
    static final Long PRODUCT_ID = 1L;
    static final String LIFE = "LIFE";
    static final LocalDate CONTRACT_DATE = LocalDate.of(2024, 1, 15);

    ContractResp contract;

    @BeforeEach
    void setUp() {
        commissionCalculator = new CommissionCalculator(ruleMapper, agentMapper);
        regulatoryChecker = new BatchRegulatoryLimitChecker(
                regulatoryLimitMapper, recruitLedgerMapper, maintainLedgerMapper, contractMapper);
        installmentGenerator = new BatchInstallmentPlanGenerator(installmentRatioMapper);
        chargebackCalculator = new BatchChargebackCalculator(chargebackGradeMapper);
        taxCalculator = new BatchTaxCalculator(configService);

        // 계약 (보험 인입)
        contract = new ContractResp();
        contract.setContractId(CONTRACT_ID);
        contract.setAgentId(KIM);
        contract.setProductId(PRODUCT_ID);
        contract.setPolicyNo("POL-0001");
        contract.setCompanyCode("SAMSUNG");
        contract.setProductCode("PRD-WL-001");
        contract.setInsuranceType("WHOLE_LIFE");
        contract.setPremium(new BigDecimal("1000000"));
        contract.setPayCycle("YEARLY");
        contract.setContractDate(CONTRACT_DATE);

        // 설계사 마스터
        AgentVO kim = new AgentVO();
        kim.setAgentId(KIM);
        kim.setGradeId(5L);
        AgentVO park = new AgentVO();
        park.setAgentId(PARK);
        park.setGradeId(3L);
        lenient().when(agentMapper.selectById(KIM)).thenReturn(kim);
        lenient().when(agentMapper.selectById(PARK)).thenReturn(park);

        // 세율: configService 는 기본값(2번째 인자)을 그대로 반환하도록
        lenient().when(configService.getDecimal(anyString(), any(BigDecimal.class)))
                .thenAnswer(inv -> inv.getArgument(1));
    }

    @Test
    @DisplayName("① 보험 인입 → 모집수수료 원장: 1,000,000 ×60% ×75% = 450,000, 세금 14,850")
    void s1_보험인입_모집수수료_원장계산() {
        when(ruleMapper.findCompanyRate(eq(PRODUCT_ID), eq(1), any(LocalDate.class)))
                .thenReturn(new BigDecimal("60.0"));
        when(ruleMapper.findPayoutRate(eq(5L), eq("WHOLE_LIFE"), eq(1), any(LocalDate.class)))
                .thenReturn(new BigDecimal("75.0"));

        LedgerVO led = commissionCalculator.calcRecruitLedger(contract, "202401");

        assertThat(led.getAgentId()).isEqualTo(KIM);
        assertThat(led.getCommissionYear()).isEqualTo(1);
        assertThat(led.getCompanyAmount()).isEqualByComparingTo("600000.00"); // 보험사지급액
        assertThat(led.getAgentAmount()).isEqualByComparingTo("450000.00");   // gross
        assertThat(led.getTaxAmount()).isEqualByComparingTo("14850.00");      // 3.3%
    }

    @Test
    @DisplayName("② 1200% 룰: 누적 한도 이내면 통과, 초과분은 위반으로 차단")
    void s2_규제_1200퍼센트_한도_통과및위반() {
        RegulatoryLimitVO limit = new RegulatoryLimitVO();
        limit.setLimitType("TOTAL_RATIO");
        limit.setLimitValue(new BigDecimal("12000000")); // 1,000,000 × 1200%
        limit.setUnit("KRW");
        when(regulatoryLimitMapper.selectActiveByType(eq("TOTAL_RATIO"), any(), anyString()))
                .thenReturn(limit);
        when(recruitLedgerMapper.sumByContract(CONTRACT_ID)).thenReturn(new BigDecimal("450000"));
        when(maintainLedgerMapper.sumByContract(CONTRACT_ID)).thenReturn(BigDecimal.ZERO);

        // 추가 100만원 → 누적 550,000 ≤ 12,000,000 : 통과
        ViolationResult ok = regulatoryChecker.checkContractTotal(
                CONTRACT_ID, new BigDecimal("100000"), CONTRACT_DATE);
        assertThat(ok.violated()).isFalse();

        // 추가 1,200만원 → 누적 12,450,000 > 12,000,000 : 위반
        ViolationResult bad = regulatoryChecker.checkContractTotal(
                CONTRACT_ID, new BigDecimal("12000000"), CONTRACT_DATE);
        assertThat(bad.violated()).isTrue();
        assertThat(bad.currentTotal()).isEqualByComparingTo("450000");
    }

    @Test
    @DisplayName("② 1200%룰 단위변환: PERCENT 한도(35%)는 연환산보험료 기준 KRW로 환산 비교")
    void s2b_규제_PERCENT한도_연보험료기준_KRW환산() {
        // 1차년 한도 35% (PERCENT). 실제 KRW 한도 = 연납보험료 1,000,000 × 35% = 350,000
        RegulatoryLimitVO limit = new RegulatoryLimitVO();
        limit.setLimitType("FIRST_YEAR_RATIO");
        limit.setLimitValue(new BigDecimal("35"));
        limit.setUnit("PERCENT");
        when(regulatoryLimitMapper.selectActiveByType(eq("FIRST_YEAR_RATIO"), any(), anyString()))
                .thenReturn(limit);
        com.ga.core.vo.product.ContractVO cv = new com.ga.core.vo.product.ContractVO();
        cv.setPremium(new BigDecimal("1000000"));
        when(contractMapper.selectById(CONTRACT_ID)).thenReturn(cv);
        when(recruitLedgerMapper.sumFirstYearByContract(CONTRACT_ID)).thenReturn(BigDecimal.ZERO);

        // 모집수수료 450,000 > 한도 350,000 → 위반 (이전엔 35(KRW)와 비교돼 항상 위반이던 버그)
        ViolationResult bad = regulatoryChecker.checkFirstYear(
                CONTRACT_ID, new BigDecimal("450000"), CONTRACT_DATE);
        assertThat(bad.violated()).isTrue();
        assertThat(bad.limitAmount()).isEqualByComparingTo("350000.00");

        // 모집수수료 300,000 ≤ 350,000 → 통과
        ViolationResult ok = regulatoryChecker.checkFirstYear(
                CONTRACT_ID, new BigDecimal("300000"), CONTRACT_DATE);
        assertThat(ok.violated()).isFalse();
    }

    @Test
    @DisplayName("③ 분급 7년 분할: 35/25/15/10/7/5/3 → 합계 정확히 450,000")
    void s3_분급_7년_분할_합계100퍼센트() {
        when(installmentRatioMapper.selectAllActiveRatios(anyString(), anyString()))
                .thenReturn(standardRatios());

        List<InstallmentPlanVO> plans = installmentGenerator.generate(
                CONTRACT_ID, new BigDecimal("450000"), CONTRACT_DATE, LIFE);

        assertThat(plans).hasSize(7);
        assertThat(plans.get(0).getExpectedAmount()).isEqualByComparingTo("157500"); // 35%
        assertThat(plans.get(6).getExpectedAmount()).isEqualByComparingTo("13500");  // 3%
        assertThat(sumExpected(plans)).isEqualByComparingTo("450000");
    }

    @Test
    @DisplayName("③ 분급 라운딩 잔차: 나눠떨어지지 않는 435,150도 합계가 원금과 정확히 일치")
    void s3b_분급_라운딩_잔차_마지막회차_흡수() {
        when(installmentRatioMapper.selectAllActiveRatios(anyString(), anyString()))
                .thenReturn(standardRatios());

        BigDecimal original = new BigDecimal("435150");
        List<InstallmentPlanVO> plans = installmentGenerator.generate(
                CONTRACT_ID, original, CONTRACT_DATE, LIFE);

        // 무결성: 분급 합계 == 원 수수료 (반올림 잔차가 새지 않아야 함)
        assertThat(sumExpected(plans)).isEqualByComparingTo(original);
    }

    @Test
    @DisplayName("④ 유지수수료 2차년: 1,000,000 ×10% ×50% = 50,000")
    void s4_유지수수료_2차년() {
        when(ruleMapper.findCompanyRate(eq(PRODUCT_ID), eq(2), any(LocalDate.class)))
                .thenReturn(new BigDecimal("10.0"));
        when(ruleMapper.findPayoutRate(eq(5L), eq("WHOLE_LIFE"), eq(2), any(LocalDate.class)))
                .thenReturn(new BigDecimal("50.0"));

        LedgerVO led = commissionCalculator.calcMaintainLedger(contract, "202501", 2);

        assertThat(led.getAgentId()).isEqualTo(KIM);
        assertThat(led.getCommissionYear()).isEqualTo(2);
        assertThat(led.getAgentAmount()).isEqualByComparingTo("50000.00");
    }

    @Test
    @DisplayName("⑤ 계약 승계: 유지수수료는 후임(박승계 등급3 40%)에게 귀속, 모집은 원설계사 유지")
    void s5_계약승계_유지수수료_후임귀속() {
        when(ruleMapper.findCompanyRate(eq(PRODUCT_ID), eq(2), any(LocalDate.class)))
                .thenReturn(new BigDecimal("10.0"));
        // 후임 박승계 등급3 의 2차년 지급률 40%
        when(ruleMapper.findPayoutRate(eq(3L), eq("WHOLE_LIFE"), eq(2), any(LocalDate.class)))
                .thenReturn(new BigDecimal("40.0"));

        LedgerVO led = commissionCalculator.calcMaintainLedger(contract, "202501", 2, PARK);

        // 후임에게 귀속 + 후임 등급 지급률로 계산
        assertThat(led.getAgentId()).isEqualTo(PARK);
        assertThat(led.getAgentAmount()).isEqualByComparingTo("40000.00"); // 100,000 × 40%
        // 모집수수료는 여전히 원 모집설계사 김설계
        assertThat(contract.getAgentId()).isEqualTo(KIM);
    }

    @Test
    @DisplayName("⑦ 조기 해지 환수: 13개월차 해지 → 50% → 450,000 ×50% = 225,000")
    void s6_조기해지_차등환수_13개월_50퍼센트() {
        ChargebackGradeVO grade = new ChargebackGradeVO();
        grade.setProductCategory(LIFE);
        grade.setMonthsFrom(13);
        grade.setMonthsTo(24);
        grade.setChargebackPercent(new BigDecimal("50"));
        when(chargebackGradeMapper.selectByMonths(eq(LIFE), eq(13), anyString()))
                .thenReturn(grade);

        ChargebackResult r = chargebackCalculator.calculate(
                CONTRACT_DATE, CONTRACT_DATE.plusMonths(13),
                new BigDecimal("450000"), LIFE);

        assertThat(r.monthsElapsed()).isEqualTo(13);
        assertThat(r.chargebackAmount()).isEqualByComparingTo("225000");
    }

    @Nested
    @DisplayName("⑧ 집계 → 세금 → 실지급액(net)")
    class TaxStage {

        @Test
        @DisplayName("과세사업자(TAXABLE): 450,000 → net 478,665, 원천세 16,335")
        void s7_세금_실지급액_과세사업자() {
            TaxBreakdown t = taxCalculator.calculate(
                    new BigDecimal("450000"), TaxType.BUSINESS_INCOME, VatType.TAXABLE);

            assertThat(t.incomeTax()).isEqualByComparingTo("14850"); // 3.3%
            assertThat(t.localTax()).isEqualByComparingTo("1485");   // 소득세의 10%
            assertThat(t.vat()).isEqualByComparingTo("45000");       // 10%
            assertThat(t.netAmount()).isEqualByComparingTo("478665");
            // settle_master.tax_amount = 소득세 + 지방세
            assertThat(t.incomeTax().add(t.localTax())).isEqualByComparingTo("16335");
        }

        @Test
        @DisplayName("비과세(NONE): 부가세 없음 → net 433,665")
        void s8_세금_비과세() {
            TaxBreakdown t = taxCalculator.calculate(
                    new BigDecimal("450000"), TaxType.BUSINESS_INCOME, VatType.NONE);

            assertThat(t.vat()).isEqualByComparingTo("0");
            assertThat(t.netAmount()).isEqualByComparingTo("433665");
        }

        @Test
        @DisplayName("음수 gross(환수>지급): 원천세 0, net=원금 — 음수 세금이 신고에 전파되지 않음")
        void s9_세금_음수gross_세금0() {
            TaxBreakdown t = taxCalculator.calculate(
                    new BigDecimal("-300000"), TaxType.BUSINESS_INCOME, VatType.TAXABLE);

            assertThat(t.incomeTax()).isEqualByComparingTo("0");
            assertThat(t.localTax()).isEqualByComparingTo("0");
            assertThat(t.vat()).isEqualByComparingTo("0");
            assertThat(t.netAmount()).isEqualByComparingTo("-300000");
        }
    }

    // ── 헬퍼: 표준 7년 분급 비율 (35/25/15/10/7/5/3) ──
    private static List<InstallmentRatioVO> standardRatios() {
        int[] pct = {35, 25, 15, 10, 7, 5, 3};
        List<InstallmentRatioVO> list = new ArrayList<>();
        for (int i = 0; i < pct.length; i++) {
            InstallmentRatioVO r = new InstallmentRatioVO();
            r.setPlanYear(i + 1);
            r.setRatioPercent(new BigDecimal(pct[i]));
            list.add(r);
        }
        return list;
    }

    private static BigDecimal sumExpected(List<InstallmentPlanVO> plans) {
        return plans.stream()
                .map(InstallmentPlanVO::getExpectedAmount)
                .reduce(BigDecimal.ZERO, BigDecimal::add);
    }
}
