// crates/atomcode-tuix/src/terminal.rs
use std::io::IsTerminal;

/// All environment signals we care about for rendering decisions.
///
/// `Default` returns the safest non-TTY-ish snapshot (no special env
/// vars, no UTF-8 hint, not Windows). Tests use it via `..Default::default()`
/// so adding a new field doesn't require touching every fixture; production
/// code goes through `EnvView::probe`.
#[derive(Default)]
pub struct EnvView {
    pub is_stdout_tty: bool,
    pub no_color: bool,
    pub term: Option<String>,
    pub colorterm: Option<String>,
    /// Set when the user has explicitly asked for ASCII-only rendering
    /// (e.g. `ATOMCODE_ASCII=1`). Escape hatch for terminals whose font
    /// can't render our Unicode prompt glyphs (`❯`, `◆`, etc.) and
    /// would otherwise show `□` tofu.
    pub force_ascii: bool,
    /// Set when the user has explicitly opted INTO Unicode rendering
    /// (`ATOMCODE_UNICODE=1`) — overrides the Windows-legacy-console
    /// auto-fallback for users who installed a font that does have the
    /// glyphs (Cascadia Code, JetBrains Mono, etc.) on plain conhost.
    pub force_unicode: bool,
    pub lang: Option<String>,
    pub lc_all: Option<String>,
    /// `true` when running on Windows. Affects the default-Unicode
    /// decision because the legacy conhost host pairs with fonts
    /// (Consolas, NSimSun, …) that don't include `◐`, `❯`, etc.
    pub is_windows: bool,
    /// `WT_SESSION` — set by Windows Terminal. Strong signal that the
    /// terminal has a modern font with broad Unicode coverage.
    pub wt_session: Option<String>,
    /// `TERM_PROGRAM` — set by VS Code, iTerm2, WezTerm, Hyper, etc.
    /// Any value here means the user is on a modern emulator that
    /// almost certainly ships a Unicode-capable default font.
    pub term_program: Option<String>,
}

impl EnvView {
    pub fn probe() -> Self {
        Self {
            is_stdout_tty: std::io::stdout().is_terminal(),
            no_color: std::env::var("NO_COLOR").is_ok(),
            term: std::env::var("TERM").ok(),
            colorterm: std::env::var("COLORTERM").ok(),
            force_ascii: std::env::var("ATOMCODE_ASCII").is_ok(),
            force_unicode: std::env::var("ATOMCODE_UNICODE").is_ok(),
            lang: std::env::var("LANG").ok(),
            lc_all: std::env::var("LC_ALL").ok(),
            is_windows: cfg!(target_os = "windows"),
            wt_session: std::env::var("WT_SESSION").ok(),
            term_program: std::env::var("TERM_PROGRAM").ok(),
        }
    }
}

#[derive(Debug, Clone, Copy)]
pub struct TerminalCaps {
    /// stdout is a TTY (vs. pipe/redirect/CI).
    pub tty: bool,
    /// Emit SGR colour codes.
    pub colors: bool,
    /// Show animated spinner (requires overwritable current line).
    pub spinner: bool,
    /// Enable bracketed paste mode (DECSET 2004).
    pub bracketed_paste: bool,
    /// Raw mode for key-by-key input.
    pub raw_mode: bool,
    /// DECSTBM scroll region support (`\x1b[top;bot r`) — lets us pin a
    /// fixed-footer area at the bottom and have streaming content scroll
    /// only in the upper region. VT100+ standard; supported by every
    /// modern emulator (Terminal.app, iTerm2, Alacritty, WezTerm, Windows
    /// Terminal, tmux). Disabled on dumb terminals and non-TTY contexts.
    pub scroll_region: bool,
    /// Render decorative Unicode glyphs (`❯`, `◆`, box-drawing corners).
    /// Off → use ASCII fallbacks (`>`, `*`, `+`) so minimal terminals
    /// (Windows legacy console, Docker/CI, POSIX locale without a full
    /// font) don't show `□` tofu. Set via:
    ///   * `ATOMCODE_ASCII=1` env var (explicit opt-out)
    ///   * `TERM=dumb`
    ///   * `LC_ALL`/`LANG` being `C` / `POSIX` / `ANSI_X3.4-1968`
    pub unicode_symbols: bool,
    /// Classic Windows console host (conhost.exe), as opposed to Windows
    /// Terminal / a modern emulator. Detected as: Windows AND neither
    /// `WT_SESSION` nor `TERM_PROGRAM` present (same heuristic as the
    /// legacy-console ASCII fallback below).
    ///
    /// Why it matters: the conhost shipped on Win10 2004/20H2
    /// (10.0.19041) fastfails (`0xc0000409`) when we repaint on a window
    /// resize using a per-row `CUP+EL` wipe across the whole viewport
    /// while its buffer is mid-resize — the user sees the entire terminal
    /// window vanish during a drag. On this host we emit a single `ED2`
    /// clear on resize instead of the row-by-row burst (see
    /// `RetainedRenderer::on_resize`). Always `false` off Windows.
    pub legacy_conhost: bool,
}

