package com.eactive.eai.custom.message;

import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.anyString;

import java.lang.reflect.Field;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.HashMap;
import java.util.Map;
import java.util.Properties;

import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestInstance;
import org.mockito.Mockito;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.stubbing.Answer;
import org.springframework.context.ApplicationContext;

import com.eactive.eai.common.monitor.EAIServiceMonitor;
import com.eactive.eai.common.property.PropManager;
import com.eactive.eai.common.server.EAIServerManager;
import com.eactive.eai.common.util.ApplicationContextProvider;
import com.eactive.eai.message.StandardItem;
import com.eactive.eai.message.StandardMessage;
import com.eactive.eai.message.manager.StandardMessageManager;
import com.eactive.eai.message.service.InterfaceMapper;
import com.ext.eai.common.stdmessage.STDMessageKeys;

/**
 * StandardMessageCoordinatorDJB 단위테스트
 *
 * DB/Spring 없는 환경에서 EAIServerManager·PropManager를 Mockito로 stub 처리.
 * ApplicationContextProvider.context에 mock ApplicationContext를 리플렉션으로 주입.
 */
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
class StandardMessageCoordinatorDJBTest {

    private static final DateTimeFormatter FMT_DATE = DateTimeFormatter.ofPattern("yyyyMMdd");

    private StandardMessageManager manager;
    private StandardMessageCoordinatorDJB coordinator;
    private InterfaceMapper mapper;
    private StandardMessage message;

    @BeforeAll
    void setUpAll() throws Exception {
        // ① PropManager mock — STDMessageErrorKeys 정적 초기화가 getProperty(group,key,def)를 호출함.
        //    def(3번째 인자)를 그대로 반환하여 기본 에러코드 상수값이 설정되도록 함.
        PropManager mockPropManager = Mockito.mock(PropManager.class);
        Mockito.when(mockPropManager.getProperty(anyString(), anyString(), anyString()))
                .thenAnswer(new Answer<String>() {
                    public String answer(InvocationOnMock inv) {
                        return (String) inv.getArguments()[2];
                    }
                });

        // ② EAIServerManager mock — DB 미연결 환경 대응.
        //    isPEAIServer/isSEAIServer → false → getSysEnvDvcd()가 "D"(개발)를 반환.
        EAIServerManager mockServer = Mockito.mock(EAIServerManager.class);
        Mockito.when(mockServer.isPEAIServer()).thenReturn(false);
        Mockito.when(mockServer.isSEAIServer()).thenReturn(false);
        Mockito.when(mockServer.isMCI()).thenReturn(false);

        // ③ EAIServiceMonitor mock — MessageUtil.getTobeCode()가 사용.
        //    isTimeOutCodes/isBizErrorCodes → false → SYSTEM_ERROR_CODE 분기로 처리.
        EAIServiceMonitor mockMonitor = Mockito.mock(EAIServiceMonitor.class);
        Mockito.when(mockMonitor.isTimeOutCodes(anyString())).thenReturn(false);
        Mockito.when(mockMonitor.isBizErrorCodes(anyString())).thenReturn(false);

        // ④ ApplicationContext mock → 세 빈을 반환하도록 설정
        ApplicationContext mockCtx = Mockito.mock(ApplicationContext.class);
        Mockito.when(mockCtx.getBean(EAIServerManager.class)).thenReturn(mockServer);
        Mockito.when(mockCtx.getBean(PropManager.class)).thenReturn(mockPropManager);
        Mockito.when(mockCtx.getBean(EAIServiceMonitor.class)).thenReturn(mockMonitor);

        // ⑤ ApplicationContextProvider.context(static 필드)에 mock 주입
        Field ctxField = ApplicationContextProvider.class.getDeclaredField("context");
        ctxField.setAccessible(true);
        ctxField.set(null, mockCtx);

        // ⑥ StandardMessageManager 초기화 (DJB 테스트용 설정)
        System.setProperty(StandardMessageManager.STANDARD_MESSAGE_CONFIG,
                "src/test/resources/standard-message-config-test.properties");
        manager = StandardMessageManager.getInstance();
        manager.init();
    }

    @BeforeEach
    void setUp() {
        message     = manager.getStandardMessage();
        mapper      = manager.getMapper();
        coordinator = new StandardMessageCoordinatorDJB();
        message.setReadPosition(0);
        assertNotNull(mapper, "mapper가 null — StandardMessageManager.init() 실패 의심");
    }

    // ================================================================
    // coordinateAfterCreateMessageByRule
    // ================================================================

