//! F1-2: golden-frame regression for TUI layout (login, help overlay, dashboard, modals).

use std::path::PathBuf;

use chrono::{TimeZone, Utc};
use ratatui::{backend::TestBackend, buffer::Buffer, Terminal};
use serial_test::serial;
use tsafe_tui::app::{App, EditFocus, LoginFocus, Screen, Theme};
use tsafe_tui::ui;

fn buffer_to_plain_string(buf: &Buffer) -> String {
    let area = buf.area;
    let mut out = String::with_capacity((area.width as usize + 1) * area.height as usize);
    for y in 0..area.height {
        for x in 0..area.width {
            out.push_str(buf[(x, y)].symbol());
        }
        out.push('\n');
    }
    out
}

fn render_frame_with_size(app: &mut App, width: u16, height: u16) -> String {
    let backend = TestBackend::new(width, height);
    let mut terminal = Terminal::new(backend).expect("terminal");
    terminal.draw(|f| ui::render(f, app)).expect("draw");
    buffer_to_plain_string(terminal.backend().buffer())
}

fn render_frame(app: &mut App) -> String {
    render_frame_with_size(app, 80, 24)
}

/// Collapse ` vMAJOR.MINOR.PATCH` (title-bar badge from `tsafe_core::update::PKG_VERSION`) to ` v0.0.0`
/// so insta snapshots stay stable when workspace crate versions change.
fn normalize_pkg_version_in_snapshot(s: &str) -> String {
    let mut out = String::with_capacity(s.len());
    let mut rest = s;
    while let Some(i) = rest.find(" v") {
        out.push_str(&rest[..i + 2]);
        rest = &rest[i + 2..];
        let end = rest
            .find(|c: char| !c.is_ascii_digit() && c != '.')
            .unwrap_or(rest.len());
        let ver = &rest[..end];
        if is_semver_triple(ver) {
            out.push_str("0.0.0");
        } else {
            out.push_str(ver);
        }
        rest = &rest[end..];
    }
    out.push_str(rest);
    out
}

fn is_semver_triple(s: &str) -> bool {
    let mut parts = s.split('.');
    match (parts.next(), parts.next(), parts.next(), parts.next()) {
        (Some(a), Some(b), Some(c), None) if !a.is_empty() && !b.is_empty() && !c.is_empty() => {
            a.chars().all(|d| d.is_ascii_digit())
                && b.chars().all(|d| d.is_ascii_digit())
                && c.chars().all(|d| d.is_ascii_digit())
        }
        _ => false,
    }
}

fn snapshot_frame(app: &mut App) -> String {
    normalize_pkg_version_in_snapshot(&render_frame(app))
}

fn base_app_dark_empty_vault() -> App {
    let mut app = App::new();
    app.update_rx = None;
    app.update_available = None;
    app.theme = Theme::Dark;
    app
}

fn app_dashboard_demo_empty_secrets(mut base: App) -> App {
    base.profiles = vec!["demo".to_string()];
    base.profile_cursor = 0;
    base.active_profile = Some("demo".to_string());
    base.screen = Screen::Dashboard;
    base.secret_keys = vec![];
    base.secret_cursor = 0;
    base.search_mode = false;
    base.search_query.clear();
    base
}

fn app_dashboard_demo_with_secrets(theme: Theme) -> App {
    let mut app = app_dashboard_demo_empty_secrets(base_app_dark_empty_vault());
    app.theme = theme;
    app.secret_keys = vec![
        "API_URL".into(),
        "prod/DB_PASS".into(),
        "prod/REDIS_URL".into(),
    ];
    app.secret_cursor = 2;
    app
}

fn assert_rendered_surface(
    case_name: &str,
    theme: Theme,
    width: u16,
    height: u16,
    mut app: App,
    markers: &[&str],
) {
    app.theme = theme;
    let rendered =
        normalize_pkg_version_in_snapshot(&render_frame_with_size(&mut app, width, height));
    let lines: Vec<&str> = rendered.lines().collect();
    assert_eq!(
        lines.len(),
        height as usize,
        "{case_name} {theme:?} {width}x{height} rendered an unexpected row count",
    );
    assert!(
        rendered.lines().any(|line| !line.trim().is_empty()),
        "{case_name} {theme:?} {width}x{height} rendered a blank frame",
    );
    assert!(
        !rendered.contains('\u{fffd}'),
        "{case_name} {theme:?} {width}x{height} rendered replacement characters:\n{rendered}",
    );

    let lower = rendered.to_lowercase();
    for marker in markers {
        assert!(
            lower.contains(&marker.to_lowercase()),
            "{case_name} {theme:?} {width}x{height} missing marker '{marker}':\n{rendered}",
        );
    }
}

