package com.eactive.eai.adapter.http.dynamic.filter;

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

import java.nio.charset.StandardCharsets;
import java.util.Base64;
import java.util.HashMap;
import java.util.Map;
import java.util.Properties;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.mockito.ArgumentCaptor;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.MethodOrderer;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestMethodOrder;
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
import org.springframework.context.support.GenericApplicationContext;

import com.eactive.eai.adapter.http.filter.AbstractCryptoFilter;
import com.eactive.eai.common.security.CryptoModuleService;
import com.eactive.eai.common.util.ApplicationContextProvider;

/**
 * InCryptoFilter / AbstractCryptoFilter 단위 테스트.
 * CryptoModuleService는 Mock으로 대체하여 필터 로직(Base64, JSON 경로, 프로퍼티 파싱)만 검증한다.
 */
@TestMethodOrder(MethodOrderer.DisplayName.class)
class InCryptoFilterTest {

    private static final String MODULE     = "TEST_MODULE";
    private static final String PLAIN_STR  = "decrypted-plain-text";
    private static final byte[] PLAIN      = PLAIN_STR.getBytes(StandardCharsets.UTF_8);
    private static final byte[] CIPHER     = new byte[]{0x10, 0x20, 0x30, 0x40, 0x50, 0x60};
    private static final String CIPHER_B64 = Base64.getEncoder().encodeToString(CIPHER);

    private static GenericApplicationContext ctx;
    private static CryptoModuleService       mockService;
    private static InCryptoFilter            filter;

    private HttpServletRequest  mockRequest;
    private HttpServletResponse mockResponse;

    @BeforeAll
    static void setUpClass() throws Exception {
        mockService = mock(CryptoModuleService.class);

        ctx = new GenericApplicationContext();
        ctx.getBeanFactory().registerSingleton("cryptoModuleService", mockService);
        ctx.registerBeanDefinition("applicationContextProvider",
                BeanDefinitionBuilder.genericBeanDefinition(ApplicationContextProvider.class)
                        .getBeanDefinition());
        ctx.refresh();

        filter = new InCryptoFilter();
    }

    @AfterAll
    static void tearDownClass() {
        if (ctx != null) ctx.close();
    }

    @BeforeEach
    void setUp() throws Exception {
        reset(mockService);
        mockRequest  = mock(HttpServletRequest.class);
        mockResponse = mock(HttpServletResponse.class);

        when(mockService.decrypt(anyString(), anyMap(), any(), any(byte[].class))).thenReturn(PLAIN);
        when(mockService.encrypt(anyString(), anyMap(), any(), any(byte[].class))).thenReturn(CIPHER);
    }

    // =========================================================================
    // 1. buildRuntimeContext
    // =========================================================================

    @Test
    @DisplayName("1-1. buildRuntimeContext — 항상 빈 Map 반환")
    void testBuildRuntimeContext_returnsEmptyMap() {
        Map<String, String> result = filter.buildRuntimeContext(new Properties(), mockRequest);

        assertNotNull(result);
        assertTrue(result.isEmpty());
    }

    @SuppressWarnings("unchecked")
    @Test
    @DisplayName("1-2. buildRuntimeContext 오버라이드 시 runtimeContext가 복호화 서비스에 전달된다")
    void testBuildRuntimeContext_override_passedToDecrypt() throws Exception {
        InCryptoFilter filterWithCtx = new InCryptoFilter() {
            @Override
            protected Map<String, String> buildRuntimeContext(Properties p, HttpServletRequest req) {
                Map<String, String> ctx = new HashMap<>();
                ctx.put("groupSeq", "G001");
                return ctx;
            }
        };
        String body = "{\"enc\":\"" + CIPHER_B64 + "\",\"result\":\"\"}";

        filterWithCtx.doPreFilter("g", "a", body, fieldDecryptProps("/enc", "/result"), mockRequest, mockResponse);

        ArgumentCaptor<Map> ctxCaptor = ArgumentCaptor.forClass(Map.class);
        verify(mockService).decrypt(eq(MODULE), ctxCaptor.capture(), isNull(), eq(CIPHER));
        assertEquals("G001", ctxCaptor.getValue().get("groupSeq"));
    }

