//! Tests for the ConfirmDelete + 5s undo grace feature: `pending_undo` state
//! lifecycle, the `'u'` undo dispatch, the `App::maybe_expire_undo` tick hook,
//! and the auto-finalize-on-competing-key-mutation rule.
//!
//! Time-based assertions rewind `PendingUndo::deleted_at` rather than sleeping
//! for the full UNDO_TTL — same trick as the clipboard / reveal tests.

use std::collections::HashMap;
use std::time::{Duration, Instant};

use tsafe_tui::app::{sensitive_string_for_test, App, PendingUndo, Screen, UNDO_TTL};
use tsafe_tui::state::{dispatch_input, AppInput};
use zeroize::Zeroizing;

type PwStore = Option<Zeroizing<String>>;

fn no_pw() -> PwStore {
    None
}

fn dashboard_app() -> App {
    let mut app = App::new_for_test(vec!["demo".into()], Screen::Dashboard);
    app.active_profile = Some("demo".into());
    app.secret_keys = vec!["API_KEY".into(), "DB_URL".into()];
    app
}

fn make_undo(key: &str, value: &str) -> PendingUndo {
    PendingUndo {
        key: key.into(),
        value: sensitive_string_for_test(value),
        tags: HashMap::new(),
        deleted_at: Instant::now(),
    }
}

// ── 'u' dispatch: no-op when nothing pending ────────────────────────────────

#[test]
fn dispatch_u_is_noop_when_no_pending_undo() {
    let mut app = dashboard_app();
    let mut store = no_pw();
    assert!(app.pending_undo.is_none());

    dispatch_input(&mut app, AppInput::Char('u'), &mut store);

    assert!(
        app.pending_undo.is_none(),
        "u with no pending undo must remain none"
    );
    assert!(
        app.status_message.is_none(),
        "no spurious status message on no-op u"
    );
}

#[test]
fn dispatch_u_with_no_vault_session_surfaces_error() {
    // Pending undo exists but no session vault — surface the missing-vault
    // error rather than silently dropping the undo on the floor.
    let mut app = dashboard_app();
    app.pending_undo = Some(make_undo("API_KEY", "secret"));
    let mut store = no_pw();

    dispatch_input(&mut app, AppInput::Char('u'), &mut store);

    let status = app.status_message.as_deref().unwrap_or("");
    assert!(
        status.contains("vault session missing"),
        "expected vault-session-missing error, got: {status:?}"
    );
}

// ── maybe_expire_undo: expiry behaviour ─────────────────────────────────────

#[test]
fn maybe_expire_undo_is_noop_when_no_pending() {
    let mut app = dashboard_app();
    app.maybe_expire_undo();
    assert!(app.pending_undo.is_none());
    assert!(app.status_message.is_none());
}

#[test]
fn maybe_expire_undo_keeps_pending_within_ttl_and_sets_countdown() {
    let mut app = dashboard_app();
    app.pending_undo = Some(make_undo("API_KEY", "supersecret"));

    app.maybe_expire_undo();

    assert!(
        app.pending_undo.is_some(),
        "fresh pending undo must not be evicted"
    );
    let status = app.status_message.as_deref().unwrap_or("");
    assert!(
        status.contains("press u to undo"),
        "tick must re-set the countdown status, got: {status:?}"
    );
    assert!(
        status.contains("API_KEY"),
        "countdown must include the key name, got: {status:?}"
    );
}

#[test]
fn maybe_expire_undo_clears_pending_past_ttl() {
    let mut app = dashboard_app();
    let mut undo = make_undo("API_KEY", "supersecret");
    undo.deleted_at = Instant::now() - UNDO_TTL - Duration::from_millis(1);
    app.pending_undo = Some(undo);

    app.maybe_expire_undo();

    assert!(
        app.pending_undo.is_none(),
        "expired pending undo must be cleared"
    );
    let status = app.status_message.as_deref().unwrap_or("");
    assert!(
        status.contains("permanent"),
        "finalisation status should announce permanence, got: {status:?}"
    );
}

// ── Auto-finalise on competing-key mutation ─────────────────────────────────

#[test]
fn finalize_pending_undo_for_key_clears_when_key_matches() {
    let mut app = dashboard_app();
    app.pending_undo = Some(make_undo("API_KEY", "v"));

    app.finalize_pending_undo_for_key("API_KEY");

    assert!(
        app.pending_undo.is_none(),
        "matching-key call must clear pending undo"
    );
}

#[test]
fn finalize_pending_undo_for_key_is_noop_for_other_keys() {
    let mut app = dashboard_app();
    app.pending_undo = Some(make_undo("API_KEY", "v"));

    app.finalize_pending_undo_for_key("DB_URL");

    assert!(
        app.pending_undo.is_some(),
        "non-matching-key call must leave pending undo intact"
    );
}

// ── Profile switch invariant ────────────────────────────────────────────────

#[test]
fn clear_active_session_drops_pending_undo() {
    let mut app = dashboard_app();
    app.pending_undo = Some(make_undo("API_KEY", "secret"));

    app.clear_active_session();

    assert!(
        app.pending_undo.is_none(),
        "session change must drop pending undo (would target the wrong vault)"
    );
}

// ── Anti-criteria: Debug never leaks pending plaintext ──────────────────────

#[test]
fn debug_render_of_app_redacts_pending_undo() {
    let mut app = dashboard_app();
    app.pending_undo = Some(make_undo("API_KEY", "supersecret-undo-value"));

    let rendered = format!("{app:?}");

    assert!(
        !rendered.contains("supersecret-undo-value"),
        "Debug impl must redact pending_undo value, got: {rendered}"
    );
    assert!(
        rendered.contains("redacted"),
        "Debug impl must show a redaction marker for pending_undo, got: {rendered}"
    );
}

// ── PendingUndo struct: structural type-check on SensitiveString value ──────

#[test]
fn pending_undo_value_uses_sensitive_string_for_zeroization() {
    // Regression guard: if a future refactor switches the field type back to
    // a raw String, the type-check here fails.
    let undo = make_undo("K", "v");
    let _: &tsafe_tui::app::SensitiveString = &undo.value;
}
