package com.eactive.eai.authserver.jwt;

import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;

import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.interfaces.RSAPrivateKey;
import java.security.interfaces.RSAPublicKey;
import java.util.Arrays;
import java.util.Base64;

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.security.jwt.JwtHelper;
import org.springframework.security.jwt.crypto.sign.InvalidSignatureException;
import org.springframework.security.jwt.crypto.sign.RsaSigner;

/**
 * RsaPssSigner / RsaPssVerifier 단위 테스트.
 * Spring 컨텍스트 불필요 — KeyPairGenerator로 테스트용 RSA 2048 키쌍을 생성하여 검증한다.
 */
@SuppressWarnings("deprecation")
@TestMethodOrder(MethodOrderer.DisplayName.class)
class RsaPssSignerVerifierTest {

    private static RSAPrivateKey privateKey;
    private static RSAPublicKey  publicKey;
    private static RsaPssSigner  signer;
    private static RsaPssVerifier verifier;

    private static final byte[] CONTENT = "header.payload".getBytes();

    @BeforeAll
    static void setUpClass() throws Exception {
        KeyPairGenerator gen = KeyPairGenerator.getInstance("RSA");
        gen.initialize(2048);
        KeyPair keyPair = gen.generateKeyPair();
        privateKey = (RSAPrivateKey) keyPair.getPrivate();
        publicKey  = (RSAPublicKey)  keyPair.getPublic();
        signer   = new RsaPssSigner(privateKey);
        verifier = new RsaPssVerifier(publicKey);
    }

    // =========================================================================
    // 1. RsaPssSigner
    // =========================================================================

    @Test
    @DisplayName("1-1. algorithm() — PS256 반환")
    void testSignerAlgorithm() {
        assertEquals("PS256", signer.algorithm());
    }

    @Test
    @DisplayName("1-2. sign() — non-null, 256바이트 (RSA-2048 서명 크기)")
    void testSignReturnsExpectedLength() {
        byte[] sig = signer.sign(CONTENT);
        assertNotNull(sig);
        assertEquals(256, sig.length);
    }

    @Test
    @DisplayName("1-3. sign() — 비결정론적 (같은 입력이라도 랜덤 salt로 인해 서명이 달라야 한다)")
    void testSignIsNonDeterministic() {
        byte[] sig1 = signer.sign(CONTENT);
        byte[] sig2 = signer.sign(CONTENT);
        assertFalse(Arrays.equals(sig1, sig2), "PSS는 매번 다른 salt를 사용하므로 서명이 달라야 한다");
    }

    // =========================================================================
    // 2. RsaPssVerifier
    // =========================================================================

    @Test
    @DisplayName("2-1. algorithm() — PS256 반환")
    void testVerifierAlgorithm() {
        assertEquals("PS256", verifier.algorithm());
    }

    @Test
    @DisplayName("2-2. 정상 서명 → 검증 성공 (예외 없음)")
    void testVerifyValidSignature() {
        byte[] sig = signer.sign(CONTENT);
        assertDoesNotThrow(() -> verifier.verify(CONTENT, sig));
    }

    @Test
    @DisplayName("2-3. 내용 변조 → InvalidSignatureException")
    void testVerifyTamperedContent() {
        byte[] sig     = signer.sign(CONTENT);
        byte[] tampered = "tampered.payload".getBytes();
        assertThrows(InvalidSignatureException.class, () -> verifier.verify(tampered, sig));
    }

    @Test
    @DisplayName("2-4. 서명 변조 (첫 바이트 반전) → InvalidSignatureException")
    void testVerifyTamperedSignature() {
        byte[] sig = signer.sign(CONTENT);
        sig[0] ^= 0xFF;
        assertThrows(InvalidSignatureException.class, () -> verifier.verify(CONTENT, sig));
    }