    @SuppressWarnings("unchecked")
    @Test
    @DisplayName("1-3. buildRuntimeContext 오버라이드 시 runtimeContext가 암호화 서비스에 전달된다")
    void testBuildRuntimeContext_override_passedToEncrypt() throws Exception {
        InCryptoFilter filterWithCtx = new InCryptoFilter() {
            @Override
            protected Map<String, String> buildRuntimeContext(Properties p, HttpServletRequest req) {
                Map<String, String> ctx = new HashMap<>();
                ctx.put("groupSeq", "G001");
                return ctx;
            }
        };

        filterWithCtx.doPostFilter("g", "a", "plain-body", fieldEncryptProps("/", "/encryptedData"), mockRequest, mockResponse);

        ArgumentCaptor<Map> ctxCaptor = ArgumentCaptor.forClass(Map.class);
        verify(mockService).encrypt(eq(MODULE), ctxCaptor.capture(), isNull(), any(byte[].class));
        assertEquals("G001", ctxCaptor.getValue().get("groupSeq"));
    }

    // =========================================================================
    // 2. buildAad
    // =========================================================================

    @Test
    @DisplayName("2-1. buildAad — CRYPTO_AAD_HEADER 미설정 → null")
    void testBuildAad_noProp_returnsNull() {
        assertNull(filter.buildAad(new Properties(), mockRequest));
    }

    @Test
    @DisplayName("2-2. buildAad — 헤더명 설정, 헤더값 없음 → null")
    void testBuildAad_headerNameSet_noValue_returnsNull() {
        Properties prop = new Properties();
        prop.setProperty(AbstractCryptoFilter.PROP_AAD_HEADER, "X-Api-Request-Id");
        when(mockRequest.getHeader("X-Api-Request-Id")).thenReturn(null);

        assertNull(filter.buildAad(prop, mockRequest));
    }

    @Test
    @DisplayName("2-3. buildAad — 헤더명 설정, 헤더값 있음 → UTF-8 바이트 반환")
    void testBuildAad_headerNameSet_valuePresent_returnsByte() {
        String headerVal = "req-id-abc123";
        Properties prop  = new Properties();
        prop.setProperty(AbstractCryptoFilter.PROP_AAD_HEADER, "X-Api-Request-Id");
        when(mockRequest.getHeader("X-Api-Request-Id")).thenReturn(headerVal);

        assertArrayEquals(headerVal.getBytes(StandardCharsets.UTF_8), filter.buildAad(prop, mockRequest));
    }

    // =========================================================================
    // 3. doPreFilter — BODY scope 복호화
    // =========================================================================

    @Test
    @DisplayName("3-1. BODY scope — Base64 암호문 복호화 → 평문 반환")
    void testPreFilter_body_decrypt_roundTrip() throws Exception {
        Object result = filter.doPreFilter("g", "a", CIPHER_B64, bodyDecryptProps(), mockRequest, mockResponse);

        assertEquals(PLAIN_STR, result);
        verify(mockService).decrypt(eq(MODULE), anyMap(), isNull(), eq(CIPHER));
    }

    @Test
    @DisplayName("3-2. CRYPTO_MODULE_NAME 누락 — IllegalArgumentException")
    void testPreFilter_missingModuleName_throwsException() {
        Properties prop = new Properties();
        prop.setProperty(AbstractCryptoFilter.PROP_SCOPE, "BODY");

        assertThrows(IllegalArgumentException.class,
                () -> filter.doPreFilter("g", "a", CIPHER_B64, prop, mockRequest, mockResponse));
    }

    @Test
    @DisplayName("3-3. message가 byte[] — UTF-8 디코딩 후 처리")
    void testPreFilter_body_byteArrayMessage() throws Exception {
        Object result = filter.doPreFilter("g", "a", CIPHER_B64.getBytes(StandardCharsets.UTF_8),
                bodyDecryptProps(), mockRequest, mockResponse);

        assertEquals(PLAIN_STR, result);
    }

    @Test
    @DisplayName("3-4. message가 String/byte[] 외 타입 — toString() 변환 후 처리")
    void testPreFilter_body_nonStringMessage_usesToString() throws Exception {
        Object nonStringMsg = new Object() {
            @Override
            public String toString() { return CIPHER_B64; }
        };

        Object result = filter.doPreFilter("g", "a", nonStringMsg, bodyDecryptProps(), mockRequest, mockResponse);

        assertEquals(PLAIN_STR, result);
    }

    // =========================================================================
    // 4. doPreFilter — FIELD scope 복호화
    // =========================================================================

