mod common;

use assert_cmd::Command;
use predicates::prelude::PredicateBooleanExt;
use predicates::str::contains;

#[test]
fn completions_bash_emits_script() {
    Command::cargo_bin("relicario").unwrap()
        .args(["completions", "bash"])
        .assert()
        .success()
        .stdout(contains("_relicario"))
        .stdout(contains("complete -F"));
}

#[test]
fn completions_zsh_emits_script() {
    Command::cargo_bin("relicario").unwrap()
        .args(["completions", "zsh"])
        .assert()
        .success()
        .stdout(contains("#compdef relicario"));
}

#[test]
fn completions_fish_emits_script() {
    Command::cargo_bin("relicario").unwrap()
        .args(["completions", "fish"])
        .assert()
        .success()
        .stdout(contains("complete -c relicario"));
}

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

    let out = v.run(&[
        "add", "login",
        "--title", "T",
        "--username", "u",
        "--group", "work",
        "--password", "hunter2",
    ]);
    assert!(out.status.success(), "add failed: {:?}", out);

    let out = v.run(&["list"]);
    assert!(out.status.success(), "list failed: {:?}", out);

    let cache_path = v.path().join(".relicario/groups.cache");
    let cache = std::fs::read_to_string(&cache_path)
        .unwrap_or_else(|e| panic!("groups.cache not found at {}: {e}", cache_path.display()));
    assert!(
        cache.lines().any(|l| l == "work"),
        "expected 'work' in groups.cache, got: {cache:?}"
    );
}

#[test]
fn no_groups_cache_env_var_suppresses_write() {
    use std::process::{Command as StdCommand, Stdio};
    use assert_cmd::cargo::CommandCargoExt as _;

    let v = common::TestVault::init();

    // Add with the env var set so no cache is created by add either.
    let out = StdCommand::cargo_bin("relicario").unwrap()
        .current_dir(v.path())
        .env("RELICARIO_IMAGE", &v.reference_image)
        .env("RELICARIO_TEST_PASSPHRASE", &v.passphrase)
        .env("RELICARIO_NO_GROUPS_CACHE", "1")
        .args([
            "add", "login",
            "--title", "T2",
            "--username", "u",
            "--group", "personal",
            "--password", "hunter2",
        ])
        .stdin(Stdio::null())
        .stdout(Stdio::piped())
        .stderr(Stdio::piped())
        .output()
        .unwrap();
    assert!(out.status.success(), "add failed: {:?}", out);

    // Run list with RELICARIO_NO_GROUPS_CACHE=1 — cache must NOT be written.
    let out = StdCommand::cargo_bin("relicario").unwrap()
        .current_dir(v.path())
        .env("RELICARIO_IMAGE", &v.reference_image)
        .env("RELICARIO_TEST_PASSPHRASE", &v.passphrase)
        .env("RELICARIO_NO_GROUPS_CACHE", "1")
        .args(["list"])
        .stdin(Stdio::null())
        .stdout(Stdio::piped())
        .stderr(Stdio::piped())
        .output()
        .unwrap();
    assert!(out.status.success(), "list failed: {:?}", out);

    let cache_path = v.path().join(".relicario/groups.cache");
    assert!(
        !cache_path.exists(),
        "groups.cache should not exist when RELICARIO_NO_GROUPS_CACHE=1"
    );
}

#[test]
fn rate_strong_passphrase_prints_score_and_guesses() {
    Command::cargo_bin("relicario").unwrap()
        .args(["rate", "correct horse battery staple table cocoa rocket spirit ferment"])
        .assert()
        .success()
        .stdout(contains("score:"))
        .stdout(contains("guesses:"))
        .stdout(contains("strong"));
}

#[test]
fn rate_weak_passphrase_exits_zero_with_weak_label() {
    // `rate` is informational — does NOT exit nonzero on weak input.
    // The hard gate lives at `init` (Plan 2B Task 10).
    Command::cargo_bin("relicario").unwrap()
        .args(["rate", "password"])
        .assert()
        .success()
        .stdout(contains("very weak").or(contains("weak")));
}

#[test]
fn rate_reads_from_stdin_when_arg_is_dash() {
    Command::cargo_bin("relicario").unwrap()
        .args(["rate", "-"])
        .write_stdin("correcthorsebatterystaple\n")
        .assert()
        .success()
        .stdout(contains("score:"));
}

fn make_test_qr(uri: &str, dest: &std::path::Path) {
    use image::{ImageBuffer, Luma};
    let code = qrcode::QrCode::new(uri).expect("QR encode failed");
    let img: ImageBuffer<Luma<u8>, Vec<u8>> = code
        .render::<Luma<u8>>()
        .module_dimensions(8, 8)
        .build();
    img.save(dest).expect("save QR PNG");
}

#[test]
fn add_login_totp_qr_decodes_otpauth_uri() {
    use tempfile::TempDir;
    let tmp = TempDir::new().unwrap();
    let qr_path = tmp.path().join("test.png");
    make_test_qr(
        "otpauth://totp/Example:alice?secret=JBSWY3DPEHPK3PXP&issuer=Example",
        &qr_path,
    );

    let v = common::TestVault::init();

    let out = v.run(&[
        "add", "login",
        "--title", "TotpTest",
        "--password", "hunter2",
        "--totp-qr", qr_path.to_str().unwrap(),
    ]);
    assert!(out.status.success(), "add failed:\nstdout: {}\nstderr: {}",
        String::from_utf8_lossy(&out.stdout),
        String::from_utf8_lossy(&out.stderr));

    let out = v.run(&["get", "TotpTest", "--show"]);
    assert!(out.status.success(), "get failed:\nstdout: {}\nstderr: {}",
        String::from_utf8_lossy(&out.stdout),
        String::from_utf8_lossy(&out.stderr));
    let stdout = String::from_utf8_lossy(&out.stdout);
    // BASE32.encode(BASE32.decode("JBSWY3DPEHPK3PXP")) should round-trip.
    // The secret bytes from JBSWY3DPEHPK3PXP decode to specific bytes,
    // then re-encode to JBSWY3DPEHPK3PXP====; we check for the core chars.
    assert!(
        stdout.contains("JBSWY3DPEHPK3PXP"),
        "expected TOTP secret in get output, got:\n{stdout}"
    );
}

#[test]
fn add_login_totp_qr_errors_on_non_otpauth_qr() {
    use tempfile::TempDir;
    let tmp = TempDir::new().unwrap();
    let qr_path = tmp.path().join("nottotp.png");
    make_test_qr("https://example.com", &qr_path);

    let v = common::TestVault::init();

    let out = v.run(&[
        "add", "login",
        "--title", "BadQR",
        "--password", "hunter2",
        "--totp-qr", qr_path.to_str().unwrap(),
    ]);
    assert!(
        !out.status.success(),
        "expected nonzero exit for non-otpauth QR, but command succeeded"
    );
    let stderr = String::from_utf8_lossy(&out.stderr);
    assert!(
        stderr.contains("not a TOTP URI"),
        "expected 'not a TOTP URI' in stderr, got:\n{stderr}"
    );
}