#[serial]
#[test]
fn primary_screens_render_across_theme_and_terminal_matrix() {
    let dir = tempfile::tempdir().expect("tempdir");
    temp_env::with_var("TSAFE_VAULT_DIR", Some(dir.path().as_os_str()), || {
        let themes = [Theme::Dark, Theme::Light, Theme::HighContrast];
        let sizes = [(60, 16), (80, 24), (100, 30)];

        for theme in themes {
            for (width, height) in sizes {
                let mut cases: Vec<(&str, App, &[&str])> = Vec::new();

                cases.push(("login", base_app_dark_empty_vault(), &["tsafe", "login"]));

                let mut login_help = base_app_dark_empty_vault();
                login_help.help_visible = true;
                cases.push(("login_help", login_help, &["help"]));

                cases.push((
                    "dashboard",
                    app_dashboard_demo_with_secrets(theme),
                    &["tsafe", "api_url", "prod"],
                ));

                let mut search = app_dashboard_demo_with_secrets(theme);
                search.search_mode = true;
                search.search_query = "db".into();
                cases.push(("dashboard_search", search, &["search", "db"]));

                let mut edit = app_dashboard_demo_with_secrets(theme);
                edit.screen = Screen::EditSecret { is_new: true };
                edit.edit_key = "MY_KEY".into();
                edit.edit_value = "hunter2".into();
                edit.edit_focus = EditFocus::Value;
                cases.push(("edit_secret", edit, &["add secret", "key", "value"]));

                let mut confirm = app_dashboard_demo_with_secrets(theme);
                confirm.screen = Screen::ConfirmDelete;
                cases.push(("confirm_delete", confirm, &["confirm delete"]));

                let mut rotate = app_dashboard_demo_with_secrets(theme);
                rotate.screen = Screen::RotatePassword;
                rotate.rotate_step = 0;
                cases.push(("rotate_password", rotate, &["rotate master password"]));

                let mut snapshots = app_dashboard_demo_with_secrets(theme);
                snapshots.screen = Screen::SnapshotRestore;
                snapshots.snapshot_paths =
                    vec![PathBuf::from("/tmp/tsafe-golden-snapshot.snap.json")];
                cases.push(("snapshot_restore", snapshots, &["restore snapshot"]));

                let mut audit = app_dashboard_demo_with_secrets(theme);
                audit.screen = Screen::AuditLog;
                audit.audit_lines = vec!["[2026-04-08 12:00:00] set ok  MY_KEY  saved".into()];
                cases.push(("audit_log", audit, &["audit log", "my_key"]));

                let mut history = app_dashboard_demo_with_secrets(theme);
                let ts = Utc.with_ymd_and_hms(2026, 4, 8, 15, 30, 0).unwrap();
                history.screen = Screen::History {
                    key: "MY_KEY".into(),
                };
                history.history_entries = vec![(0, ts), (1, ts)];
                cases.push(("history", history, &["history", "my_key"]));

                let mut profile = base_app_dark_empty_vault();
                profile.screen = Screen::NewProfile { step: 0 };
                cases.push(("new_profile", profile, &["create new profile"]));

                let mut move_secret = app_dashboard_demo_with_secrets(theme);
                move_secret.screen = Screen::MoveSecret {
                    source: "prod/API_KEY".into(),
                };
                move_secret.mv_dest_buf = "staging/API_KEY".into();
                cases.push(("move_secret", move_secret, &["move", "prod/api_key"]));

                let mut ns_bulk = app_dashboard_demo_with_secrets(theme);
                ns_bulk.screen = Screen::NsBulk {
                    copy: true,
                    from: "prod".into(),
                };
                ns_bulk.mv_dest_buf = "staging".into();
                cases.push(("namespace_bulk", ns_bulk, &["copy", "prod"]));

                for (case_name, app, markers) in cases {
                    assert_rendered_surface(case_name, theme, width, height, app, markers);
                }
            }
        }
    });
}

