package com.codebyte.api.config.ssl;

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

import com.codebyte.api.support.TestPki;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.attribute.PosixFilePermissions;
import java.security.PrivateKey;
import java.security.cert.X509Certificate;
import java.util.List;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;

class PemSslLoaderTest {

    private static List<X509Certificate> parse(String pem) {
        return PemSslLoader.parseCertificates(pem);
    }

    @Test
    void should_load_and_validate_a_valid_trio(@TempDir Path dir) throws Exception {
        TestPki pki = TestPki.threeLevel();
        Path cert = write(dir, "tls.crt", pki.leafCertPem());
        Path ca = write(dir, "ca.crt", pki.caChainPem());
        Path key = write(dir, "tls.key", pki.keyPkcs8Pem());

        PemSslMaterial material =
                new PemSslLoader(
                                new SslProperties(
                                        true, cert.toString(), ca.toString(), key.toString(), null))
                        .load();

        // Leaf + intermediate; the self-signed root is stripped.
        assertThat(material.chain()).hasSize(2);
        assertThat(material.leaf().getSubjectX500Principal().getName()).contains("localhost");
    }

    @Test
    void should_reject_key_that_does_not_match_certificate() {
        TestPki pki = TestPki.threeLevel();
        PrivateKey otherKey = TestPki.threeLevel().leafKey;

        assertThatThrownBy(() -> PemSslLoader.validateKeyMatchesLeaf(otherKey, pki.leaf))
                .isInstanceOf(SslConfigurationException.class)
                .hasMessageContaining("does not match");
    }

    @Test
    void should_reject_expired_leaf() {
        TestPki pki = TestPki.expiredLeaf();
        List<X509Certificate> chain =
                PemSslLoader.assembleChain(List.of(pki.leaf), List.of(pki.intermediate, pki.root));

        assertThatThrownBy(() -> PemSslLoader.validateChain(chain))
                .isInstanceOf(SslConfigurationException.class)
                .hasMessageContaining("expired");
    }

    @Test
    void should_reorder_ca_certificates_provided_in_the_wrong_order() {
        TestPki pki = TestPki.threeLevel();
        List<X509Certificate> caCerts = parse(pki.caChainPemWrongOrder());

        List<X509Certificate> chain = PemSslLoader.assembleChain(List.of(pki.leaf), caCerts);

        assertThat(chain).hasSize(2);
        assertThat(chain.get(0)).isEqualTo(pki.leaf);
        assertThat(chain.get(1)).isEqualTo(pki.intermediate); // root stripped, order fixed
        assertThatCode(() -> PemSslLoader.validateChain(chain)).doesNotThrowAnyException();
    }

    @Test
    void should_load_encrypted_pkcs8_key_with_correct_passphrase() {
        TestPki pki = TestPki.threeLevel();

        PrivateKey key =
                PemSslLoader.parsePrivateKey(
                        pki.keyPkcs8EncryptedPem("s3cret"), "s3cret".toCharArray());

        assertThatCode(() -> PemSslLoader.validateKeyMatchesLeaf(key, pki.leaf))
                .doesNotThrowAnyException();
    }

    @Test
    void should_reject_encrypted_key_with_wrong_passphrase() {
        TestPki pki = TestPki.threeLevel();
        String encrypted = pki.keyPkcs8EncryptedPem("s3cret");

        assertThatThrownBy(() -> PemSslLoader.parsePrivateKey(encrypted, "wrong".toCharArray()))
                .isInstanceOf(SslConfigurationException.class);
    }

    @Test
    void should_load_pkcs1_and_pkcs8_keys() {
        TestPki pki = TestPki.threeLevel();

        PrivateKey pkcs1 = PemSslLoader.parsePrivateKey(pki.keyPkcs1Pem(), null);
        PrivateKey pkcs8 = PemSslLoader.parsePrivateKey(pki.keyPkcs8Pem(), null);

        assertThatCode(() -> PemSslLoader.validateKeyMatchesLeaf(pkcs1, pki.leaf))
                .doesNotThrowAnyException();
        assertThatCode(() -> PemSslLoader.validateKeyMatchesLeaf(pkcs8, pki.leaf))
                .doesNotThrowAnyException();
    }

    @Test
    void should_reject_missing_file(@TempDir Path dir) {
        assertThatThrownBy(
                        () ->
                                new PemSslLoader(
                                                new SslProperties(
                                                        true,
                                                        dir.resolve("nope.crt").toString(),
                                                        dir.resolve("ca.crt").toString(),
                                                        dir.resolve("tls.key").toString(),
                                                        null))
                                        .load())
                .isInstanceOf(SslConfigurationException.class)
                .hasMessageContaining("does not exist");
    }

    @Test
    void should_reject_unreadable_file(@TempDir Path dir) throws Exception {
        TestPki pki = TestPki.threeLevel();
        Path cert = write(dir, "tls.crt", pki.leafCertPem());
        Path ca = write(dir, "ca.crt", pki.caChainPem());
        Path key = write(dir, "tls.key", pki.keyPkcs8Pem());
        Files.setPosixFilePermissions(cert, PosixFilePermissions.fromString("---------"));

        assertThatThrownBy(
                        () ->
                                new PemSslLoader(
                                                new SslProperties(
                                                        true,
                                                        cert.toString(),
                                                        ca.toString(),
                                                        key.toString(),
                                                        null))
                                        .load())
                .isInstanceOf(SslConfigurationException.class)
                .hasMessageContaining("not readable");
    }

    private static Path write(Path dir, String name, String content) throws Exception {
        Path path = dir.resolve(name);
        Files.writeString(path, content);
        return path;
    }
}