    @Test
    @DisplayName("4-1. FIELD scope — fromPath 복호화 → toPath 반영")
    void testPreFilter_field_decryptToPath() throws Exception {
        String body = "{\"enc\":\"" + CIPHER_B64 + "\",\"result\":\"\"}";
        Properties prop = fieldDecryptProps("/enc", "/result");

        Object result = filter.doPreFilter("g", "a", body, prop, mockRequest, mockResponse);

        assertTrue(result.toString().contains("\"result\":\"" + PLAIN_STR + "\""),
                "toPath에 복호화된 값이 반영되어야 한다");
        verify(mockService).decrypt(eq(MODULE), anyMap(), isNull(), eq(CIPHER));
    }

    @Test
    @DisplayName("4-2. FIELD scope, toPath='/' — 복호화 결과가 비-JSON이면 텍스트 그대로 반환 (fallback)")
    void testPreFilter_field_decryptToRoot_nonJsonFallback() throws Exception {
        String body = "{\"enc\":\"" + CIPHER_B64 + "\"}";
        Properties prop = fieldDecryptProps("/enc", "/");

        Object result = filter.doPreFilter("g", "a", body, prop, mockRequest, mockResponse);

        assertEquals(PLAIN_STR, result, "복호화 결과가 JSON 객체가 아니면 텍스트 그대로 반환");
    }

    @Test
    @DisplayName("4-4. FIELD scope, toPath='/' — 복호화된 JSON을 body에 병합 (fromPath 필드 제거)")
    void testPreFilter_field_decryptToRoot_mergesJson() throws Exception {
        String decryptedJson = "{\"userId\":\"U001\",\"name\":\"test\"}";
        when(mockService.decrypt(anyString(), anyMap(), any(), any(byte[].class)))
                .thenReturn(decryptedJson.getBytes(StandardCharsets.UTF_8));

        String body = "{\"encryptedData\":\"" + CIPHER_B64 + "\",\"requestId\":\"REQ001\"}";
        Properties prop = fieldDecryptProps("/encryptedData", "/");

        Object result = filter.doPreFilter("g", "a", body, prop, mockRequest, mockResponse);

        String r = result.toString();
        assertFalse(r.contains("encryptedData"), "암호화 필드(encryptedData)는 제거되어야 한다");
        assertTrue(r.contains("\"requestId\":\"REQ001\""), "원본 body의 다른 필드는 유지되어야 한다");
        assertTrue(r.contains("\"userId\":\"U001\""), "복호화된 JSON 필드가 병합되어야 한다");
        assertTrue(r.contains("\"name\":\"test\""), "복호화된 JSON 필드가 병합되어야 한다");
    }

    @Test
    @DisplayName("4-3. FIELD scope — fromPath 값 없음 → body 그대로 반환")
    void testPreFilter_field_missingFromPathValue_passThrough() throws Exception {
        String body = "{\"other\":\"value\"}";
        Properties prop = fieldDecryptProps("/enc", "/other");

        Object result = filter.doPreFilter("g", "a", body, prop, mockRequest, mockResponse);

        assertEquals(body, result);
        verifyNoInteractions(mockService);
    }

    // =========================================================================
    // 5. doPostFilter — BODY scope 암호화
    // =========================================================================

    @Test
    @DisplayName("5-1. BODY scope — 평문 암호화 → Base64 반환")
    void testPostFilter_body_encrypt() throws Exception {
        String body = "plain-response-body";
        Object result = filter.doPostFilter("g", "a", body, bodyEncryptProps(), mockRequest, mockResponse);

        assertEquals(CIPHER_B64, result);
        verify(mockService).encrypt(eq(MODULE), anyMap(), isNull(), eq(body.getBytes(StandardCharsets.UTF_8)));
    }

    // =========================================================================
    // 6. doPostFilter — FIELD scope 암호화
    // =========================================================================

    @Test
    @DisplayName("6-1. FIELD scope — fromPath 평문 암호화 → toPath Base64 반영")
    void testPostFilter_field_encryptToPath() throws Exception {
        String body = "{\"plain\":\"hello\",\"enc\":\"\"}";
        Properties prop = fieldEncryptProps("/plain", "/enc");

        Object result = filter.doPostFilter("g", "a", body, prop, mockRequest, mockResponse);

        assertTrue(result.toString().contains("\"enc\":\"" + CIPHER_B64 + "\""),
                "toPath에 Base64 암호문이 반영되어야 한다");
    }

    @Test
    @DisplayName("6-2. FIELD scope, fromPath='/' — body 전체 암호화 → toPath 필드명으로 래핑")
    void testPostFilter_field_encryptFromRoot() throws Exception {
        String body = "{\"plain\":\"hello\"}";
        Properties prop = fieldEncryptProps("/", "/encrypted");

        Object result = filter.doPostFilter("g", "a", body, prop, mockRequest, mockResponse);

        assertEquals("{\"encrypted\":\"" + CIPHER_B64 + "\"}", result);
    }