    @Test
    void GUID_40자_생성_설정() {
        coordinator.coordinateAfterCreateMessageByRule(message, null, null);

        String guid = mapper.getGuid(message);
        assertNotNull(guid, "GUID가 null");
        assertEquals(GUIDGeneratorDJB.GUID_LENGTH, guid.length(),
                "GUID 길이가 40자가 아님: " + guid.length());
    }

    @Test
    void guid_prgs_no_001_초기화() {
        coordinator.coordinateAfterCreateMessageByRule(message, null, null);

        assertEquals("001", mapper.getGuidSeq(message), "guid_prgs_no 초기값은 001이어야 함");
    }

    @Test
    void ortr_guid_GUID_동일값_설정() {
        coordinator.coordinateAfterCreateMessageByRule(message, null, null);

        String guid    = mapper.getGuid(message);
        String orgGuid = mapper.getOrgGuid(message);
        assertEquals(guid, orgGuid, "원거래GUID는 GUID와 동일해야 함");
    }

    @Test
    void frst_mesg_dman_dt_오늘날짜_설정() {
        coordinator.coordinateAfterCreateMessageByRule(message, null, null);

        String expected = LocalDate.now().format(FMT_DATE);
        assertEquals(expected, message.findItemValue("HEAD.frst_mesg_dman_dt"),
                "frst_mesg_dman_dt가 오늘 날짜가 아님");
    }

    @Test
    void frst_mesg_dman_time_설정_9자리() {
        coordinator.coordinateAfterCreateMessageByRule(message, null, null);

        String time = message.findItemValue("HEAD.frst_mesg_dman_time");
        assertNotNull(time, "frst_mesg_dman_time이 null");
        assertEquals(9, time.length(), "frst_mesg_dman_time은 HHmmssSSS(9자)여야 함: " + time);
        assertTrue(time.matches("\\d{9}"), "frst_mesg_dman_time 형식 오류: " + time);
    }

    @Test
    void trnm_sys_dvcd_EAI_설정() {
        coordinator.coordinateAfterCreateMessageByRule(message, null, null);

        assertEquals("EAI", message.findItemValue("HEAD.trnm_sys_dvcd"));
    }

    @Test
    void sys_env_dvcd_D_설정_mock_isPEAIServer_false() {
        // mock: isPEAIServer=false, isSEAIServer=false → "D"(개발) 반환
        coordinator.coordinateAfterCreateMessageByRule(message, null, null);

        assertEquals("D", message.findItemValue("HEAD.sys_env_dvcd"),
                "mock 환경에서 sys_env_dvcd는 D여야 함");
    }

    @Test
    void frst_trnm_ipad_설정() {
        coordinator.coordinateAfterCreateMessageByRule(message, null, null);

        String ip = message.findItemValue("HEAD.frst_trnm_ipad");
        assertNotNull(ip, "frst_trnm_ipad가 null");
        assertFalse(ip.trim().isEmpty(), "frst_trnm_ipad가 공백");
    }

    // ================================================================
    // coordinateBeforeStandardMessageSend
    // ================================================================

    @Test
    void data_scop_len_정수값으로_설정() {
        coordinator.coordinateBeforeStandardMessageSend(message, "FLAT", "euc-kr");

        String len = message.findItemValue("DATA.data_scop_len");
        assertNotNull(len, "DATA.data_scop_len이 null");
        // NUMBER 타입은 선행 0을 모두 제거: "00000000"(0 바이트) → "" 반환됨
        // 빈 문자열은 0 바이트를 의미하므로 0 이상 조건 통과
        int dataLen = (len.trim().isEmpty()) ? 0 : Integer.parseInt(len.trim());
        assertTrue(dataLen >= 0, "data_scop_len은 0 이상이어야 함: '" + len + "'");
    }

    @Test
    void data_scop_len_BIZ_DATA_비어있을때_0() {
        // BIZ_DATA에 데이터 미설정 → 0 바이트 → "00000000" → NUMBER 타입이 "" 반환
        coordinator.coordinateBeforeStandardMessageSend(message, "JSON", "utf-8");

        String len = message.findItemValue("DATA.data_scop_len");
        assertNotNull(len, "DATA.data_scop_len이 null");
        int dataLen = (len.trim().isEmpty()) ? 0 : Integer.parseInt(len.trim());
        assertEquals(0, dataLen, "BIZ_DATA 미설정 시 data_scop_len은 0이어야 함");
    }

    // ================================================================
    // coordinateBeforeResponse
    // ================================================================

