mod common;

use assert_cmd::cargo::CommandCargoExt as _;
use common::TestVault;

#[test]
fn init_creates_expected_layout() {
    let v = TestVault::init();
    assert!(v.path().join(".relicario/salt").exists());
    assert!(v.path().join(".relicario/params.json").exists());
    // devices.json removed — device key system was security theater
    assert!(!v.path().join(".relicario/devices.json").exists());
    assert!(v.path().join("manifest.enc").exists());
    assert!(v.path().join("settings.enc").exists());
    assert!(v.path().join("reference.jpg").exists());
    assert!(v.path().join(".gitignore").exists());
    assert!(v.path().join(".git").is_dir());
}

#[test]
fn init_params_json_is_format_v2() {
    let v = TestVault::init();
    let s = std::fs::read_to_string(v.path().join(".relicario/params.json")).unwrap();
    let parsed: serde_json::Value = serde_json::from_str(&s).unwrap();
    assert_eq!(parsed["format_version"], 2);
    assert_eq!(parsed["kdf"]["algorithm"], "argon2id-v0x13");
    assert_eq!(parsed["aead"], "xchacha20poly1305");
}

#[test]
fn add_login_then_list_shows_it() {
    let v = TestVault::init();
    let out = v.run(&[
        "add",
        "login",
        "--title",
        "GitHub",
        "--username",
        "alice",
        "--url",
        "https://github.com",
        "--password",
        "hunter2",
    ]);
    assert!(out.status.success(), "add failed: {:?}", out);
    let out = v.run(&["list"]);
    let stdout = String::from_utf8(out.stdout).unwrap();
    assert!(stdout.contains("GitHub"), "list missing GitHub: {stdout}");
}

#[test]
fn get_masks_by_default_shows_with_flag() {
    let v = TestVault::init();
    v.run(&[
        "add",
        "login",
        "--title",
        "gmail",
        "--username",
        "u",
        "--password",
        "super-secret",
    ]);

    let masked = v.run(&["get", "gmail"]);
    let stdout = String::from_utf8(masked.stdout).unwrap();
    assert!(stdout.contains("********"), "expected masked: {stdout}");
    assert!(
        !stdout.contains("super-secret"),
        "leaked plaintext: {stdout}"
    );

    let shown = v.run(&["get", "gmail", "--show"]);
    let stdout = String::from_utf8(shown.stdout).unwrap();
    assert!(stdout.contains("super-secret"), "expected plaintext: {stdout}");
}

#[test]
fn rm_restore_purge_cycle() {
    let v = TestVault::init();
    v.run(&[
        "add",
        "login",
        "--title",
        "target",
        "--username",
        "u",
        "--password",
        "p",
    ]);

    let rm = v.run(&["rm", "target"]);
    assert!(rm.status.success());

    let out = v.run(&["list"]);
    assert!(!String::from_utf8(out.stdout).unwrap().contains("target"));

    let out = v.run(&["list", "--trashed"]);
    assert!(String::from_utf8(out.stdout).unwrap().contains("target"));

    let restore = v.run(&["restore", "target"]);
    assert!(restore.status.success());
    let out = v.run(&["list"]);
    assert!(String::from_utf8(out.stdout).unwrap().contains("target"));

    let purge = v.run(&["purge", "target"]);
    assert!(purge.status.success());
    let out = v.run(&["list"]);
    assert!(!String::from_utf8(out.stdout).unwrap().contains("target"));
}

#[test]
fn trash_empty_batches_into_one_commit() {
    let v = TestVault::init();

    // Add 3 items.
    for title in ["alpha", "bravo", "charlie"] {
        let out = v.run(&[
            "add", "login",
            "--title", title,
            "--username", "u",
            "--password", "p",
        ]);
        assert!(out.status.success(), "add {title} failed");
    }

    // Soft-delete all 3.
    for title in ["alpha", "bravo", "charlie"] {
        let out = v.run(&["rm", title]);
        assert!(out.status.success(), "rm {title} failed");
    }

    // Set retention to 0 days so the recently-trashed items become purgeable
    // (should_purge: now - trashed_at > 0 * 86400 = 0).
    let out = v.run(&["settings", "trash-retention", "--days", "0"]);
    assert!(out.status.success(), "settings trash-retention failed");

    // should_purge uses strict > on (now - trashed_at), so equal-second
    // timestamps don't qualify.
    std::thread::sleep(std::time::Duration::from_secs(1));

    // Count commits before.
    let before = std::process::Command::new("git")
        .args(["rev-list", "--count", "HEAD"])
        .current_dir(v.path())
        .output()
        .unwrap();
    let before_count: u32 = String::from_utf8(before.stdout).unwrap().trim().parse().unwrap();

    // Run trash empty.
    let out = v.run(&["trash", "empty"]);
    assert!(out.status.success(), "trash empty failed: stderr={}",
            String::from_utf8_lossy(&out.stderr));

    // Count commits after.
    let after = std::process::Command::new("git")
        .args(["rev-list", "--count", "HEAD"])
        .current_dir(v.path())
        .output()
        .unwrap();
    let after_count: u32 = String::from_utf8(after.stdout).unwrap().trim().parse().unwrap();

    assert_eq!(
        after_count - before_count, 1,
        "trash empty should fire exactly one commit; before={before_count} after={after_count}"
    );

    // The remaining `list --trashed` should be empty.
    let out = v.run(&["list", "--trashed"]);
    let stdout = String::from_utf8(out.stdout).unwrap();
    let stderr = String::from_utf8(out.stderr).unwrap();
    assert!(
        !stdout.contains("alpha") && !stdout.contains("bravo") && !stdout.contains("charlie"),
        "items still in trashed list: stdout={stdout} stderr={stderr}"
    );
}

#[test]
fn generate_random_and_bip39() {
    let dir = tempfile::TempDir::new().unwrap();

    let out = std::process::Command::cargo_bin("relicario")
        .unwrap()
        .current_dir(dir.path())
        .args(["generate", "--length", "32"])
        .output()
        .unwrap();
    assert!(out.status.success());
    assert_eq!(
        String::from_utf8(out.stdout).unwrap().trim().len(),
        32
    );

    let out = std::process::Command::cargo_bin("relicario")
        .unwrap()
        .current_dir(dir.path())
        .args(["generate", "--bip39", "--words", "5"])
        .output()
        .unwrap();
    assert!(out.status.success());
    let phrase = String::from_utf8(out.stdout).unwrap();
    assert_eq!(phrase.trim().split(' ').count(), 5);
}

#[test]
fn add_card_via_stdin_flags_is_non_interactive() {
    let v = TestVault::init();
    let out = v.run_with_input(
        &["add", "card", "--title", "Visa", "--kind", "credit",
          "--number-stdin", "--cvv-stdin", "--pin-stdin"],
        &["4111111111111111", "123", "4321"],
    );
    assert!(out.status.success(), "add card via stdin failed: {}", String::from_utf8_lossy(&out.stderr));

    let got = v.run(&["get", "Visa"]);
    assert!(got.status.success(), "get Visa failed: {}", String::from_utf8_lossy(&got.stderr));
    let stdout = String::from_utf8_lossy(&got.stdout);
    assert!(stdout.contains("********"), "card number should be masked without --show: {stdout}");
    assert!(!stdout.contains("4111111111111111"), "card number leaked without --show: {stdout}");
}