#[serial]
#[test]
fn login_empty_vault_dir_dark_theme() {
    let dir = tempfile::tempdir().expect("tempdir");
    temp_env::with_var("TSAFE_VAULT_DIR", Some(dir.path().as_os_str()), || {
        let mut app = base_app_dark_empty_vault();
        let rendered = snapshot_frame(&mut app);
        insta::assert_snapshot!(rendered);
    });
}

#[serial]
#[test]
fn help_overlay_on_login_empty_vault_dark_theme() {
    let dir = tempfile::tempdir().expect("tempdir");
    temp_env::with_var("TSAFE_VAULT_DIR", Some(dir.path().as_os_str()), || {
        let mut app = base_app_dark_empty_vault();
        app.help_visible = true;
        let rendered = snapshot_frame(&mut app);
        insta::assert_snapshot!("help_overlay_on_login_empty_vault_dark_theme", rendered);
    });
}

#[serial]
#[test]
fn dashboard_empty_secrets_dark_theme() {
    let dir = tempfile::tempdir().expect("tempdir");
    temp_env::with_var("TSAFE_VAULT_DIR", Some(dir.path().as_os_str()), || {
        let mut app = app_dashboard_demo_empty_secrets(base_app_dark_empty_vault());
        let rendered = snapshot_frame(&mut app);
        insta::assert_snapshot!("dashboard_empty_secrets_dark_theme", rendered);
    });
}

#[serial]
#[test]
fn dashboard_status_message_dark_theme() {
    let dir = tempfile::tempdir().expect("tempdir");
    temp_env::with_var("TSAFE_VAULT_DIR", Some(dir.path().as_os_str()), || {
        let mut app = app_dashboard_demo_empty_secrets(base_app_dark_empty_vault());
        app.set_status("Status: vault ready.");
        let rendered = snapshot_frame(&mut app);
        insta::assert_snapshot!("dashboard_status_message_dark_theme", rendered);
    });
}

#[serial]
#[test]
fn help_overlay_on_dashboard_empty_secrets_dark_theme() {
    let dir = tempfile::tempdir().expect("tempdir");
    temp_env::with_var("TSAFE_VAULT_DIR", Some(dir.path().as_os_str()), || {
        let mut app = app_dashboard_demo_empty_secrets(base_app_dark_empty_vault());
        app.help_visible = true;
        let rendered = snapshot_frame(&mut app);
        insta::assert_snapshot!(
            "help_overlay_on_dashboard_empty_secrets_dark_theme",
            rendered
        );
    });
}

#[serial]
#[test]
fn dashboard_search_mode_empty_query_dark_theme() {
    let dir = tempfile::tempdir().expect("tempdir");
    temp_env::with_var("TSAFE_VAULT_DIR", Some(dir.path().as_os_str()), || {
        let mut app = app_dashboard_demo_empty_secrets(base_app_dark_empty_vault());
        app.search_mode = true;
        app.search_query.clear();
        let rendered = snapshot_frame(&mut app);
        insta::assert_snapshot!("dashboard_search_mode_empty_query_dark_theme", rendered);
    });
}

#[serial]
#[test]
fn add_secret_modal_new_dark_theme() {
    let dir = tempfile::tempdir().expect("tempdir");
    temp_env::with_var("TSAFE_VAULT_DIR", Some(dir.path().as_os_str()), || {
        let mut app = app_dashboard_demo_empty_secrets(base_app_dark_empty_vault());
        app.screen = Screen::EditSecret { is_new: true };
        app.edit_key = "MY_KEY".to_string();
        app.edit_value = "hunter2".to_string();
        app.edit_focus = EditFocus::Key;
        app.edit_error = None;
        let rendered = snapshot_frame(&mut app);
        insta::assert_snapshot!("add_secret_modal_new_dark_theme", rendered);
    });
}

#[serial]
#[test]
fn add_secret_modal_value_focus_dark_theme() {
    let dir = tempfile::tempdir().expect("tempdir");
    temp_env::with_var("TSAFE_VAULT_DIR", Some(dir.path().as_os_str()), || {
        let mut app = app_dashboard_demo_empty_secrets(base_app_dark_empty_vault());
        app.screen = Screen::EditSecret { is_new: true };
        app.edit_key = "MY_KEY".to_string();
        app.edit_value = "hunter2".to_string();
        app.edit_focus = EditFocus::Value;
        app.edit_error = None;
        let rendered = snapshot_frame(&mut app);
        insta::assert_snapshot!("add_secret_modal_value_focus_dark_theme", rendered);
    });
}

