package net.siegeln.cameleer.saas.certificate;

import net.siegeln.cameleer.saas.provisioning.DockerCertificateManager;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;

import java.nio.file.Files;
import java.nio.file.Path;

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

class DockerCertificateManagerTest {

    @TempDir
    Path certsDir;

    private DockerCertificateManager manager;

    @BeforeEach
    void setUp() {
        manager = new DockerCertificateManager(certsDir);
    }

    @Test
    void isAvailable_returnsTrueForWritableDirectory() {
        assertThat(manager.isAvailable()).isTrue();
    }

    @Test
    void getActive_returnsNullWhenNoCert() {
        assertThat(manager.getActive()).isNull();
    }

    @Test
    void stage_validatesAndWritesFiles() throws Exception {
        byte[][] pair = generateCertAndKey("CN=test.example.com");

        var result = manager.stage(pair[0], pair[1], null);

        assertThat(result.valid()).isTrue();
        assertThat(result.info()).isNotNull();
        assertThat(result.info().subject()).contains("test.example.com");
        assertThat(result.info().selfSigned()).isTrue();
        assertThat(Files.exists(certsDir.resolve("staged/cert.pem"))).isTrue();
        assertThat(Files.exists(certsDir.resolve("staged/key.pem"))).isTrue();
    }

    @Test
    void stage_rejectsInvalidCertPem() {
        var result = manager.stage("not a cert".getBytes(), "not a key".getBytes(), null);
        assertThat(result.valid()).isFalse();
        assertThat(result.errors()).isNotEmpty();
    }

    @Test
    void stage_rejectsMismatchedKeyAndCert() throws Exception {
        byte[][] pair1 = generateCertAndKey("CN=test.example.com");
        byte[][] pair2 = generateCertAndKey("CN=other.example.com");

        var result = manager.stage(pair1[0], pair2[1], null);
        assertThat(result.valid()).isFalse();
        assertThat(result.errors()).anyMatch(e -> e.contains("does not match"));
    }

    @Test
    void activateAndRestore_lifecycle() throws Exception {
        // Set up an initial active cert
        byte[][] pair1 = generateCertAndKey("CN=first.example.com");
        Files.write(certsDir.resolve("cert.pem"), pair1[0]);
        Files.write(certsDir.resolve("key.pem"), pair1[1]);

        // Stage a second cert
        byte[][] pair2 = generateCertAndKey("CN=second.example.com");
        var stageResult = manager.stage(pair2[0], pair2[1], null);
        assertThat(stageResult.valid()).isTrue();

        // Activate: first -> archived, second -> active
        manager.activate();

        var active = manager.getActive();
        assertThat(active).isNotNull();
        assertThat(active.subject()).contains("second.example.com");

        var archived = manager.getArchived();
        assertThat(archived).isNotNull();
        assertThat(archived.subject()).contains("first.example.com");

        assertThat(manager.getStaged()).isNull();

        // Restore: swap active <-> archived
        manager.restore();

        active = manager.getActive();
        assertThat(active.subject()).contains("first.example.com");

        archived = manager.getArchived();
        assertThat(archived.subject()).contains("second.example.com");
    }

    @Test
    void activate_failsWithNoStagedCert() {
        assertThatThrownBy(() -> manager.activate())
            .isInstanceOf(IllegalStateException.class)
            .hasMessageContaining("No staged certificate");
    }

    @Test
    void restore_failsWithNoArchivedCert() {
        assertThatThrownBy(() -> manager.restore())
            .isInstanceOf(IllegalStateException.class)
            .hasMessageContaining("No archived certificate");
    }

    @Test
    void discardStaged_removesFiles() throws Exception {
        byte[][] pair = generateCertAndKey("CN=test.example.com");
        manager.stage(pair[0], pair[1], null);

        assertThat(Files.exists(certsDir.resolve("staged/cert.pem"))).isTrue();

        manager.discardStaged();

        assertThat(Files.exists(certsDir.resolve("staged"))).isFalse();
    }

