package com.ga.batch.settlement.transform;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.ga.core.mapper.ledger.MaintainLedgerMapper;
import com.ga.core.mapper.ledger.RecruitLedgerMapper;
import com.ga.core.mapper.product.ContractMapper;
import com.ga.core.mapper.receive.ReceiveMapper;
import com.ga.core.vo.ledger.LedgerVO;
import com.ga.core.vo.product.ContractResp;
import com.ga.core.vo.product.ContractSearchParam;
import com.ga.core.vo.product.ContractVO;
import com.ga.core.vo.receive.CompanyFieldMappingVO;
import com.ga.core.vo.receive.ParseErrorLogVO;
import com.ga.core.vo.receive.RawCommissionDataVO;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor;

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.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

class MappingEngineTest {

    private ReceiveMapper receiveMapper;
    private ContractMapper contractMapper;
    private RecruitLedgerMapper recruitMapper;
    private MaintainLedgerMapper maintainMapper;
    private MappingEngine engine;

    @BeforeEach
    void setUp() {
        receiveMapper = mock(ReceiveMapper.class);
        contractMapper = mock(ContractMapper.class);
        recruitMapper = mock(RecruitLedgerMapper.class);
        maintainMapper = mock(MaintainLedgerMapper.class);
        engine = new MappingEngine(receiveMapper, contractMapper, recruitMapper, maintainMapper, new ObjectMapper());
    }

    /** 매핑 정의 1건 생성 헬퍼 */
    private CompanyFieldMappingVO mapping(String src, String tgt, String dataType, String rule, String defVal, String required) {
        CompanyFieldMappingVO m = new CompanyFieldMappingVO();
        m.setSourceField(src);
        m.setTargetField(tgt);
        m.setDataType(dataType);
        m.setTransformRule(rule);
        m.setDefaultValue(defVal);
        m.setIsRequired(required);
        return m;
    }

    /** raw 1건 + 정상 mapping 정의 + 계약 매칭까지 갖춘 행복 경로 셋업 */
    private RawCommissionDataVO basicRecruitRaw(String json) {
        RawCommissionDataVO raw = new RawCommissionDataVO();
        raw.setRawId(1000L);
        raw.setCompanyCode("SAMSUNG");
        raw.setSettleMonth("202405");
        raw.setDataType("RECRUIT");
        raw.setRawContent(json);
        return raw;
    }

    private void setupContractMatch(String policyNo, Long contractId, Long agentId) {
        ContractResp resp = new ContractResp();
        resp.setContractId(contractId);
        when(contractMapper.selectList(any(ContractSearchParam.class)))
                .thenReturn(List.of(resp));

        ContractVO vo = new ContractVO();
        vo.setContractId(contractId);
        vo.setAgentId(agentId);
        vo.setPolicyNo(policyNo);
        vo.setPremium(new BigDecimal("500000"));
        vo.setPayCycle("MONTHLY");
        when(contractMapper.selectById(contractId)).thenReturn(vo);
    }

    @Test
    @DisplayName("RECRUIT 행복경로: TRIM/DATE/CODE_MAP 모두 적용 + recruitMapper.insert 호출")
    void recruit_happyPath() {
        String json = """
                {"policyNo":"  POL-001  ", "premium":"1000000", "amt":"75000",
                 "agentAmt":"45000", "compRate":"7.5", "payRate":"60",
                 "type":"WL_RAW"}
                """;
        RawCommissionDataVO raw = basicRecruitRaw(json);

        when(receiveMapper.selectFieldMappings("SAMSUNG", "recruit_ledger")).thenReturn(List.of(
                mapping("policyNo",  "policy_no",      "STRING",  "TRIM",     null, "Y"),
                mapping("premium",   "premium",        "DECIMAL", null,        null, "N"),
                mapping("amt",       "company_amount", "DECIMAL", null,        null, "N"),
                mapping("agentAmt",  "agent_amount",   "DECIMAL", null,        null, "N"),
                mapping("compRate",  "company_rate",   "DECIMAL", null,        null, "N"),
                mapping("payRate",   "payout_rate",    "DECIMAL", null,        null, "N"),
                mapping("type",      "insurance_type", "STRING",  "CODE_MAP", "INSURANCE_TYPE", "N")
        ));
        when(receiveMapper.findTargetCode("SAMSUNG", "INSURANCE_TYPE", "WL_RAW")).thenReturn("WHOLE_LIFE");
        setupContractMatch("POL-001", 100L, 10L);

        Long ledgerId = engine.transform(raw);

        // recruit insert 호출 / maintain 미호출
        ArgumentCaptor<LedgerVO> cap = ArgumentCaptor.forClass(LedgerVO.class);
        verify(recruitMapper).insert(cap.capture());
        verify(maintainMapper, never()).insert(any());

        LedgerVO led = cap.getValue();
        assertThat(led.getContractId()).isEqualTo(100L);
        assertThat(led.getAgentId()).isEqualTo(10L);
        assertThat(led.getPolicyNo()).isEqualTo("POL-001");          // TRIM
        assertThat(led.getCompanyCode()).isEqualTo("SAMSUNG");
        assertThat(led.getInsuranceType()).isEqualTo("WHOLE_LIFE");  // CODE_MAP 변환
        assertThat(led.getPremium()).isEqualByComparingTo("1000000");
        assertThat(led.getCompanyAmount()).isEqualByComparingTo("75000");
        assertThat(led.getAgentAmount()).isEqualByComparingTo("45000");
        assertThat(led.getCompanyRate()).isEqualByComparingTo("7.5");
        assertThat(led.getPayoutRate()).isEqualByComparingTo("60");
        // 세금은 agent_amount × 3.3% 자동 계산: 45000 × 3.3% = 1485
        assertThat(led.getTaxAmount()).isEqualByComparingTo("1485.00");
        assertThat(led.getSettleMonth()).isEqualTo("202405");
        assertThat(led.getStatus()).isEqualTo("CALCULATED");
        assertThat(led.getReconcileStatus()).isEqualTo("PENDING");
        // ledger 의 ID 는 insert 후 채워지지만, 반환값은 mapped LedgerId 그대로
        assertThat(ledgerId).isEqualTo(led.getLedgerId());
    }