    @Test
    void mesg_rspn_dt_오늘날짜_설정() {
        coordinator.coordinateBeforeResponse(message, null, null, "euc-kr");

        String expected = LocalDate.now().format(FMT_DATE);
        assertEquals(expected, message.findItemValue("HEAD.mesg_rspn_dt"),
                "mesg_rspn_dt가 오늘 날짜가 아님");
    }

    @Test
    void mesg_rspn_time_9자리_설정() {
        coordinator.coordinateBeforeResponse(message, null, null, "euc-kr");

        String time = message.findItemValue("HEAD.mesg_rspn_time");
        assertNotNull(time, "mesg_rspn_time이 null");
        assertEquals(9, time.length(), "mesg_rspn_time은 HHmmssSSS(9자)여야 함: " + time);
        assertTrue(time.matches("\\d{9}"), "mesg_rspn_time 형식 오류: " + time);
    }

    @Test
    void coordinateBeforeResponse_data_scop_len_설정() {
        coordinator.coordinateBeforeResponse(message, null, null, "euc-kr");

        String len = message.findItemValue("DATA.data_scop_len");
        assertNotNull(len);
        int dataLen = (len.trim().isEmpty()) ? 0 : Integer.parseInt(len.trim());
        assertTrue(dataLen >= 0);
    }

    // ================================================================
    // coordinateSetStandardMessageError
    // ================================================================

    @Test
    void 에러시_procs_rslt_dvcd_F_설정() {
        coordinator.coordinateSetStandardMessageError(message, mapper, "RECEAICMM999", "테스트 오류");

        assertEquals("F", message.findItemValue("HEAD.procs_rslt_dvcd"),
                "오류 시 procs_rslt_dvcd는 F여야 함");
    }

    @Test
    void 에러시_dman_rspn_dvcd_R_설정() {
        coordinator.coordinateSetStandardMessageError(message, mapper, "RECEAICMM999", "테스트 오류");

        assertEquals(STDMessageKeys.SEND_RECV_CD_RECV,
                message.findItemValue("HEAD.dman_rspn_dvcd"),
                "오류 시 dman_rspn_dvcd는 R이어야 함");
    }

    @Test
    void 에러시_MSG_msg_dvcd_EM_설정() {
        coordinator.coordinateSetStandardMessageError(message, mapper, "RECEAICMM999", "테스트 오류");

        assertEquals("EM", message.findItemValue("MSG.msg_dvcd"),
                "오류 시 msg_dvcd는 EM이어야 함");
    }

    @Test
    void 에러시_outp_atrb_cd_1_설정() {
        coordinator.coordinateSetStandardMessageError(message, mapper, "RECEAICMM999", "테스트 오류");

        assertEquals("1", message.findItemValue("MSG.MAIN_MSG.outp_atrb_cd"),
                "오류 시 outp_atrb_cd는 1(팝업)이어야 함");
    }

    @Test
    void 에러시_outp_msg_cd_설정됨() {
        coordinator.coordinateSetStandardMessageError(message, mapper, "RECEAICMM999", "테스트 오류");

        // mock PropManager가 defaultValue("900400")를 반환 → SYSTEM_ERROR_CODE_VALUE="900400"
        String errCode = message.findItemValue("MSG.MAIN_MSG.outp_msg_cd");
        assertNotNull(errCode, "outp_msg_cd가 null");
        assertFalse(errCode.trim().isEmpty(), "outp_msg_cd가 공백");
    }

    @Test
    void 에러시_outp_msg_ctnt_설정됨() {
        coordinator.coordinateSetStandardMessageError(message, mapper, "RECEAICMM999", "테스트 오류");

        String errMsg = message.findItemValue("MSG.MAIN_MSG.outp_msg_ctnt");
        assertNotNull(errMsg, "outp_msg_ctnt가 null");
        assertFalse(errMsg.trim().isEmpty(), "outp_msg_ctnt가 공백");
    }

    @Test
    void 에러시_반환값_false() {
        boolean result = coordinator.coordinateSetStandardMessageError(message, mapper, "RECEAICMM999", "테스트 오류");

        assertFalse(result, "coordinateSetStandardMessageError는 false를 반환해야 함 (BIZ_DATA null 처리)");
    }

    @Test
    void 에러시_msg_scop_len_정수값으로_설정() {
        coordinator.coordinateSetStandardMessageError(message, mapper, "RECEAICMM999", "테스트 오류");

        // NUMBER 타입이므로 선행 0 제거됨 — 숫자 파싱 가능 여부와 0 이상인지 검증
        String scopLen = message.findItemValue("MSG.msg_scop_len");
        assertNotNull(scopLen, "msg_scop_len이 null");
        assertTrue(Integer.parseInt(scopLen.trim()) >= 0, "msg_scop_len은 0 이상이어야 함: " + scopLen);
    }

