use anyhow::{anyhow, Context};
use assert_cmd::cargo::cargo_bin;
use expectrl::{self, Expect, Session};
use serde_json::Value;
use serial_test::serial;
use std::env;
use std::io::Read;
use std::path::PathBuf;
use std::process::Command;
use tempfile::tempdir;
use totp_rs::{Algorithm as TotpAlgorithmLib, Secret as TotpSecret, TOTP};
use uuid::Uuid;

type AnyhowResult<T> = Result<T, anyhow::Error>;

struct EnvGuard {
    key: &'static str,
}

impl EnvGuard {
    fn set(key: &'static str, value: &PathBuf) -> Self {
        env::set_var(key, value);
        Self { key }
    }
}

impl Drop for EnvGuard {
    fn drop(&mut self) {
        env::remove_var(self.key);
    }
}

#[test]
#[serial]
fn cli_smoke_add_api_record() -> AnyhowResult<()> {
    let tmp = tempdir()?;
    let vault_path = tmp.path().join("vault-smoke.cbor");
    let _guard = EnvGuard::set("BLACK_BAG_VAULT_PATH", &vault_path);
    let passphrase = "tty-passphrase";

    run_init(passphrase)?;
    run_add_api(passphrase)?;
    let output = run_list_api(passphrase)?;

    assert!(
        output.contains("intel-api"),
        "expected API service in list output: {output}"
    );
    assert!(
        output.contains("env=production"),
        "expected environment in list output: {output}"
    );

    Ok(())
}

#[test]
#[serial]
fn cli_smoke_doctor_and_get_reveal() -> AnyhowResult<()> {
    let tmp = tempdir()?;
    let vault_path = tmp.path().join("vault-doctor.cbor");
    let _guard = EnvGuard::set("BLACK_BAG_VAULT_PATH", &vault_path);
    let passphrase = "tty-passphrase";

    run_init(passphrase)?;
    run_add_api(passphrase)?;
    let list_output = run_list_api(passphrase)?;
    let record_id = extract_first_uuid(&list_output)?;

    let doc = run_doctor_json(passphrase)?;
    let ready = doc
        .get("ready")
        .and_then(Value::as_bool)
        .ok_or_else(|| anyhow!("doctor output missing ready flag: {doc}"))?;
    anyhow::ensure!(ready, "expected doctor ready state");

    let reveal_output = run_get_api_reveal(passphrase, &record_id)?;
    anyhow::ensure!(
        reveal_output.contains("secret_key: super-secret-token"),
        "expected secret key in reveal output: {reveal_output}"
    );

    Ok(())
}

#[test]
#[serial]
fn cli_smoke_totp_code() -> AnyhowResult<()> {
    let tmp = tempdir()?;
    let vault_path = tmp.path().join("vault-totp.cbor");
    let _guard = EnvGuard::set("BLACK_BAG_VAULT_PATH", &vault_path);
    let passphrase = "tty-passphrase";

    run_init(passphrase)?;
    let secret = "JBSWY3DPEHPK3PXPJBSWY3DPEHPK3PXP";
    let issuer = "CLI";
    let account = "totp@test";
    run_add_totp(passphrase, secret, issuer, account)?;
    let list_output = run_list_totp(passphrase)?;
    let record_id = extract_first_uuid(&list_output)?;

    let secret_bytes = TotpSecret::Encoded(secret.to_string()).to_bytes().unwrap();
    let expected = TOTP::new(
        TotpAlgorithmLib::SHA1,
        6,
        1,
        30,
        secret_bytes,
        Some(issuer.to_string()),
        account.to_string(),
    )
    .unwrap()
    .generate(59);

    let output = run_totp_code(passphrase, &record_id, Some(59))?;
    anyhow::ensure!(
        output.contains(&format!("code: {}", expected)),
        "unexpected totp output: {output}"
    );
    Ok(())
}