    @Test
    @DisplayName("MAINTAIN 데이터타입은 maintainMapper.insert 로 라우팅")
    void maintain_routedToMaintainMapper() {
        String json = """
                {"policyNo":"POL-002", "premium":"500000"}
                """;
        RawCommissionDataVO raw = new RawCommissionDataVO();
        raw.setRawId(2000L);
        raw.setCompanyCode("HANWHA");
        raw.setSettleMonth("202405");
        raw.setDataType("MAINTAIN");
        raw.setRawContent(json);

        when(receiveMapper.selectFieldMappings("HANWHA", "maintain_ledger")).thenReturn(List.of(
                mapping("policyNo", "policy_no", "STRING",  null, null, "Y"),
                mapping("premium",  "premium",   "DECIMAL", null, null, "N")
        ));
        setupContractMatch("POL-002", 200L, 20L);

        engine.transform(raw);

        verify(maintainMapper).insert(any(LedgerVO.class));
        verify(recruitMapper, never()).insert(any());
    }

    @Test
    @DisplayName("UPPER/LOWER/DIVIDE/MULTIPLY 변환 규칙 검증")
    void multipleTransformRules() {
        String json = """
                {"upper":"hello", "lower":"WORLD",
                 "div":"100000", "mul":"5"}
                """;
        RawCommissionDataVO raw = basicRecruitRaw(json);
        // policy_no 도 필수이므로 추가
        String j2 = json.replace("{", "{\"pol\":\"POL-X\",");
        raw.setRawContent(j2);

        when(receiveMapper.selectFieldMappings("SAMSUNG", "recruit_ledger")).thenReturn(List.of(
                mapping("pol",    "policy_no",      "STRING",  "TRIM",     null,  "Y"),
                // attr1 등 표준 외 필드는 mapped 에 들어가지만 LedgerVO 에 매핑되진 않음 — 변환만 검증
                mapping("upper",  "product_code",   "STRING",  "UPPER",    null,  "N"),
                mapping("lower",  "pay_method",     "STRING",  "LOWER",    null,  "N"),
                mapping("div",    "agent_amount",   "DECIMAL", "DIVIDE",   "100", "N"),
                mapping("mul",    "company_amount", "DECIMAL", "MULTIPLY", "1000","N")
        ));
        setupContractMatch("POL-X", 300L, 30L);

        engine.transform(raw);

        ArgumentCaptor<LedgerVO> cap = ArgumentCaptor.forClass(LedgerVO.class);
        verify(recruitMapper).insert(cap.capture());
        LedgerVO led = cap.getValue();

        assertThat(led.getProductCode()).isEqualTo("HELLO");        // UPPER
        assertThat(led.getPayMethod()).isEqualTo("world");          // LOWER
        // DIVIDE: 100000 / 100 = 1000.00
        assertThat(led.getAgentAmount()).isEqualByComparingTo("1000.00");
        // MULTIPLY: 5 × 1000 = 5000
        assertThat(led.getCompanyAmount()).isEqualByComparingTo("5000");
    }