#[serial]
#[test]
fn confirm_delete_modal_dark_theme() {
    let dir = tempfile::tempdir().expect("tempdir");
    temp_env::with_var("TSAFE_VAULT_DIR", Some(dir.path().as_os_str()), || {
        let mut app = app_dashboard_demo_empty_secrets(base_app_dark_empty_vault());
        app.secret_keys = vec!["prod/API_KEY".to_string()];
        app.secret_cursor = 0;
        app.screen = Screen::ConfirmDelete;
        let rendered = snapshot_frame(&mut app);
        insta::assert_snapshot!("confirm_delete_modal_dark_theme", rendered);
    });
}

#[serial]
#[test]
fn new_profile_wizard_step0_dark_theme() {
    let dir = tempfile::tempdir().expect("tempdir");
    temp_env::with_var("TSAFE_VAULT_DIR", Some(dir.path().as_os_str()), || {
        let mut app = base_app_dark_empty_vault();
        app.screen = Screen::NewProfile { step: 0 };
        app.new_profile_name.clear();
        app.new_profile_error = None;
        let rendered = snapshot_frame(&mut app);
        insta::assert_snapshot!("new_profile_wizard_step0_dark_theme", rendered);
    });
}

#[serial]
#[test]
fn rotate_password_modal_step0_dark_theme() {
    let dir = tempfile::tempdir().expect("tempdir");
    temp_env::with_var("TSAFE_VAULT_DIR", Some(dir.path().as_os_str()), || {
        let mut app = app_dashboard_demo_empty_secrets(base_app_dark_empty_vault());
        app.screen = Screen::RotatePassword;
        app.rotate_step = 0;
        app.rotate_error = None;
        let rendered = snapshot_frame(&mut app);
        insta::assert_snapshot!("rotate_password_modal_step0_dark_theme", rendered);
    });
}

#[serial]
#[test]
fn audit_log_dark_theme() {
    let dir = tempfile::tempdir().expect("tempdir");
    temp_env::with_var("TSAFE_VAULT_DIR", Some(dir.path().as_os_str()), || {
        let mut app = app_dashboard_demo_empty_secrets(base_app_dark_empty_vault());
        app.screen = Screen::AuditLog;
        app.audit_lines = vec!["[2026-04-08 12:00:00] set ok  MY_KEY  saved".into()];
        app.audit_scroll = 0;
        let rendered = snapshot_frame(&mut app);
        insta::assert_snapshot!("audit_log_dark_theme", rendered);
    });
}

#[serial]
#[test]
fn move_secret_modal_dark_theme() {
    let dir = tempfile::tempdir().expect("tempdir");
    temp_env::with_var("TSAFE_VAULT_DIR", Some(dir.path().as_os_str()), || {
        let mut app = app_dashboard_demo_empty_secrets(base_app_dark_empty_vault());
        app.screen = Screen::MoveSecret {
            source: "prod/API_KEY".into(),
        };
        app.mv_dest_buf = "staging/API_KEY".into();
        app.mv_error = None;
        let rendered = snapshot_frame(&mut app);
        insta::assert_snapshot!("move_secret_modal_dark_theme", rendered);
    });
}

#[serial]
#[test]
fn ns_bulk_copy_namespace_modal_dark_theme() {
    let dir = tempfile::tempdir().expect("tempdir");
    temp_env::with_var("TSAFE_VAULT_DIR", Some(dir.path().as_os_str()), || {
        let mut app = app_dashboard_demo_empty_secrets(base_app_dark_empty_vault());
        app.screen = Screen::NsBulk {
            copy: true,
            from: "prod".into(),
        };
        app.mv_dest_buf = "staging".into();
        app.mv_error = None;
        let rendered = snapshot_frame(&mut app);
        insta::assert_snapshot!("ns_bulk_copy_namespace_modal_dark_theme", rendered);
    });
}

#[serial]
#[test]
fn ns_bulk_move_namespace_modal_dark_theme() {
    let dir = tempfile::tempdir().expect("tempdir");
    temp_env::with_var("TSAFE_VAULT_DIR", Some(dir.path().as_os_str()), || {
        let mut app = app_dashboard_demo_empty_secrets(base_app_dark_empty_vault());
        app.screen = Screen::NsBulk {
            copy: false,
            from: "oldapp".into(),
        };
        app.mv_dest_buf = "newapp".into();
        app.mv_error = None;
        let rendered = snapshot_frame(&mut app);
        insta::assert_snapshot!("ns_bulk_move_namespace_modal_dark_theme", rendered);
    });
}

