//! B3 — the gate false-accept gauntlet (deterministic, seeded).
//!
//! The GO/NO-GO experiment for the Foreman thesis (Path B): since the
//! merge-verify gate is the entire soundness story, its **false-accept rate** is
//! the number that decides whether the design survives. This runs a fixed set of
//! hand-crafted adversarial scenarios — each with a known-correct expectation —
//! through the real B1 gate + B2 harness (per-worktree and union), with NO
//! external CLIs and NO model calls. Fully reproducible, zero cost.
//!
//! ## What actually does the catching (so the claim isn't oversold)
//!
//! Different broken scenarios are caught by different legs, on purpose:
//! - **duplicate definition** → the gate's AST duplicate scan (rustc never runs;
//!   the AST failure short-circuits the build).
//! - **overlapping edit** → a loud `git apply` conflict at integration.
//! - **cross-symbol type break / orphan-target break** → the **build gate**
//!   (`cargo check --all-targets`) on the integrated union — the *only* legs that
//!   exercise the compiler on the integration path, and the reason the verifier
//!   must be `cargo --all-targets` (a single-file `rustc` is blind to
//!   `src/bin`, `tests/`, examples and `Cargo.toml`).
//!
//! A **false accept** is the only fatal outcome: the gate marked a broken thing
//! safe to integrate. The test asserts zero false-accepts AND that every
//! scenario reached its expected verdict (so it cannot pass while vacuous — an
//! earlier `rustc -o /dev/null` version rejected everything and still showed
//! false_accepts=0; the `passed == N` assertion is the tripwire against that).
//!
//! Unix-only: the build gate is driven through `sh`. The cross-platform / real
//! multi-repo evaluation is B7.

#![cfg(unix)]

use std::collections::HashMap;
use std::path::Path;
use std::process::Command;

use async_trait::async_trait;
use car_multi::{
    integrate_and_verify, run_farm_out, AgentRunSummary, FarmOutConfig, ForemanError, SharedInfra,
    Subtask, WorktreeAgent, WorktreeAgentRequest,
};

/// (relative path, full file content).
type Files = Vec<(&'static str, &'static str)>;

struct SeededSubtask {
    id: &'static str,
    files: &'static [&'static str],
    edits: Files,
}

#[derive(Clone, Copy, Debug)]
enum Expect {
    /// Every subtask accepted alone AND the union integrates cleanly.
    GoodIntegrates,
    /// Every subtask accepted alone, but the UNION must NOT integrate cleanly.
    BrokenUnion,
    /// The (single) broken subtask must be rejected per-worktree.
    BrokenPerWorktree,
}

struct Scenario {
    name: &'static str,
    base: Files,
    subtasks: Vec<SeededSubtask>,
    expect: Expect,
}

struct SeededAgent {
    by_id: HashMap<String, Files>,
}

#[async_trait]
impl WorktreeAgent for SeededAgent {
    async fn run_in(
        &self,
        req: &WorktreeAgentRequest<'_>,
    ) -> Result<AgentRunSummary, ForemanError> {
        if let Some(edits) = self.by_id.get(&req.subtask.id) {
            for (path, content) in edits {
                write(req.cwd, path, content).map_err(|e| ForemanError::Agent(e.to_string()))?;
            }
        }
        Ok(AgentRunSummary::default())
    }
}

fn write(root: &Path, path: &str, content: &str) -> std::io::Result<()> {
    let target = root.join(path);
    if let Some(parent) = target.parent() {
        std::fs::create_dir_all(parent)?;
    }
    std::fs::write(target, content)
}

fn git_ok(cwd: &Path, args: &[&str]) {
    let out = Command::new("git")
        .args(args)
        .current_dir(cwd)
        .output()
        .unwrap();
    assert!(
        out.status.success(),
        "git {args:?}: {}",
        String::from_utf8_lossy(&out.stderr)
    );
}