#[test]
#[serial]
fn cli_smoke_rotate_and_list_query() -> AnyhowResult<()> {
    let tmp = tempdir()?;
    let vault_path = tmp.path().join("vault-rotate.cbor");
    let _guard = EnvGuard::set("BLACK_BAG_VAULT_PATH", &vault_path);
    let passphrase = "tty-passphrase";

    run_init(passphrase)?;
    run_add_api(passphrase)?;
    run_rotate(passphrase)?;
    let output = run_list_query(passphrase, "intel")?;

    anyhow::ensure!(
        output.contains("intel-api"),
        "expected query output to include service: {output}"
    );
    Ok(())
}
fn run_init(pass: &str) -> AnyhowResult<()> {
    let bin = cargo_bin("black-bag");
    let mut cmd = Command::new(bin);
    cmd.arg("init").arg("--mem-kib").arg("32768");
    propagate_env(&mut cmd);
    let mut session = Session::spawn(cmd)?;
    session.expect("Master passphrase: ")?;
    session.send_line(pass)?;
    session.expect("Confirm passphrase: ")?;
    session.send_line(pass)?;
    session.expect("Initialized vault")?;
    session.expect(expectrl::Eof)?;
    Ok(())
}

fn run_add_api(pass: &str) -> AnyhowResult<()> {
    let bin = cargo_bin("black-bag");
    let mut cmd = Command::new(bin);
    cmd.args([
        "add",
        "api",
        "--service",
        "intel-api",
        "--environment",
        "production",
        "--access-key",
        "AKIA-XYZ",
        "--scopes",
        "read,write",
    ]);
    propagate_env(&mut cmd);
    let mut session = Session::spawn(cmd)?;
    session.expect("Master passphrase: ")?;
    session.send_line(pass)?;
    session.expect("Secret key: ")?;
    session.send_line("super-secret-token")?;
    session.expect("Record added")?;
    session.expect(expectrl::Eof)?;
    Ok(())
}

fn run_list_api(pass: &str) -> AnyhowResult<String> {
    let bin = cargo_bin("black-bag");
    let mut cmd = Command::new(bin);
    cmd.args(["list", "--kind", "api"]);
    propagate_env(&mut cmd);
    let mut session = Session::spawn(cmd)?;
    session.expect("Master passphrase: ")?;
    session.send_line(pass)?;
    let mut output = String::new();
    match session.read_to_string(&mut output) {
        Ok(_) => {}
        Err(err) if err.kind() == std::io::ErrorKind::UnexpectedEof => {}
        Err(err) => return Err(err.into()),
    }
    Ok(output)
}

fn run_rotate(pass: &str) -> AnyhowResult<()> {
    let bin = cargo_bin("black-bag");
    let mut cmd = Command::new(bin);
    cmd.args(["rotate", "--mem-kib", "32768"]);
    propagate_env(&mut cmd);
    let mut session = Session::spawn(cmd)?;
    session.expect("Master passphrase: ")?;
    session.send_line(pass)?;
    session.expect("Rotation complete")?;
    session.expect(expectrl::Eof)?;
    Ok(())
}

fn run_list_query(pass: &str, query: &str) -> AnyhowResult<String> {
    let bin = cargo_bin("black-bag");
    let mut cmd = Command::new(bin);
    cmd.args(["list", "--query", query]);
    propagate_env(&mut cmd);
    let mut session = Session::spawn(cmd)?;
    session.expect("Master passphrase: ")?;
    session.send_line(pass)?;
    let mut output = String::new();
    match session.read_to_string(&mut output) {
        Ok(_) => {}
        Err(err) if err.kind() == std::io::ErrorKind::UnexpectedEof => {}
        Err(err) => return Err(err.into()),
    }
    Ok(output)
}
fn run_add_totp(pass: &str, secret: &str, issuer: &str, account: &str) -> AnyhowResult<()> {
    let bin = cargo_bin("black-bag");
    let mut cmd = Command::new(bin);
    cmd.args([
        "add",
        "totp",
        "--secret",
        secret,
        "--issuer",
        issuer,
        "--account",
        account,
        "--digits",
        "6",
        "--step",
        "30",
        "--skew",
        "1",
        "--algorithm",
        "sha1",
    ]);
    propagate_env(&mut cmd);
    let mut session = Session::spawn(cmd)?;
    session.expect("Master passphrase: ")?;
    session.send_line(pass)?;
    session.expect("Record added")?;
    session.expect(expectrl::Eof)?;
    Ok(())
}

