//! Acceptance tests for `relicario-server verify-commit`.
//!
//! Four scenarios from audit S1:
//! 1. Registered non-revoked key → exit 0
//! 2. Unregistered key → exit 1 (stderr contains "unregistered")
//! 3. Revoked key, commit AFTER revoked_at → exit 1 (stderr contains "revoked")
//! 4. Revoked key, commit BEFORE revoked_at (historical) → exit 0

use std::fs;
use std::path::{Path, PathBuf};
use std::process::Command;

use assert_cmd::Command as AssertCommand;
use predicates::prelude::*;
use relicario_core::device::{generate_keypair, DeviceEntry, RevokedEntry};
use tempfile::TempDir;

fn write_keypair(dir: &Path, name: &str) -> (PathBuf, PathBuf, String) {
    let (priv_pem, pub_line) = generate_keypair().expect("generate keypair");
    let priv_path = dir.join(format!("{name}.key"));
    let pub_path = dir.join(format!("{name}.pub"));
    fs::write(&priv_path, priv_pem.as_str()).unwrap();
    fs::write(&pub_path, &pub_line).unwrap();
    #[cfg(unix)]
    {
        use std::os::unix::fs::PermissionsExt;
        fs::set_permissions(&priv_path, fs::Permissions::from_mode(0o600)).unwrap();
    }
    (priv_path, pub_path, pub_line)
}

fn git(repo: &Path, args: &[&str], extra_env: &[(&str, &str)]) {
    let mut cmd = Command::new("git");
    cmd.current_dir(repo).args(args);
    for (k, v) in extra_env {
        cmd.env(k, v);
    }
    let status = cmd.status().expect("spawn git");
    assert!(status.success(), "git {args:?} failed");
}

fn init_repo(repo: &Path) {
    git(repo, &["init", "-q", "-b", "main"], &[]);
    git(repo, &["config", "user.email", "test@test"], &[]);
    git(repo, &["config", "user.name", "test"], &[]);
    git(repo, &["commit", "--allow-empty", "-q", "-m", "init"], &[]);
}

fn sign_commit(
    repo: &Path,
    signing_key: &Path,
    allowed_signers: &Path,
    committer_unix: i64,
    msg: &str,
    file_path: &str,
    file_content: &str,
) -> String {
    fs::write(repo.join(file_path), file_content).unwrap();
    git(repo, &["add", file_path], &[]);
    let date = format!("@{committer_unix} +0000");
    git(
        repo,
        &[
            "-c", "gpg.format=ssh",
            "-c", &format!("user.signingkey={}", signing_key.display()),
            "-c", &format!("gpg.ssh.allowedSignersFile={}", allowed_signers.display()),
            "commit", "-S", "-q", "-m", msg,
        ],
        &[
            ("GIT_AUTHOR_DATE", &date),
            ("GIT_COMMITTER_DATE", &date),
        ],
    );
    let out = Command::new("git")
        .current_dir(repo)
        .args(["rev-parse", "HEAD"])
        .output()
        .unwrap();
    String::from_utf8(out.stdout).unwrap().trim().to_string()
}

fn write_device_files(repo: &Path, devices: &[DeviceEntry], revoked: &[RevokedEntry]) {
    let dir = repo.join(".relicario");
    fs::create_dir_all(&dir).unwrap();
    fs::write(dir.join("devices.json"), serde_json::to_string_pretty(devices).unwrap()).unwrap();
    fs::write(dir.join("revoked.json"), serde_json::to_string_pretty(revoked).unwrap()).unwrap();
    git(repo, &["add", ".relicario"], &[]);
    git(repo, &["commit", "-q", "-m", "device files"], &[]);
}

