//! Interactive prompt for the runtime-bootstrap flow.
//!
//! `decide` is split out of `bootstrap.rs` so it can be unit-tested with
//! injected stdin / stdout / tty-status, instead of relying on the real
//! file descriptors that are not controllable from `cargo test`.

use std::io::{BufRead, IsTerminal, Write};

use anyhow::{Context, Result, bail};

#[derive(Debug)]
pub(super) enum Decision {
    Proceed,
    Decline,
}

/// Decide whether to proceed with a bootstrap install. Wraps the production
/// stdin/stdout (and TTY check); tests use [`decide_with`] directly.
pub(super) fn decide(yes: bool, label: &str, pkg: &str) -> Result<Decision> {
    let stdin = std::io::stdin();
    let is_tty = stdin.is_terminal();
    let stdout = std::io::stdout();
    decide_with(yes, label, pkg, is_tty, &mut stdin.lock(), &mut stdout.lock())
}

/// Pure form of [`decide`] — all I/O dependencies are arguments. The
/// production caller supplies real stdin / stdout / `IsTerminal`; tests
/// pass byte slices and arbitrary `bool` for `is_tty`.
fn decide_with(
    yes: bool,
    label: &str,
    pkg: &str,
    is_tty: bool,
    reader: &mut dyn BufRead,
    writer: &mut dyn Write,
) -> Result<Decision> {
    if yes {
        return Ok(Decision::Proceed);
    }
    if !is_tty {
        bail!(
            "{label} not installed and stdin not a TTY; pass `--yes` to auto-confirm \
             runtime install via brew"
        );
    }
    write!(writer, "Install {pkg} via brew? [Y/n] ").ok();
    writer.flush().ok();
    let mut line = String::new();
    reader.read_line(&mut line).context("read prompt")?;
    let ans = line.trim().to_ascii_lowercase();
    if ans.is_empty() || ans == "y" || ans == "yes" {
        Ok(Decision::Proceed)
    } else {
        Ok(Decision::Decline)
    }
}

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

    fn run(yes: bool, is_tty: bool, input: &[u8]) -> Result<(Decision, String)> {
        let mut reader = Cursor::new(input);
        let mut writer = Vec::<u8>::new();
        let decision = decide_with(yes, "ruby", "ruby", is_tty, &mut reader, &mut writer)?;
        Ok((decision, String::from_utf8(writer).unwrap()))
    }

    fn proceeded(d: Decision) -> bool {
        matches!(d, Decision::Proceed)
    }

    #[test]
    fn yes_flag_skips_prompt_and_proceeds() {
        let (d, out) = run(true, false, b"").unwrap();
        assert!(proceeded(d));
        assert!(out.is_empty(), "no prompt should be written");
    }

    #[test]
    fn yes_flag_overrides_non_tty() {
        let (d, _) = run(true, false, b"").unwrap();
        assert!(proceeded(d));
    }

    #[test]
    fn empty_input_defaults_to_proceed() {
        let (d, out) = run(false, true, b"\n").unwrap();
        assert!(proceeded(d));
        assert!(out.contains("[Y/n]"));
    }

    #[test]
    fn lowercase_y_proceeds() {
        let (d, _) = run(false, true, b"y\n").unwrap();
        assert!(proceeded(d));
    }

    #[test]
    fn uppercase_y_proceeds() {
        let (d, _) = run(false, true, b"Y\n").unwrap();
        assert!(proceeded(d));
    }

    #[test]
    fn yes_word_proceeds() {
        let (d, _) = run(false, true, b"yes\n").unwrap();
        assert!(proceeded(d));
    }

    #[test]
    fn yes_word_with_whitespace_proceeds() {
        let (d, _) = run(false, true, b"  YES  \n").unwrap();
        assert!(proceeded(d));
    }

    #[test]
    fn n_declines() {
        let (d, _) = run(false, true, b"n\n").unwrap();
        assert!(!proceeded(d));
    }

    #[test]
    fn no_word_declines() {
        let (d, _) = run(false, true, b"no\n").unwrap();
        assert!(!proceeded(d));
    }

    #[test]
    fn arbitrary_word_declines() {
        let (d, _) = run(false, true, b"maybe\n").unwrap();
        assert!(!proceeded(d));
    }

    #[test]
    fn non_tty_without_yes_bails() {
        let mut reader = Cursor::new(&b""[..]);
        let mut writer = Vec::<u8>::new();
        let err = decide_with(false, "ruby", "ruby", false, &mut reader, &mut writer)
            .expect_err("must bail on non-TTY without --yes");
        let msg = format!("{err}");
        assert!(msg.contains("not a TTY"), "msg: {msg}");
        assert!(msg.contains("--yes"), "msg: {msg}");
    }
}
