package com.eactive.eai.custom.message;

import static org.junit.jupiter.api.Assertions.*;

import java.lang.reflect.Field;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;

import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Nested;
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.StandardMessage;
import com.eactive.eai.message.manager.StandardMessageManager;
import com.eactive.eai.message.parser.JsonReader;
import com.eactive.eai.message.service.InterfaceMapper;
import com.eactive.eai.common.message.MessageType;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;

import static org.mockito.ArgumentMatchers.anyString;

/**
 * DJBank 표준전문 처리 스펙 전체 검증 테스트
 *
 * 요청 메시지 필수 처리 항목
 *  - HEAD 레이아웃 기본값 (nxgn_stnd_idfr, stnd_mesg_ver, frst_trnm_sys_dvcd, trnm_sys_dvcd, ...)
 *  - coordinateAfterCreateMessageByRule: GUID, 날짜, IP, MAC, ERP 분기
 *  - ERP 채널: HEAD 지점코드 필드 + EZDATA 전자금융공통영역
 *  - BIZ_DATA 임의 데이터
 *
 * 응답 메시지 필수 처리 항목
 *  - coordinateBeforeResponse: guid_prgs_no+1, dman_rspn_dvcd=R, 응답일시
 *  - 정상 MSG: NM, msg_list_rowcnt=0
 *  - 오류 MSG: EM, msg_list_rowcnt=1, MSG_LIST outp_msg_desc/err_occu_loct
 *  - BIZ_DATA 임의 데이터
 */
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
class StandardMessageSpecTest {

    private static final String TEST_GUID =
            "20260420" + "120000" + "000" + "EAI" + "01" + "123456789012" + "000001";
    private static final String TEST_IF_ID = "IF_DJBTEST_SPEC";
    private static final String TEST_TX_ID = "TXSPEC000001";
    private static final DateTimeFormatter FMT_DATE = DateTimeFormatter.ofPattern("yyyyMMdd");

    /** 일반(비ERP) 요청 JSON */
    private static final String REQ_JSON =
        "{"
        + "\"HEAD\":{"
        +   "\"nxgn_stnd_idfr\":\"JERA\","
        +   "\"guid\":\"" + TEST_GUID + "\","
        +   "\"guid_prgs_no\":\"1\","
        +   "\"stnd_mesg_ver\":\"R10\","
        +   "\"ortr_guid\":\"" + TEST_GUID + "\","
        +   "\"dman_rspn_dvcd\":\"S\","
        +   "\"if_id\":\"" + TEST_IF_ID + "\","
        +   "\"procs_rslt_dvcd\":\"S\","
        +   "\"tx_id\":\"" + TEST_TX_ID + "\","
        +   "\"chnl_tycd\":\"EAI\","
        +   "\"hmab_dvcd\":\"1\","
        +   "\"trnm_sys_dvcd\":\"OPA\","
        +   "\"frst_trnm_sys_dvcd\":\"OPA\","
        +   "\"sys_env_dvcd\":\"D\","
        +   "\"frst_mesg_dman_dt\":\"20260420\","
        +   "\"frst_mesg_dman_time\":\"120000000\""
        + "},"
        + "\"DATA\":{"
        +   "\"data_dvcd\":\"IO\","
        +   "\"data_scop_len\":\"00000037\","
        +   "\"BIZ_DATA\":{\"acct_no\":\"987654321\",\"amt\":\"500000\"}"
        + "}"
        + "}";

