package com.eactive.eai.adapter.http.dynamic.filter;

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

import java.io.File;
import java.io.InputStream;
import java.lang.reflect.Constructor;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.UUID;
import java.util.concurrent.TimeUnit;

import javax.crypto.KeyGenerator;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

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.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.adapter.http.dynamic.filter.InCryptoFilter;
import com.eactive.eai.adapter.http.filter.AbstractCryptoFilter;
import com.eactive.eai.common.hsm.HsmCryptoService;
import com.eactive.eai.common.hsm.HsmManager;
import com.eactive.eai.common.property.PropManager;
import com.eactive.eai.common.security.CryptoModuleConfigVO;
import com.eactive.eai.common.security.CryptoModuleManager;
import com.eactive.eai.common.security.CryptoModuleService;
import com.eactive.eai.common.security.keyderiv.strategy.HsmContextSha256KeyDerivationStrategy;
import com.eactive.eai.common.security.keyderiv.strategy.HsmKeyDerivationStrategy;
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;

/**
 * CryptoFilter HSM 연동 통합 테스트 (SoftHSM2 / SunPKCS11)
 *
 * SoftHSM2에서 MASTER_KEY를 조회하여 키를 도출한 뒤
 * InCryptoFilter 의 암복호화 전체 흐름을 검증한다.
 *
 * 사전 조건:
 *   C:\SoftHSM2 에 SoftHSM2 설치 (미설치 시 자동 건너뜀)
 *   MASTER_KEY alias는 없으면 자동 생성됨
 *
 * 검증 전략:
 *   HSM → 키 도출 → CryptoModuleExtension → InCryptoFilter doPostFilter(암호화) → doPreFilter(복호화) → 원문 일치
 */
@TestMethodOrder(MethodOrderer.DisplayName.class)
public class CryptoFilterHsmIntegrationTest {

    // -------------------------------------------------------------------------
    // 상수
    // -------------------------------------------------------------------------

    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 MASTER_KEY_ALIAS = "MASTER_KEY";

    /** HsmKeyDerivationStrategy: HSM 마스터키를 파생 없이 직접 사용 */
    private static final String MOD_HSM_GCM      = "HSM_GCM";
    /** HsmContextSha256KeyDerivationStrategy: HSM 마스터키 + 컨텍스트값 SHA-256 파생 */
    private static final String MOD_CTX_SHA_GCM  = "CTX_SHA256_GCM";

    private static final String CTX_KEY          = "X-Api-Group-Seq";
    private static final String CTX_VAL_A        = "GROUP-A";
    private static final String CTX_VAL_B        = "GROUP-B";

    // -------------------------------------------------------------------------
    // 정적 필드
    // -------------------------------------------------------------------------

    private static GenericApplicationContext springCtx;
    private static HsmManager                hsmManager;
    private static File                      tempCfgFile;

    // -------------------------------------------------------------------------
    // 내부 클래스: runtimeContext를 고정값으로 반환하는 필터
    // -------------------------------------------------------------------------

    static class ContextAwareCryptoFilter extends InCryptoFilter {
        private final String key;
        private final String value;

        ContextAwareCryptoFilter(String key, String value) {
            this.key   = key;
            this.value = value;
        }

        @Override
        protected Map<String, String> buildRuntimeContext(Properties prop, HttpServletRequest request) {
            Map<String, String> ctx = new HashMap<>();
            ctx.put(key, value);
            return ctx;
        }
    }

    // -------------------------------------------------------------------------
    // 인스턴스 필드
    // -------------------------------------------------------------------------

    private HttpServletRequest  mockReq;
    private HttpServletResponse mockRes;

    // -------------------------------------------------------------------------
    // 전체 설정 / 해제
    // -------------------------------------------------------------------------

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

        // 1. 토큰 초기화
        ensureTokenInitialized();

        // 2. PKCS11 설정 문자열
        // attributes(*,...): generate/import 모두 CKA_SENSITIVE=false → getEncoded() 사용 가능
        String cfgContent = "name = SoftHSM\n"
                + "library = " + SOFTHSM2_DLL + "\n"
                + "slotListIndex = 0\n"
                + "attributes(*, CKO_SECRET_KEY, CKK_AES) = {\n"
                + "  CKA_SENSITIVE = false\n"
                + "  CKA_EXTRACTABLE = true\n"
                + "}\n";
        tempCfgFile = File.createTempFile("pkcs11-filter-it-", ".cfg");
        Files.write(tempCfgFile.toPath(), cfgContent.getBytes("UTF-8"));

