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.Map;
import java.util.Properties;

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

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.common.security.CryptoModuleService;
import com.eactive.eai.common.util.ApplicationContextProvider;

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

    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 BaseCryptoFilter          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 BaseCryptoFilter();
    }

    @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());
    }

    // =========================================================================
    // 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(CryptoFilter.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(CryptoFilter.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-3. CRYPTO_MODULE_NAME 누락 — IllegalArgumentException")
    void testPreFilter_missingModuleName_throwsException() {
        Properties prop = new Properties();
        prop.setProperty(CryptoFilter.PROP_SCOPE, "BODY");

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

    @Test
    @DisplayName("3-4. 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);
    }

    // =========================================================================
    // 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='/' — 복호화된 텍스트로 body 전체 교체")
    void testPreFilter_field_decryptToRoot() 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, "toPath='/'이면 body 전체가 복호화된 텍스트로 교체되어야 한다");
    }

    @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, CRYPTO_ENC_ENABLED=Y — 평문 암호화 → 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(CryptoFilter.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(CryptoFilter.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(CryptoFilter.PROP_MODULE_NAME, MODULE);
        p.setProperty(CryptoFilter.PROP_SCOPE, "BODY");
        return p;
    }

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

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

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