package com.eactive.eai.common.security;

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

import java.lang.reflect.Constructor;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.Base64;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;

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

import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
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.loader.CryptoModuleConfigLoader;
import com.eactive.eai.common.security.mapper.CryptoModuleConfigMapper;
import com.eactive.eai.common.util.ApplicationContextProvider;
import com.eactive.eai.data.entity.onl.security.CryptoModuleConfig;

/**
 * CryptoModuleService 단위 테스트
 * CryptoModuleManager를 통한 암복호화 진입점 전체 흐름을 검증한다.
 */
@TestMethodOrder(MethodOrderer.DisplayName.class)
class CryptoModuleServiceTest {

    private static final byte[] KEY_128     = "0123456789abcdef".getBytes(StandardCharsets.UTF_8);
    private static final byte[] IV_16       = "abcdef0123456789".getBytes(StandardCharsets.UTF_8);
    private static final String ENC_KEY_HEX = toHex(KEY_128);
    private static final String IV_HEX      = toHex(IV_16);

    private static GenericApplicationContext ctx;
    private static CryptoModuleManager       manager;
    private static CryptoModuleService       service;
    private static CryptoModuleConfigLoader  mockLoader;
    private static HsmCryptoService          mockHsm;

    @BeforeAll
    static void setUpClass() throws Exception {
        mockLoader = mock(CryptoModuleConfigLoader.class);
        mockHsm    = mock(HsmCryptoService.class);

        SecretKey masterKey = new SecretKeySpec(KEY_128, "AES");
        System.out.println("masterKey:"+new String(KEY_128));
        when(mockHsm.getSecretKey(anyString())).thenReturn(masterKey);

        CryptoModuleConfig aes    = buildStatic("AES_SVC",      "AES",  "CBC", "PKCS5Padding");
        CryptoModuleConfig aria   = buildStatic("ARIA_SVC",     "ARIA", "CBC", "PKCS5Padding");
        CryptoModuleConfig ecb    = buildStaticNoIv("AES_ECB_SVC", "AES", "ECB", "NoPadding");
        CryptoModuleConfig dyn    = buildDynamic("DYN_SVC",     "AES",  "CBC", "PKCS5Padding");
        CryptoModuleConfig gcm    = buildStatic("AES_GCM_SVC",  "AES",  "GCM", "NoPadding");
        CryptoModuleConfig gcmDyn = buildDynamic("DYN_GCM_SVC", "AES",  "GCM", "NoPadding");
        when(mockLoader.findAll()).thenReturn(Arrays.asList(aes, aria, ecb, dyn, gcm, gcmDyn));

        Constructor<CryptoModuleManager> ctor = CryptoModuleManager.class.getDeclaredConstructor();
        ctor.setAccessible(true);
        manager = ctor.newInstance();
        service  = new CryptoModuleService();

        ctx = new GenericApplicationContext();
        ctx.getBeanFactory().registerSingleton("cryptoModuleConfigLoader", mockLoader);
        ctx.getBeanFactory().registerSingleton("hsmCryptoService", mockHsm);
        ctx.getBeanFactory().registerSingleton("cryptoModuleManager", manager);
        ctx.getBeanFactory().registerSingleton("cryptoModuleService", service);
        ctx.getBeanFactory().registerSingleton("cryptoModuleConfigMapper", testMapper());
        ctx.registerBeanDefinition("applicationContextProvider",
                BeanDefinitionBuilder.genericBeanDefinition(ApplicationContextProvider.class)
                        .getBeanDefinition());
        ctx.refresh();

        manager.start();
    }

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

    // =========================================================================
    // 1. STATIC 키 — encrypt / decrypt
    // =========================================================================

    @Test
    @DisplayName("1-1. STATIC AES/CBC — encrypt → decrypt 라운드트립")
    void testStaticAes_encryptDecrypt() throws Exception {
        byte[] plain = "CryptoModuleService AES 테스트".getBytes(StandardCharsets.UTF_8);

        byte[] encrypted = service.encrypt("AES_SVC", plain);
        byte[] decrypted = service.decrypt("AES_SVC", encrypted);

        assertArrayEquals(plain, decrypted);
    }