fn init_repo(base: &Files) -> tempfile::TempDir {
    let dir = tempfile::tempdir().unwrap();
    let root = dir.path();
    git_ok(root, &["init", "-q", "-b", "main"]);
    git_ok(root, &["config", "user.email", "t@t.t"]);
    git_ok(root, &["config", "user.name", "t"]);
    for (path, content) in base {
        write(root, path, content).unwrap();
    }
    git_ok(root, &["add", "-A"]);
    git_ok(root, &["commit", "-q", "-m", "base"]);
    dir
}

/// A real build gate over the WHOLE crate: `cargo check --all-targets` sees the
/// lib, bins, tests and examples (not just `src/lib.rs`), and any cargo
/// invocation also validates `Cargo.toml`. The build dir is a fresh `mktemp -d`
/// per invocation so concurrent subtasks don't race and nothing lands in the
/// worktree.
fn cargo_verify() -> Vec<String> {
    vec![
        "sh".into(),
        "-c".into(),
        "CARGO_TARGET_DIR=\"$(mktemp -d)\" cargo check --all-targets --quiet".into(),
    ]
}

const CARGO_TOML: &str = "\
[package]
name = \"gauntlet\"
version = \"0.0.0\"
edition = \"2021\"

[lib]
path = \"src/lib.rs\"
";

