//! Help text pager integration for CLI help output.
//!
//! Provides pager support for `--help` output, following git's pager precedence:
//! GIT_PAGER → core.pager config → PAGER environment variable → "less" default.
//!
//! # Difference from the diff preview pager
//!
//! This pager is INTERACTIVE (spawned with TTY access) so users can scroll
//! `--help` output. The diff preview pager in `src/commands/picker/pager.rs`
//! runs inside the picker's preview window — a non-TTY context — so it spawns
//! differently:
//!
//! - Help pager: top-level user command, needs a TTY for interactive scrolling
//! - Diff preview pager: pipes the diff over stdin, appends `--paging=never`
//!   for pagers that would otherwise launch their own `less` (delta, bat), and
//!   is killed by a `PAGER_TIMEOUT` watchdog if it blocks — it is never given a
//!   TTY, so it can't hang the picker's event loop
//!
//! Both follow git's pager detection but spawn differently based on their usage context.
//!
//! # Cross-Platform Support
//!
//! On Windows, Git Bash (if available) enables standard pagers like `less`.
//! Without Git Bash, we only use a pager if the configured command works under
//! the PowerShell fallback; otherwise we print directly.

use std::io::{IsTerminal, Write};
use std::process::Stdio;
use worktrunk::shell_exec::ShellConfig;
use worktrunk::styling::print;

use crate::pager::{git_config_pager, parse_pager_value};

/// Detect pager for help output, following git's pager precedence.
///
/// Checks in order: GIT_PAGER → git config core.pager → PAGER → "less"
///
/// On Windows without Git Bash, returns None if only `less` would be selected
/// (since `less` isn't available without Git for Windows).
fn detect_help_pager() -> Option<String> {
    let shell = ShellConfig::get().ok()?;

    // Check environment variables in git's precedence order
    let pager = std::env::var("GIT_PAGER")
        .ok()
        .and_then(|s| parse_pager_value(&s))
        .or_else(git_config_pager)
        .or_else(|| {
            std::env::var("PAGER")
                .ok()
                .and_then(|s| parse_pager_value(&s))
        });

    // If user explicitly configured a pager, use it
    if pager.is_some() {
        return pager;
    }

    // Default to "less" only if we have a POSIX shell (Unix or Git Bash on Windows)
    // Without Git Bash, less isn't typically available on Windows
    if shell.is_posix {
        Some("less".to_string())
    } else {
        tracing::debug!("No POSIX shell available, skipping pager (less not available)");
        None
    }
}

/// Show help text through a pager with TTY access for interactive scrolling.
///
/// The `use_pager` flag controls whether to attempt pager display:
/// - `true` (--help): Uses pager when available and terminal is detected
/// - `false` (-h): Always prints directly to stdout, never uses pager
///
/// This follows git's convention where `-h` never opens a pager (muscle-memory safe)
/// while `--help` uses a pager for longer content.
///
/// Even when `use_pager=true`, falls back to direct output if:
/// - No pager configured (prints to stdout)
/// - stdout is not a TTY (prints to stdout)
/// - Pager spawn fails (prints to stdout)
/// - Pager closes stdin early or wait fails (prints to stdout)
///
/// Help text goes to stdout — POSIX convention (`wt --help | less` should work
/// without redirection), matching `cargo`, `curl`, `python`, `git <cmd> -h`,
/// and `--version` (see #2072).
pub(crate) fn show_help_in_pager(help_text: &str, use_pager: bool) {
    // Short help (-h) never uses a pager
    if !use_pager {
        tracing::debug!("Short help (-h) requested, printing directly to stdout");
        print!("{}", help_text);
        return;
    }

    let Some(pager_cmd) = detect_help_pager() else {
        tracing::debug!("No pager configured, printing help directly to stdout");
        print!("{}", help_text);
        return;
    };

    // Only page when our output destination is a terminal.
    // If stdout is piped/redirected (e.g., `wt --help | grep foo`), print directly.
    if !std::io::stdout().is_terminal() {
        tracing::debug!("stdout is not a TTY, skipping pager");
        print!("{}", help_text);
        return;
    }

    tracing::debug!(pager_cmd = %pager_cmd, "Invoking pager: {}", pager_cmd);
    if let Err(e) = pipe_through_pager(&pager_cmd, help_text) {
        tracing::debug!(error = %e, "Pager failed, falling back to stdout: {}", e);
        print!("{}", help_text);
    }
}

/// Pipe `help_text` to the given pager command and wait for it to exit.
///
/// Spawn / write / wait failures all surface as `Err` so the caller can fall back
/// to direct stdout. Extracted from `show_help_in_pager` so the failure paths can
/// be exercised by unit tests with stand-in commands (`true`, `cat > /dev/null`)
/// that don't depend on a real TTY.
fn pipe_through_pager(pager_cmd: &str, help_text: &str) -> std::io::Result<()> {
    let less_flags = compute_less_flags(std::env::var("LESS").ok().as_deref());
    let shell = ShellConfig::get().map_err(std::io::Error::other)?;
    tracing::debug!(pager_cmd = %pager_cmd, "$ {} (pager)", pager_cmd);
    let mut cmd = shell.command(pager_cmd);
    worktrunk::shell_exec::scrub_directive_env_vars(&mut cmd);
    let mut child = cmd.stdin(Stdio::piped()).env("LESS", &less_flags).spawn()?;
    if let Some(mut stdin) = child.stdin.take() {
        stdin.write_all(help_text.as_bytes())?;
    }
    child.wait()?;
    Ok(())
}