    @Test
    @DisplayName("1-2. STATIC ARIA/CBC — encrypt → decrypt 라운드트립")
    void testStaticAria_encryptDecrypt() throws Exception {
        byte[] plain = "CryptoModuleService ARIA 테스트".getBytes(StandardCharsets.UTF_8);

        byte[] encrypted = service.encrypt("ARIA_SVC", plain);
        byte[] decrypted = service.decrypt("ARIA_SVC", encrypted);

        assertArrayEquals(plain, decrypted);
    }

    @Test
    @DisplayName("1-3. 암호문은 평문과 다른 바이트 배열")
    void testEncrypt_ciphertextDiffersFromPlaintext() throws Exception {
        byte[] plain = "평문입니다.".getBytes(StandardCharsets.UTF_8);

        byte[] encrypted = service.encrypt("AES_SVC", plain);

        assertFalse(java.util.Arrays.equals(plain, encrypted));
    }

    @Test
    @DisplayName("1-4. 미등록 모듈명 — IllegalArgumentException")
    void testEncrypt_unknownModule_throwsException() {
        byte[] plain = "test".getBytes(StandardCharsets.UTF_8);

        assertThrows(IllegalArgumentException.class,
                () -> service.encrypt("UNKNOWN_SVC", plain));
    }

    // =========================================================================
    // 2. DYNAMIC 키 — encrypt / decrypt
    // =========================================================================

    @Test
    @DisplayName("2-1. DYNAMIC AES/CBC — runtimeContext 전달 encrypt → decrypt 라운드트립")
    void testDynamicAes_encryptDecrypt() throws Exception {
        Map<String, String> runtimeCtx = new HashMap<>();
        runtimeCtx.put("X-Api-Enc-Key", "clientKey0000001");

        byte[] plain = "{\"test\": \"DYNAMIC AES/CBC 테스트\"}".getBytes(StandardCharsets.UTF_8);

        byte[] encrypted = service.encrypt("DYN_SVC", runtimeCtx, plain);
        byte[] decrypted = service.decrypt("DYN_SVC", runtimeCtx, encrypted);
        
        String encBase64 = Base64.getEncoder().encodeToString(encrypted);
        String decBase64 = Base64.getEncoder().encodeToString(decrypted);
        System.out.println("DYNAMIC AES/CBC");
        
        System.out.println("plain:"+new String(plain));
        System.out.println("encBase64:"+encBase64);
        System.out.println("decBase64:"+decBase64);
        System.out.println("plain:"+new String(decrypted));
        
        assertArrayEquals(plain, decrypted);
    }

    @Test
    @DisplayName("2-2. DYNAMIC (HsmKeyDerivationStrategy) — runtimeContext는 키 도출에 미사용, 다른 컨텍스트로도 복호화 성공")
    void testDynamic_differentContext_decryptSucceeds() throws Exception {
        Map<String, String> encCtx = new HashMap<>();
        encCtx.put("X-Api-Enc-Key", "encryptKeyValue0");

        Map<String, String> decCtx = new HashMap<>();
        decCtx.put("X-Api-Enc-Key", "differentKeyVal0");

        byte[] plain = "키불일치 테스트".getBytes(StandardCharsets.UTF_8);
        byte[] encrypted = service.encrypt("DYN_SVC", encCtx, plain);
        byte[] decrypted = service.decrypt("DYN_SVC", decCtx, encrypted);

        assertArrayEquals(plain, decrypted,
                "HsmKeyDerivationStrategy는 runtimeContext를 사용하지 않으므로 컨텍스트가 달라도 복호화 성공");
    }

    // =========================================================================
    // 3. AES/ECB/NoPadding
    // =========================================================================

    @Test
    @DisplayName("3-1. AES/ECB/NoPadding — 16바이트 평문 encrypt → decrypt 라운드트립")
    void testEcb_encryptDecrypt_exactBlock() throws Exception {
        byte[] plain = "ExactSixteenByte".getBytes(StandardCharsets.UTF_8); // 정확히 16바이트

        byte[] encrypted = service.encrypt("AES_ECB_SVC", plain);
        byte[] decrypted = service.decrypt("AES_ECB_SVC", encrypted);

        assertArrayEquals(plain, decrypted);
    }

