package com.ga.batch.settlement.step;

import com.ga.batch.settlement.SettlementContext;
import com.ga.batch.settlement.calc.CommissionCalculator;
import com.ga.batch.settlement.domain.BatchInstallmentPlanGenerator;
import com.ga.batch.settlement.domain.BatchRegulatoryLimitChecker;
import com.ga.batch.settlement.domain.ViolationResult;
import com.ga.core.mapper.installment.InstallmentPlanMapper;
import com.ga.core.mapper.ledger.RecruitLedgerMapper;
import com.ga.core.mapper.product.ContractMapper;
import com.ga.core.vo.installment.InstallmentPlanVO;
import com.ga.core.vo.ledger.LedgerVO;
import com.ga.core.vo.product.ContractResp;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.ArgumentCaptor;
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.List;

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

/**
 * 1200%룰 1차년 한도초과 → clip(한도까지만 지급) + 이연(초과분 분급) 검증.
 */
@ExtendWith(MockitoExtension.class)
@MockitoSettings(strictness = Strictness.LENIENT)
class CalcRecruitStepTest {

    @Mock SettlementContext ctx;
    @Mock ContractMapper contractMapper;
    @Mock RecruitLedgerMapper recruitMapper;
    @Mock CommissionCalculator calculator;
    @Mock BatchRegulatoryLimitChecker regulatoryChecker;
    @Mock BatchInstallmentPlanGenerator installmentGenerator;
    @Mock InstallmentPlanMapper installmentPlanMapper;

    CalcRecruitStep step;

    static final String MONTH = "202401";
    static final Long CONTRACT_ID = 100L;
    static final LocalDate CONTRACT_DATE = LocalDate.of(2024, 1, 15);

    @BeforeEach
    void setUp() {
        step = new CalcRecruitStep(ctx, contractMapper, recruitMapper,
                calculator, regulatoryChecker, installmentGenerator, installmentPlanMapper);
        when(ctx.getSettleMonth()).thenReturn(MONTH);
        // 기본: 두 한도 모두 위반 아님(각 테스트에서 필요한 한도만 violated 로 오버라이드)
        when(regulatoryChecker.checkFirstYear(any(), any(BigDecimal.class), any(LocalDate.class)))
                .thenReturn(ViolationResult.ok(BigDecimal.ZERO, null, "FIRST_YEAR_RATIO"));
        when(regulatoryChecker.checkContractTotal(any(), any(BigDecimal.class), any(LocalDate.class)))
                .thenReturn(ViolationResult.ok(BigDecimal.ZERO, null, "TOTAL_RATIO"));
    }

    private ContractResp contract() {
        ContractResp c = new ContractResp();
        c.setContractId(CONTRACT_ID);
        c.setAgentId(10L);
        c.setInsuranceType("WHOLE_LIFE");
        c.setContractDate(CONTRACT_DATE);
        return c;
    }

    private LedgerVO recruitLedger(ContractResp c, String amount, String tax) {
        LedgerVO led = new LedgerVO();
        led.setContractId(CONTRACT_ID);
        led.setAgentId(10L);
        led.setAgentAmount(new BigDecimal(amount));
        led.setTaxAmount(new BigDecimal(tax));
        when(calculator.calcRecruitLedger(eq(c), eq(MONTH))).thenReturn(led);
        return led;
    }

    @Test
    @DisplayName("1차년 한도 350,000 < 모집수수료 450,000 → 350,000 지급 + 초과 100,000 분급 이연")
    void firstYearLimit_clipAndDefer() {
        ContractResp c = new ContractResp();
        c.setContractId(CONTRACT_ID);
        c.setAgentId(10L);
        c.setInsuranceType("WHOLE_LIFE");
        c.setContractDate(CONTRACT_DATE);
        when(contractMapper.selectList(any())).thenReturn(List.of(c));

        LedgerVO led = new LedgerVO();
        led.setContractId(CONTRACT_ID);
        led.setAgentId(10L);
        led.setAgentAmount(new BigDecimal("450000"));
        led.setTaxAmount(new BigDecimal("14850"));
        when(calculator.calcRecruitLedger(eq(c), eq(MONTH))).thenReturn(led);

        // 한도 350,000(=연납 1,000,000 × 35%), 기지급 0 → 위반
        when(regulatoryChecker.checkFirstYear(eq(CONTRACT_ID), any(BigDecimal.class), any(LocalDate.class)))
                .thenReturn(ViolationResult.violated(
                        BigDecimal.ZERO, new BigDecimal("350000"), "FIRST_YEAR_RATIO", "초과"));

        when(installmentGenerator.generate(eq(CONTRACT_ID), any(BigDecimal.class), any(LocalDate.class), anyString()))
                .thenReturn(List.of(new InstallmentPlanVO()));

        ArgumentCaptor<List<LedgerVO>> ledgerCaptor = ArgumentCaptor.forClass(List.class);
        when(recruitMapper.batchInsert(ledgerCaptor.capture())).thenReturn(1);

        step.execute(null, null);

        // 초과분 100,000(=450,000−350,000) 이 분급으로 이연 생성
        ArgumentCaptor<BigDecimal> excessCaptor = ArgumentCaptor.forClass(BigDecimal.class);
        org.mockito.Mockito.verify(installmentGenerator)
                .generate(eq(CONTRACT_ID), excessCaptor.capture(), any(LocalDate.class), anyString());
        assertThat(excessCaptor.getValue()).isEqualByComparingTo("100000");

        // 당월 원장은 한도까지만(350,000) + 세액 재계산(350,000 × 3.3% = 11,550)
        LedgerVO saved = ledgerCaptor.getValue().get(0);
        assertThat(saved.getAgentAmount()).isEqualByComparingTo("350000");
        assertThat(saved.getTaxAmount()).isEqualByComparingTo("11550.00");
    }