    /** ERP 채널 요청 JSON — corp_tlwn_virt_brcd=ERP */
    private static final String ERP_REQ_JSON =
        "{"
        + "\"HEAD\":{"
        +   "\"nxgn_stnd_idfr\":\"JERA\","
        +   "\"guid\":\"" + TEST_GUID + "\","
        +   "\"guid_prgs_no\":\"1\","
        +   "\"stnd_mesg_ver\":\"R10\","
        +   "\"ortr_guid\":\"" + TEST_GUID + "\","
        +   "\"dman_rspn_dvcd\":\"S\","
        +   "\"if_id\":\"" + TEST_IF_ID + "\","
        +   "\"procs_rslt_dvcd\":\"S\","
        +   "\"tx_id\":\"" + TEST_TX_ID + "\","
        +   "\"chnl_tycd\":\"EAI\","
        +   "\"hmab_dvcd\":\"1\","
        +   "\"corp_tlwn_virt_brcd\":\"ERP\","
        +   "\"trnm_sys_dvcd\":\"OPA\","
        +   "\"frst_trnm_sys_dvcd\":\"OPA\","
        +   "\"sys_env_dvcd\":\"D\","
        +   "\"frst_mesg_dman_dt\":\"20260420\","
        +   "\"frst_mesg_dman_time\":\"120000000\""
        + "},"
        + "\"EZDATA\":{"
        +   "\"elfn_bnkn_dvcd\":\"01\","
        +   "\"usr_no\":\"USR0000000000001\""
        + "},"
        + "\"DATA\":{"
        +   "\"data_dvcd\":\"IO\","
        +   "\"data_scop_len\":\"00000037\","
        +   "\"BIZ_DATA\":{\"acct_no\":\"111222333\",\"bal\":\"1000000\"}"
        + "}"
        + "}";

    private StandardMessageManager manager;
    private InterfaceMapper mapper;
    private JsonReader jsonReader;
    private StandardMessageCoordinatorDJB coordinator;
    private ObjectMapper jacksonMapper;

    @BeforeAll
    void setUpAll() throws Exception {
        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 mockServer = Mockito.mock(EAIServerManager.class);
        Mockito.when(mockServer.isPEAIServer()).thenReturn(false);
        Mockito.when(mockServer.isSEAIServer()).thenReturn(false);
        Mockito.when(mockServer.isMCI()).thenReturn(false);
        Mockito.when(mockServer.getLocalServerName()).thenReturn("TEST_SERVER");
        Mockito.when(mockServer.getGroupInstId()).thenReturn("TEST_INST");

        EAIServiceMonitor mockMonitor = Mockito.mock(EAIServiceMonitor.class);
        Mockito.when(mockMonitor.isTimeOutCodes(anyString())).thenReturn(false);
        Mockito.when(mockMonitor.isBizErrorCodes(anyString())).thenReturn(false);

        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);

        Field ctxField = ApplicationContextProvider.class.getDeclaredField("context");
        ctxField.setAccessible(true);
        ctxField.set(null, mockCtx);