    @Test
    @DisplayName("3-2. AES/ECB/NoPadding — 동일 평문은 항상 동일 암호문 (ECB 특성)")
    void testEcb_deterministicOutput() throws Exception {
        byte[] plain = "ExactSixteenByte".getBytes(StandardCharsets.UTF_8);

        byte[] enc1 = service.encrypt("AES_ECB_SVC", plain);
        byte[] enc2 = service.encrypt("AES_ECB_SVC", plain);

        assertArrayEquals(enc1, enc2, "ECB는 IV 없으므로 동일 평문 → 동일 암호문");
    }

    @Test
    @DisplayName("3-3. AES/ECB/NoPadding — 16 배수 아닌 평문 → 예외")
    void testEcb_nonBlockAligned_throwsException() {
        byte[] plain = "NotSixteenBytes!X".getBytes(StandardCharsets.UTF_8); // 17바이트

        assertThrows(Exception.class,
                () -> service.encrypt("AES_ECB_SVC", plain),
                "NoPadding 모드에서 블록 크기 미정렬 평문은 예외가 발생해야 한다");
    }

    // =========================================================================
    // 4. STATIC AAD — encrypt(name, aad, plain) / decrypt(name, aad, cipher)
    // =========================================================================

    @Test
    @DisplayName("4-1. STATIC AES/GCM — AAD 명시 encrypt → decrypt 라운드트립")
    void testStaticGcm_explicitAad_roundTrip() throws Exception {
        byte[] aad   = "request-header-ctx".getBytes(StandardCharsets.UTF_8);
        byte[] plain = "GCM AAD 서비스 테스트".getBytes(StandardCharsets.UTF_8);

        byte[] encrypted = service.encrypt("AES_GCM_SVC", aad, plain);
        byte[] decrypted = service.decrypt("AES_GCM_SVC", aad, encrypted);

        assertArrayEquals(plain, decrypted);
    }

    @Test
    @DisplayName("4-2. STATIC AES/GCM — AAD null이면 IV를 AAD로 사용하는 기본 동작")
    void testStaticGcm_nullAad_usesIvAsAad() throws Exception {
        byte[] plain = "{\"test\": \"테스트\"}".getBytes(StandardCharsets.UTF_8);

        byte[] encrypted = service.encrypt("AES_GCM_SVC", (byte[]) null, plain);
        byte[] decrypted = service.decrypt("AES_GCM_SVC", (byte[]) null, encrypted);

        assertArrayEquals(plain, decrypted);
        
        String encBase64 = Base64.getEncoder().encodeToString(encrypted);
        String decBase64 = Base64.getEncoder().encodeToString(decrypted);
        
        System.out.println("STATIC AES/GCM");
        
        System.out.println("plain:"+new String(plain));
        System.out.println("encBase64:"+encBase64);
        System.out.println("decBase64:"+decBase64);
        System.out.println("plain:"+new String(decrypted));
    }

    @Test
    @DisplayName("4-3. STATIC AES/GCM — 다른 AAD로 복호화 시 인증 실패 예외")
    void testStaticGcm_wrongAad_throwsAuthException() throws Exception {
        byte[] aad      = "correct-aad-value".getBytes(StandardCharsets.UTF_8);
        byte[] wrongAad = "wrong-aad-value!!".getBytes(StandardCharsets.UTF_8);
        byte[] plain    = "GCM 인증 실패 테스트".getBytes(StandardCharsets.UTF_8);

        byte[] encrypted = service.encrypt("AES_GCM_SVC", aad, plain);

        assertThrows(Exception.class,
                () -> service.decrypt("AES_GCM_SVC", wrongAad, encrypted),
                "잘못된 AAD로 GCM 복호화 시 인증 실패 예외가 발생해야 한다");
    }

    @Test
    @DisplayName("4-4. STATIC CBC — AAD 파라미터는 무시되고 정상 복호화")
    void testStaticCbc_aadIgnored_normalDecrypt() throws Exception {
        byte[] aad   = "ignored-aad".getBytes(StandardCharsets.UTF_8);
        byte[] plain = "CBC AAD 무시 테스트".getBytes(StandardCharsets.UTF_8);

        byte[] encrypted = service.encrypt("AES_SVC", aad, plain);
        byte[] decrypted = service.decrypt("AES_SVC", aad, encrypted);

        assertArrayEquals(plain, decrypted, "CBC 모드에서 AAD는 무시되고 정상 암복호화되어야 한다");
    }