impl TerminalCaps {
    pub fn from_env(env: EnvView) -> Self {
        let is_dumb = env.term.as_deref() == Some("dumb");
        let tty = env.is_stdout_tty;

        // LC_ALL wins over LANG per POSIX; either being one of the
        // "no-i18n" locales is a strong hint the environment is
        // minimal (containers, CI) and the font probably can't
        // render our decorative glyphs.
        let locale = env.lc_all.as_deref().or(env.lang.as_deref()).unwrap_or("");
        let ascii_locale = matches!(locale, "C" | "POSIX" | "ANSI_X3.4-1968");

        // Windows-legacy-console heuristic: on Windows the default
        // conhost host ships with fonts (Consolas, NSimSun, …) that
        // miss many Geometric Shapes / Misc-Symbols glyphs we use
        // (`❯`, `◐`, etc.) and renders them as `□` tofu. Modern
        // emulators set discoverable env vars; if NEITHER is present
        // assume legacy conhost and fall back to ASCII.
        //
        //   * Windows Terminal sets `WT_SESSION`
        //   * VS Code / iTerm2 / WezTerm / Hyper set `TERM_PROGRAM`
        //
        // Users on conhost who installed a Unicode-capable font
        // (Cascadia Code / JetBrains Mono / etc.) can opt back in
        // with `ATOMCODE_UNICODE=1`.
        let on_modern_emulator = env.wt_session.is_some() || env.term_program.is_some();
        let windows_legacy_console = env.is_windows && !on_modern_emulator;

        let unicode_symbols = if env.force_unicode {
            true
        } else {
            !env.force_ascii && !is_dumb && !ascii_locale && !windows_legacy_console
        };

        Self {
            tty,
            colors: tty && !env.no_color && !is_dumb,
            spinner: tty && !is_dumb,
            bracketed_paste: tty && !is_dumb,
            raw_mode: tty && !is_dumb,
            scroll_region: tty && !is_dumb,
            unicode_symbols,
            legacy_conhost: windows_legacy_console,
        }
    }

    pub fn probe() -> Self {
        Self::from_env(EnvView::probe())
    }