fn scenarios() -> Vec<Scenario> {
    vec![
        // ---- GOOD ----
        Scenario {
            name: "good_single_clean_edit",
            base: vec![("Cargo.toml", CARGO_TOML), ("src/lib.rs", "pub fn original() {}\n")],
            subtasks: vec![SeededSubtask {
                id: "a",
                files: &["src/lib.rs"],
                edits: vec![("src/lib.rs", "pub fn original() {}\npub fn added() {}\n")],
            }],
            expect: Expect::GoodIntegrates,
        },
        Scenario {
            name: "good_disjoint_additions_two_files",
            base: vec![
                ("Cargo.toml", CARGO_TOML),
                ("src/lib.rs", "pub mod a;\npub mod b;\n"),
                ("src/a.rs", "pub fn a() {}\n"),
                ("src/b.rs", "pub fn b() {}\n"),
            ],
            subtasks: vec![
                SeededSubtask {
                    id: "a",
                    files: &["src/a.rs"],
                    edits: vec![("src/a.rs", "pub fn a() {}\npub fn a2() {}\n")],
                },
                SeededSubtask {
                    id: "b",
                    files: &["src/b.rs"],
                    edits: vec![("src/b.rs", "pub fn b() {}\npub fn b2() {}\n")],
                },
            ],
            expect: Expect::GoodIntegrates,
        },
        // ---- BROKEN UNION ----
        Scenario {
            // Both add `fn dup` at opposite ends of a padded file → disjoint
            // hunks both apply → two definitions. Caught by the AST scan.
            name: "broken_union_duplicate_definition",
            base: vec![
                ("Cargo.toml", CARGO_TOML),
                (
                    "src/lib.rs",
                    "pub fn top() {}\npub fn p1() {}\npub fn p2() {}\npub fn p3() {}\npub fn p4() {}\npub fn bot() {}\n",
                ),
            ],
            subtasks: vec![
                SeededSubtask {
                    id: "a",
                    files: &["src/lib.rs"],
                    edits: vec![(
                        "src/lib.rs",
                        "pub fn top() {}\npub fn dup() {}\npub fn p1() {}\npub fn p2() {}\npub fn p3() {}\npub fn p4() {}\npub fn bot() {}\n",
                    )],
                },
                SeededSubtask {
                    id: "b",
                    files: &["src/lib.rs"],
                    edits: vec![(
                        "src/lib.rs",
                        "pub fn top() {}\npub fn p1() {}\npub fn p2() {}\npub fn p3() {}\npub fn p4() {}\npub fn dup() {}\npub fn bot() {}\n",
                    )],
                },
            ],
            expect: Expect::BrokenUnion,
        },
        Scenario {
            // Both rewrite the SAME line differently → loud apply conflict.
            name: "broken_union_overlapping_edit",
            base: vec![("Cargo.toml", CARGO_TOML), ("src/lib.rs", "pub fn original() -> u8 { 0 }\n")],
            subtasks: vec![
                SeededSubtask {
                    id: "a",
                    files: &["src/lib.rs"],
                    edits: vec![("src/lib.rs", "pub fn original() -> u8 { 1 }\n")],
                },
                SeededSubtask {
                    id: "b",
                    files: &["src/lib.rs"],
                    edits: vec![("src/lib.rs", "pub fn original() -> u8 { 2 }\n")],
                },
            ],
            expect: Expect::BrokenUnion,
        },
        Scenario {
            // The cargo-only case: each subtask is well-typed ALONE; the union is
            // ill-typed. A edits `get`+its own caller to u16 (compiles); B adds a
            // NEW caller (in b.rs, disjoint) that relies on the base u8 type.
            // Union: get->u16 but b's caller expects u8 -> type error only the
            // build gate can see. No duplicate, no apply conflict.
            name: "broken_union_type_break",
            base: vec![
                ("Cargo.toml", CARGO_TOML),
                (
                    "src/lib.rs",
                    "pub mod consumer;\npub fn get() -> u8 { 1 }\npub fn here() { let _x: u8 = get(); }\n",
                ),
                ("src/consumer.rs", "// extended by subtask b\n"),
            ],
            subtasks: vec![
                SeededSubtask {
                    id: "a",
                    files: &["src/lib.rs"],
                    edits: vec![(
                        "src/lib.rs",
                        "pub mod consumer;\npub fn get() -> u16 { 1 }\npub fn here() { let _x: u16 = get(); }\n",
                    )],
                },
                SeededSubtask {
                    id: "b",
                    files: &["src/consumer.rs"],
                    edits: vec![(
                        "src/consumer.rs",
                        "pub fn use_get() { let _y: u8 = crate::get(); }\n",
                    )],
                },
            ],
            expect: Expect::BrokenUnion,
        },
        Scenario {
            // Orphan-target break: a single-file `rustc src/lib.rs` would MISS
            // this; `cargo check --all-targets` catches it. A renames `helper` in
            // the lib (no caller in base, so A is fine alone). B adds a NEW BIN
            // that calls `helper` (fine alone — base still has helper). Union:
            // helper renamed away but the bin calls it -> only --all-targets sees
            // the bin.
            name: "broken_union_orphan_target_break",
            base: vec![
                ("Cargo.toml", CARGO_TOML),
                ("src/lib.rs", "pub fn helper() -> u8 { 1 }\n"),
            ],
            subtasks: vec![
                SeededSubtask {
                    id: "a",
                    files: &["src/lib.rs"],
                    edits: vec![("src/lib.rs", "pub fn helper_renamed() -> u8 { 1 }\n")],
                },
                SeededSubtask {
                    id: "b",
                    files: &["src/bin/tool.rs"],
                    edits: vec![("src/bin/tool.rs", "fn main() { let _ = gauntlet::helper(); }\n")],
                },
            ],
            expect: Expect::BrokenUnion,
        },
        // ---- BROKEN PER-WORKTREE ----
        Scenario {
            name: "broken_per_worktree_type_error",
            base: vec![("Cargo.toml", CARGO_TOML), ("src/lib.rs", "pub fn ok() {}\n")],
            subtasks: vec![SeededSubtask {
                id: "a",
                files: &["src/lib.rs"],
                edits: vec![("src/lib.rs", "pub fn ok() {}\npub fn bad() { let _x: u8 = \"nope\"; }\n")],
            }],
            expect: Expect::BrokenPerWorktree,
        },
    ]
}

#[derive(Default, Debug)]
struct Tally {
    false_accepts: usize,
    false_rejects: usize,
    passed: usize,
}