        // 3. Mock PropManager (DB 없이 HSM 설정값 제공)
        PropManager mockPropManager = mock(PropManager.class);
        when(mockPropManager.getProperty("HSM", "PKCS11_CONFIG")).thenReturn(cfgContent);
        when(mockPropManager.getProperty("HSM", "PIN")).thenReturn(PIN);
        when(mockPropManager.getProperty(eq("HSM"), eq("CACHE_RELOAD_YN"), anyString())).thenReturn("N");

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

        // 5. CryptoModuleManager (private 생성자 → 리플렉션)
        CryptoModuleConfigLoader mockLoader = mock(CryptoModuleConfigLoader.class);
        when(mockLoader.findAll()).thenReturn(buildModuleConfigs());

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

        CryptoModuleService  cryptoService    = new CryptoModuleService();
        HsmCryptoService     hsmCryptoService = new HsmCryptoService();

        // 6. Spring ApplicationContext 구성
        springCtx = new GenericApplicationContext();
        springCtx.getBeanFactory().registerSingleton("propManager",            mockPropManager);
        springCtx.getBeanFactory().registerSingleton("hsmManager",             hsmManager);
        springCtx.getBeanFactory().registerSingleton("hsmCryptoService",       hsmCryptoService);
        springCtx.getBeanFactory().registerSingleton("cryptoModuleConfigLoader", mockLoader);
        springCtx.getBeanFactory().registerSingleton("cryptoModuleManager",    manager);
        springCtx.getBeanFactory().registerSingleton("cryptoModuleService",    cryptoService);
        springCtx.getBeanFactory().registerSingleton("cryptoModuleConfigMapper", testMapper());
        springCtx.registerBeanDefinition("applicationContextProvider",
                BeanDefinitionBuilder.genericBeanDefinition(ApplicationContextProvider.class)
                        .getBeanDefinition());
        springCtx.refresh();

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

        // 8. MASTER_KEY 등록 (없으면 자동 생성)
        ensureMasterKey();