    // =========================================================================
    // 5. DYNAMIC AAD — encrypt(name, ctx, aad, plain) / decrypt(name, ctx, aad, cipher)
    // =========================================================================

    @Test
    @DisplayName("5-1. DYNAMIC AES/GCM — runtimeContext + AAD 명시 encrypt → decrypt 라운드트립")
    void testDynamicGcm_explicitAad_roundTrip() throws Exception {
        Map<String, String> runtimeCtx = new HashMap<>();
        runtimeCtx.put("X-Api-Enc-Key", "clientKey0000001");

        byte[] aad   = "dynamic-gcm-aad-ctx".getBytes(StandardCharsets.UTF_8);
        byte[] plain = "{\"test\": \"테스트\"}".getBytes(StandardCharsets.UTF_8);

        byte[] encrypted = service.encrypt("DYN_GCM_SVC", runtimeCtx, aad, plain);
        byte[] decrypted = service.decrypt("DYN_GCM_SVC", runtimeCtx, aad, encrypted);
        
        String encBase64 = Base64.getEncoder().encodeToString(encrypted);
        String decBase64 = Base64.getEncoder().encodeToString(decrypted);
        
        System.out.println("DYNAMIC AES/GCM");
        
        System.out.println("aad:"+new String(aad));
        System.out.println("plain:"+new String(plain));
        System.out.println("encBase64:"+encBase64);
        System.out.println("decBase64:"+decBase64);
        System.out.println("plain:"+new String(decrypted));
        
        assertArrayEquals(plain, decrypted);
    }

    @Test
    @DisplayName("5-2. DYNAMIC AES/GCM — AAD null이면 IV를 AAD로 사용하는 기본 동작")
    void testDynamicGcm_nullAad_usesIvAsAad() throws Exception {
        Map<String, String> runtimeCtx = new HashMap<>();
        runtimeCtx.put("X-Api-Enc-Key", "clientKey0000001");

        byte[] plain = "{\"test\": \"테스트\"}".getBytes(StandardCharsets.UTF_8);
        
        byte[] encrypted = service.encrypt("DYN_GCM_SVC", runtimeCtx, null, plain);
        byte[] decrypted = service.decrypt("DYN_GCM_SVC", runtimeCtx, null, encrypted);
        
        String encBase64 = Base64.getEncoder().encodeToString(encrypted);
        String decBase64 = Base64.getEncoder().encodeToString(decrypted);
        System.out.println("DYNAMIC AES/GCM");
        System.out.println("aad:null");
        System.out.println("plain:"+new String(plain));
        System.out.println("encBase64:"+encBase64);
        System.out.println("decBase64:"+decBase64);
        System.out.println("plain:"+new String(decrypted));
        
        
        assertArrayEquals(plain, decrypted);
    }

    @Test
    @DisplayName("5-3. DYNAMIC AES/GCM — 다른 AAD로 복호화 시 인증 실패 예외")
    void testDynamicGcm_wrongAad_throwsAuthException() throws Exception {
        Map<String, String> runtimeCtx = new HashMap<>();
        runtimeCtx.put("X-Api-Enc-Key", "clientKey0000001");

        byte[] aad      = "correct-aad-value".getBytes(StandardCharsets.UTF_8);
        byte[] wrongAad = "wrong-aad-value!!".getBytes(StandardCharsets.UTF_8);
        byte[] plain    = "DYNAMIC GCM 인증 실패".getBytes(StandardCharsets.UTF_8);

        byte[] encrypted = service.encrypt("DYN_GCM_SVC", runtimeCtx, aad, plain);

        assertThrows(Exception.class,
                () -> service.decrypt("DYN_GCM_SVC", runtimeCtx, wrongAad, encrypted),
                "잘못된 AAD로 DYNAMIC GCM 복호화 시 인증 실패 예외가 발생해야 한다");
    }