    @Test
    @DisplayName("DATE 변환: defaultValue 의 패턴으로 파싱")
    void dateTransform() {
        // mapped 의 "contract_date" 등은 LedgerVO 에 직접 안 들어가므로,
        // 변환만 일어나고 buildLedger 에서 기본값을 쓰는지 확인하기 어려움.
        // 대신 변환 자체가 실패해서 에러가 안 나는지로 확인.
        String json = """
                {"pol":"POL-D", "dateField":"20240515"}
                """;
        RawCommissionDataVO raw = basicRecruitRaw(json);
        raw.setRawContent(json);

        when(receiveMapper.selectFieldMappings("SAMSUNG", "recruit_ledger")).thenReturn(List.of(
                mapping("pol",       "policy_no",     "STRING", "TRIM", null,       "Y"),
                mapping("dateField", "contract_date", "DATE",   "DATE", "yyyyMMdd", "N")
        ));
        setupContractMatch("POL-D", 400L, 40L);

        Long id = engine.transform(raw);

        // 정상 처리 (예외 없음)
        verify(recruitMapper).insert(any(LedgerVO.class));
        verify(receiveMapper, never()).insertError(any());
    }

    @Test
    @DisplayName("INVALID_INPUT: company_code 가 null 이면 에러 로그 + null 반환")
    void invalidInput_nullCompanyCode() {
        RawCommissionDataVO raw = new RawCommissionDataVO();
        raw.setRawId(1L);
        raw.setRawContent("{}");
        raw.setDataType("RECRUIT");
        // companyCode 누락

        Long id = engine.transform(raw);

        assertThat(id).isNull();
        ArgumentCaptor<ParseErrorLogVO> cap = ArgumentCaptor.forClass(ParseErrorLogVO.class);
        verify(receiveMapper).insertError(cap.capture());
        assertThat(cap.getValue().getErrorType()).isEqualTo("INVALID_INPUT");
        verify(recruitMapper, never()).insert(any());
    }

    @Test
    @DisplayName("UNKNOWN_TYPE: 지원하지 않는 data_type")
    void unknownDataType() {
        RawCommissionDataVO raw = new RawCommissionDataVO();
        raw.setRawId(2L);
        raw.setCompanyCode("SAMSUNG");
        raw.setRawContent("{}");
        raw.setDataType("STRANGE");

        Long id = engine.transform(raw);

        assertThat(id).isNull();
        ArgumentCaptor<ParseErrorLogVO> cap = ArgumentCaptor.forClass(ParseErrorLogVO.class);
        verify(receiveMapper).insertError(cap.capture());
        assertThat(cap.getValue().getErrorType()).isEqualTo("UNKNOWN_TYPE");
    }

    @Test
    @DisplayName("NO_MAPPING: 매핑 정의가 비어있음")
    void noMapping() {
        RawCommissionDataVO raw = basicRecruitRaw("{}");
        when(receiveMapper.selectFieldMappings("SAMSUNG", "recruit_ledger"))
                .thenReturn(new ArrayList<>());

        Long id = engine.transform(raw);

        assertThat(id).isNull();
        ArgumentCaptor<ParseErrorLogVO> cap = ArgumentCaptor.forClass(ParseErrorLogVO.class);
        verify(receiveMapper).insertError(cap.capture());
        assertThat(cap.getValue().getErrorType()).isEqualTo("NO_MAPPING");
    }

    @Test
    @DisplayName("JSON_PARSE: 잘못된 raw_content")
    void jsonParseError() {
        RawCommissionDataVO raw = basicRecruitRaw("{not-json");
        when(receiveMapper.selectFieldMappings(any(), any())).thenReturn(List.of(
                mapping("a", "policy_no", "STRING", null, null, "Y")
        ));

        Long id = engine.transform(raw);

        assertThat(id).isNull();
        ArgumentCaptor<ParseErrorLogVO> cap = ArgumentCaptor.forClass(ParseErrorLogVO.class);
        verify(receiveMapper).insertError(cap.capture());
        assertThat(cap.getValue().getErrorType()).isEqualTo("JSON_PARSE");
    }

    @Test
    @DisplayName("REQUIRED: 필수 필드가 raw 에 없음")
    void requiredMissing() {
        RawCommissionDataVO raw = basicRecruitRaw("{\"other\":\"x\"}");
        when(receiveMapper.selectFieldMappings(any(), any())).thenReturn(List.of(
                mapping("policyNo", "policy_no", "STRING", "TRIM", null, "Y") // 필수
        ));

        Long id = engine.transform(raw);

        assertThat(id).isNull();
        ArgumentCaptor<ParseErrorLogVO> cap = ArgumentCaptor.forClass(ParseErrorLogVO.class);
        verify(receiveMapper).insertError(cap.capture());
        assertThat(cap.getValue().getErrorType()).isEqualTo("REQUIRED");
        assertThat(cap.getValue().getErrorField()).isEqualTo("policy_no");
    }

