package com.eactive.eai.common.hsm;

import static org.junit.jupiter.api.Assertions.assertArrayEquals;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertInstanceOf;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assumptions.assumeTrue;

import java.io.File;
import java.io.InputStream;
import java.nio.file.Files;
import java.security.Key;
import java.security.KeyStore;
import java.security.PrivateKey;
import java.security.Provider;
import java.security.PublicKey;
import java.security.Security;
import java.security.cert.Certificate;
import java.util.concurrent.TimeUnit;

import javax.crypto.Cipher;
import javax.crypto.KeyGenerator;
import javax.crypto.SecretKey;
import javax.crypto.spec.IvParameterSpec;

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;

/**
 * SoftHSM2 연동 통합 테스트 (JDK 8 / SunPKCS11)
 *
 * 사전 조건:
 *   1. SoftHSM2 설치: https://github.com/disig/SoftHSM2-for-Windows/releases
 *      → SoftHSM2-*-x64.msi 다운로드 후 C:\SoftHSM2 에 설치
 *   2. 토큰 초기화 (최초 1회):
 *      "C:\SoftHSM2\bin\softhsm2-util.exe" --init-token --free --label "eapim-test" --pin 1234 --so-pin 1234
 *      → 이미 초기화된 경우 @BeforeAll 에서 자동 처리
 *
 * SoftHSM2 미설치 시 모든 테스트는 자동으로 건너뜀.
 */
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
public class HsmSoftHsm2IntegrationTest {

    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 Provider provider;
    private static KeyStore keyStore;
    private static File     tempCfgFile;

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

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

        // 1. 토큰 초기화 (이미 존재하면 출력만 남기고 계속 진행)
        initToken();

        // 2. pkcs11 설정 내용 (slotListIndex = 0 → 첫 번째 초기화된 토큰 사용)
        String cfgContent = "name = SoftHSM\n"
                + "library = " + SOFTHSM2_DLL.replace("\\", "/") + "\n"
                + "slotListIndex = 0\n";

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

        // 3. SunPKCS11 Provider 초기화 (JDK 8 / JDK 9+ 공용)
        provider = HsmManager.createProvider(cfgContent);

        Provider existing = Security.getProvider(provider.getName());
        if (existing != null) {
            Security.removeProvider(existing.getName());
        }
        Security.addProvider(provider);

        // 4. PKCS11 KeyStore 오픈
        keyStore = KeyStore.getInstance("PKCS11", provider);
        keyStore.load(null, PIN.toCharArray());

        // 5. RSA 키쌍 생성 (keytool 경유 → 자체서명 인증서 포함)
        if (!keyStore.containsAlias(RSA_ALIAS)) {
            generateRsaKeyPair();
            keyStore.load(null, PIN.toCharArray()); // keytool 결과 반영
        }

