package com.infoepoch.pms.agent.service.auth;

import com.infoepoch.pms.agent.platform.shared.exception.BusinessException;
import com.infoepoch.pms.agent.properties.SsoProperties;
import com.infoepoch.pms.agent.service.auth.dto.ExternalSsoUser;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

import javax.crypto.Cipher;
import javax.crypto.spec.GCMParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.security.SecureRandom;
import java.util.Base64;
import java.util.LinkedHashMap;
import java.util.Map;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;

class ExternalSsoTokenVerifierTest {

    private static final String ISSUER = "union-js";
    private static final String KID = "2026-04";
    private static final String AUDIENCE = "pms-agent";
    private static final String USERNAME = "zhangsan";
    private static final String TOKEN_ID = "token-1";
    private static final String AES_KEY = "MDEyMzQ1Njc4OWFiY2RlZjAxMjM0NTY3ODlhYmNkZWY=";

    private FakeUsedSsoTokenService usedSsoTokenService;
    private ExternalSsoTokenVerifier verifier;

    @BeforeEach
    void setUp() {
        usedSsoTokenService = new FakeUsedSsoTokenService();
        verifier = new ExternalSsoTokenVerifier(ssoProperties(), usedSsoTokenService);
    }

    @Test
    void verifiesValidToken() {
        String token = token(payload(System.currentTimeMillis(), System.currentTimeMillis() + 60_000, AUDIENCE, TOKEN_ID));

        ExternalSsoUser user = verifier.verify(token);

        assertThat(user.getIssuer()).isEqualTo(ISSUER);
        assertThat(user.getUsername()).isEqualTo(USERNAME);
        assertThat(user.getTokenId()).isEqualTo(TOKEN_ID);
    }

    @Test
    void rejectsMismatchedAudience() {
        String token = token(payload(System.currentTimeMillis(), System.currentTimeMillis() + 60_000, "other-app", TOKEN_ID));

        assertThatThrownBy(() -> verifier.verify(token))
                .isInstanceOf(BusinessException.class)
                .hasMessageContaining("Token 接收方错误");
    }

    @Test
    void rejectsTamperedOuterIssuer() {
        String token = token(payload(System.currentTimeMillis(), System.currentTimeMillis() + 60_000, AUDIENCE, TOKEN_ID))
                .replaceFirst("v1\\.union-js\\.", "v1.portal.");

        assertThatThrownBy(() -> verifier.verify(token))
                .isInstanceOf(BusinessException.class)
                .hasMessageContaining("未信任的来源系统或密钥版本");
    }

    @Test
    void rejectsTamperedCiphertext() {
        String token = token(payload(System.currentTimeMillis(), System.currentTimeMillis() + 60_000, AUDIENCE, TOKEN_ID));
        String tampered = token.substring(0, token.length() - 2) + "AA";

        assertThatThrownBy(() -> verifier.verify(tampered))
                .isInstanceOf(BusinessException.class)
                .hasMessageContaining("Token 解密或校验失败");
    }

    @Test
    void rejectsExpiredToken() {
        long now = System.currentTimeMillis();
        String token = token(payload(now - 120_000, now - 1, AUDIENCE, TOKEN_ID));

        assertThatThrownBy(() -> verifier.verify(token))
                .isInstanceOf(BusinessException.class)
                .hasMessageContaining("Token 已过期");
    }

    @Test
    void rejectsFutureIssuedToken() {
        long now = System.currentTimeMillis();
        String token = token(payload(now + 120_000, now + 180_000, AUDIENCE, TOKEN_ID));

        assertThatThrownBy(() -> verifier.verify(token))
                .isInstanceOf(BusinessException.class)
                .hasMessageContaining("Token 签发时间异常");
    }

    @Test
    void rejectsLongLifetimeToken() {
        long now = System.currentTimeMillis();
        String token = token(payload(now, now + 300_000, AUDIENCE, TOKEN_ID));

        assertThatThrownBy(() -> verifier.verify(token))
                .isInstanceOf(BusinessException.class)
                .hasMessageContaining("Token 有效期异常");
    }

    @Test
    void rejectsReplayedTokenId() {
        usedSsoTokenService.markUsedResult = false;
        String token = token(payload(System.currentTimeMillis(), System.currentTimeMillis() + 60_000, AUDIENCE, TOKEN_ID));

        assertThatThrownBy(() -> verifier.verify(token))
                .isInstanceOf(BusinessException.class)
                .hasMessageContaining("Token 已被使用");
    }

    private SsoProperties ssoProperties() {
        SsoProperties properties = new SsoProperties();
        properties.setAudience(AUDIENCE);
        properties.setTokenExpireSeconds(120);
        properties.setClockSkewSeconds(60);

        SsoProperties.TrustedIssuer trustedIssuer = new SsoProperties.TrustedIssuer();
        trustedIssuer.setActiveKid(KID);
        trustedIssuer.setKeys(Map.of(KID, AES_KEY));
        properties.setTrustedIssuers(Map.of(ISSUER, trustedIssuer));
        return properties;
    }

    private Map<String, Object> payload(long iat, long exp, String audience, String tokenId) {
        Map<String, Object> payload = new LinkedHashMap<>();
        payload.put("iss", ISSUER);
        payload.put("kid", KID);
        payload.put("aud", audience);
        payload.put("username", USERNAME);
        payload.put("iat", iat);
        payload.put("exp", exp);
        payload.put("jti", tokenId);
        return payload;
    }

    private String token(Map<String, Object> payload) {
        try {
            byte[] iv = new byte[12];
            new SecureRandom().nextBytes(iv);

            Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
            SecretKeySpec keySpec = new SecretKeySpec(Base64.getDecoder().decode(AES_KEY), "AES");
            cipher.init(Cipher.ENCRYPT_MODE, keySpec, new GCMParameterSpec(128, iv));
            cipher.updateAAD(("v1." + ISSUER + "." + KID + "." + AUDIENCE).getBytes(StandardCharsets.UTF_8));
            byte[] encrypted = cipher.doFinal(toJson(payload).getBytes(StandardCharsets.UTF_8));

            Base64.Encoder encoder = Base64.getUrlEncoder().withoutPadding();
            return "v1." + ISSUER + "." + KID + "."
                    + encoder.encodeToString(iv) + "."
                    + encoder.encodeToString(encrypted);
        } catch (Exception ex) {
            throw new IllegalStateException(ex);
        }
    }

    private String toJson(Map<String, Object> payload) {
        return "{"
                + "\"iss\":\"" + payload.get("iss") + "\","
                + "\"kid\":\"" + payload.get("kid") + "\","
                + "\"aud\":\"" + payload.get("aud") + "\","
                + "\"username\":\"" + payload.get("username") + "\","
                + "\"iat\":" + payload.get("iat") + ","
                + "\"exp\":" + payload.get("exp") + ","
                + "\"jti\":\"" + payload.get("jti") + "\""
                + "}";
    }

    private static class FakeUsedSsoTokenService extends UsedSsoTokenService {
        private boolean markUsedResult = true;

        FakeUsedSsoTokenService() {
            super(null, null);
        }

        @Override
        public boolean markUsed(String issuer, String tokenId, long expireAtMillis) {
            return markUsedResult;
        }
    }
}