    @Test
    @DisplayName("NO_POLICY: policy_no 매핑이 아예 정의되지 않음")
    void noPolicyMapping() {
        RawCommissionDataVO raw = basicRecruitRaw("{\"premium\":\"100\"}");
        when(receiveMapper.selectFieldMappings(any(), any())).thenReturn(List.of(
                mapping("premium", "premium", "DECIMAL", null, null, "N")
        ));

        Long id = engine.transform(raw);

        assertThat(id).isNull();
        ArgumentCaptor<ParseErrorLogVO> cap = ArgumentCaptor.forClass(ParseErrorLogVO.class);
        verify(receiveMapper).insertError(cap.capture());
        assertThat(cap.getValue().getErrorType()).isEqualTo("NO_POLICY");
    }

    @Test
    @DisplayName("CONTRACT_NOT_FOUND: policy_no 로 계약 매칭 실패")
    void contractNotFound() {
        RawCommissionDataVO raw = basicRecruitRaw("{\"policyNo\":\"POL-NONE\"}");
        when(receiveMapper.selectFieldMappings(any(), any())).thenReturn(List.of(
                mapping("policyNo", "policy_no", "STRING", "TRIM", null, "Y")
        ));
        when(contractMapper.selectList(any(ContractSearchParam.class))).thenReturn(List.of());

        Long id = engine.transform(raw);

        assertThat(id).isNull();
        ArgumentCaptor<ParseErrorLogVO> cap = ArgumentCaptor.forClass(ParseErrorLogVO.class);
        verify(receiveMapper).insertError(cap.capture());
        assertThat(cap.getValue().getErrorType()).isEqualTo("CONTRACT_NOT_FOUND");
        assertThat(cap.getValue().getErrorValue()).isEqualTo("POL-NONE");
    }

    @Test
    @DisplayName("CODE_MAP: 매칭 실패 시 원본 값 그대로 사용")
    void codeMap_fallback() {
        String json = """
                {"pol":"POL-CM", "type":"UNKNOWN_TYPE"}
                """;
        RawCommissionDataVO raw = basicRecruitRaw(json);

        when(receiveMapper.selectFieldMappings(any(), any())).thenReturn(List.of(
                mapping("pol",  "policy_no",      "STRING", "TRIM",     null, "Y"),
                mapping("type", "insurance_type", "STRING", "CODE_MAP", "INSURANCE_TYPE", "N")
        ));
        when(receiveMapper.findTargetCode("SAMSUNG", "INSURANCE_TYPE", "UNKNOWN_TYPE")).thenReturn(null);
        setupContractMatch("POL-CM", 500L, 50L);

        engine.transform(raw);

        ArgumentCaptor<LedgerVO> cap = ArgumentCaptor.forClass(LedgerVO.class);
        verify(recruitMapper).insert(cap.capture());
        // 매핑 미존재 → 원본 그대로
        assertThat(cap.getValue().getInsuranceType()).isEqualTo("UNKNOWN_TYPE");
    }

    @Test
    @DisplayName("계약값 폴백: premium/pay_method 가 mapped 에 없으면 contract 값 사용")
    void buildLedger_fallbackToContract() {
        // premium, pay_method 매핑 없음 → contract 의 값(500000, MONTHLY) 사용
        RawCommissionDataVO raw = basicRecruitRaw("{\"pol\":\"POL-FB\"}");
        when(receiveMapper.selectFieldMappings(any(), any())).thenReturn(List.of(
                mapping("pol", "policy_no", "STRING", "TRIM", null, "Y")
        ));
        setupContractMatch("POL-FB", 600L, 60L);

        engine.transform(raw);

        ArgumentCaptor<LedgerVO> cap = ArgumentCaptor.forClass(LedgerVO.class);
        verify(recruitMapper).insert(cap.capture());
        LedgerVO led = cap.getValue();
        assertThat(led.getPremium()).isEqualByComparingTo("500000"); // contract.premium
        assertThat(led.getPayMethod()).isEqualTo("MONTHLY");          // contract.payCycle
        assertThat(led.getInsuranceType()).isEqualTo("WHOLE_LIFE");   // 기본값
        assertThat(led.getCommissionYear()).isEqualTo(1);             // 기본값
    }
}