    @Test
    @DisplayName("1200% 누적한도 잔여 300,000 < 모집수수료 450,000 → 300,000 지급 + 초과 150,000 분급 이연")
    void contractTotalLimit_clipAndDefer() {
        ContractResp c = contract();
        when(contractMapper.selectList(any())).thenReturn(List.of(c));
        recruitLedger(c, "450000", "14850");

        // 1차년은 통과(기본), 누적 1200% 한도만 위반: 한도 12,000,000 / 기지급 11,700,000 → 잔여 300,000
        when(regulatoryChecker.checkContractTotal(eq(CONTRACT_ID), any(BigDecimal.class), any(LocalDate.class)))
                .thenReturn(ViolationResult.violated(
                        new BigDecimal("11700000"), new BigDecimal("12000000"), "TOTAL_RATIO", "초과"));
        when(installmentGenerator.generate(eq(CONTRACT_ID), any(BigDecimal.class), any(LocalDate.class), anyString()))
                .thenReturn(List.of(new InstallmentPlanVO()));
        ArgumentCaptor<List<LedgerVO>> ledgerCaptor = ArgumentCaptor.forClass(List.class);
        when(recruitMapper.batchInsert(ledgerCaptor.capture())).thenReturn(1);

        step.execute(null, null);

        ArgumentCaptor<BigDecimal> excessCaptor = ArgumentCaptor.forClass(BigDecimal.class);
        org.mockito.Mockito.verify(installmentGenerator)
                .generate(eq(CONTRACT_ID), excessCaptor.capture(), any(LocalDate.class), anyString());
        assertThat(excessCaptor.getValue()).isEqualByComparingTo("150000");

        LedgerVO saved = ledgerCaptor.getValue().get(0);
        assertThat(saved.getAgentAmount()).isEqualByComparingTo("300000");
        assertThat(saved.getTaxAmount()).isEqualByComparingTo("9900.00");
    }

    @Test
    @DisplayName("1차년·누적 둘 다 위반 → 더 제한적인 누적한도 잔여 300,000 으로 clip")
    void bothLimits_mostRestrictiveApplies() {
        ContractResp c = contract();
        when(contractMapper.selectList(any())).thenReturn(List.of(c));
        recruitLedger(c, "450000", "14850");

        // 1차년 잔여 350,000, 누적 잔여 300,000 → min(450000, 350000, 300000) = 300,000
        when(regulatoryChecker.checkFirstYear(eq(CONTRACT_ID), any(BigDecimal.class), any(LocalDate.class)))
                .thenReturn(ViolationResult.violated(
                        BigDecimal.ZERO, new BigDecimal("350000"), "FIRST_YEAR_RATIO", "초과"));
        when(regulatoryChecker.checkContractTotal(eq(CONTRACT_ID), any(BigDecimal.class), any(LocalDate.class)))
                .thenReturn(ViolationResult.violated(
                        new BigDecimal("11700000"), new BigDecimal("12000000"), "TOTAL_RATIO", "초과"));
        when(installmentGenerator.generate(eq(CONTRACT_ID), any(BigDecimal.class), any(LocalDate.class), anyString()))
                .thenReturn(List.of(new InstallmentPlanVO()));
        ArgumentCaptor<List<LedgerVO>> ledgerCaptor = ArgumentCaptor.forClass(List.class);
        when(recruitMapper.batchInsert(ledgerCaptor.capture())).thenReturn(1);

        step.execute(null, null);

        ArgumentCaptor<BigDecimal> excessCaptor = ArgumentCaptor.forClass(BigDecimal.class);
        org.mockito.Mockito.verify(installmentGenerator)
                .generate(eq(CONTRACT_ID), excessCaptor.capture(), any(LocalDate.class), anyString());
        assertThat(excessCaptor.getValue()).isEqualByComparingTo("150000");

        LedgerVO saved = ledgerCaptor.getValue().get(0);
        assertThat(saved.getAgentAmount()).isEqualByComparingTo("300000");
    }
}
