package com.eactive.eai.manage.crypto;

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

import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.Base64;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

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.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
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.CryptoModuleConfigVO;
import com.eactive.eai.common.security.CryptoModuleDiagnosticDTO;
import com.eactive.eai.common.security.CryptoModuleManager;
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;

/**
 * CryptoModuleManageService 단위 테스트.
 *
 * <p>DB / 실 Spring 컨텍스트 없이 Mock으로 동작한다. CryptoModuleManager는 실제 인스턴스를
 * 사용하며(싱글턴 getInstance() 경유), HSM 및 DB 로더만 Mock 처리한다.
 */
class CryptoModuleManageServiceTest {

    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 CryptoModuleConfigLoader  mockLoader;
    private static HsmCryptoService          mockHsmCryptoService;
    private static CryptoModuleManager       manager;

    private final CryptoModuleManageService service = new CryptoModuleManageService();

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

        SecretKey masterKey = new SecretKeySpec(KEY_128, "AES");
        when(mockHsmCryptoService.getSecretKey(anyString())).thenReturn(masterKey);
        when(mockLoader.findAll()).thenReturn(Collections.emptyList());

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

        ctx = new GenericApplicationContext();
        ctx.getBeanFactory().registerSingleton("cryptoModuleConfigLoader", mockLoader);
        ctx.getBeanFactory().registerSingleton("hsmCryptoService", mockHsmCryptoService);
        ctx.getBeanFactory().registerSingleton("cryptoModuleManager", manager);
        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();
    }

    @BeforeEach
    void resetMocks() throws Exception {
        reset(mockLoader);
        when(mockLoader.findAll()).thenReturn(Collections.emptyList());
        reload();
    }

    // =========================================================================
    // 1. 진단정보 위임 — listAll / get
    // =========================================================================

    @Test
    @DisplayName("1-1. listAll() — CryptoModuleManager.describeAll() 위임")
    void testListAll_delegatesToManager() throws Exception {
        CryptoModuleConfig config = buildStaticConfig("SVC_LIST", "AES", "CBC", "PKCS5Padding");
        when(mockLoader.findAll()).thenReturn(Arrays.asList(config));
        reload();

        List<CryptoModuleDiagnosticDTO> result = service.listAll();

        assertEquals(1, result.size());
        assertEquals("SVC_LIST", result.get(0).getCryptoName());
    }

    @Test
    @DisplayName("1-2. get() — 미등록 cryptoName은 IllegalArgumentException 전파")
    void testGet_unknownName_throwsException() {
        assertThrows(IllegalArgumentException.class, () -> service.get("UNKNOWN_MODULE"));
    }

    // =========================================================================
    // 2. 동적 키 캐시 목록 — listDynamicCacheKeys
    // =========================================================================

    @Test
    @DisplayName("2-1. listDynamicCacheKeys() — DYNAMIC 암호화 테스트 후 캐시키 노출")
    void testListDynamicCacheKeys_afterTestEncrypt_containsCacheKey() throws Exception {
        CryptoModuleConfig config = buildDynamicConfig("SVC_CACHE_KEYS", "AES", "CBC", "PKCS5Padding");
        when(mockLoader.findAll()).thenReturn(Arrays.asList(config));
        reload();

        Map<String, String> runtimeCtx = new HashMap<>();
        runtimeCtx.put("X-Api-Enc-Key", "clientKeyValue01");
        String plainB64 = Base64.getEncoder().encodeToString("hello".getBytes(StandardCharsets.UTF_8));

        service.testEncrypt("SVC_CACHE_KEYS", runtimeCtx, plainB64, null);

        assertEquals(Collections.singletonList("SVC_CACHE_KEYS:clientKeyValue01"), service.listDynamicCacheKeys());
    }

    // =========================================================================
    // 3. 암복호화 테스트 — testEncrypt / testDecrypt
    // =========================================================================

    @Test
    @DisplayName("3-1. STATIC — 암호화 후 복호화 라운드트립 (Base64)")
    void testEncryptDecrypt_static_roundTrip() throws Exception {
        CryptoModuleConfig config = buildStaticConfig("SVC_STATIC_RT", "AES", "CBC", "PKCS5Padding");
        when(mockLoader.findAll()).thenReturn(Arrays.asList(config));
        reload();

        String plainText = "테스트 평문";
        String plainB64 = Base64.getEncoder().encodeToString(plainText.getBytes(StandardCharsets.UTF_8));

        String cipherB64 = service.testEncrypt("SVC_STATIC_RT", null, plainB64, null);
        assertNotNull(cipherB64);
        assertNotEquals(plainB64, cipherB64);

        String decryptedB64 = service.testDecrypt("SVC_STATIC_RT", null, cipherB64, null);
        assertEquals(plainText, new String(Base64.getDecoder().decode(decryptedB64), StandardCharsets.UTF_8));
    }

    @Test
    @DisplayName("3-2. DYNAMIC — runtimeContext 전달 시 암복호화 라운드트립 (Base64)")
    void testEncryptDecrypt_dynamic_roundTrip() throws Exception {
        CryptoModuleConfig config = buildDynamicConfig("SVC_DYNAMIC_RT", "AES", "CBC", "PKCS5Padding");
        when(mockLoader.findAll()).thenReturn(Arrays.asList(config));
        reload();

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

        String plainText = "DYNAMIC 키 테스트";
        String plainB64 = Base64.getEncoder().encodeToString(plainText.getBytes(StandardCharsets.UTF_8));

        String cipherB64 = service.testEncrypt("SVC_DYNAMIC_RT", runtimeCtx, plainB64, null);
        String decryptedB64 = service.testDecrypt("SVC_DYNAMIC_RT", runtimeCtx, cipherB64, null);

        assertEquals(plainText, new String(Base64.getDecoder().decode(decryptedB64), StandardCharsets.UTF_8));
    }

    @Test
    @DisplayName("3-3. testDecrypt() — 잘못된 Base64 입력은 예외 전파")
    void testDecrypt_invalidBase64_throwsException() throws Exception {
        CryptoModuleConfig config = buildStaticConfig("SVC_BAD_B64", "AES", "CBC", "PKCS5Padding");
        when(mockLoader.findAll()).thenReturn(Arrays.asList(config));
        reload();

        assertThrows(IllegalArgumentException.class,
                () -> service.testDecrypt("SVC_BAD_B64", null, "not-valid-base64!!", null));
    }

    @Test
    @DisplayName("3-4. testEncrypt() — 미등록 cryptoName은 예외 전파")
    void testEncrypt_unknownName_throwsException() {
        String plainB64 = Base64.getEncoder().encodeToString("x".getBytes(StandardCharsets.UTF_8));
        assertThrows(IllegalArgumentException.class,
                () -> service.testEncrypt("UNKNOWN_MODULE", null, plainB64, null));
    }

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

    private void reload() throws Exception {
        clearField("configMap");
        clearField("dynamicKeyCache");
        clearField("strategyCache");

        Method loadAll = CryptoModuleManager.class.getDeclaredMethod("loadAll");
        loadAll.setAccessible(true);
        loadAll.invoke(manager);

        reset(mockHsmCryptoService);
        SecretKey masterKey = new SecretKeySpec(KEY_128, "AES");
        when(mockHsmCryptoService.getSecretKey(anyString())).thenReturn(masterKey);
    }

    private void clearField(String fieldName) throws Exception {
        Field field = CryptoModuleManager.class.getDeclaredField(fieldName);
        field.setAccessible(true);
        ((Map<?, ?>) field.get(manager)).clear();
    }

    private CryptoModuleConfig buildStaticConfig(String name, String alg, String mode, String padding) {
        CryptoModuleConfig c = new CryptoModuleConfig();
        c.setCryptoId(java.util.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 CryptoModuleConfig buildDynamicConfig(String name, String alg, String mode, String padding) {
        CryptoModuleConfig c = new CryptoModuleConfig();
        c.setCryptoId(java.util.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.HsmContextXorKeyDerivationStrategy");
        c.setKeyDerivParams("{\"hsmKeyAlias\":\"MASTER_KEY\",\"contextKey\":\"X-Api-Enc-Key\",\"offset\":\"0\",\"length\":\"16\"}");
        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;
            }
        };
    }
}