        // 6. AES 키 생성 (KeyGenerator 경유)
        if (!keyStore.containsAlias(AES_ALIAS)) {
            generateAesKey();
        }
    }

    @AfterAll
    static void tearDown() {
        if (provider != null) {
            Security.removeProvider(provider.getName());
        }
        if (tempCfgFile != null) {
            tempCfgFile.delete();
        }
    }

    // -------------------------------------------------------------------------
    // 테스트: Provider / KeyStore
    // -------------------------------------------------------------------------

    @Test
    @Order(1)
    void testProviderInitialized() {
        assertNotNull(provider);
        assertTrue(provider.getName().startsWith("SunPKCS11"),
                "예상 Provider 이름: SunPKCS11-*, 실제: " + provider.getName());
        System.out.println("[HSM] Provider: " + provider.getName()
                + " / version: " + provider.getVersion());
    }

    @Test
    @Order(2)
    void testKeyStoreContainsRsaAlias() throws Exception {
        assertTrue(keyStore.containsAlias(RSA_ALIAS),
                "RSA 키 없음. alias=" + RSA_ALIAS);
    }

    @Test
    @Order(3)
    void testKeyStoreContainsAesAlias() throws Exception {
        assertTrue(keyStore.containsAlias(AES_ALIAS),
                "AES 키 없음. alias=" + AES_ALIAS);
    }

    // -------------------------------------------------------------------------
    // 테스트: 키 조회 (HsmCryptoService.getPublicKey / getSecretKey 해당)
    // -------------------------------------------------------------------------

    @Test
    @Order(4)
    void testGetPublicKey() throws Exception {
        Certificate cert = keyStore.getCertificate(RSA_ALIAS);
        assertNotNull(cert, "인증서 없음. alias=" + RSA_ALIAS);

        PublicKey pk = cert.getPublicKey();
        assertNotNull(pk);
        assertEquals("RSA", pk.getAlgorithm());
        System.out.println("[HSM] PublicKey algorithm=" + pk.getAlgorithm()
                + " / format=" + pk.getFormat());
    }

    @Test
    @Order(5)
    void testGetSecretKey() throws Exception {
        Key key = keyStore.getKey(AES_ALIAS, null);
        assertNotNull(key, "AES 키 없음. alias=" + AES_ALIAS);
        assertInstanceOf(SecretKey.class, key);
        assertEquals("AES", key.getAlgorithm());
        System.out.println("[HSM] SecretKey algorithm=" + key.getAlgorithm());
    }

    // -------------------------------------------------------------------------
    // 테스트: RSA 암복호화 (HsmCryptoService.encryptRsa / decryptRsa 해당)
    // -------------------------------------------------------------------------

    @Test
    @Order(6)
    void testRsaEncrypt() throws Exception {
        PublicKey publicKey = keyStore.getCertificate(RSA_ALIAS).getPublicKey();

        // Provider 미명시 → JVM SunRsaSign 으로 암호화 (HsmCryptoService.encryptRsa 동작)
        Cipher cipher = Cipher.getInstance("RSA/ECB/PKCS1Padding");
        cipher.init(Cipher.ENCRYPT_MODE, publicKey);
        byte[] ciphertext = cipher.doFinal("테스트 평문".getBytes("UTF-8"));

        assertNotNull(ciphertext);
        assertTrue(ciphertext.length > 0);
        System.out.println("[HSM] RSA 암호화 성공. ciphertext.length=" + ciphertext.length
                + " / provider=" + cipher.getProvider().getName());
    }

    @Test
    @Order(7)
    void testRsaEncryptDecryptRoundTrip() throws Exception {
        byte[] plaintext = "HSM RSA 암복호화 테스트 - eapim-online".getBytes("UTF-8");

        // 암호화: 공개키 → JVM 소프트웨어 (Provider 미명시)
        PublicKey publicKey = keyStore.getCertificate(RSA_ALIAS).getPublicKey();
        Cipher encCipher = Cipher.getInstance("RSA/ECB/PKCS1Padding");
        encCipher.init(Cipher.ENCRYPT_MODE, publicKey);
        byte[] ciphertext = encCipher.doFinal(plaintext);

        // 복호화: 개인키 핸들 → HSM 내부 수행 (pkcs11Provider 명시)
        PrivateKey privateKey = (PrivateKey) keyStore.getKey(RSA_ALIAS, PIN.toCharArray());
        Cipher decCipher = Cipher.getInstance("RSA/ECB/PKCS1Padding", provider);
        decCipher.init(Cipher.DECRYPT_MODE, privateKey);
        byte[] decrypted = decCipher.doFinal(ciphertext);

        assertArrayEquals(plaintext, decrypted);
        System.out.println("[HSM] RSA 암복호화 성공."
                + " enc.provider=" + encCipher.getProvider().getName()
                + " / dec.provider=" + decCipher.getProvider().getName()
                + " / plaintext=" + new String(decrypted, "UTF-8"));
    }

    // -------------------------------------------------------------------------
    // 테스트: AES 암복호화 (HsmCryptoService.encryptAes / decryptAes 해당)
    // -------------------------------------------------------------------------

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

        SecretKey secretKey = (SecretKey) keyStore.getKey(AES_ALIAS, null);

        // 암호화
        Cipher encCipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
        encCipher.init(Cipher.ENCRYPT_MODE, secretKey, new IvParameterSpec(iv));
        byte[] ciphertext = encCipher.doFinal(plaintext);

        // 복호화
        Cipher decCipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
        decCipher.init(Cipher.DECRYPT_MODE, secretKey, new IvParameterSpec(iv));
        byte[] decrypted = decCipher.doFinal(ciphertext);

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

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

    private static void initToken() 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 output = readOutput(p);
        p.waitFor(10, TimeUnit.SECONDS);
        // 이미 초기화된 슬롯이 없으면 실패할 수 있으나 무시 (기존 토큰 재사용)
        System.out.println("[SoftHSM2] init-token: " + output.trim());
    }

    /**
     * keytool 로 RSA 키쌍 + 자체서명 인증서를 PKCS11 토큰에 생성.
     * 인증서가 있어야 PKCS11 KeyStore 에서 alias 로 조회 가능.
     */
    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 output = readOutput(p);
        boolean finished = p.waitFor(30, TimeUnit.SECONDS);
        if (!finished) {
            p.destroy();
            throw new RuntimeException("keytool -genkeypair timeout");
        }
        System.out.println("[keytool] genkeypair exit=" + p.exitValue() + ": " + output.trim());
    }

    /**
     * KeyGenerator 로 AES-256 키를 HSM 토큰에 직접 생성.
     * SoftHSM2 의 AES 키는 기본적으로 extractable(추출 가능).
     */
    private static void generateAesKey() throws Exception {
        KeyGenerator kg = KeyGenerator.getInstance("AES", provider);
        kg.init(256);
        SecretKey sk = kg.generateKey();
        keyStore.setKeyEntry(AES_ALIAS, sk, null, null);
        System.out.println("[HSM] 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();
    }
}