    @Test
    void stage_withCaBundle() throws Exception {
        byte[][] pair = generateCertAndKey("CN=test.example.com");

        // Use the cert itself as a "CA" for testing purposes
        var result = manager.stage(pair[0], pair[1], pair[0]);

        assertThat(result.valid()).isTrue();
        assertThat(result.info().hasCaBundle()).isTrue();
        assertThat(Files.exists(certsDir.resolve("staged/ca.pem"))).isTrue();
    }

    @Test
    void getCaBundle_returnsNullWhenMissing() {
        assertThat(manager.getCaBundle()).isNull();
    }

    @Test
    void getCaBundle_returnsContentWhenPresent() throws Exception {
        byte[] caContent = "test-ca-content".getBytes();
        Files.write(certsDir.resolve("ca.pem"), caContent);

        assertThat(manager.getCaBundle()).isEqualTo(caContent);
    }

    @Test
    void activate_deletesExistingArchive() throws Exception {
        // Create initial active
        byte[][] pair1 = generateCertAndKey("CN=first.example.com");
        Files.write(certsDir.resolve("cert.pem"), pair1[0]);
        Files.write(certsDir.resolve("key.pem"), pair1[1]);

        // Create existing archive
        Files.createDirectories(certsDir.resolve("prev"));
        byte[][] pairOld = generateCertAndKey("CN=old.example.com");
        Files.write(certsDir.resolve("prev/cert.pem"), pairOld[0]);
        Files.write(certsDir.resolve("prev/key.pem"), pairOld[1]);

        // Stage new cert
        byte[][] pair2 = generateCertAndKey("CN=second.example.com");
        manager.stage(pair2[0], pair2[1], null);

        // Activate: old archive should be deleted, first becomes archive
        manager.activate();

        var archived = manager.getArchived();
        assertThat(archived).isNotNull();
        assertThat(archived.subject()).contains("first.example.com");
    }

    // --- Test helpers: generate self-signed certs via keytool + openssl ---

    /**
     * Generates a matched cert.pem + key.pem pair using keytool and openssl.
     * Returns [certPem, keyPem].
     */
    private static byte[][] generateCertAndKey(String cn) throws Exception {
        Path tmpDir = Files.createTempDirectory("cert-test");
        Path ks = tmpDir.resolve("keystore.p12");
        Path certFile = tmpDir.resolve("cert.pem");
        Path keyFile = tmpDir.resolve("key.pem");

        try {
            // Generate keystore with self-signed cert
            exec("keytool", "-genkeypair", "-alias", "test", "-keyalg", "RSA", "-keysize", "2048",
                "-validity", "365", "-dname", cn, "-storetype", "PKCS12",
                "-keystore", ks.toString(), "-storepass", "changeit");

            // Export cert to PEM
            exec("keytool", "-exportcert", "-alias", "test", "-rfc",
                "-keystore", ks.toString(), "-storepass", "changeit",
                "-file", certFile.toString());

            // Export key via openssl
            exec("openssl", "pkcs12", "-in", ks.toString(), "-passin", "pass:changeit",
                "-nocerts", "-nodes", "-out", keyFile.toString());

            return new byte[][] { Files.readAllBytes(certFile), Files.readAllBytes(keyFile) };
        } finally {
            Files.deleteIfExists(ks);
            Files.deleteIfExists(certFile);
            Files.deleteIfExists(keyFile);
            Files.deleteIfExists(tmpDir);
        }
    }

    private static void exec(String... cmd) throws Exception {
        var pb = new ProcessBuilder(cmd);
        pb.redirectErrorStream(true);
        var proc = pb.start();
        proc.getInputStream().readAllBytes(); // consume output
        int exit = proc.waitFor();
        if (exit != 0) {
            throw new RuntimeException("Command failed (exit " + exit + "): " + String.join(" ", cmd));
        }
    }
}