    @Test
    @DisplayName("6-3. FIELD scope — fromPath 값 없음 → body 그대로 반환")
    void testPostFilter_field_missingFromPathValue_passThrough() throws Exception {
        String body = "{\"other\":\"value\"}";
        Properties prop = fieldEncryptProps("/plain", "/enc");

        Object result = filter.doPostFilter("g", "a", body, prop, mockRequest, mockResponse);

        assertEquals(body, result);
        verifyNoInteractions(mockService);
    }

    // =========================================================================
    // 7. AAD 헤더 연동
    // =========================================================================

    @Test
    @DisplayName("7-1. doPreFilter BODY — CRYPTO_AAD_HEADER 설정 시 헤더값을 AAD로 전달")
    void testPreFilter_body_aadFromHeader_passedToService() throws Exception {
        String aadVal = "request-id-xyz";
        Properties prop = bodyDecryptProps();
        prop.setProperty(AbstractCryptoFilter.PROP_AAD_HEADER, "X-Api-Request-Id");
        when(mockRequest.getHeader("X-Api-Request-Id")).thenReturn(aadVal);

        filter.doPreFilter("g", "a", CIPHER_B64, prop, mockRequest, mockResponse);

        verify(mockService).decrypt(eq(MODULE), anyMap(),
                eq(aadVal.getBytes(StandardCharsets.UTF_8)), eq(CIPHER));
    }

    @Test
    @DisplayName("7-2. doPostFilter BODY — CRYPTO_AAD_HEADER 미설정 시 aad=null로 전달")
    void testPostFilter_body_noAadHeader_nullAadPassedToService() throws Exception {
        filter.doPostFilter("g", "a", "plain", bodyEncryptProps(), mockRequest, mockResponse);

        verify(mockService).encrypt(eq(MODULE), anyMap(), isNull(), any(byte[].class));
    }

    @Test
    @DisplayName("7-3. doPreFilter FIELD — CRYPTO_AAD_HEADER 설정 시 헤더값을 AAD로 전달")
    void testPreFilter_field_aadFromHeader_passedToService() throws Exception {
        String aadVal = "field-aad-value";
        String body   = "{\"enc\":\"" + CIPHER_B64 + "\",\"result\":\"\"}";
        Properties prop = fieldDecryptProps("/enc", "/result");
        prop.setProperty(AbstractCryptoFilter.PROP_AAD_HEADER, "X-Api-Aad");
        when(mockRequest.getHeader("X-Api-Aad")).thenReturn(aadVal);

        filter.doPreFilter("g", "a", body, prop, mockRequest, mockResponse);

        verify(mockService).decrypt(eq(MODULE), anyMap(),
                eq(aadVal.getBytes(StandardCharsets.UTF_8)), eq(CIPHER));
    }

    // =========================================================================
    // 헬퍼
    // =========================================================================

    private Properties bodyDecryptProps() {
        Properties p = new Properties();
        p.setProperty(AbstractCryptoFilter.PROP_MODULE_NAME, MODULE);
        p.setProperty(AbstractCryptoFilter.PROP_SCOPE, "BODY");
        return p;
    }

    private Properties bodyEncryptProps() {
        return bodyDecryptProps();
    }

    private Properties fieldDecryptProps(String fromPath, String toPath) {
        Properties p = new Properties();
        p.setProperty(AbstractCryptoFilter.PROP_MODULE_NAME, MODULE);
        p.setProperty(AbstractCryptoFilter.PROP_SCOPE, "FIELD");
        p.setProperty(AbstractCryptoFilter.PROP_DEC_FROM_PATH, fromPath);
        p.setProperty(AbstractCryptoFilter.PROP_DEC_TO_PATH, toPath);
        return p;
    }

    private Properties fieldEncryptProps(String fromPath, String toPath) {
        Properties p = new Properties();
        p.setProperty(AbstractCryptoFilter.PROP_MODULE_NAME, MODULE);
        p.setProperty(AbstractCryptoFilter.PROP_SCOPE, "FIELD");
        p.setProperty(AbstractCryptoFilter.PROP_ENC_FROM_PATH, fromPath);
        p.setProperty(AbstractCryptoFilter.PROP_ENC_TO_PATH, toPath);
        return p;
    }
}