#[test]
fn registered_non_revoked_key_accepted() {
    let tmp = TempDir::new().unwrap();
    let repo = tmp.path();
    init_repo(repo);

    let (priv_a, _, pub_a) = write_keypair(repo, "alice");
    write_device_files(
        repo,
        &[DeviceEntry {
            name: "alice".into(),
            public_key: pub_a.clone(),
            added_at: 1_700_000_000,
            added_by: "bootstrap".into(),
        }],
        &[],
    );

    let allowed = repo.join("test_allowed_signers");
    fs::write(&allowed, format!("relicario {}\n", pub_a.trim())).unwrap();

    let sha = sign_commit(repo, &priv_a, &allowed, 1_710_000_000, "x", "a.txt", "hi");

    AssertCommand::cargo_bin("relicario-server")
        .unwrap()
        .current_dir(repo)
        .args(["verify-commit", &sha])
        .assert()
        .success();
}

#[test]
fn unregistered_key_rejected() {
    let tmp = TempDir::new().unwrap();
    let repo = tmp.path();
    init_repo(repo);

    let (_, _, pub_a) = write_keypair(repo, "alice");
    let (priv_evil, _, pub_evil) = write_keypair(repo, "evil");

    // Only Alice is registered.
    write_device_files(
        repo,
        &[DeviceEntry {
            name: "alice".into(),
            public_key: pub_a.clone(),
            added_at: 1_700_000_000,
            added_by: "bootstrap".into(),
        }],
        &[],
    );

    // Evil signs against a file containing both keys so git commit signing works,
    // but the binary's allowed-signers (from devices.json) only has Alice.
    let allowed = repo.join("test_allowed_signers");
    fs::write(
        &allowed,
        format!("relicario {}\nrelicario {}\n", pub_a.trim(), pub_evil.trim()),
    )
    .unwrap();

    let sha = sign_commit(repo, &priv_evil, &allowed, 1_710_000_000, "evil", "a.txt", "hi");

    AssertCommand::cargo_bin("relicario-server")
        .unwrap()
        .current_dir(repo)
        .args(["verify-commit", &sha])
        .assert()
        .failure()
        .stderr(predicate::str::contains("unregistered"));
}

#[test]
fn revoked_key_after_revoked_at_rejected() {
    let tmp = TempDir::new().unwrap();
    let repo = tmp.path();
    init_repo(repo);

    let (priv_a, _, pub_a) = write_keypair(repo, "alice");

    // Alice's entry is only in revoked.json (was removed from devices.json after revocation).
    write_device_files(
        repo,
        &[],
        &[RevokedEntry {
            name: "alice".into(),
            public_key: pub_a.clone(),
            revoked_at: 1_705_000_000,
            revoked_by: "admin".into(),
        }],
    );

    let allowed = repo.join("test_allowed_signers");
    fs::write(&allowed, format!("relicario {}\n", pub_a.trim())).unwrap();

    // Commit dated AFTER revocation.
    let sha = sign_commit(repo, &priv_a, &allowed, 1_710_000_000, "post", "a.txt", "hi");

    AssertCommand::cargo_bin("relicario-server")
        .unwrap()
        .current_dir(repo)
        .args(["verify-commit", &sha])
        .assert()
        .failure()
        .stderr(predicate::str::contains("revoked"));
}

#[test]
fn revoked_key_before_revoked_at_accepted_historical() {
    let tmp = TempDir::new().unwrap();
    let repo = tmp.path();
    init_repo(repo);

    let (priv_a, _, pub_a) = write_keypair(repo, "alice");

    // Same as above: Alice only in revoked.json.
    write_device_files(
        repo,
        &[],
        &[RevokedEntry {
            name: "alice".into(),
            public_key: pub_a.clone(),
            revoked_at: 1_705_000_000,
            revoked_by: "admin".into(),
        }],
    );

    let allowed = repo.join("test_allowed_signers");
    fs::write(&allowed, format!("relicario {}\n", pub_a.trim())).unwrap();

    // Commit dated BEFORE revocation -- historical case must pass.
    let sha = sign_commit(repo, &priv_a, &allowed, 1_700_000_000, "historical", "a.txt", "hi");

    AssertCommand::cargo_bin("relicario-server")
        .unwrap()
        .current_dir(repo)
        .args(["verify-commit", &sha])
        .assert()
        .success();
}
