package com.eactive.eai.common.hsm;

import static org.junit.jupiter.api.Assertions.*;
import static org.junit.jupiter.api.Assumptions.assumeTrue;

import java.io.File;
import java.io.InputStream;
import java.lang.reflect.Constructor;
import java.nio.file.Files;
import java.security.PublicKey;
import java.util.concurrent.TimeUnit;

import javax.crypto.KeyGenerator;
import javax.crypto.SecretKey;

import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.MethodOrderer;
import org.junit.jupiter.api.Order;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestMethodOrder;
import org.mockito.Mockito;
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
import org.springframework.context.support.GenericApplicationContext;

import com.eactive.eai.common.property.PropManager;
import com.eactive.eai.common.util.ApplicationContextProvider;

/**
 * HsmManager / HsmCryptoService 통합 테스트 (JDK 8 / SunPKCS11 / SoftHSM2)
 *
 * 사전 조건:
 *   SoftHSM2 설치: https://github.com/disig/SoftHSM2-for-Windows/releases
 *   → C:\SoftHSM2 에 설치 (미설치 시 테스트 자동 건너뜀)
 *
 * PropManager 는 DB 없이 Mockito Mock 으로 대체하여 테스트합니다.
 */
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
public class HsmManagerServiceTest {

    private static final String SOFTHSM2_DLL  = "C:/SoftHSM2/lib/softhsm2-x64.dll";
    private static final String SOFTHSM2_UTIL = "C:/SoftHSM2/bin/softhsm2-util.exe";
    private static final String TOKEN_LABEL   = "eapim-test";
    private static final String PIN           = "1234";
    private static final String RSA_ALIAS     = "test-rsa-key";
    private static final String AES_ALIAS     = "test-aes-key";

    private static HsmManager               hsmManager;
    private static HsmCryptoService         hsmCryptoService;
    private static GenericApplicationContext springContext;
    private static File                     tempCfgFile;

    // -------------------------------------------------------------------------
    // 설정 / 해제
    // -------------------------------------------------------------------------

    @BeforeAll
    static void setUp() throws Exception {
        System.setProperty("SOFTHSM2_CONF", "C:\\SoftHSM2\\etc\\softhsm2.conf");
        assumeTrue(new File(SOFTHSM2_DLL).exists(),
                "SoftHSM2 미설치 (C:/SoftHSM2) - 테스트 건너뜀");

        // 1. 토큰 초기화 (없으면 신규 생성, 있으면 기존 사용)
        ensureTokenInitialized();

        // 2. PKCS11 설정 문자열 구성 (slotListIndex = 0 → 첫 번째 초기화 토큰)
        String cfgContent = "name = SoftHSM\n"
                + "library = " + SOFTHSM2_DLL.replace("\\", "/") + "\n"
                + "slotListIndex = 0\n";

        // keytool 호출 시 파일 경로가 필요하므로 임시 파일에도 저장
        tempCfgFile = File.createTempFile("pkcs11-svc-test-", ".cfg");
        Files.write(tempCfgFile.toPath(), cfgContent.getBytes("UTF-8"));

        // 3. PropManager Mock - DB 없이 HSM 설정값 반환
        PropManager mockPropManager = Mockito.mock(PropManager.class);
        Mockito.when(mockPropManager.getProperty("HSM", "PKCS11_CONFIG")).thenReturn(cfgContent);
        Mockito.when(mockPropManager.getProperty("HSM", "PIN")).thenReturn(PIN);

        // 4. HsmManager 인스턴스 생성 (private 생성자 → 리플렉션)
        Constructor<HsmManager> ctor = HsmManager.class.getDeclaredConstructor();
        ctor.setAccessible(true);
        hsmManager = ctor.newInstance();

        // 5. Spring ApplicationContext 구성
        //    ApplicationContextProvider.context 를 세팅하여
        //    HsmManager.getInstance() / PropManager.getInstance() 가 동작하게 함
        springContext = new GenericApplicationContext();
        springContext.getBeanFactory().registerSingleton("propManager", mockPropManager);
        springContext.getBeanFactory().registerSingleton("hsmManager", hsmManager);
        springContext.registerBeanDefinition("applicationContextProvider",
                BeanDefinitionBuilder.genericBeanDefinition(ApplicationContextProvider.class)
                        .getBeanDefinition());
        springContext.refresh();

        // 6. HsmManager 시작 (SunPKCS11 Provider 초기화 + KeyStore 오픈)
        hsmManager.start();
        assertTrue(hsmManager.isReady(), "HsmManager 초기화 실패");
        System.out.println("[HsmManager] 초기화 완료. provider=" + hsmManager.getPkcs11Provider().getName());

        // 7. RSA 키쌍 생성 (없는 경우에만)
        if (!hsmManager.getKeyStore().containsAlias(RSA_ALIAS)) {
            generateRsaKeyPair();
            hsmManager.getKeyStore().load(null, PIN.toCharArray()); // keytool 결과 반영
        }

        // 8. AES 키 생성 (없는 경우에만)
        if (!hsmManager.getKeyStore().containsAlias(AES_ALIAS)) {
            generateAesKey();
        }

        // 9. HsmCryptoService 생성 (HsmManager.getInstance() 를 내부에서 사용)
        hsmCryptoService = new HsmCryptoService();
    }