#[serial]
#[test]
fn snapshot_restore_picker_dark_theme() {
    let dir = tempfile::tempdir().expect("tempdir");
    temp_env::with_var("TSAFE_VAULT_DIR", Some(dir.path().as_os_str()), || {
        let mut app = app_dashboard_demo_empty_secrets(base_app_dark_empty_vault());
        app.screen = Screen::SnapshotRestore;
        app.snapshot_paths = vec![PathBuf::from("/tmp/tsafe-golden-snapshot.snap.json")];
        app.snapshot_cursor = 0;
        let rendered = snapshot_frame(&mut app);
        insta::assert_snapshot!("snapshot_restore_picker_dark_theme", rendered);
    });
}

#[serial]
#[test]
fn history_viewer_dark_theme() {
    let dir = tempfile::tempdir().expect("tempdir");
    temp_env::with_var("TSAFE_VAULT_DIR", Some(dir.path().as_os_str()), || {
        let mut app = app_dashboard_demo_empty_secrets(base_app_dark_empty_vault());
        let ts = Utc.with_ymd_and_hms(2026, 4, 8, 15, 30, 0).unwrap();
        app.screen = Screen::History {
            key: "MY_KEY".into(),
        };
        app.history_entries = vec![(0, ts), (1, ts)];
        app.history_scroll = 0;
        let rendered = snapshot_frame(&mut app);
        insta::assert_snapshot!("history_viewer_dark_theme", rendered);
    });
}

#[serial]
#[test]
fn dashboard_error_status_dark_theme() {
    let dir = tempfile::tempdir().expect("tempdir");
    temp_env::with_var("TSAFE_VAULT_DIR", Some(dir.path().as_os_str()), || {
        let mut app = app_dashboard_demo_empty_secrets(base_app_dark_empty_vault());
        app.set_status("Error: example failure message.");
        let rendered = snapshot_frame(&mut app);
        insta::assert_snapshot!("dashboard_error_status_dark_theme", rendered);
    });
}

#[serial]
#[test]
fn rotate_password_modal_step1_dark_theme() {
    let dir = tempfile::tempdir().expect("tempdir");
    temp_env::with_var("TSAFE_VAULT_DIR", Some(dir.path().as_os_str()), || {
        let mut app = app_dashboard_demo_empty_secrets(base_app_dark_empty_vault());
        app.screen = Screen::RotatePassword;
        app.rotate_step = 1;
        app.rotate_error = None;
        let rendered = snapshot_frame(&mut app);
        insta::assert_snapshot!("rotate_password_modal_step1_dark_theme", rendered);
    });
}

#[serial]
#[test]
fn new_profile_wizard_step1_dark_theme() {
    let dir = tempfile::tempdir().expect("tempdir");
    temp_env::with_var("TSAFE_VAULT_DIR", Some(dir.path().as_os_str()), || {
        let mut app = base_app_dark_empty_vault();
        app.screen = Screen::NewProfile { step: 1 };
        app.new_profile_error = None;
        let rendered = snapshot_frame(&mut app);
        insta::assert_snapshot!("new_profile_wizard_step1_dark_theme", rendered);
    });
}

#[serial]
#[test]
fn dashboard_with_secrets_dark_theme() {
    let dir = tempfile::tempdir().expect("tempdir");
    temp_env::with_var("TSAFE_VAULT_DIR", Some(dir.path().as_os_str()), || {
        let mut app = app_dashboard_demo_empty_secrets(base_app_dark_empty_vault());
        app.secret_keys = vec![
            "API_URL".into(),
            "prod/DB_PASS".into(),
            "prod/REDIS_URL".into(),
        ];
        // Cursor on `prod/DB_PASS` (root key, namespace header, first prod key).
        app.secret_cursor = 2;
        let rendered = snapshot_frame(&mut app);
        insta::assert_snapshot!("dashboard_with_secrets_dark_theme", rendered);
    });
}

