use super::*;
use std::cell::RefCell;
use std::collections::{HashMap, HashSet};
use std::ffi::OsString;

/// Records every `BootstrapEnv` call and scripts responses. `available`
/// drives `has_command`. `prefixes` drives `brew --prefix`.
/// `install_adds` mutates `available` when `brew install <pkg>` runs.
/// `run_adds` mutates `available` for non-brew commands keyed by the
/// full "<cmd> <args>" string.
/// `path_calls` counts how many times `augmented_path()` was called —
/// used by the C2 spawn-coverage test.
#[derive(Default)]
struct MockEnv {
    available: RefCell<HashSet<String>>,
    prefixes: HashMap<String, String>,
    install_adds: HashMap<String, Vec<String>>,
    run_adds: HashMap<String, Vec<String>>,
    log: RefCell<Vec<String>>,
    path_calls: RefCell<u32>,
}

impl MockEnv {
    fn with_brew() -> Self {
        let e = Self::default();
        e.available.borrow_mut().insert("brew".into());
        e
    }
    fn add_available(self, name: &str) -> Self {
        self.available.borrow_mut().insert(name.into());
        self
    }
    fn with_prefix(mut self, pkg: &str, prefix: &str) -> Self {
        self.prefixes.insert(pkg.into(), prefix.into());
        self
    }
    /// "After `brew install <pkg>` succeeds, these binaries are on PATH."
    fn install_adds(mut self, pkg: &str, adds: &[&str]) -> Self {
        self.install_adds
            .insert(pkg.into(), adds.iter().map(|s| s.to_string()).collect());
        self
    }
    /// "After `<cmd> <args>` succeeds, these binaries are on PATH."
    fn run_adds(mut self, key: &str, adds: &[&str]) -> Self {
        self.run_adds
            .insert(key.into(), adds.iter().map(|s| s.to_string()).collect());
        self
    }
}

impl BootstrapEnv for MockEnv {
    fn has_command(&self, name: &str) -> bool {
        let avail = self.available.borrow().contains(name);
        self.log
            .borrow_mut()
            .push(format!("has_command({name})={avail}"));
        avail
    }
    fn run_status(&self, cmd: &str, args: &[&str]) -> Result<bool> {
        let key = format!("{cmd} {}", args.join(" "));
        // Mirror RealEnv: call augmented_path() once per spawn so the
        // C2 coverage tests can verify the spawn count equals path_calls.
        self.augmented_path();
        self.log.borrow_mut().push(format!("run_status({key})"));
        if cmd == "brew"
            && args.first() == Some(&"install")
            && let Some(pkg) = args.get(1)
            && let Some(adds) = self.install_adds.get(*pkg)
        {
            for a in adds {
                self.available.borrow_mut().insert(a.clone());
            }
        } else if let Some(adds) = self.run_adds.get(&key) {
            for a in adds {
                self.available.borrow_mut().insert(a.clone());
            }
        }
        Ok(true)
    }
    fn run_output(&self, cmd: &str, args: &[&str]) -> Result<(bool, Vec<u8>)> {
        let key = format!("{cmd} {}", args.join(" "));
        // Mirror RealEnv: call augmented_path() once per spawn.
        self.augmented_path();
        self.log.borrow_mut().push(format!("run_output({key})"));
        if cmd == "brew"
            && args.first() == Some(&"--prefix")
            && let Some(pkg) = args.get(1)
            && let Some(p) = self.prefixes.get(*pkg)
        {
            return Ok((true, p.as_bytes().to_vec()));
        }
        Ok((false, vec![]))
    }
    fn prepend_path(&self, dir: &std::path::Path) {
        self.log
            .borrow_mut()
            .push(format!("prepend_path({})", dir.display()));
    }
    fn assert_online(&self, _action: &str) -> Result<()> {
        Ok(())
    }
    fn augmented_path(&self) -> OsString {
        *self.path_calls.borrow_mut() += 1;
        self.log.borrow_mut().push("augmented_path()".into());
        // Return a sentinel value so tests can assert it was forwarded.
        OsString::from("/mock-augmented-path")
    }
}

fn ctx() -> InstallCtx<'static> {
    InstallCtx {
        bin_dir: std::path::Path::new("/tmp/ckg-bin"),
        yes: true,
    }
}

/// **C-RUSTUP regression pin.** brew installs `rustup-init`; only the
/// post-install hook converts that into `rustup`. If the implementation
/// ever moves the final `has_command(self.binary)` check ahead of
/// `post_install`, this test fails.
#[test]
fn rust_post_install_runs_before_final_binary_check() {
    let env = MockEnv::with_brew()
        .with_prefix("rustup-init", "/opt/homebrew/opt/rustup-init")
        .install_adds("rustup-init", &["rustup-init"])
        .run_adds(
            "rustup-init -y --no-modify-path --default-toolchain stable",
            &["rustup"],
        );
    RuntimeBootstrap::for_rust()
        .ensure_with(&ctx(), &env)
        .expect("rust bootstrap should succeed end-to-end");
    let log = env.log.borrow();
    let post_install_idx = log
        .iter()
        .position(|e| e.contains("rustup-init -y"))
        .expect("post_install rustup-init -y must run");
    let final_check_idx = log
        .iter()
        .rposition(|e| e == "has_command(rustup)=true")
        .expect("final has_command(rustup) must succeed");
    assert!(
        post_install_idx < final_check_idx,
        "post_install must run BEFORE the final has_command(rustup) check.\nlog: {:#?}",
        *log
    );
}