    @Test
    @DisplayName("5-4. DYNAMIC CBC — AAD 파라미터는 무시되고 정상 복호화")
    void testDynamicCbc_aadIgnored_normalDecrypt() throws Exception {
        Map<String, String> runtimeCtx = new HashMap<>();
        runtimeCtx.put("X-Api-Enc-Key", "clientKey0000001");

        byte[] aad   = "ignored-aad".getBytes(StandardCharsets.UTF_8);
        byte[] plain = "DYNAMIC CBC AAD 무시 테스트".getBytes(StandardCharsets.UTF_8);

        byte[] encrypted = service.encrypt("DYN_SVC", runtimeCtx, aad, plain);
        byte[] decrypted = service.decrypt("DYN_SVC", runtimeCtx, aad, encrypted);

        assertArrayEquals(plain, decrypted, "DYNAMIC CBC 모드에서 AAD는 무시되고 정상 암복호화되어야 한다");
    }

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

    private static CryptoModuleConfig buildStatic(String name, String alg, String mode, String padding) {
        CryptoModuleConfig c = new CryptoModuleConfig();
        c.setCryptoId(UUID.randomUUID().toString());
        c.setCryptoName(name);
        c.setAlgType(alg);
        c.setCipherMode(mode);
        c.setPadding(padding);
        c.setIvHex(IV_HEX);
        c.setKeySourceType("STATIC");
        c.setEncKeyHex(ENC_KEY_HEX);
        c.setDecKeyHex(ENC_KEY_HEX);
        c.setCacheYn("Y");
        c.setCacheTtlSec(300);
        c.setUseYn("Y");
        return c;
    }

    private static CryptoModuleConfig buildStaticNoIv(String name, String alg, String mode, String padding) {
        CryptoModuleConfig c = new CryptoModuleConfig();
        c.setCryptoId(UUID.randomUUID().toString());
        c.setCryptoName(name);
        c.setAlgType(alg);
        c.setCipherMode(mode);
        c.setPadding(padding);
        c.setKeySourceType("STATIC");
        c.setEncKeyHex(ENC_KEY_HEX);
        c.setDecKeyHex(ENC_KEY_HEX);
        c.setCacheYn("Y");
        c.setCacheTtlSec(300);
        c.setUseYn("Y");
        return c;
    }

    private static CryptoModuleConfig buildDynamic(String name, String alg, String mode, String padding) {
        CryptoModuleConfig c = new CryptoModuleConfig();
        c.setCryptoId(UUID.randomUUID().toString());
        c.setCryptoName(name);
        c.setAlgType(alg);
        c.setCipherMode(mode);
        c.setPadding(padding);
        c.setIvHex(IV_HEX);
        c.setKeySourceType("DYNAMIC");
        c.setKeyDerivStrategy("com.eactive.eai.common.security.keyderiv.strategy.HsmKeyDerivationStrategy");
        c.setKeyDerivParams("{\"hsmKeyAlias\":\"MASTER_KEY\"}");
        c.setCacheYn("Y");
        c.setCacheTtlSec(300);
        c.setUseYn("Y");
        return c;
    }

    private static String toHex(byte[] bytes) {
        StringBuilder sb = new StringBuilder();
        for (byte b : bytes) sb.append(String.format("%02x", b));
        return sb.toString();
    }

    /** MapStruct APT 없이도 동작하는 인라인 매퍼 구현체 */
    private static CryptoModuleConfigMapper testMapper() {
        return new CryptoModuleConfigMapper() {
            @Override
            public CryptoModuleConfigVO toVo(CryptoModuleConfig e) {
                CryptoModuleConfigVO vo = new CryptoModuleConfigVO();
                vo.setCryptoId(e.getCryptoId());
                vo.setCryptoName(e.getCryptoName());
                vo.setCryptoDesc(e.getCryptoDesc());
                vo.setAlgType(e.getAlgType());
                vo.setCipherMode(e.getCipherMode());
                vo.setPadding(e.getPadding());
                vo.setIvHex(e.getIvHex());
                vo.setKeySourceType(e.getKeySourceType());
                vo.setEncKeyHex(e.getEncKeyHex());
                vo.setDecKeyHex(e.getDecKeyHex());
                vo.setKeyDerivStrategy(e.getKeyDerivStrategy());
                vo.setKeyDerivParams(e.getKeyDerivParams());
                vo.setCacheYn(e.getCacheYn());
                vo.setCacheTtlSec(e.getCacheTtlSec());
                vo.setUseYn(e.getUseYn());
                return vo;
            }
            @Override
            public CryptoModuleConfig toEntity(CryptoModuleConfigVO vo) {
                return null;
            }
        };
    }
}