/// Compute LESS flags by appending our required flags to user's existing LESS setting.
///
/// Returns flags suitable for setting LESS env var when spawning less.
/// Ensures F (quit if one screen), R (colors), X (no termcap init) are always active.
fn compute_less_flags(user_less: Option<&str>) -> String {
    compute_less_flags_for(user_less, cfg!(windows))
}

/// Platform-parameterized core of [`compute_less_flags`], split out so both
/// branches are testable on any host.
///
/// On Windows we additionally pass `-K` (`--quit-on-intr`) so that pressing
/// Ctrl-C makes `less` exit cleanly — running its normal deinit that restores
/// the console input mode — instead of returning to its own prompt. `wt` has
/// no Windows console Ctrl-C handler (signal forwarding is Unix-only, see
/// `signal_forwarder.rs`), so a Ctrl-C otherwise terminates both `less` and
/// the blocked `wt` parent before the console mode is restored, leaving the
/// terminal wedged (#2968). `-K` routes Ctrl-C through the same clean-exit
/// path as quitting with `q`, which restores the terminal correctly.
///
/// `-K` is Windows-only on purpose: on Unix the signal forwarder coordinates a
/// clean teardown, and many users rely on Ctrl-C returning `less` to its
/// prompt rather than quitting.
fn compute_less_flags_for(user_less: Option<&str>, windows: bool) -> String {
    let base = user_less.unwrap_or_default();
    if windows {
        format!("{base} -FRX -K")
    } else {
        format!("{base} -FRX")
    }
}

#[cfg(test)]
mod tests {
    use super::{compute_less_flags_for, parse_pager_value};

    /// `cat > /dev/null` exercises the full spawn → write → wait path with a
    /// stand-in pager that always succeeds. Covers every line of
    /// `pipe_through_pager`'s body; the `?` propagations are line-hit even on
    /// the Ok branch.
    #[cfg(unix)]
    #[test]
    fn test_pipe_through_pager_pipes_to_real_command() {
        super::pipe_through_pager("cat > /dev/null", "help text").expect("cat should succeed");
    }

    #[test]
    fn test_validate_excludes_cat() {
        assert_eq!(parse_pager_value("cat"), None);
        assert_eq!(parse_pager_value("  cat  "), None);
        assert_eq!(parse_pager_value(""), None);
        assert_eq!(parse_pager_value("  "), None);
    }

    #[test]
    fn test_validate_accepts_valid_pagers() {
        assert_eq!(parse_pager_value("less"), Some("less".to_string()));
        assert_eq!(parse_pager_value("  less  "), Some("less".to_string()));
        assert_eq!(parse_pager_value("delta"), Some("delta".to_string()));
        assert_eq!(parse_pager_value("less -R"), Some("less -R".to_string()));
    }

    #[test]
    fn test_compute_less_flags_empty() {
        // Leading space is fine - less ignores it
        assert_eq!(compute_less_flags_for(None, false), " -FRX");
        assert_eq!(compute_less_flags_for(Some(""), false), " -FRX");
    }

    #[test]
    fn test_compute_less_flags_short_options() {
        // Common case: user has -R (oh-my-zsh default)
        assert_eq!(compute_less_flags_for(Some("-R"), false), "-R -FRX");
        // User has multiple short flags
        assert_eq!(compute_less_flags_for(Some("-iMRS"), false), "-iMRS -FRX");
    }

    #[test]
    fn test_compute_less_flags_long_options() {
        // Issue #594: --mouse must not become --mouseFRX
        assert_eq!(
            compute_less_flags_for(Some("--mouse"), false),
            "--mouse -FRX"
        );
        // Multiple long options
        assert_eq!(
            compute_less_flags_for(Some("--mouse --shift=4"), false),
            "--mouse --shift=4 -FRX"
        );
    }

    #[test]
    fn test_compute_less_flags_mixed() {
        assert_eq!(
            compute_less_flags_for(Some("-R --mouse"), false),
            "-R --mouse -FRX"
        );
    }

    #[test]
    fn test_compute_less_flags_windows_appends_quit_on_intr() {
        // #2968: on Windows we add -K so Ctrl-C makes less exit cleanly and
        // restore the console, instead of wedging the terminal.
        assert_eq!(compute_less_flags_for(None, true), " -FRX -K");
        assert_eq!(compute_less_flags_for(Some("-R"), true), "-R -FRX -K");
        assert_eq!(
            compute_less_flags_for(Some("--mouse"), true),
            "--mouse -FRX -K"
        );
    }
}