#[serial]
#[test]
fn edit_secret_modal_existing_dark_theme() {
    let dir = tempfile::tempdir().expect("tempdir");
    temp_env::with_var("TSAFE_VAULT_DIR", Some(dir.path().as_os_str()), || {
        let mut app = app_dashboard_demo_empty_secrets(base_app_dark_empty_vault());
        app.screen = Screen::EditSecret { is_new: false };
        app.edit_key = "prod/DB_PASS".into();
        app.edit_value = "postgres://…".into();
        app.edit_focus = EditFocus::Key;
        app.edit_error = None;
        let rendered = snapshot_frame(&mut app);
        insta::assert_snapshot!("edit_secret_modal_existing_dark_theme", rendered);
    });
}

#[serial]
#[test]
fn new_profile_wizard_step2_dark_theme() {
    let dir = tempfile::tempdir().expect("tempdir");
    temp_env::with_var("TSAFE_VAULT_DIR", Some(dir.path().as_os_str()), || {
        let mut app = base_app_dark_empty_vault();
        app.screen = Screen::NewProfile { step: 2 };
        app.new_profile_error = None;
        let rendered = snapshot_frame(&mut app);
        insta::assert_snapshot!("new_profile_wizard_step2_dark_theme", rendered);
    });
}

#[serial]
#[test]
fn login_two_profiles_dark_theme() {
    let dir = tempfile::tempdir().expect("tempdir");
    temp_env::with_var("TSAFE_VAULT_DIR", Some(dir.path().as_os_str()), || {
        let mut app = base_app_dark_empty_vault();
        app.profiles = vec!["alpha".into(), "beta".into()];
        app.profile_cursor = 1;
        app.login_focus = LoginFocus::Profile;
        app.screen = Screen::Login;
        let rendered = snapshot_frame(&mut app);
        insta::assert_snapshot!("login_two_profiles_dark_theme", rendered);
    });
}

#[serial]
#[test]
fn login_empty_vault_light_theme() {
    let dir = tempfile::tempdir().expect("tempdir");
    temp_env::with_var("TSAFE_VAULT_DIR", Some(dir.path().as_os_str()), || {
        let mut app = base_app_dark_empty_vault();
        app.theme = Theme::Light;
        let rendered = snapshot_frame(&mut app);
        insta::assert_snapshot!("login_empty_vault_light_theme", rendered);
    });
}

#[serial]
#[test]
fn login_empty_vault_high_contrast_theme() {
    let dir = tempfile::tempdir().expect("tempdir");
    temp_env::with_var("TSAFE_VAULT_DIR", Some(dir.path().as_os_str()), || {
        let mut app = base_app_dark_empty_vault();
        app.theme = Theme::HighContrast;
        let rendered = snapshot_frame(&mut app);
        insta::assert_snapshot!("login_empty_vault_high_contrast_theme", rendered);
    });
}

#[serial]
#[test]
fn dashboard_collapsed_namespace_dark_theme() {
    let dir = tempfile::tempdir().expect("tempdir");
    temp_env::with_var("TSAFE_VAULT_DIR", Some(dir.path().as_os_str()), || {
        let mut app = app_dashboard_demo_empty_secrets(base_app_dark_empty_vault());
        app.secret_keys = vec!["API_URL".into(), "prod/DB_PASS".into()];
        app.collapsed_namespaces.insert("prod".into());
        app.secret_cursor = 0;
        let rendered = snapshot_frame(&mut app);
        insta::assert_snapshot!("dashboard_collapsed_namespace_dark_theme", rendered);
    });
}

#[serial]
#[test]
fn add_secret_modal_with_error_dark_theme() {
    let dir = tempfile::tempdir().expect("tempdir");
    temp_env::with_var("TSAFE_VAULT_DIR", Some(dir.path().as_os_str()), || {
        let mut app = app_dashboard_demo_empty_secrets(base_app_dark_empty_vault());
        app.screen = Screen::EditSecret { is_new: true };
        app.edit_key.clear();
        app.edit_value.clear();
        app.edit_focus = EditFocus::Key;
        app.edit_error = Some("KEY must not be empty.".into());
        let rendered = snapshot_frame(&mut app);
        insta::assert_snapshot!("add_secret_modal_with_error_dark_theme", rendered);
    });
}

#[serial]
#[test]
fn dashboard_empty_secrets_light_theme() {
    let dir = tempfile::tempdir().expect("tempdir");
    temp_env::with_var("TSAFE_VAULT_DIR", Some(dir.path().as_os_str()), || {
        let mut app = app_dashboard_demo_empty_secrets(base_app_dark_empty_vault());
        app.theme = Theme::Light;
        let rendered = snapshot_frame(&mut app);
        insta::assert_snapshot!("dashboard_empty_secrets_light_theme", rendered);
    });
}