fn run_list_totp(pass: &str) -> AnyhowResult<String> {
    let bin = cargo_bin("black-bag");
    let mut cmd = Command::new(bin);
    cmd.args(["list", "--kind", "totp"]);
    propagate_env(&mut cmd);
    let mut session = Session::spawn(cmd)?;
    session.expect("Master passphrase: ")?;
    session.send_line(pass)?;
    let mut output = String::new();
    match session.read_to_string(&mut output) {
        Ok(_) => {}
        Err(err) if err.kind() == std::io::ErrorKind::UnexpectedEof => {}
        Err(err) => return Err(err.into()),
    }
    Ok(output)
}

fn run_totp_code(pass: &str, id: &str, time: Option<i64>) -> AnyhowResult<String> {
    let bin = cargo_bin("black-bag");
    let mut cmd = Command::new(bin);
    cmd.args(["totp", "code", id]);
    if let Some(ts) = time {
        cmd.args(["--time", &ts.to_string()]);
    }
    propagate_env(&mut cmd);
    let mut session = Session::spawn(cmd)?;
    session.expect("Master passphrase: ")?;
    session.send_line(pass)?;
    let mut output = String::new();
    match session.read_to_string(&mut output) {
        Ok(_) => {}
        Err(err) if err.kind() == std::io::ErrorKind::UnexpectedEof => {}
        Err(err) => return Err(err.into()),
    }
    Ok(output)
}

fn propagate_env(cmd: &mut Command) {
    if let Ok(path) = env::var("BLACK_BAG_VAULT_PATH") {
        cmd.env("BLACK_BAG_VAULT_PATH", path);
    }
}

fn run_doctor_json(pass: &str) -> AnyhowResult<Value> {
    let bin = cargo_bin("black-bag");
    let mut cmd = Command::new(bin);
    cmd.args(["doctor", "--json"]);
    propagate_env(&mut cmd);
    let mut session = Session::spawn(cmd)?;
    session.expect("Master passphrase: ")?;
    session.send_line(pass)?;
    let mut output = String::new();
    session.read_to_string(&mut output)?;
    let value = serde_json::from_str(&output)
        .with_context(|| format!("failed to parse doctor json: {output}"))?;
    Ok(value)
}

fn run_get_api_reveal(pass: &str, id: &str) -> AnyhowResult<String> {
    let bin = cargo_bin("black-bag");
    let mut cmd = Command::new(bin);
    cmd.args(["get", id, "--reveal"]);
    propagate_env(&mut cmd);
    let mut session = Session::spawn(cmd)?;
    session.expect("Master passphrase: ")?;
    session.send_line(pass)?;
    let mut output = String::new();
    match session.read_to_string(&mut output) {
        Ok(_) => {}
        Err(err) if err.kind() == std::io::ErrorKind::UnexpectedEof => {}
        Err(err) => return Err(err.into()),
    }
    Ok(output)
}

fn extract_first_uuid(output: &str) -> AnyhowResult<String> {
    let line = output
        .lines()
        .find(|l| !l.trim().is_empty())
        .ok_or_else(|| anyhow!("no output lines to parse uuid"))?;
    let candidate = line
        .split('|')
        .next()
        .map(str::trim)
        .filter(|s| !s.is_empty())
        .ok_or_else(|| anyhow!("unable to extract uuid from line: {line}"))?;
    let parsed = Uuid::parse_str(candidate)?;
    Ok(parsed.to_string())
}