    // ================================================================
    // coordinateAfterRecvNonStdSyncResponse
    // ================================================================

    @Test
    void 정상수신시_procs_rslt_dvcd_S_설정() {
        coordinator.coordinateAfterRecvNonStdSyncResponse(message);

        assertEquals("S", message.findItemValue("HEAD.procs_rslt_dvcd"),
                "정상 수신 시 procs_rslt_dvcd는 S여야 함");
    }

    @Test
    void 정상수신시_msg_dvcd_NM_설정() {
        coordinator.coordinateAfterRecvNonStdSyncResponse(message);

        assertEquals("NM", message.findItemValue("MSG.msg_dvcd"),
                "정상 수신 시 msg_dvcd는 NM이어야 함");
    }

    @Test
    void 정상수신시_outp_msg_cd_설정됨() {
        coordinator.coordinateAfterRecvNonStdSyncResponse(message);

        String msgCd = message.findItemValue("MSG.MAIN_MSG.outp_msg_cd");
        assertFalse(msgCd == null || msgCd.trim().isEmpty(), "outp_msg_cd가 설정되어야 함");
    }

    @Test
    void 정상수신시_outp_msg_ctnt_설정됨() {
        coordinator.coordinateAfterRecvNonStdSyncResponse(message);

        String ctnt = message.findItemValue("MSG.MAIN_MSG.outp_msg_ctnt");
        assertFalse(ctnt == null || ctnt.trim().isEmpty(), "outp_msg_ctnt가 설정되어야 함");
    }

    @Test
    void 정상수신시_msg_list_rowcnt_0() {
        coordinator.coordinateAfterRecvNonStdSyncResponse(message);

        String rowcnt = message.findItemValue("MSG.MAIN_MSG.msg_list_rowcnt");
        String normalized = (rowcnt == null || rowcnt.trim().isEmpty()) ? "0" : rowcnt.trim();
        assertEquals(0, Integer.parseInt(normalized), "정상 응답 msg_list는 0건이어야 함");
    }

    @Test
    void 정상수신시_msg_scop_len_정수값으로_설정() {
        coordinator.coordinateAfterRecvNonStdSyncResponse(message);

        // NUMBER 타입이므로 선행 0 제거됨 — 숫자 파싱 가능 여부와 0 이상인지 검증
        String scopLen = message.findItemValue("MSG.msg_scop_len");
        assertNotNull(scopLen, "msg_scop_len이 null");
        assertTrue(Integer.parseInt(scopLen.trim()) >= 0, "msg_scop_len은 0 이상이어야 함: " + scopLen);
    }

    // ================================================================
    // resolveCallPropRefValues — ${callProp.키} 치환
    // ================================================================

    @Test
    void callProp_refValue_단순키_값_주입() throws Exception {
        // chnl_dtls_clcd: CSV 기본값 없음, refValue를 ${callProp.CHNL_CD}로 주입
        setRefValue(message.findItem("HEAD.chnl_dtls_clcd"), "${callProp.CHNL_CD}");

        Properties prop = new Properties();
        prop.setProperty("CHNL_CD", "MOB");
        coordinator.coordinateAfterCreateMessageByRule(message, null, prop);

        assertEquals("MOB", message.findItemValue("HEAD.chnl_dtls_clcd"));
    }

    @Test
    void callProp_refValue_기본값_키_없을때_기본값_사용() throws Exception {
        setRefValue(message.findItem("HEAD.chnl_dtls_clcd"), "${callProp.MISSING:IB1}");

        coordinator.coordinateAfterCreateMessageByRule(message, null, new Properties());

        assertEquals("IB1", message.findItemValue("HEAD.chnl_dtls_clcd"));
    }

    @Test
    void callProp_refValue_기본값_키_있을때_prop값으로_덮어씀() throws Exception {
        setRefValue(message.findItem("HEAD.chnl_dtls_clcd"), "${callProp.CHNL_CD:IB1}");

        Properties prop = new Properties();
        prop.setProperty("CHNL_CD", "ATM");
        coordinator.coordinateAfterCreateMessageByRule(message, null, prop);

        assertEquals("ATM", message.findItemValue("HEAD.chnl_dtls_clcd"));
    }