#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
async fn foreman_false_accept_gauntlet() {
    // cargo is always present under `cargo test`; if not, that's a real failure,
    // not a reason to silently pass.
    assert!(
        Command::new("cargo").arg("--version").output().is_ok(),
        "cargo must be on PATH for the gauntlet build gate"
    );

    let verify = cargo_verify();
    let mut tally = Tally::default();
    let mut rows: Vec<String> = Vec::new();
    let all = scenarios();
    let total = all.len();

    for sc in all {
        let repo = init_repo(&sc.base);
        let infra = SharedInfra::new();
        let by_id: HashMap<String, Files> = sc
            .subtasks
            .iter()
            .map(|s| (s.id.to_string(), s.edits.clone()))
            .collect();
        let agent = SeededAgent { by_id };
        let subtasks: Vec<Subtask> = sc
            .subtasks
            .iter()
            .map(|s| {
                Subtask::files_only(
                    s.id,
                    format!("seeded {}", s.id),
                    s.files.iter().map(|f| f.to_string()).collect(),
                )
            })
            .collect();
        let config = FarmOutConfig {
            verify_command: Some(verify.clone()),
            ..Default::default()
        };

        let farmed = run_farm_out(repo.path(), &subtasks, &agent, &config, &infra).await;
        let accepted: Vec<(String, String)> = farmed
            .outcomes
            .iter()
            .filter(|o| o.is_accepted())
            .filter_map(|o| o.patch.clone().map(|p| (o.subtask_id.clone(), p)))
            .collect();
        let all_accepted = farmed.accepted_count() == subtasks.len();

        let (verdict, class) = match sc.expect {
            Expect::BrokenPerWorktree => {
                if farmed.accepted_count() == 0 {
                    ("rejected per-worktree", Ok(()))
                } else {
                    (
                        "ACCEPTED a per-worktree-broken subtask",
                        Err("false_accept"),
                    )
                }
            }
            Expect::GoodIntegrates => {
                if !all_accepted {
                    ("good subtask wrongly rejected", Err("false_reject"))
                } else {
                    let integ =
                        integrate_and_verify(repo.path(), sc.name, &accepted, &config, &infra)
                            .await
                            .unwrap();
                    if integ.integrated_cleanly() {
                        ("integrated cleanly", Ok(()))
                    } else {
                        ("good union wrongly rejected", Err("false_reject"))
                    }
                }
            }
            Expect::BrokenUnion => {
                if !all_accepted {
                    ("subtask rejected before union (safe)", Ok(()))
                } else {
                    let integ =
                        integrate_and_verify(repo.path(), sc.name, &accepted, &config, &infra)
                            .await
                            .unwrap();
                    if integ.integrated_cleanly() {
                        (
                            "UNION wrongly integrated a broken merge",
                            Err("false_accept"),
                        )
                    } else {
                        ("union correctly rejected", Ok(()))
                    }
                }
            }
        };

        match class {
            Ok(()) => tally.passed += 1,
            Err("false_accept") => tally.false_accepts += 1,
            Err("false_reject") => tally.false_rejects += 1,
            Err(_) => {}
        }
        rows.push(format!(
            "  [{}] {:42} expect={:?} -> {}",
            if class.is_ok() { "ok" } else { "!!" },
            sc.name,
            sc.expect,
            verdict
        ));
    }

    eprintln!("\n=== Foreman false-accept gauntlet ===");
    for r in &rows {
        eprintln!("{r}");
    }
    eprintln!(
        "\n  scenarios={total} passed={} false_accepts={} false_rejects={}\n",
        tally.passed, tally.false_accepts, tally.false_rejects
    );

    // GO/NO-GO. Any false accept is fatal. `passed == total` is the
    // anti-vacuity tripwire: a verifier that rejects everything would surface as
    // false_rejects on the good scenarios and fail here, not pass silently.
    assert_eq!(
        tally.false_accepts, 0,
        "GATE FALSE-ACCEPT — broken merge marked safe; thesis NO-GO"
    );
    assert_eq!(
        tally.passed, total,
        "not every scenario reached its expected verdict ({} false_rejects) — gauntlet is not discriminating",
        tally.false_rejects
    );
}