    @AfterAll
    static void tearDown() throws Exception {
        if (hsmManager != null && hsmManager.isStarted()) {
            hsmManager.stop();
        }
        if (springContext != null) {
            springContext.close();
        }
        if (tempCfgFile != null) {
            tempCfgFile.delete();
        }
    }

    // -------------------------------------------------------------------------
    // 테스트: HsmManager 상태
    // -------------------------------------------------------------------------

    @Test
    @Order(1)
    void testHsmManagerReady() {
        assertTrue(hsmManager.isReady());
        assertNotNull(hsmManager.getPkcs11Provider());
        assertNotNull(hsmManager.getKeyStore());
        System.out.println("[HsmManager] isReady=true / provider="
                + hsmManager.getPkcs11Provider().getName());
    }

    // -------------------------------------------------------------------------
    // 테스트: HsmCryptoService - 키 조회
    // -------------------------------------------------------------------------

    @Test
    @Order(2)
    void testGetPublicKey() throws Exception {
        PublicKey pk = hsmCryptoService.getPublicKey(RSA_ALIAS);
        assertNotNull(pk);
        assertEquals("RSA", pk.getAlgorithm());
        System.out.println("[HsmCryptoService] getPublicKey: algorithm=" + pk.getAlgorithm()
                + " / format=" + pk.getFormat());
    }

    @Test
    @Order(3)
    void testGetSecretKey() throws Exception {
        SecretKey sk = hsmCryptoService.getSecretKey(AES_ALIAS);
        assertNotNull(sk);
        assertEquals("AES", sk.getAlgorithm());
        System.out.println("[HsmCryptoService] getSecretKey: algorithm=" + sk.getAlgorithm());
    }

    // -------------------------------------------------------------------------
    // 테스트: HsmCryptoService - RSA 암복호화
    // -------------------------------------------------------------------------

    @Test
    @Order(4)
    void testEncryptRsaByAlias() throws Exception {
        byte[] plaintext  = "RSA alias 암호화 테스트".getBytes("UTF-8");
        byte[] ciphertext = hsmCryptoService.encryptRsa(RSA_ALIAS, plaintext);
        assertNotNull(ciphertext);
        assertTrue(ciphertext.length > 0);
        System.out.println("[HsmCryptoService] encryptRsa(alias): ciphertext.length=" + ciphertext.length);
    }

    @Test
    @Order(5)
    void testEncryptRsaByPublicKey() throws Exception {
        PublicKey pk      = hsmCryptoService.getPublicKey(RSA_ALIAS);
        byte[] plaintext  = "RSA PublicKey 직접 암호화 테스트".getBytes("UTF-8");
        byte[] ciphertext = hsmCryptoService.encryptRsa(pk, plaintext);
        assertNotNull(ciphertext);
        assertTrue(ciphertext.length > 0);
        System.out.println("[HsmCryptoService] encryptRsa(PublicKey): ciphertext.length=" + ciphertext.length);
    }

    @Test
    @Order(6)
    void testRsaRoundTrip() throws Exception {
        byte[] plaintext = "HSM RSA 암복호화 통합 테스트 - HsmCryptoService".getBytes("UTF-8");

        // 암호화: 공개키 → JVM 소프트웨어 (encryptRsa 내부에서 Provider 미명시)
        byte[] ciphertext = hsmCryptoService.encryptRsa(RSA_ALIAS, plaintext);

        // 복호화: 개인키 핸들 → HSM 내부 (decryptRsa 내부에서 pkcs11Provider 명시)
        byte[] decrypted  = hsmCryptoService.decryptRsa(RSA_ALIAS, PIN.toCharArray(), ciphertext);

        assertArrayEquals(plaintext, decrypted);
        System.out.println("[HsmCryptoService] RSA 암복호화 성공: " + new String(decrypted, "UTF-8"));
    }