        // 9. CryptoModuleManager 시작 (모듈 설정 로드)
        manager.start();
        System.out.println("[Crypto] 모듈 로드 완료");
    }

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

    @BeforeEach
    void setUp() {
        mockReq = mock(HttpServletRequest.class);
        mockRes = mock(HttpServletResponse.class);
    }

    // =========================================================================
    // 1. HsmKeyDerivationStrategy — HSM 마스터키 직접 사용 (InCryptoFilter)
    // =========================================================================

    @Test
    @DisplayName("1-1. FIELD/GCM HsmKey — doPostFilter 암호화 → doPreFilter 복호화 라운드트립")
    void testHsmGcm_field_roundTrip() throws Exception {
        InCryptoFilter filter   = new InCryptoFilter();
        String           original = "{\"acno\":\"1234567890\",\"amount\":\"100000\"}";

        Properties prop = new Properties();
        prop.setProperty(AbstractCryptoFilter.PROP_MODULE_NAME, MOD_HSM_GCM);
        // SCOPE 기본값 FIELD, 경로 기본값 사용

        // 암호화: body 전체 → /encrypted_data 필드
        String encrypted = (String) filter.doPostFilter("G", "A", original, prop, mockReq, mockRes);
        System.out.println("[1-1] encrypted: " + encrypted);
        assertTrue(encrypted.contains("encrypted_data"), "/encrypted_data 필드가 있어야 한다");

        // 복호화: /encrypted_data 필드 → body 전체 교체
        String decrypted = (String) filter.doPreFilter("G", "A", encrypted, prop, mockReq, mockRes);
        System.out.println("[1-1] decrypted: " + decrypted);
        assertEquals(original, decrypted);
    }

    @Test
    @DisplayName("1-2. BODY/GCM HsmKey — doPostFilter 암호화 → doPreFilter 복호화 라운드트립")
    void testHsmGcm_body_roundTrip() throws Exception {
        InCryptoFilter filter   = new InCryptoFilter();
        String           original = "{\"acno\":\"1234567890\",\"amount\":\"100000\"}";

        Properties prop = new Properties();
        prop.setProperty(AbstractCryptoFilter.PROP_MODULE_NAME, MOD_HSM_GCM);
        prop.setProperty(AbstractCryptoFilter.PROP_SCOPE, "BODY");

        // 암호화: body 전체 → Base64 암호문
        String encrypted = (String) filter.doPostFilter("G", "A", original, prop, mockReq, mockRes);
        System.out.println("[1-2] encrypted(Base64): " + encrypted);
        assertNotEquals(original, encrypted, "암호문은 평문과 달라야 한다");

        // 복호화: Base64 암호문 → 원문
        String decrypted = (String) filter.doPreFilter("G", "A", encrypted, prop, mockReq, mockRes);
        System.out.println("[1-2] decrypted: " + decrypted);
        assertEquals(original, decrypted);
    }

    @Test
    @DisplayName("1-3. FIELD/GCM HsmKey + AAD 헤더 — 동일 요청 ID로 암복호화 성공")
    void testHsmGcm_field_withAad_success() throws Exception {
        InCryptoFilter filter    = new InCryptoFilter();
        String           original  = "{\"amount\":\"500000\"}";
        String           requestId = "REQ-20240101-001";

        when(mockReq.getHeader("X-Request-Id")).thenReturn(requestId);

        Properties prop = new Properties();
        prop.setProperty(AbstractCryptoFilter.PROP_MODULE_NAME, MOD_HSM_GCM);
        prop.setProperty(AbstractCryptoFilter.PROP_AAD_HEADER,  "X-Request-Id");

        String encrypted = (String) filter.doPostFilter("G", "A", original, prop, mockReq, mockRes);
        String decrypted = (String) filter.doPreFilter( "G", "A", encrypted, prop, mockReq, mockRes);
        System.out.println("[1-3] decrypted: " + decrypted);
        assertEquals(original, decrypted);
    }

    @Test
    @DisplayName("1-4. FIELD/GCM HsmKey + AAD 헤더 — 다른 요청 ID로 복호화 시 GCM 인증 실패")
    void testHsmGcm_field_withAad_wrongAad_fails() throws Exception {
        InCryptoFilter filter    = new InCryptoFilter();
        String           original  = "{\"amount\":\"500000\"}";

        // 암호화 시 요청 ID
        HttpServletRequest encReq = mock(HttpServletRequest.class);
        when(encReq.getHeader("X-Request-Id")).thenReturn("REQ-CORRECT");

        // 복호화 시 다른 요청 ID
        HttpServletRequest decReq = mock(HttpServletRequest.class);
        when(decReq.getHeader("X-Request-Id")).thenReturn("REQ-WRONG");

        Properties prop = new Properties();
        prop.setProperty(AbstractCryptoFilter.PROP_MODULE_NAME, MOD_HSM_GCM);
        prop.setProperty(AbstractCryptoFilter.PROP_AAD_HEADER,  "X-Request-Id");

        String encrypted = (String) filter.doPostFilter("G", "A", original, prop, encReq, mockRes);

        assertThrows(Exception.class,
                () -> filter.doPreFilter("G", "A", encrypted, prop, decReq, mockRes),
                "잘못된 AAD로 GCM 복호화 시 예외가 발생해야 한다");
        System.out.println("[1-4] 다른 AAD 복호화 예외 확인 완료");
    }

    // =========================================================================
    // 2. HsmContextSha256KeyDerivationStrategy — 컨텍스트 기반 SHA-256 파생 키
    // =========================================================================

    @Test
    @DisplayName("2-1. FIELD/GCM ContextSha256 — 동일 컨텍스트로 암복호화 라운드트립")
    void testCtxSha256Gcm_field_roundTrip() throws Exception {
        InCryptoFilter filter   = new ContextAwareCryptoFilter(CTX_KEY, CTX_VAL_A);
        String       original = "{\"accNo\":\"0011234567890\",\"amount\":\"50000\"}";

        Properties prop = new Properties();
        prop.setProperty(AbstractCryptoFilter.PROP_MODULE_NAME, MOD_CTX_SHA_GCM);

        String encrypted = (String) filter.doPostFilter("G", "A", original, prop, mockReq, mockRes);
        System.out.println("[2-1] encrypted: " + encrypted);
        assertTrue(encrypted.contains("encrypted_data"));

        String decrypted = (String) filter.doPreFilter("G", "A", encrypted, prop, mockReq, mockRes);
        System.out.println("[2-1] decrypted: " + decrypted);
        assertEquals(original, decrypted);
    }

    @Test
    @DisplayName("2-2. FIELD/GCM ContextSha256 — 암호화와 다른 컨텍스트로 복호화 시 GCM 인증 실패")
    void testCtxSha256Gcm_differentContext_throwsException() throws Exception {
        InCryptoFilter encFilter = new ContextAwareCryptoFilter(CTX_KEY, CTX_VAL_A);
        InCryptoFilter decFilter = new ContextAwareCryptoFilter(CTX_KEY, CTX_VAL_B);
        String       original  = "{\"data\":\"sensitive\"}";

        Properties prop = new Properties();
        prop.setProperty(AbstractCryptoFilter.PROP_MODULE_NAME, MOD_CTX_SHA_GCM);

        String encrypted = (String) encFilter.doPostFilter("G", "A", original, prop, mockReq, mockRes);

        assertThrows(Exception.class,
                () -> decFilter.doPreFilter("G", "A", encrypted, prop, mockReq, mockRes),
                "다른 컨텍스트 키로 파생된 키는 복호화에 실패해야 한다");
        System.out.println("[2-2] 다른 컨텍스트 복호화 예외 확인 완료");
    }

    @Test
    @DisplayName("2-3. FIELD/GCM ContextSha256 — 컨텍스트별 독립 암복호화 (GROUP-A, GROUP-B 각각 성공)")
    void testCtxSha256Gcm_perGroupRoundTrip() throws Exception {
        InCryptoFilter filterA  = new ContextAwareCryptoFilter(CTX_KEY, CTX_VAL_A);
        InCryptoFilter filterB  = new ContextAwareCryptoFilter(CTX_KEY, CTX_VAL_B);
        String       plainA   = "{\"group\":\"A\",\"amount\":\"1000\"}";
        String       plainB   = "{\"group\":\"B\",\"amount\":\"2000\"}";

        Properties prop = new Properties();
        prop.setProperty(AbstractCryptoFilter.PROP_MODULE_NAME, MOD_CTX_SHA_GCM);

        String encA = (String) filterA.doPostFilter("G", "A", plainA, prop, mockReq, mockRes);
        String encB = (String) filterB.doPostFilter("G", "B", plainB, prop, mockReq, mockRes);

        assertEquals(plainA, (String) filterA.doPreFilter("G", "A", encA, prop, mockReq, mockRes));
        assertEquals(plainB, (String) filterB.doPreFilter("G", "B", encB, prop, mockReq, mockRes));
        System.out.println("[2-3] GROUP-A, GROUP-B 각각 독립 암복호화 성공");
    }

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

    private static List<CryptoModuleConfig> buildModuleConfigs() {
        return Arrays.asList(
                buildDynamic(MOD_HSM_GCM, "AES", "GCM", "NoPadding",
                        HsmKeyDerivationStrategy.class.getName(),
                        "{\"hsmKeyAlias\":\"" + MASTER_KEY_ALIAS + "\"}"),
                buildDynamic(MOD_CTX_SHA_GCM, "AES", "GCM", "NoPadding",
                        HsmContextSha256KeyDerivationStrategy.class.getName(),
                        "{\"hsmKeyAlias\":\"" + MASTER_KEY_ALIAS + "\",\"contextKey\":\"" + CTX_KEY + "\"}")
        );
    }

    private static CryptoModuleConfig buildDynamic(String name, String alg, String mode, String padding,
            String strategy, String params) {
        CryptoModuleConfig c = new CryptoModuleConfig();
        c.setCryptoId(UUID.randomUUID().toString());
        c.setCryptoName(name);
        c.setAlgType(alg);
        c.setCipherMode(mode);
        c.setPadding(padding);
        c.setKeySourceType("DYNAMIC");
        c.setKeyDerivStrategy(strategy);
        c.setKeyDerivParams(params);
        c.setCacheYn("Y");
        c.setCacheTtlSec(60);
        c.setUseYn("Y");
        // ivHex = null: GCM에서 IV를 AAD 대체값으로 사용하지 않도록 의도적으로 미설정
        return c;
    }

    private static void ensureMasterKey() throws Exception {
        java.security.KeyStore ks = hsmManager.getKeyStore();

        // 이전 실행 키 삭제 (sensitive 여부 무관하게 항상 고정키로 재등록)
        if (ks.containsAlias(MASTER_KEY_ALIAS)) {
            ks.deleteEntry(MASTER_KEY_ALIAS);
            System.out.println("[HSM] 기존 MASTER_KEY 삭제");
        }

        // CryptoModuleServiceTest.KEY_128 과 동일한 고정 키 → 두 테스트 간 암호문 비교 가능
        // attributes(*, CKO_SECRET_KEY, CKK_AES) 지시어로 임포트 시 CKA_SENSITIVE=false 적용
        byte[] keyBytes = "0123456789abcdef".getBytes(StandardCharsets.UTF_8);
        SecretKey fixedKey = new SecretKeySpec(keyBytes, "AES");
        ks.setKeyEntry(MASTER_KEY_ALIAS, fixedKey, null, null);

        // 임포트 후 getEncoded() 검증
        SecretKey stored = (SecretKey) ks.getKey(MASTER_KEY_ALIAS, null);
        byte[] encoded = stored.getEncoded();
        System.out.println("[HSM] MASTER_KEY 등록 완료: alias=" + MASTER_KEY_ALIAS
                + " / getEncoded()=" + (encoded != null ? encoded.length + "bytes, " + new String(encoded) : "null (추출 불가!)"));
    }

    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 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;
            }
        };
    }

    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();
    }
}
