package com.eactive.eai.adapter.http.client.impl.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.Properties;

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;

/**
 * OutCryptoFilter 단위 테스트.
 *
 * doPreFilter  = 외부로 보내는 요청 암호화 (InCryptoFilter와 반대 방향)
 * doPostFilter = 외부에서 받은 응답 복호화
 */
@TestMethodOrder(MethodOrderer.DisplayName.class)
class OutCryptoFilterTest {

    private static final String MODULE     = "TEST_MODULE";
    private static final String PLAIN_STR  = "plain-request-body";
    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 OutCryptoFilter           filter;

    private Properties tempProp;

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

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

    @BeforeEach
    void setUp() throws Exception {
        reset(mockService);
        tempProp = new Properties();

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

    // =========================================================================
    // 1. buildAad
    // =========================================================================

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

    @Test
    @DisplayName("1-2. buildAad — CRYPTO_AAD_PROP 설정, tempProp에 값 없음 → null")
    void testBuildAad_propSet_noTempValue_returnsNull() {
        Properties prop = new Properties();
        prop.setProperty(AbstractCryptoFilter.PROP_AAD_HEADER, "aadKey");

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

    @Test
    @DisplayName("1-3. buildAad — CRYPTO_AAD_PROP 설정, tempProp에 값 있음 → UTF-8 바이트 반환")
    void testBuildAad_propSet_tempValuePresent_returnsByte() {
        String aadVal = "aad-value-xyz";
        Properties prop = new Properties();
        prop.setProperty(AbstractCryptoFilter.PROP_AAD_HEADER, "aadKey");
        tempProp.setProperty("aadKey", aadVal);

        assertArrayEquals(aadVal.getBytes(StandardCharsets.UTF_8), filter.buildAad(prop, tempProp));
    }

    // =========================================================================
    // 2. doPreFilter — 암호화 (요청을 외부로 보내기 전)
    // =========================================================================

    @Test
    @DisplayName("2-1. BODY scope — 평문 암호화 → Base64 반환")
    void testPreFilter_body_encrypt() throws Exception {
        Object result = filter.doPreFilter("g", "a", bodyEncryptProps(), PLAIN_STR, tempProp);

        assertEquals(CIPHER_B64, result);
        verify(mockService).encrypt(eq(MODULE), anyMap(), isNull(), eq(PLAIN));
    }

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

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

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

    @Test
    @DisplayName("2-3. FIELD scope — fromPath 평문 암호화 → toPath Base64 반영")
    void testPreFilter_field_encryptSpecificPath() throws Exception {
        String body = "{\"data\":\"secret\",\"enc\":\"\"}";
        Properties prop = fieldEncryptProps(tempProp, "/data", "/enc");

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

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

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

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

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

    @Test
    @DisplayName("2-5. 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", prop, PLAIN_STR, tempProp));
    }

    // =========================================================================
    // 3. doPostFilter — 복호화 (외부로부터 응답 받은 후)
    // =========================================================================

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

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

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

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

        assertTrue(result.toString().contains("\"result\":\"" + PLAIN_STR + "\""),
                "toPath에 복호화된 값이 반영되어야 한다");
    }

    @Test
    @DisplayName("3-3. FIELD scope, toPath='/' — 복호화된 텍스트로 body 전체 교체")
    void testPostFilter_field_decryptToRoot() throws Exception {
        String body = "{\"enc\":\"" + CIPHER_B64 + "\"}";
        Properties prop = fieldDecryptProps(tempProp, "/enc", "/");

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

        assertEquals(PLAIN_STR, result);
    }

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

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

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

    // =========================================================================
    // 4. AAD (tempProp 경유)
    // =========================================================================

    @Test
    @DisplayName("4-1. doPreFilter BODY — CRYPTO_AAD_PROP 설정 시 tempProp 값을 AAD로 전달")
    void testPreFilter_body_aadFromTempProp() throws Exception {
        String aadVal = "out-aad-value";
        Properties prop = bodyEncryptProps();
        prop.setProperty(AbstractCryptoFilter.PROP_AAD_HEADER, "myAadKey");
        tempProp.setProperty("myAadKey", aadVal);

        filter.doPreFilter("g", "a", prop, PLAIN_STR, tempProp);

        verify(mockService).encrypt(eq(MODULE), anyMap(),
                eq(aadVal.getBytes(StandardCharsets.UTF_8)), any(byte[].class));
    }

    @Test
    @DisplayName("4-2. doPostFilter BODY — CRYPTO_AAD_PROP 미설정 시 aad=null로 전달")
    void testPostFilter_body_noAadProp_nullAad() throws Exception {
        filter.doPostFilter("g", "a", bodyDecryptProps(), CIPHER_B64, tempProp);

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

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

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

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

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

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