#[test]
fn already_installed_short_circuits() {
    let env = MockEnv::default().add_available("rustup");
    RuntimeBootstrap::for_rust()
        .ensure_with(&ctx(), &env)
        .unwrap();
    let log = env.log.borrow();
    assert_eq!(log.len(), 1, "must short-circuit on first has_command");
    assert_eq!(log[0], "has_command(rustup)=true");
}

#[test]
fn ruby_keg_only_prefix_is_exposed() {
    // Ruby is keg-only on macOS. After `brew install ruby`, `gem` is
    // NOT on the standard brew bin dir — it's at `<brew --prefix
    // ruby>/bin/gem`. `expose_brew_pkg_bin` must call `brew --prefix
    // ruby` and prepend that bin dir.
    let env = MockEnv::with_brew()
        .with_prefix("ruby", "/opt/homebrew/opt/ruby")
        .install_adds("ruby", &["gem"]);
    RuntimeBootstrap::for_ruby()
        .ensure_with(&ctx(), &env)
        .unwrap();
    let log = env.log.borrow();
    assert!(
        log.iter().any(|e| e == "run_output(brew --prefix ruby)"),
        "must query brew --prefix ruby"
    );
    assert!(
        log.iter()
            .any(|e| e == "prepend_path(/opt/homebrew/opt/ruby/bin)"),
        "must prepend the keg-only bin dir"
    );
}

#[test]
fn bails_when_brew_install_succeeds_but_binary_missing() {
    // brew install reports success but `gem` never becomes available.
    let env = MockEnv::with_brew().with_prefix("ruby", "/opt/homebrew/opt/ruby");
    let err = RuntimeBootstrap::for_ruby()
        .ensure_with(&ctx(), &env)
        .expect_err("must bail when post-install check fails");
    let msg = format!("{err}");
    assert!(msg.contains("not found on PATH"), "msg: {msg}");
    assert!(msg.contains("gem"), "msg: {msg}");
}

#[test]
fn node_bootstrap_no_post_install() {
    let env = MockEnv::with_brew()
        .with_prefix("node", "/opt/homebrew/opt/node")
        .install_adds("node", &["npm"]);
    RuntimeBootstrap::for_node()
        .ensure_with(&ctx(), &env)
        .unwrap();
    let log = env.log.borrow();
    // No post_install for node — must NOT spawn rustup-init or similar.
    assert!(
        !log.iter().any(|e| e.contains("rustup-init")),
        "node bootstrap must not run rust post-install"
    );
}

/// **C2 regression pin — augmented_path() reaches every bootstrap spawn.**
///
/// `MockEnv` counts every `augmented_path()` call via `path_calls`. Both
/// `run_status` and `run_output` in `RealEnv` call `build_augmented_path()`
/// once per spawn, so `path_calls` must equal the number of subprocess
/// spawns. If a future refactor adds a raw `Command::new` that bypasses
/// `BootstrapEnv`, the `run_status`/`run_output` wrappers won't be called
/// and this assertion will fail.
#[test]
fn bootstrap_every_spawn_uses_augmented_path_ruby() {
    let env = MockEnv::with_brew()
        .with_prefix("ruby", "/opt/homebrew/opt/ruby")
        .install_adds("ruby", &["gem"]);
    RuntimeBootstrap::for_ruby()
        .ensure_with(&ctx(), &env)
        .expect("ruby bootstrap should succeed");
    let log = env.log.borrow();
    let augmented_count = log.iter().filter(|e| e.as_str() == "augmented_path()").count();
    let spawn_count = log
        .iter()
        .filter(|e| e.starts_with("run_status(") || e.starts_with("run_output("))
        .count();
    assert!(spawn_count > 0, "test must exercise at least one spawn");
    assert_eq!(
        augmented_count, spawn_count,
        "augmented_path() must be called exactly once per spawn (ruby path). \
         spawns={spawn_count} augmented_path_calls={augmented_count}\nlog: {log:#?}"
    );
}

#[test]
fn bootstrap_every_spawn_uses_augmented_path_rust() {
    let env = MockEnv::with_brew()
        .with_prefix("rustup-init", "/opt/homebrew/opt/rustup-init")
        .install_adds("rustup-init", &["rustup-init"])
        .run_adds(
            "rustup-init -y --no-modify-path --default-toolchain stable",
            &["rustup"],
        );
    RuntimeBootstrap::for_rust()
        .ensure_with(&ctx(), &env)
        .expect("rust bootstrap should succeed");
    let log = env.log.borrow();
    let augmented_count = log.iter().filter(|e| e.as_str() == "augmented_path()").count();
    let spawn_count = log
        .iter()
        .filter(|e| e.starts_with("run_status(") || e.starts_with("run_output("))
        .count();
    assert!(spawn_count > 0, "test must exercise at least one spawn");
    assert_eq!(
        augmented_count, spawn_count,
        "augmented_path() must be called exactly once per spawn (rust path). \
         spawns={spawn_count} augmented_path_calls={augmented_count}\nlog: {log:#?}"
    );
}
