package com.eactive.eai.custom.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 javax.xml.bind.DatatypeConverter;

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

/**
 * HsmKeyAndIvSliceDerivationStrategy 단위 테스트
 */
@TestMethodOrder(MethodOrderer.DisplayName.class)
class HsmKeyAndIvSliceDerivationStrategyTest {

    private static final String HSM_KEY_ALIAS = "MASTER_KEY_AES";
    private static final String HSM_IV_ALIAS  = "MASTER_IV_AES";

    /** 32바이트 암호화 키 (AES-256) */
    private static final byte[] MASTER_KEY = new byte[32];
    /** 32바이트 IV 원본 → hex 64자 → substring(32,64) → 16바이트 IV */
    private static final byte[] MASTER_IV  = new byte[32];

    static {
        for (int i = 0; i < MASTER_KEY.length; i++) MASTER_KEY[i] = (byte) (i + 1);
        for (int i = 0; i < MASTER_IV.length;  i++) MASTER_IV[i]  = (byte) (i + 0x10);
    }

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

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

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

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

        strategy = new HsmKeyAndIvSliceDerivationStrategy();
    }

    @BeforeEach
    void setUpParams() {
        params = new HashMap<>();
        params.put("hsmKeyAlias", HSM_KEY_ALIAS);
        params.put("hsmIvAlias",  HSM_IV_ALIAS);

        runtimeContext = new HashMap<>();
    }

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

    @Test
    @DisplayName("1-1. 정상 파라미터 — DerivedKey 반환, encKey/iv 모두 non-null")
    void testDeriveKey_validParams_returnsKeyAndIv() throws Exception {
        DerivedKey dk = strategy.deriveKey(params, runtimeContext);

        assertNotNull(dk);
        assertNotNull(dk.getEncKey());
        assertNotNull(dk.getIv());
    }

    @Test
    @DisplayName("1-2. encKey는 hsmKeyAlias에 해당하는 HSM 키")
    void testDeriveKey_encKeyMatchesMasterKey() throws Exception {
        DerivedKey dk = strategy.deriveKey(params, runtimeContext);

        assertArrayEquals(MASTER_KEY, dk.getEncKey());
    }

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

        assertArrayEquals(dk.getEncKey(), dk.getDecKey());
    }

    @Test
    @DisplayName("1-4. iv는 HSM IV hex 문자열의 substring(32,64)와 일치")
    void testDeriveKey_ivIsHexSubstring32To64() throws Exception {
        DerivedKey dk = strategy.deriveKey(params, runtimeContext);

        String ivHex = DatatypeConverter.printHexBinary(MASTER_IV);
        byte[] expected = DatatypeConverter.parseHexBinary(ivHex.substring(32, 64));
        assertArrayEquals(expected, dk.getIv(), "iv는 HSM IV hex substring(32,64) 파싱 결과여야 한다");
    }

    @Test
    @DisplayName("1-5. iv 길이 == 16바이트 (hex 32자 = 16바이트)")
    void testDeriveKey_ivLength16() throws Exception {
        DerivedKey dk = strategy.deriveKey(params, runtimeContext);

        assertEquals(16, dk.getIv().length);
    }

    @Test
    @DisplayName("1-6. hsmIvAlias hex 길이가 64자 미만 — IllegalArgumentException")
    void testDeriveKey_ivTooShort_throwsException() throws Exception {
        byte[] shortIv = new byte[15]; // hex 30자 → 64자 미만
        when(mockHsmCryptoService.getSecretKey("SHORT_IV"))
                .thenReturn(new SecretKeySpec(shortIv, "AES"));
        params.put("hsmIvAlias", "SHORT_IV");

        assertThrows(IllegalArgumentException.class,
                () -> strategy.deriveKey(params, runtimeContext),
                "IV hex 길이 부족 시 IllegalArgumentException이 발생해야 한다");
    }

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

        assertThrows(IllegalArgumentException.class,
                () -> strategy.deriveKey(params, runtimeContext));
    }

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

        assertThrows(IllegalArgumentException.class,
                () -> strategy.deriveKey(params, runtimeContext));
    }

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

    @Test
    @DisplayName("2-1. buildCacheKey — cryptoName 단독 반환")
    void testBuildCacheKey_returnsCryptoName() {
        assertEquals("CRYPTO_A", strategy.buildCacheKey("CRYPTO_A", params, runtimeContext));
    }

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