package com.eactive.eai.common.security.keyderiv;

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

import java.util.HashMap;
import java.util.Map;

import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;

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.hsm.HsmCryptoService;
import com.eactive.eai.common.security.keyderiv.strategy.HsmContextXorKeyDerivationStrategy;
import com.eactive.eai.common.util.ApplicationContextProvider;

/**
 * HsmContextXorKeyDerivationStrategy 단위 테스트
 * HsmCryptoService는 Mockito Mock으로 대체한다.
 */
@TestMethodOrder(MethodOrderer.DisplayName.class)
class HsmContextXorKeyDerivationStrategyTest {

    private static final String HSM_KEY_ALIAS = "MASTER_KEY_AES";
    private static final byte[] MASTER_KEY    = "0123456789abcdef".getBytes(java.nio.charset.StandardCharsets.UTF_8);

    private static GenericApplicationContext        ctx;
    private static HsmCryptoService                 mockHsmCryptoService;
    private static HsmContextXorKeyDerivationStrategy strategy;

    private Map<String, String> params;
    private Map<String, String> runtimeContext;

    @BeforeAll
    static void setUpClass() throws Exception {
        mockHsmCryptoService = mock(HsmCryptoService.class);
        SecretKey mockSecretKey = new SecretKeySpec(MASTER_KEY, "AES");
        when(mockHsmCryptoService.getSecretKey(HSM_KEY_ALIAS)).thenReturn(mockSecretKey);

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

        strategy = new HsmContextXorKeyDerivationStrategy();
    }

    @BeforeEach
    void setUpParams() {
        params = new HashMap<>();
        params.put("hsmKeyAlias", HSM_KEY_ALIAS);
        params.put("contextKey", "X-Api-Enc-Key");
        params.put("offset", "0");
        params.put("length", "16");

        runtimeContext = new HashMap<>();
        runtimeContext.put("X-Api-Enc-Key", "clientKeyValue01");
    }

    // =========================================================================
    // 1. deriveKey
    // =========================================================================

    @Test
    @DisplayName("1-1. 정상 파라미터 — DerivedKey 반환")
    void testDeriveKey_validParams_returnsDerivedKey() throws Exception {
        DerivedKey dk = strategy.deriveKey(params, runtimeContext);

        assertNotNull(dk);
        assertNotNull(dk.getEncKey());
        assertNotNull(dk.getDecKey());
        assertEquals(16, dk.getEncKey().length, "도출된 키 길이는 마스터키 길이(16)여야 한다");
    }

    @Test
    @DisplayName("1-2. XOR 계산 결과 검증")
    void testDeriveKey_xorResultIsCorrect() throws Exception {
        DerivedKey dk = strategy.deriveKey(params, runtimeContext);

        byte[] contextBytes = "clientKeyValue01".getBytes(java.nio.charset.StandardCharsets.UTF_8);
        byte[] expected = new byte[MASTER_KEY.length];
        for (int i = 0; i < MASTER_KEY.length; i++) {
            expected[i] = (i < contextBytes.length)
                    ? (byte) (MASTER_KEY[i] ^ contextBytes[i])
                    : MASTER_KEY[i];
        }

        assertArrayEquals(expected, dk.getEncKey(), "XOR 계산 결과가 일치해야 한다");
    }

    @Test
    @DisplayName("1-3. 대칭 알고리즘 — encKey == decKey")
    void testDeriveKey_encKeyEqualsDecKey() throws Exception {
        DerivedKey dk = strategy.deriveKey(params, runtimeContext);

        assertArrayEquals(dk.getEncKey(), dk.getDecKey(), "대칭 방식이므로 encKey와 decKey가 동일해야 한다");
    }

    @Test
    @DisplayName("1-4. runtimeContext 값 없음 — 빈 문자열로 처리 (XOR 불변)")
    void testDeriveKey_missingContextValue_noXorChange() throws Exception {
        runtimeContext.remove("X-Api-Enc-Key");
        DerivedKey dk = strategy.deriveKey(params, runtimeContext);

        assertArrayEquals(MASTER_KEY, dk.getEncKey(), "컨텍스트 값이 없으면 마스터키 그대로 반환되어야 한다");
    }

    @Test
    @DisplayName("1-5. offset 지정 — 해당 위치부터 XOR 적용")
    void testDeriveKey_withOffset() throws Exception {
        params.put("offset", "4");
        params.put("length", "8");
        runtimeContext.put("X-Api-Enc-Key", "01234567890abcde");

        DerivedKey dk = strategy.deriveKey(params, runtimeContext);

        assertNotNull(dk);
        assertEquals(MASTER_KEY.length, dk.getEncKey().length);
    }

    @Test
    @DisplayName("1-6. 필수 파라미터 누락(hsmKeyAlias) — IllegalArgumentException")
    void testDeriveKey_missingHsmKeyAlias_throwsException() {
        params.remove("hsmKeyAlias");

        assertThrows(IllegalArgumentException.class,
                () -> strategy.deriveKey(params, runtimeContext),
                "hsmKeyAlias 누락 시 IllegalArgumentException이 발생해야 한다");
    }

    @Test
    @DisplayName("1-7. 필수 파라미터 누락(contextKey) — IllegalArgumentException")
    void testDeriveKey_missingContextKey_throwsException() {
        params.remove("contextKey");

        assertThrows(IllegalArgumentException.class,
                () -> strategy.deriveKey(params, runtimeContext),
                "contextKey 누락 시 IllegalArgumentException이 발생해야 한다");
    }

    @Test
    @DisplayName("1-8. 다른 runtimeContext 값 — 다른 DerivedKey 생성")
    void testDeriveKey_differentContextValue_differentKey() throws Exception {
        DerivedKey dk1 = strategy.deriveKey(params, runtimeContext);

        runtimeContext.put("X-Api-Enc-Key", "differentValue00");
        DerivedKey dk2 = strategy.deriveKey(params, runtimeContext);

        assertFalse(java.util.Arrays.equals(dk1.getEncKey(), dk2.getEncKey()),
                "컨텍스트 값이 다르면 도출된 키도 달라야 한다");
    }

    // =========================================================================
    // 2. buildCacheKey
    // =========================================================================

    @Test
    @DisplayName("2-1. buildCacheKey — cryptoName:contextValue 형식")
    void testBuildCacheKey_format() {
        String cacheKey = strategy.buildCacheKey("CRYPTO_A", params, runtimeContext);

        assertEquals("CRYPTO_A:clientKeyValue01", cacheKey);
    }

    @Test
    @DisplayName("2-2. buildCacheKey — contextKey 값 없으면 빈 문자열")
    void testBuildCacheKey_missingContextValue_emptyString() {
        runtimeContext.remove("X-Api-Enc-Key");
        String cacheKey = strategy.buildCacheKey("CRYPTO_A", params, runtimeContext);

        assertEquals("CRYPTO_A:", cacheKey);
    }

    @Test
    @DisplayName("2-3. buildCacheKey — 다른 cryptoName은 다른 캐시 키")
    void testBuildCacheKey_differentCryptoName_differentKey() {
        String key1 = strategy.buildCacheKey("CRYPTO_A", params, runtimeContext);
        String key2 = strategy.buildCacheKey("CRYPTO_B", params, runtimeContext);

        assertNotEquals(key1, key2);
    }
}