    // -------------------------------------------------------------------------
    // 테스트: HsmCryptoService - AES 암복호화
    // -------------------------------------------------------------------------

    @Test
    @Order(7)
    void testAesRoundTrip() throws Exception {
        byte[] plaintext = "HSM AES 암복호화 통합 테스트 - HsmCryptoService".getBytes("UTF-8");
        byte[] iv        = "1234567890123456".getBytes("UTF-8"); // 16바이트

        SecretKey sk = hsmCryptoService.getSecretKey(AES_ALIAS);

        byte[] ciphertext = hsmCryptoService.encryptAes(sk, iv, plaintext);
        byte[] decrypted  = hsmCryptoService.decryptAes(sk, iv, ciphertext);

        assertArrayEquals(plaintext, decrypted);
        System.out.println("[HsmCryptoService] AES 암복호화 성공: " + new String(decrypted, "UTF-8"));
    }

    // -------------------------------------------------------------------------
    // 테스트: HsmManager Lifecycle - stop / restart
    // -------------------------------------------------------------------------

    @Test
    @Order(8)
    void testHsmManagerStopAndRestart() throws Exception {
        // stop
        hsmManager.stop();
        assertFalse(hsmManager.isStarted());
        assertFalse(hsmManager.isReady());
        System.out.println("[HsmManager] stop 완료");

        // restart - PropManager Mock 에서 재설정 읽어 재초기화
        hsmManager.start();
        assertTrue(hsmManager.isStarted());
        assertTrue(hsmManager.isReady());
        System.out.println("[HsmManager] restart 완료. provider="
                + hsmManager.getPkcs11Provider().getName());
    }

    // -------------------------------------------------------------------------
    // 헬퍼
    // -------------------------------------------------------------------------

    private static void ensureTokenInitialized() throws Exception {
        ProcessBuilder pb = new ProcessBuilder(
                SOFTHSM2_UTIL, "--init-token", "--free",
                "--label", TOKEN_LABEL,
                "--pin",    PIN,
                "--so-pin", PIN);
        pb.redirectErrorStream(true);
        Process p   = pb.start();
        String  out = readOutput(p);
        p.waitFor(10, TimeUnit.SECONDS);
        System.out.println("[SoftHSM2] init-token: " + out.trim());
    }

    private static void generateRsaKeyPair() throws Exception {
        ProcessBuilder pb = new ProcessBuilder(
                "keytool", "-genkeypair",
                "-alias",         RSA_ALIAS,
                "-keyalg",        "RSA",
                "-keysize",       "2048",
                "-storetype",     "PKCS11",
                "-providerclass", "sun.security.pkcs11.SunPKCS11",
                "-providerarg",   tempCfgFile.getAbsolutePath(),
                "-keystore",      "NONE",
                "-storepass",     PIN,
                "-dname",         "CN=HSM Test, O=eActive, C=KR",
                "-noprompt");
        pb.redirectErrorStream(true);
        Process p   = pb.start();
        String  out = readOutput(p);
        if (!p.waitFor(30, TimeUnit.SECONDS)) {
            p.destroy();
            throw new RuntimeException("keytool -genkeypair timeout");
        }
        System.out.println("[keytool] genkeypair exit=" + p.exitValue() + ": " + out.trim());
    }

    private static void generateAesKey() throws Exception {
        KeyGenerator kg = KeyGenerator.getInstance("AES", hsmManager.getPkcs11Provider());
        kg.init(256);
        SecretKey sk = kg.generateKey();
        hsmManager.getKeyStore().setKeyEntry(AES_ALIAS, sk, null, null);
        System.out.println("[HsmManager] AES-256 키 생성 완료. alias=" + AES_ALIAS);
    }

    private static String readOutput(Process p) throws Exception {
        InputStream   is  = p.getInputStream();
        byte[]        buf = new byte[4096];
        StringBuilder sb  = new StringBuilder();
        int len;
        while ((len = is.read(buf)) != -1) {
            sb.append(new String(buf, 0, len, "UTF-8"));
        }
        return sb.toString();
    }
}