    @Test
    @DisplayName("2-5. 다른 키쌍의 공개키로 검증 → InvalidSignatureException")
    void testVerifyWithWrongPublicKey() throws Exception {
        KeyPairGenerator gen = KeyPairGenerator.getInstance("RSA");
        gen.initialize(2048);
        RSAPublicKey otherPublicKey = (RSAPublicKey) gen.generateKeyPair().getPublic();
        RsaPssVerifier wrongVerifier = new RsaPssVerifier(otherPublicKey);

        byte[] sig = signer.sign(CONTENT);
        assertThrows(InvalidSignatureException.class, () -> wrongVerifier.verify(CONTENT, sig));
    }

    // =========================================================================
    // 3. Signer + Verifier 통합
    // =========================================================================

    @Test
    @DisplayName("3-1. sign → verify 라운드트립 — 성공")
    void testRoundTrip() {
        byte[] sig = signer.sign(CONTENT);
        assertDoesNotThrow(() -> verifier.verify(CONTENT, sig));
    }

    @Test
    @DisplayName("3-2. 다른 키쌍의 서명을 원래 verifier로 검증 → 실패")
    void testRoundTripCrossKeyFails() throws Exception {
        KeyPairGenerator gen = KeyPairGenerator.getInstance("RSA");
        gen.initialize(2048);
        KeyPair other = gen.generateKeyPair();
        RsaPssSigner otherSigner = new RsaPssSigner((RSAPrivateKey) other.getPrivate());

        byte[] sigByOther = otherSigner.sign(CONTENT);
        assertThrows(InvalidSignatureException.class, () -> verifier.verify(CONTENT, sigByOther));
    }

    @Test
    @DisplayName("3-3. 원래 서명을 다른 키쌍의 verifier로 검증 → 실패")
    void testRoundTripCrossVerifierFails() throws Exception {
        KeyPairGenerator gen = KeyPairGenerator.getInstance("RSA");
        gen.initialize(2048);
        KeyPair other = gen.generateKeyPair();
        RsaPssVerifier otherVerifier = new RsaPssVerifier((RSAPublicKey) other.getPublic());

        byte[] sig = signer.sign(CONTENT);
        assertThrows(InvalidSignatureException.class, () -> otherVerifier.verify(CONTENT, sig));
    }

    // =========================================================================
    // 4. 시각 확인 — 콘솔 출력으로 RS256(결정론적) vs PS256(비결정론적) 비교
    // =========================================================================

    @Test
    @DisplayName("4-1. [시각 확인] RS256은 결정론적, PS256은 비결정론적 — 콘솔 hex 출력")
    void testVisualComparison() {
        RsaSigner rs256Signer = new RsaSigner(privateKey);

        byte[] rs256Sig1 = rs256Signer.sign(CONTENT);
        byte[] rs256Sig2 = rs256Signer.sign(CONTENT);
        byte[] ps256Sig1 = signer.sign(CONTENT);
        byte[] ps256Sig2 = signer.sign(CONTENT);

        System.out.println();
        System.out.println("━━━ 서명 알고리즘 시각 비교 (동일 데이터: \"" + new String(CONTENT) + "\") ━━━");
        System.out.println();
        System.out.printf("[RS256  서명 1] %s%n", toHex(rs256Sig1));
        System.out.printf("[RS256  서명 2] %s%n", toHex(rs256Sig2));
        System.out.printf("  → 두 서명 일치 여부: %b  (결정론적 — 항상 같음)%n", Arrays.equals(rs256Sig1, rs256Sig2));
        System.out.println();
        System.out.printf("[PS256  서명 1] %s%n", toHex(ps256Sig1));
        System.out.printf("[PS256  서명 2] %s%n", toHex(ps256Sig2));
        System.out.printf("  → 두 서명 일치 여부: %b  (비결정론적 — 매번 다름, PSS 랜덤 salt 때문)%n", Arrays.equals(ps256Sig1, ps256Sig2));
        System.out.println();

        assertTrue(Arrays.equals(rs256Sig1, rs256Sig2),  "RS256은 결정론적이어야 한다");
        assertFalse(Arrays.equals(ps256Sig1, ps256Sig2), "PS256은 비결정론적이어야 한다");
    }