    @Test
    void callProp_refValue_prop_null이면_setValue_호출_안함() throws Exception {
        setRefValue(message.findItem("HEAD.chnl_dtls_clcd"), "${callProp.CHNL_CD}");

        coordinator.coordinateAfterCreateMessageByRule(message, null, null);

        String val = message.findItemValue("HEAD.chnl_dtls_clcd");
        assertTrue(val == null || val.trim().isEmpty(),
                "prop=null이면 callProp refValue 치환하지 않아야 함: " + val);
    }

    @Test
    void callProp_refValue_패턴_불일치_필드는_skip() throws Exception {
        setRefValue(message.findItem("HEAD.chnl_dtls_clcd"), "PLAIN");

        Properties prop = new Properties();
        prop.setProperty("chnl_dtls_clcd", "SHOULD_NOT_SET");
        coordinator.coordinateAfterCreateMessageByRule(message, null, prop);

        assertNotEquals("SHOULD_NOT_SET", message.findItemValue("HEAD.chnl_dtls_clcd"),
                "패턴 불일치 refValue는 치환하지 않아야 함");
    }

    @Test
    void callProp_refValue_여러_필드_동시_주입() throws Exception {
        setRefValue(message.findItem("HEAD.chnl_dtls_clcd"), "${callProp.CHNL_CD}");
        setRefValue(message.findItem("HEAD.xtis_cd"),        "${callProp.XTIS_CD}");

        Properties prop = new Properties();
        prop.setProperty("CHNL_CD", "IB1");
        prop.setProperty("XTIS_CD", "OBK");
        coordinator.coordinateAfterCreateMessageByRule(message, null, prop);

        assertEquals("IB1", message.findItemValue("HEAD.chnl_dtls_clcd"));
        assertEquals("OBK", message.findItemValue("HEAD.xtis_cd"));
    }

    @Test
    void callProp_중첩_Properties_점경로_조회() throws Exception {
        // prop.get("OUTBOUND") = subProps, subProps.getProperty("IF_ID") = "SVC001"
        // → ${callProp.OUTBOUND.IF_ID} → "SVC001"
        setRefValue(message.findItem("HEAD.chnl_dtls_clcd"), "${callProp.OUTBOUND.IF_CD}");

        Properties subProps = new Properties();
        subProps.setProperty("IF_CD", "IB1");
        Properties prop = new Properties();
        prop.put("OUTBOUND", subProps);
        coordinator.coordinateAfterCreateMessageByRule(message, null, prop);

        assertEquals("IB1", message.findItemValue("HEAD.chnl_dtls_clcd"));
    }

    @Test
    void callProp_중첩_Map_점경로_조회() throws Exception {
        // prop.get("CTX") = Map{"CHNL":"MOB"}
        // → ${callProp.CTX.CHNL} → "MOB"
        setRefValue(message.findItem("HEAD.chnl_dtls_clcd"), "${callProp.CTX.CHNL}");

        Map<String, Object> ctxMap = new HashMap<>();
        ctxMap.put("CHNL", "MOB");
        Properties prop = new Properties();
        prop.put("CTX", ctxMap);
        coordinator.coordinateAfterCreateMessageByRule(message, null, prop);

        assertEquals("MOB", message.findItemValue("HEAD.chnl_dtls_clcd"));
    }

    @Test
    void callProp_중간값_비Properties_폴백_전체키_조회() throws Exception {
        // prop.get("aaa") = "plain" (Properties가 아님) → 전체 키 "aaa.bbb" 폴백
        setRefValue(message.findItem("HEAD.chnl_dtls_clcd"), "${callProp.aaa.bbb}");

        Properties prop = new Properties();
        prop.setProperty("aaa", "plain");
        prop.setProperty("aaa.bbb", "ATM");
        coordinator.coordinateAfterCreateMessageByRule(message, null, prop);

        assertEquals("ATM", message.findItemValue("HEAD.chnl_dtls_clcd"));
    }

    @Test
    void callProp_중첩_키_없을때_기본값_사용() throws Exception {
        setRefValue(message.findItem("HEAD.chnl_dtls_clcd"), "${callProp.OUTBOUND.MISSING:IB1}");

        Properties subProps = new Properties();          // MISSING 키 없음
        Properties prop = new Properties();
        prop.put("OUTBOUND", subProps);
        coordinator.coordinateAfterCreateMessageByRule(message, null, prop);

        assertEquals("IB1", message.findItemValue("HEAD.chnl_dtls_clcd"));
    }

    // ----------------------------------------------------------------
    // 헬퍼
    // ----------------------------------------------------------------

    private static void setRefValue(StandardItem item, String refValue) throws Exception {
        Field f = StandardItem.class.getDeclaredField("refValue");
        f.setAccessible(true);
        f.set(item, refValue);
    }
}