#[serial]
#[test]
fn dashboard_with_secrets_light_theme() {
    let dir = tempfile::tempdir().expect("tempdir");
    temp_env::with_var("TSAFE_VAULT_DIR", Some(dir.path().as_os_str()), || {
        let mut app = app_dashboard_demo_empty_secrets(base_app_dark_empty_vault());
        app.theme = Theme::Light;
        app.secret_keys = vec![
            "API_URL".into(),
            "prod/DB_PASS".into(),
            "prod/REDIS_URL".into(),
        ];
        app.secret_cursor = 2;
        let rendered = snapshot_frame(&mut app);
        insta::assert_snapshot!("dashboard_with_secrets_light_theme", rendered);
    });
}

#[serial]
#[test]
fn login_unlock_error_dark_theme() {
    let dir = tempfile::tempdir().expect("tempdir");
    temp_env::with_var("TSAFE_VAULT_DIR", Some(dir.path().as_os_str()), || {
        let mut app = base_app_dark_empty_vault();
        app.profiles = vec!["demo".into()];
        app.profile_cursor = 0;
        app.login_focus = LoginFocus::Password;
        app.login_error = Some("Decryption failed — wrong password or corrupt vault.".into());
        let rendered = snapshot_frame(&mut app);
        insta::assert_snapshot!("login_unlock_error_dark_theme", rendered);
    });
}

#[serial]
#[test]
fn new_profile_wizard_step0_with_error_dark_theme() {
    let dir = tempfile::tempdir().expect("tempdir");
    temp_env::with_var("TSAFE_VAULT_DIR", Some(dir.path().as_os_str()), || {
        let mut app = base_app_dark_empty_vault();
        app.screen = Screen::NewProfile { step: 0 };
        app.new_profile_name.clear();
        app.new_profile_error = Some("Profile name must not be empty.".into());
        let rendered = snapshot_frame(&mut app);
        insta::assert_snapshot!("new_profile_wizard_step0_with_error_dark_theme", rendered);
    });
}

#[serial]
#[test]
fn rotate_password_modal_step0_with_error_dark_theme() {
    let dir = tempfile::tempdir().expect("tempdir");
    temp_env::with_var("TSAFE_VAULT_DIR", Some(dir.path().as_os_str()), || {
        let mut app = app_dashboard_demo_empty_secrets(base_app_dark_empty_vault());
        app.screen = Screen::RotatePassword;
        app.rotate_step = 0;
        app.rotate_error = Some("Password must not be empty.".into());
        let rendered = snapshot_frame(&mut app);
        insta::assert_snapshot!(
            "rotate_password_modal_step0_with_error_dark_theme",
            rendered
        );
    });
}

#[serial]
#[test]
fn move_secret_modal_with_error_dark_theme() {
    let dir = tempfile::tempdir().expect("tempdir");
    temp_env::with_var("TSAFE_VAULT_DIR", Some(dir.path().as_os_str()), || {
        let mut app = app_dashboard_demo_empty_secrets(base_app_dark_empty_vault());
        app.screen = Screen::MoveSecret {
            source: "prod/API_KEY".into(),
        };
        app.mv_dest_buf.clear();
        app.mv_error = Some("Destination key must not be empty.".into());
        let rendered = snapshot_frame(&mut app);
        insta::assert_snapshot!("move_secret_modal_with_error_dark_theme", rendered);
    });
}

#[cfg(test)]
mod normalize_unit {
    use super::*;

    #[test]
    fn normalizes_triple_semver_after_space_v() {
        assert_eq!(
            normalize_pkg_version_in_snapshot("head  v12.34.56 tail"),
            "head  v0.0.0 tail"
        );
    }

    #[test]
    fn leaves_non_triple_version_unchanged() {
        assert_eq!(
            normalize_pkg_version_in_snapshot("x  v1.2  y"),
            "x  v1.2  y"
        );
    }

    #[test]
    fn normalizes_multiple_badges() {
        assert_eq!(
            normalize_pkg_version_in_snapshot("a v1.0.1 b v2.0.0"),
            "a v0.0.0 b v0.0.0"
        );
    }
}