    // =========================================================================
    // 4-2. JWT 토큰 시각 확인 — 헤더 alg 필드 + 서명 파트 비교
    // =========================================================================

    @Test
    @DisplayName("4-2. [JWT 시각 확인] alg 헤더(RS256/PS256)와 서명 파트 변화를 JWT 문자열로 확인")
    void testJwtVisualComparison() {
        String claims = "{\"sub\":\"test-client\",\"scope\":\"read\"}";
        RsaSigner rs256Signer = new RsaSigner(privateKey);
        PssJwtAccessTokenConverter pssConverter = new PssJwtAccessTokenConverter(privateKey, publicKey);

        String rs256Jwt1 = JwtHelper.encode(claims, rs256Signer).getEncoded();
        String rs256Jwt2 = JwtHelper.encode(claims, rs256Signer).getEncoded();
        String ps256Jwt1 = pssConverter.buildJwt(claims);
        String ps256Jwt2 = pssConverter.buildJwt(claims);

        System.out.println();
        System.out.println("━━━ JWT 토큰 시각 비교 ━━━");
        System.out.println();
        printJwt("RS256 JWT 1", rs256Jwt1);
        printJwt("RS256 JWT 2", rs256Jwt2);
        System.out.printf("  → 두 JWT 완전 일치: %b  (헤더·페이로드·서명 모두 동일)%n",
                rs256Jwt1.equals(rs256Jwt2));
        System.out.println();
        printJwt("PS256 JWT 1", ps256Jwt1);
        printJwt("PS256 JWT 2", ps256Jwt2);
        System.out.printf("  → 두 JWT 완전 일치: %b  (헤더·페이로드 동일, 서명만 다름)%n",
                ps256Jwt1.equals(ps256Jwt2));
        System.out.println();
        System.out.println("  [서명 파트만 비교]");
        System.out.printf("  RS256 서명1: %s%n", signaturePart(rs256Jwt1));
        System.out.printf("  RS256 서명2: %s%n", signaturePart(rs256Jwt2));
        System.out.printf("  PS256 서명1: %s%n", signaturePart(ps256Jwt1));
        System.out.printf("  PS256 서명2: %s%n", signaturePart(ps256Jwt2));

        assertEquals(rs256Jwt1, rs256Jwt2,   "RS256 JWT는 동일해야 한다");
        assertNotEquals(ps256Jwt1, ps256Jwt2, "PS256 JWT는 서명 파트가 달라야 한다");
    }

    private static void printJwt(String label, String jwt) {
        String[] parts = jwt.split("\\.");
        String header  = new String(Base64.getUrlDecoder().decode(padBase64Url(parts[0])));
        String payload = new String(Base64.getUrlDecoder().decode(padBase64Url(parts[1])));
        System.out.printf("[%s]%n", label);
        System.out.printf("  전체  : %s%n", jwt);
        System.out.printf("  헤더  : %s%n", header);   // {"alg":"RS256","typ":"JWT"} 또는 PS256
        System.out.printf("  페이로드: %s%n", payload);
        System.out.printf("  서명  : %s%n", parts[2]);
        System.out.println();
    }

    private static String signaturePart(String jwt) {
        return jwt.split("\\.")[2];
    }

    /** Base64URL 패딩 보정 (Java 표준 디코더는 패딩 필수) */
    private static String padBase64Url(String base64url) {
        int mod = base64url.length() % 4;
        if (mod == 2) return base64url + "==";
        if (mod == 3) return base64url + "=";
        return base64url;
    }

    private static String toHex(byte[] bytes) {
        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < bytes.length; i++) {
            if (i > 0 && i % 32 == 0) sb.append("\n               ");
            sb.append(String.format("%02X", bytes[i]));
        }
        return sb.toString();
    }
}