        System.setProperty(StandardMessageManager.STANDARD_MESSAGE_CONFIG,
                "src/test/resources/standard-message-config-test.properties");
        manager = StandardMessageManager.getInstance();
        manager.init();
    }

    @BeforeEach
    void setUp() {
        mapper      = manager.getMapper();
        jsonReader  = (JsonReader) manager.getReader("JSON");
        coordinator = new StandardMessageCoordinatorDJB();
        jacksonMapper = new ObjectMapper();
    }

    // ================================================================
    // 1. 요청 HEAD — 레이아웃 기본값 검증
    // ================================================================

    @Nested
    @TestInstance(TestInstance.Lifecycle.PER_CLASS)
    class 요청HEAD_레이아웃_기본값 {

        @Test
        void nxgn_stnd_idfr_기본값_JERA() {
            StandardMessage msg = manager.getStandardMessage();
            assertEquals("JERA", msg.findItemValue("HEAD.nxgn_stnd_idfr"));
        }

        @Test
        void stnd_mesg_ver_기본값_R10() {
            StandardMessage msg = manager.getStandardMessage();
            assertEquals("R10", msg.findItemValue("HEAD.stnd_mesg_ver"));
        }

        @Test
        void frst_trnm_sys_dvcd_기본값_OPA() {
            StandardMessage msg = manager.getStandardMessage();
            assertEquals("OPA", msg.findItemValue("HEAD.frst_trnm_sys_dvcd"));
        }

        @Test
        void trnm_sys_dvcd_기본값_EAI() {
            StandardMessage msg = manager.getStandardMessage();
            assertEquals("EAI", msg.findItemValue("HEAD.trnm_sys_dvcd"));
        }

        @Test
        void dman_rspn_dvcd_기본값_S() {
            StandardMessage msg = manager.getStandardMessage();
            assertEquals("S", msg.findItemValue("HEAD.dman_rspn_dvcd"));
        }

        @Test
        void sys_env_dvcd_기본값_D() {
            StandardMessage msg = manager.getStandardMessage();
            assertEquals("D", msg.findItemValue("HEAD.sys_env_dvcd"));
        }

        @Test
        void tx_tycd_기본값_O() {
            StandardMessage msg = manager.getStandardMessage();
            assertEquals("O", msg.findItemValue("HEAD.tx_tycd"));
        }

        @Test
        void synz_dvcd_기본값_S() {
            StandardMessage msg = manager.getStandardMessage();
            assertEquals("S", msg.findItemValue("HEAD.synz_dvcd"));
        }

        @Test
        void chnl_tycd_기본값_EAI() {
            StandardMessage msg = manager.getStandardMessage();
            assertEquals("EAI", msg.findItemValue("HEAD.chnl_tycd"));
        }
    }

    // ================================================================
    // 2. 요청 처리 — coordinateAfterCreateMessageByRule
    // ================================================================

    @Nested
    @TestInstance(TestInstance.Lifecycle.PER_CLASS)
    class 요청처리_coordinateAfterCreateMessageByRule {

        @Test
        void GUID_40자_생성() throws Exception {
            StandardMessage msg = parseReq();
            coordinator.coordinateAfterCreateMessageByRule(msg, null, null);
            String guid = mapper.getGuid(msg);
            assertNotNull(guid);
            assertEquals(GUIDGeneratorDJB.GUID_LENGTH, guid.length());
        }

        @Test
        void GUID_기존값_갱신() throws Exception {
            StandardMessage msg = parseReq();
            coordinator.coordinateAfterCreateMessageByRule(msg, null, null);
            assertNotEquals(TEST_GUID, mapper.getGuid(msg), "새 GUID가 생성되어야 함");
        }

        @Test
        void guid_prgs_no_초기값_001() throws Exception {
            StandardMessage msg = parseReq();
            coordinator.coordinateAfterCreateMessageByRule(msg, null, null);
            assertEquals("001", mapper.getGuidSeq(msg));
        }

        @Test
        void ortr_guid_equals_guid() throws Exception {
            StandardMessage msg = parseReq();
            coordinator.coordinateAfterCreateMessageByRule(msg, null, null);
            assertEquals(mapper.getGuid(msg), mapper.getOrgGuid(msg), "ortr_guid는 guid와 동일해야 함");
        }

        @Test
        void frst_mesg_dman_dt_오늘날짜() throws Exception {
            StandardMessage msg = parseReq();
            coordinator.coordinateAfterCreateMessageByRule(msg, null, null);
            assertEquals(LocalDate.now().format(FMT_DATE), msg.findItemValue("HEAD.frst_mesg_dman_dt"));
        }

        @Test
        void frst_mesg_dman_time_9자리_숫자() throws Exception {
            StandardMessage msg = parseReq();
            coordinator.coordinateAfterCreateMessageByRule(msg, null, null);
            String time = msg.findItemValue("HEAD.frst_mesg_dman_time");
            assertNotNull(time);
            assertEquals(9, time.length(), "HHmmssSSS 9자리여야 함");
            assertTrue(time.matches("\\d{9}"));
        }

        @Test
        void trnm_sys_dvcd_EAI_설정() throws Exception {
            StandardMessage msg = parseReq();
            coordinator.coordinateAfterCreateMessageByRule(msg, null, null);
            assertEquals("EAI", msg.findItemValue("HEAD.trnm_sys_dvcd"));
        }

        @Test
        void sys_env_dvcd_D_개발환경() throws Exception {
            StandardMessage msg = parseReq();
            coordinator.coordinateAfterCreateMessageByRule(msg, null, null);
            String env = msg.findItemValue("HEAD.sys_env_dvcd");
            assertTrue("D".equals(env) || "T".equals(env) || "P".equals(env),
                    "sys_env_dvcd는 D/T/P 중 하나여야 함: " + env);
        }

        @Test
        void frst_trnm_ipad_설정됨() throws Exception {
            StandardMessage msg = parseReq();
            coordinator.coordinateAfterCreateMessageByRule(msg, null, null);
            String ip = msg.findItemValue("HEAD.frst_trnm_ipad");
            assertFalse(ip == null || ip.trim().isEmpty(), "frst_trnm_ipad가 설정되어야 함");
        }

        @Test
        void frst_trnm_ipv4_addr_설정됨() throws Exception {
            StandardMessage msg = parseReq();
            coordinator.coordinateAfterCreateMessageByRule(msg, null, null);
            String ip = msg.findItemValue("HEAD.frst_trnm_ipv4_addr");
            assertFalse(ip == null || ip.trim().isEmpty(), "frst_trnm_ipv4_addr가 설정되어야 함");
        }

        @Test
        void frst_trnm_mac_설정됨() throws Exception {
            StandardMessage msg = parseReq();
            coordinator.coordinateAfterCreateMessageByRule(msg, null, null);
            // MAC 주소는 가상환경에서 null일 수 있으므로 존재만 확인
            assertNotNull(msg.findItem("HEAD.frst_trnm_mac"), "frst_trnm_mac 필드가 없음");
        }

        @Test
        void BIZ_DATA_임의값_처리_후_유지() throws Exception {
            StandardMessage msg = parseReq();
            coordinator.coordinateAfterCreateMessageByRule(msg, null, null);
            String bizData = msg.findItemValue("DATA.BIZ_DATA");
            assertNotNull(bizData);
            assertFalse(bizData.trim().isEmpty(), "BIZ_DATA가 유지되어야 함");
        }
    }

    // ================================================================
    // 3. ERP 채널 — HEAD 지점코드 + EZDATA 전자금융공통영역
    // ================================================================

    @Nested
    @TestInstance(TestInstance.Lifecycle.PER_CLASS)
    class ERP채널_처리 {

        @Test
        void HEAD_tx_cmgr_cd_S004() throws Exception {
            StandardMessage msg = parseErpReq();
            coordinator.coordinateAfterCreateMessageByRule(msg, null, null);
            assertEquals("S004", msg.findItemValue("HEAD.tx_cmgr_cd").trim());
        }

        @Test
        void HEAD_mgmt_brcd_260() throws Exception {
            StandardMessage msg = parseErpReq();
            coordinator.coordinateAfterCreateMessageByRule(msg, null, null);
            assertEquals("260", msg.findItemValue("HEAD.mgmt_brcd").trim());
        }

        @Test
        void HEAD_tx_brcd_260() throws Exception {
            StandardMessage msg = parseErpReq();
            coordinator.coordinateAfterCreateMessageByRule(msg, null, null);
            assertEquals("260", msg.findItemValue("HEAD.tx_brcd").trim());
        }

        @Test
        void HEAD_insl_brcd_260() throws Exception {
            StandardMessage msg = parseErpReq();
            coordinator.coordinateAfterCreateMessageByRule(msg, null, null);
            assertEquals("260", msg.findItemValue("HEAD.insl_brcd").trim());
        }

        @Test
        void HEAD_blng_brcd_260() throws Exception {
            StandardMessage msg = parseErpReq();
            coordinator.coordinateAfterCreateMessageByRule(msg, null, null);
            assertEquals("260", msg.findItemValue("HEAD.blng_brcd").trim());
        }

        @Test
        void EZDATA_elfn_bnkn_dvcd_10() throws Exception {
            StandardMessage msg = parseErpReq();
            coordinator.coordinateAfterCreateMessageByRule(msg, null, null);
            assertEquals("10", msg.findItemValue("EZDATA.elfn_bnkn_dvcd").trim());
        }

        @Test
        void EZDATA_svr_tx_seqno_10자리_숫자() throws Exception {
            StandardMessage msg = parseErpReq();
            coordinator.coordinateAfterCreateMessageByRule(msg, null, null);
            String seqno = msg.findItemValue("EZDATA.svr_tx_seqno");
            assertNotNull(seqno);
            String trimmed = seqno.trim();
            assertTrue(trimmed.matches("\\d{1,10}"), "svr_tx_seqno는 unix timestamp 숫자여야 함: " + trimmed);
            assertTrue(Long.parseLong(trimmed) > 0, "svr_tx_seqno는 양수여야 함");
        }

        @Test
        void EZDATA_elfn_mac_설정됨() throws Exception {
            StandardMessage msg = parseErpReq();
            coordinator.coordinateAfterCreateMessageByRule(msg, null, null);
            assertNotNull(msg.findItem("EZDATA.elfn_mac"), "EZDATA.elfn_mac 필드가 없음");
        }

        @Test
        void EZDATA_pc_ofcl_ipad_설정됨() throws Exception {
            StandardMessage msg = parseErpReq();
            coordinator.coordinateAfterCreateMessageByRule(msg, null, null);
            String ip = msg.findItemValue("EZDATA.pc_ofcl_ipad");
            assertFalse(ip == null || ip.trim().isEmpty(), "EZDATA.pc_ofcl_ipad가 설정되어야 함");
        }

        @Test
        void EZDATA_pc_prvat_ipad_설정됨() throws Exception {
            StandardMessage msg = parseErpReq();
            coordinator.coordinateAfterCreateMessageByRule(msg, null, null);
            String ip = msg.findItemValue("EZDATA.pc_prvat_ipad");
            assertFalse(ip == null || ip.trim().isEmpty(), "EZDATA.pc_prvat_ipad가 설정되어야 함");
        }

        @Test
        void EZDATA_elfn_spr_fild_ERP() throws Exception {
            StandardMessage msg = parseErpReq();
            coordinator.coordinateAfterCreateMessageByRule(msg, null, null);
            assertEquals("ERP", msg.findItemValue("EZDATA.elfn_spr_fild").trim());
        }

    }

    // ================================================================
    // 4. 응답 처리 — coordinateBeforeResponse
    // ================================================================

    @Nested
    @TestInstance(TestInstance.Lifecycle.PER_CLASS)
    class 응답처리_coordinateBeforeResponse {

        @Test
        void guid_prgs_no_1_증가() throws Exception {
            StandardMessage msg = parseReq();
            // 파싱 후 guid_prgs_no = 1
            coordinator.coordinateBeforeResponse(msg, null, null, "utf-8");
            String seq = mapper.getGuidSeq(msg);
            assertEquals("002", seq, "응답 시 guid_prgs_no는 002여야 함");
        }

        @Test
        void dman_rspn_dvcd_R_응답설정() throws Exception {
            StandardMessage msg = parseReq();
            coordinator.coordinateBeforeResponse(msg, null, null, "utf-8");
            String json = msg.getDataString(MessageType.JSON, "utf-8");
            JsonNode root = jacksonMapper.readTree(json);
            assertEquals("R", root.path("HEAD").path("dman_rspn_dvcd").asText(),
                    "응답 시 dman_rspn_dvcd는 R이어야 함");
        }

        @Test
        void mesg_rspn_dt_오늘날짜() throws Exception {
            StandardMessage msg = parseReq();
            coordinator.coordinateBeforeResponse(msg, null, null, "utf-8");
            assertEquals(LocalDate.now().format(FMT_DATE), msg.findItemValue("HEAD.mesg_rspn_dt"));
        }

        @Test
        void mesg_rspn_time_9자리_숫자() throws Exception {
            StandardMessage msg = parseReq();
            coordinator.coordinateBeforeResponse(msg, null, null, "utf-8");
            String time = msg.findItemValue("HEAD.mesg_rspn_time");
            assertNotNull(time);
            assertEquals(9, time.length());
            assertTrue(time.matches("\\d{9}"));
        }

        @Test
        void 요청_HEAD_guid_응답에서_유지() throws Exception {
            StandardMessage msg = parseReq();
            coordinator.coordinateAfterCreateMessageByRule(msg, null, null);
            String guidAfterReq = mapper.getGuid(msg);
            coordinator.coordinateBeforeResponse(msg, null, null, "utf-8");
            assertEquals(guidAfterReq, mapper.getGuid(msg), "응답 시 guid가 변경되면 안 됨");
        }

        @Test
        void 요청_HEAD_if_id_응답에서_유지() throws Exception {
            StandardMessage msg = parseReq();
            coordinator.coordinateBeforeResponse(msg, null, null, "utf-8");
            assertEquals(TEST_IF_ID, msg.findItemValue("HEAD.if_id").trim());
        }

        @Test
        void 요청_HEAD_tx_id_응답에서_유지() throws Exception {
            StandardMessage msg = parseReq();
            coordinator.coordinateBeforeResponse(msg, null, null, "utf-8");
            assertEquals(TEST_TX_ID, msg.findItemValue("HEAD.tx_id").trim());
        }

        @Test
        void BIZ_DATA_임의값_응답에서_유지() throws Exception {
            StandardMessage msg = parseReq();
            coordinator.coordinateBeforeResponse(msg, null, null, "utf-8");
            String json = msg.getDataString(MessageType.JSON, "utf-8");
            JsonNode root = jacksonMapper.readTree(json);
            assertNotNull(root.path("DATA").get("BIZ_DATA"), "응답에서 BIZ_DATA가 유지되어야 함");
        }
    }

    // ================================================================
    // 5. 정상 응답 MSG — coordinateAfterRecvNonStdSyncResponse
    // ================================================================

    @Nested
    @TestInstance(TestInstance.Lifecycle.PER_CLASS)
    class 정상응답_MSG {

        @Test
        void procs_rslt_dvcd_S() {
            StandardMessage msg = manager.getStandardMessage();
            coordinator.coordinateAfterRecvNonStdSyncResponse(msg);
            assertEquals("S", msg.findItemValue("HEAD.procs_rslt_dvcd"));
        }

        @Test
        void msg_dvcd_NM() {
            StandardMessage msg = manager.getStandardMessage();
            coordinator.coordinateAfterRecvNonStdSyncResponse(msg);
            assertEquals("NM", msg.findItemValue("MSG.msg_dvcd"));
        }

        @Test
        void msg_list_rowcnt_0() {
            StandardMessage msg = manager.getStandardMessage();
            coordinator.coordinateAfterRecvNonStdSyncResponse(msg);
            String rowcnt = msg.findItemValue("MSG.MAIN_MSG.msg_list_rowcnt");
            // NUMBER 타입: "00000" → trim 후 "" 또는 "0" 모두 0을 의미함
            String normalized = (rowcnt == null || rowcnt.trim().isEmpty()) ? "0" : rowcnt.trim();
            assertEquals(0, Integer.parseInt(normalized), "정상 응답 msg_list_rowcnt는 0이어야 함");
        }

        @Test
        void outp_atrb_cd_0() {
            StandardMessage msg = manager.getStandardMessage();
            coordinator.coordinateAfterRecvNonStdSyncResponse(msg);
            assertEquals("0", msg.findItemValue("MSG.MAIN_MSG.outp_atrb_cd"));
        }

        @Test
        void outp_msg_cd_정상코드() {
            StandardMessage msg = manager.getStandardMessage();
            coordinator.coordinateAfterRecvNonStdSyncResponse(msg);
            String msgCd = msg.findItemValue("MSG.MAIN_MSG.outp_msg_cd");
            assertFalse(msgCd == null || msgCd.trim().isEmpty(), "outp_msg_cd가 설정되어야 함");
        }

        @Test
        void outp_msg_ctnt_정상처리() {
            StandardMessage msg = manager.getStandardMessage();
            coordinator.coordinateAfterRecvNonStdSyncResponse(msg);
            String ctnt = msg.findItemValue("MSG.MAIN_MSG.outp_msg_ctnt");
            assertFalse(ctnt == null || ctnt.trim().isEmpty(), "outp_msg_ctnt가 설정되어야 함");
        }

        @Test
        void JSON_직렬화_MSG_NM_포함() throws Exception {
            StandardMessage msg = manager.getStandardMessage();
            coordinator.coordinateAfterRecvNonStdSyncResponse(msg);
            String json = msg.getDataString(MessageType.JSON, "utf-8");
            JsonNode root = jacksonMapper.readTree(json);
            assertTrue(root.has("MSG"), "정상응답 JSON에 MSG 블록 없음");
            assertEquals("NM", root.path("MSG").path("msg_dvcd").asText());
        }

        @Test
        void BIZ_DATA_임의값_설정_가능() throws Exception {
            StandardMessage msg = manager.getStandardMessage();
            msg.setData("DATA.BIZ_DATA", "{\"result\":\"OK\"}");
            coordinator.coordinateAfterRecvNonStdSyncResponse(msg);
            String bizData = msg.findItemValue("DATA.BIZ_DATA");
            assertFalse(bizData == null || bizData.trim().isEmpty(), "BIZ_DATA가 설정된 상태로 유지되어야 함");
        }
    }

    // ================================================================
    // 6. 오류 응답 MSG — coordinateSetStandardMessageError
    // ================================================================

    @Nested
    @TestInstance(TestInstance.Lifecycle.PER_CLASS)
    class 오류응답_MSG {

        @Test
        void procs_rslt_dvcd_F() {
            StandardMessage msg = manager.getStandardMessage();
            coordinator.coordinateSetStandardMessageError(msg, mapper, "RECEAICMM999", "오류발생");
            assertEquals("F", msg.findItemValue("HEAD.procs_rslt_dvcd"));
        }

        @Test
        void dman_rspn_dvcd_R() {
            StandardMessage msg = manager.getStandardMessage();
            coordinator.coordinateSetStandardMessageError(msg, mapper, "RECEAICMM999", "오류발생");
            assertEquals("R", msg.findItemValue("HEAD.dman_rspn_dvcd"));
        }

        @Test
        void msg_dvcd_EM() {
            StandardMessage msg = manager.getStandardMessage();
            coordinator.coordinateSetStandardMessageError(msg, mapper, "RECEAICMM999", "오류발생");
            assertEquals("EM", msg.findItemValue("MSG.msg_dvcd"));
        }

        @Test
        void outp_atrb_cd_1_팝업() {
            StandardMessage msg = manager.getStandardMessage();
            coordinator.coordinateSetStandardMessageError(msg, mapper, "RECEAICMM999", "오류발생");
            assertEquals("1", msg.findItemValue("MSG.MAIN_MSG.outp_atrb_cd"));
        }

        @Test
        void msg_list_rowcnt_1() {
            StandardMessage msg = manager.getStandardMessage();
            coordinator.coordinateSetStandardMessageError(msg, mapper, "RECEAICMM999", "오류발생");
            String rowcnt = msg.findItemValue("MSG.MAIN_MSG.msg_list_rowcnt");
            String normalized = (rowcnt == null || rowcnt.trim().isEmpty()) ? "0" : rowcnt.trim();
            assertEquals(1, Integer.parseInt(normalized), "오류응답 msg_list_rowcnt는 1이어야 함");
        }

        @Test
        void outp_msg_cd_에러코드_설정됨() {
            StandardMessage msg = manager.getStandardMessage();
            coordinator.coordinateSetStandardMessageError(msg, mapper, "RECEAICMM999", "오류발생");
            String msgCd = msg.findItemValue("MSG.MAIN_MSG.outp_msg_cd");
            assertFalse(msgCd == null || msgCd.trim().isEmpty(), "outp_msg_cd가 설정되어야 함");
        }

        @Test
        void outp_msg_ctnt_에러메시지_설정됨() {
            StandardMessage msg = manager.getStandardMessage();
            coordinator.coordinateSetStandardMessageError(msg, mapper, "RECEAICMM999", "오류발생");
            String ctnt = msg.findItemValue("MSG.MAIN_MSG.outp_msg_ctnt");
            assertFalse(ctnt == null || ctnt.trim().isEmpty(), "outp_msg_ctnt가 설정되어야 함");
        }

        @Test
        void MSG_LIST_outp_msg_cd_설정됨() throws Exception {
            StandardMessage msg = manager.getStandardMessage();
            coordinator.coordinateSetStandardMessageError(msg, mapper, "RECEAICMM999", "오류발생");
            String json = msg.getDataString(MessageType.JSON, "utf-8");
            JsonNode root = jacksonMapper.readTree(json);
            JsonNode msgList = root.path("MSG").path("MSG_LIST");
            assertFalse(msgList.isMissingNode() || msgList.isEmpty(),
                    "MSG_LIST에 1건이 있어야 함");
            String cd = msgList.get(0).path("outp_msg_cd").asText();
            assertFalse(cd.isEmpty(), "MSG_LIST[0].outp_msg_cd가 설정되어야 함");
        }

        @Test
        void MSG_LIST_outp_msg_ctnt_설정됨() throws Exception {
            StandardMessage msg = manager.getStandardMessage();
            coordinator.coordinateSetStandardMessageError(msg, mapper, "RECEAICMM999", "오류발생");
            String json = msg.getDataString(MessageType.JSON, "utf-8");
            JsonNode root = jacksonMapper.readTree(json);
            String ctnt = root.path("MSG").path("MSG_LIST").get(0).path("outp_msg_ctnt").asText();
            assertFalse(ctnt.isEmpty(), "MSG_LIST[0].outp_msg_ctnt가 설정되어야 함");
        }

        @Test
        void MSG_LIST_outp_msg_desc_설정됨() throws Exception {
            StandardMessage msg = manager.getStandardMessage();
            coordinator.coordinateSetStandardMessageError(msg, mapper, "RECEAICMM999", "오류설명");
            String json = msg.getDataString(MessageType.JSON, "utf-8");
            JsonNode root = jacksonMapper.readTree(json);
            String desc = root.path("MSG").path("MSG_LIST").get(0).path("outp_msg_desc").asText();
            assertFalse(desc.isEmpty(), "MSG_LIST[0].outp_msg_desc가 설정되어야 함");
        }

        @Test
        void MSG_LIST_err_occu_loct_설정됨() throws Exception {
            StandardMessage msg = manager.getStandardMessage();
            coordinator.coordinateSetStandardMessageError(msg, mapper, "RECEAICMM999", "오류발생");
            String json = msg.getDataString(MessageType.JSON, "utf-8");
            JsonNode root = jacksonMapper.readTree(json);
            String loct = root.path("MSG").path("MSG_LIST").get(0).path("err_occu_loct").asText();
            assertFalse(loct.isEmpty(), "MSG_LIST[0].err_occu_loct가 설정되어야 함");
        }

        @Test
        void JSON_직렬화_MSG_EM_포함() throws Exception {
            StandardMessage msg = manager.getStandardMessage();
            coordinator.coordinateSetStandardMessageError(msg, mapper, "RECEAICMM999", "오류발생");
            String json = msg.getDataString(MessageType.JSON, "utf-8");
            JsonNode root = jacksonMapper.readTree(json);
            assertTrue(root.has("MSG"), "오류응답 JSON에 MSG 블록 없음");
            assertEquals("EM", root.path("MSG").path("msg_dvcd").asText());
        }

        @Test
        void BIZ_DATA_임의값_설정_가능() throws Exception {
            StandardMessage msg = manager.getStandardMessage();
            msg.setData("DATA.BIZ_DATA", "{\"error_info\":\"test\"}");
            coordinator.coordinateSetStandardMessageError(msg, mapper, "RECEAICMM999", "오류발생");
            String bizData = msg.findItemValue("DATA.BIZ_DATA");
            assertFalse(bizData == null || bizData.trim().isEmpty());
        }
    }

    // ================================================================
    // helper
    // ================================================================

    private StandardMessage parseReq() throws Exception {
        StandardMessage msg = manager.getStandardMessage();
        jsonReader.parse(msg, REQ_JSON);
        return msg;
    }

    private StandardMessage parseErpReq() throws Exception {
        StandardMessage msg = manager.getStandardMessage();
        jsonReader.parse(msg, ERP_REQ_JSON);
        return msg;
    }
}
