//! [H1c] Gate de evidencia en el orquestador contra un bus stub.
//!
//! El lab (aquí, un stub) ejecuta la evidencia tras sellar el artefacto y devuelve el veredicto
//! en `BuildReady`. Si algún check falla, `Orchestrator::run` debe ABORTAR con "no se propone" y
//! NO hidratar nada al `prefix` — el gate "falla ⇒ no se propone" del SDD 15 §H1.

use std::io::{BufRead, BufReader, Write};
use std::os::unix::net::{UnixListener, UnixStream};
use std::path::PathBuf;
use std::thread;
use std::time::Duration;

use hammer_agent::orchestrator::{ApplyTarget, CompileMode, Orchestrator};
use hammer_agent::MockTranslator;
use hammer_core::proto::{
    Cap, Command, Event, EvidenceCheckVerdict, EvidenceVerdict, Peer, PROTOCOL_VERSION,
};
use hammer_core::swm::{Base, Mutation, Swm, SwmBuild};
use hammer_core::{BaseRef, Evidence, EvidenceCheck, EvidenceKind};
use std::collections::BTreeMap;

/// Bus stub que responde a `Compile` con `BuildReady` cuyo veredicto tiene `all_passed=false`.
fn start_failing_bus(sock: PathBuf) {
    let _ = std::fs::remove_file(&sock);
    let listener = UnixListener::bind(&sock).expect("bind stub");
    thread::spawn(move || {
        if let Some(Ok(stream)) = listener.incoming().next() {
            thread::spawn(move || serve(stream));
        }
    });
}

fn serve(stream: UnixStream) {
    let mut writer = stream.try_clone().unwrap();
    let mut reader = BufReader::new(stream);
    let send = |w: &mut UnixStream, ev: &Event| {
        w.write_all(serde_json::to_string(ev).unwrap().as_bytes()).unwrap();
        w.write_all(b"\n").unwrap();
        w.flush().unwrap();
    };

    let mut first = String::new();
    reader.read_line(&mut first).unwrap();
    let Ok(Command::Hello { .. }) = serde_json::from_str::<Command>(first.trim()) else {
        return;
    };
    send(
        &mut writer,
        &Event::Welcome {
            ver: PROTOCOL_VERSION,
            caps: vec![Cap::Query, Cap::Compile, Cap::Inject, Cap::Init],
            peer: Peer { uid: 1000, gid: 1000, pid: 1 },
        },
    );

    loop {
        let mut line = String::new();
        match reader.read_line(&mut line) {
            Ok(0) | Err(_) => break,
            Ok(_) => {}
        }
        if line.trim().is_empty() {
            continue;
        }
        let Ok(cmd) = serde_json::from_str::<Command>(line.trim()) else {
            continue;
        };
        if let Command::Compile { recipe } = cmd {
            send(
                &mut writer,
                &Event::BuildReady {
                    recipe: recipe.name.clone(),
                    artifact: "b3:stub-fail".into(),
                    verdict: Some(EvidenceVerdict {
                        all_passed: false,
                        max_level: None,
                        checks: vec![EvidenceCheckVerdict {
                            kind: EvidenceKind::CmdExit,
                            cmd: "grep --version".into(),
                            passed: false,
                            detail: "exit 7 ≠ esperado 0".into(),
                        }],
                    }),
                },
            );
        }
    }
}

fn swm_with_evidence() -> Swm {
    Swm {
        swm_version: 1,
        base: Base { distro_version: "2026-06-06".into(), pins: BTreeMap::new() },
        mutations: vec![Mutation::SourcePatch {
            repo: Some("git://x/grep.git".into()),
            commit: Some("abc".into()),
            tarball: None,
            sha256: None,
            strip_components: None,
            patch: None,
            patch_url: None,
            build: SwmBuild {
                compiler: "zig-cc".into(),
                target: "x86_64-linux-musl".into(),
                link: "static".into(),
                flags: vec![],
                phases: Default::default(),
                zig_version: None,
            },
            target_bin: "/usr/bin/grep".into(),
            expected_hash: None,
            deps: Default::default(),
            evidence: Evidence {
                checks: vec![EvidenceCheck {
                    kind: EvidenceKind::CmdExit,
                    cmd: "grep --version".into(),
                    expected_exit: 0,
                    expected_output: None,
                }],
            },
            slots: Default::default(),
        }],
        signature: None,
    }
}

#[test]
fn failing_evidence_aborts_and_does_not_hydrate() {
    let d = tempfile::tempdir().unwrap();
    let sock = d.path().join("agent.sock");
    start_failing_bus(sock.clone());
    for _ in 0..50 {
        if sock.exists() {
            break;
        }
        thread::sleep(Duration::from_millis(10));
    }

    let prefix = d.path().join("prefix");
    let store = d.path().join("store");
    let base = BaseRef { distro_version: "2026-06-06".into(), pins: BTreeMap::new() };
    let t = MockTranslator::empty().with_intent("compile grep", swm_with_evidence());
    let o = Orchestrator::new(
        t,
        base,
        ApplyTarget::Prefix { prefix: prefix.clone() },
        CompileMode::ViaBus { sock, store, timeout: Duration::from_secs(5) },
    );

    let err = o.run("compile grep").unwrap_err().to_string();
    assert!(err.contains("no se propone"), "mensaje inesperado: {err}");
    assert!(err.contains("evidencia") || err.contains("check(s)"), "{err}");

    // El gate corta ANTES de hidratar: el target_bin no debe existir bajo el prefix.
    assert!(
        !prefix.join("usr/bin/grep").exists(),
        "el binario se hidrató pese al fallo de evidencia"
    );
}