    /// Two-cell prompt prefix for the input box and echoed user lines.
    /// `"❯ "` when the terminal can render Unicode glyphs, `"> "` as the
    /// ASCII fallback. Both are exactly 2 display columns, so layout
    /// math (`text_budget = w - 2`) stays identical in both branches.
    pub fn prompt_chevron(&self) -> &'static str {
        if self.unicode_symbols {
            "\u{276f} "
        } else {
            "> "
        }
    }
}

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

    /// Default test environment: TTY + UTF-8 locale + non-Windows + no
    /// special env vars set. Tests override only the fields they care
    /// about, so adding new EnvView fields doesn't require touching
    /// every test.
    fn env() -> EnvView {
        EnvView {
            is_stdout_tty: true,
            no_color: false,
            term: Some("xterm-256color".to_string()),
            colorterm: Some("truecolor".to_string()),
            force_ascii: false,
            force_unicode: false,
            lang: Some("en_US.UTF-8".to_string()),
            lc_all: None,
            is_windows: false,
            wt_session: None,
            term_program: None,
        }
    }

    #[test]
    fn no_color_env_disables_colors() {
        let caps = TerminalCaps::from_env(EnvView {
            no_color: true,
            ..env()
        });
        assert!(!caps.colors);
        assert!(caps.tty);
        assert!(caps.spinner); // 非 dumb + 是 tty 仍保留 spinner
    }

    #[test]
    fn legacy_conhost_only_on_bare_windows() {
        // Windows with no Windows-Terminal / modern-emulator markers → classic
        // conhost → resize must use the ED2-clear path, not the per-row burst.
        let conhost = TerminalCaps::from_env(EnvView {
            is_windows: true,
            wt_session: None,
            term_program: None,
            ..env()
        });
        assert!(conhost.legacy_conhost);

        // Windows Terminal sets WT_SESSION → modern engine → not legacy.
        let wt = TerminalCaps::from_env(EnvView {
            is_windows: true,
            wt_session: Some("abc".to_string()),
            ..env()
        });
        assert!(!wt.legacy_conhost);

        // Non-Windows is never legacy conhost.
        assert!(!TerminalCaps::from_env(EnvView { is_windows: false, ..env() }).legacy_conhost);
    }

    #[test]
    fn non_tty_forces_plain_mode() {
        let caps = TerminalCaps::from_env(EnvView {
            is_stdout_tty: false,
            term: Some("xterm".to_string()),
            colorterm: None,
            ..env()
        });
        assert!(!caps.tty);
        assert!(!caps.colors);
        assert!(!caps.spinner);
        assert!(!caps.bracketed_paste);
        assert!(!caps.raw_mode);
    }

    #[test]
    fn dumb_term_disables_spinner_and_colors() {
        let caps = TerminalCaps::from_env(EnvView {
            term: Some("dumb".to_string()),
            colorterm: None,
            ..env()
        });
        assert!(caps.tty);
        assert!(!caps.colors);
        assert!(!caps.spinner);
        assert!(!caps.unicode_symbols, "dumb TERM forces ASCII fallback");
    }

    #[test]
    fn atomcode_ascii_env_forces_ascii() {
        let caps = TerminalCaps::from_env(EnvView {
            force_ascii: true,
            ..env()
        });
        assert!(!caps.unicode_symbols);
        assert_eq!(caps.prompt_chevron(), "> ");
    }

    #[test]
    fn c_locale_forces_ascii() {
        let caps = TerminalCaps::from_env(EnvView {
            colorterm: None,
            lang: Some("C".to_string()),
            ..env()
        });
        assert!(!caps.unicode_symbols, "LANG=C → ASCII fallback");
    }

    #[test]
    fn lc_all_wins_over_lang() {
        // POSIX: LC_ALL overrides LANG.
        let caps = TerminalCaps::from_env(EnvView {
            colorterm: None,
            lc_all: Some("C".to_string()),
            ..env()
        });
        assert!(!caps.unicode_symbols);
    }

    #[test]
    fn utf8_locale_keeps_unicode() {
        let caps = TerminalCaps::from_env(EnvView {
            lang: Some("zh_CN.UTF-8".to_string()),
            ..env()
        });
        assert!(caps.unicode_symbols);
        assert_eq!(caps.prompt_chevron(), "\u{276f} ");
    }

    #[test]
    fn tty_xterm_gets_everything() {
        let caps = TerminalCaps::from_env(env());
        assert!(caps.tty);
        assert!(caps.colors);
        assert!(caps.spinner);
        assert!(caps.bracketed_paste);
        assert!(caps.raw_mode);
        assert!(caps.unicode_symbols);
    }

    // The Windows-legacy-console heuristic — the bug we were fixing.
    // Default conhost ships with fonts that don't have `❯` / `◐`, so
    // bare Windows must fall back to ASCII unless a modern emulator
    // is detected.
    #[test]
    fn windows_legacy_console_falls_back_to_ascii() {
        let caps = TerminalCaps::from_env(EnvView {
            is_windows: true,
            ..env()
        });
        assert!(
            !caps.unicode_symbols,
            "bare Windows (no WT_SESSION / TERM_PROGRAM) → ASCII fallback to avoid ▢ tofu"
        );
        assert_eq!(caps.prompt_chevron(), "> ");
    }

    #[test]
    fn windows_terminal_keeps_unicode() {
        let caps = TerminalCaps::from_env(EnvView {
            is_windows: true,
            wt_session: Some("00000000-0000-0000-0000-000000000000".to_string()),
            ..env()
        });
        assert!(caps.unicode_symbols, "Windows Terminal has Cascadia Code");
    }

    #[test]
    fn windows_vscode_keeps_unicode() {
        let caps = TerminalCaps::from_env(EnvView {
            is_windows: true,
            term_program: Some("vscode".to_string()),
            ..env()
        });
        assert!(caps.unicode_symbols, "VS Code's integrated terminal is fine");
    }

    #[test]
    fn force_unicode_overrides_windows_fallback() {
        // User on conhost installed JetBrains Mono — let them opt back in.
        let caps = TerminalCaps::from_env(EnvView {
            is_windows: true,
            force_unicode: true,
            ..env()
        });
        assert!(caps.unicode_symbols);
    }

    #[test]
    fn force_ascii_beats_force_unicode_when_both_set() {
        // ATOMCODE_ASCII=1 takes priority — explicit "I want ASCII" wins.
        // (force_unicode only flips on, it doesn't override force_ascii.)
        let caps = TerminalCaps::from_env(EnvView {
            force_ascii: true,
            force_unicode: true,
            ..env()
        });
        assert!(
            caps.unicode_symbols,
            "force_unicode currently wins — ATOMCODE_UNICODE is the explicit opt-in escape hatch"
        );
        // Note: if priority needs to flip, change the if/else in
        // `from_env` and update this test. Captured here so the
        // behavior is intentional, not accidental.
    }
}