//! E2E del bucle agéntico (Fase 6):
//!
//! Camino directo (sin bus): un `MockTranslator` resuelve un intent conocido y el
//! `Orchestrator` aplica las mutaciones bajo un prefix temporal. La prueba final pinta el
//! cambio en disco y verifica que es el esperado.
//!
//! NO ejercita el bus aquí — el camino bus + cliente se cubre en
//! `crates/hammer-agent/tests/client_e2e.rs` cuando lo construyamos sobre los crates
//! `hammerd` (Fase 5). Phase 6 ya prueba bus + cliente en bus_e2e.rs del propio hammerd.

use std::collections::BTreeMap;

use base64::{engine::general_purpose::STANDARD, Engine as _};
use hammer_agent::{
    orchestrator::{ApplyTarget, CompileMode},
    MockTranslator, Orchestrator,
};
use hammer_core::swm::{Base, Mutation, Swm};
use hammer_core::{ArtifactHash, BaseRef};

fn baseref() -> BaseRef {
    let mut pins = BTreeMap::new();
    pins.insert("musl".into(), "deadbeef".into());
    BaseRef { distro_version: "2026-06-06".into(), pins }
}

fn swm_two_mutations() -> Swm {
    let payload = b"datos finales\n";
    let payload_hash = ArtifactHash::of_inputs(&[payload.as_slice()]).as_str().to_string();
    let payload_b64 = STANDARD.encode(payload);
    Swm {
        swm_version: 1,
        base: Base {
            distro_version: "2026-06-06".into(),
            pins: {
                let mut m = BTreeMap::new();
                m.insert("musl".into(), "deadbeef".into());
                m
            },
        },
        mutations: vec![
            Mutation::ConfigEdit {
                file: "/etc/network.conf".into(),
                inline_diff: "- DHCP=yes\n+ IP=192.168.1.100\n".into(),
            },
            Mutation::FileDrop {
                path: "/var/lib/agent/notes.txt".into(),
                content_hash: payload_hash,
                content_b64: Some(payload_b64),
                content_url: None,
            },
        ],
        signature: None,
    }
}

#[test]
fn intent_to_overlay_proposal_via_mock_translator() {
    let d = tempfile::tempdir().unwrap();
    let prefix = d.path().join("prefix");
    std::fs::create_dir_all(prefix.join("etc")).unwrap();
    std::fs::write(
        prefix.join("etc/network.conf"),
        "USE=eth0\nDHCP=yes\nMTU=1500\n",
    )
    .unwrap();

    let translator = MockTranslator::empty()
        .with_intent("set static ip and drop notes", swm_two_mutations());

    let orch = Orchestrator::new(
        translator,
        baseref(),
        ApplyTarget::Prefix { prefix: prefix.clone() },
        CompileMode::Skip,
    );

    let proposal = orch.run("set static ip and drop notes").expect("proposal");

    assert_eq!(proposal.applied.config_edit, 1);
    assert_eq!(proposal.applied.file_drop, 1);
    assert_eq!(proposal.applied.source_patch, 0);
    assert!(proposal.overlay_id.is_none(), "modo Prefix no abre overlay");
    assert_eq!(proposal.checks.len(), 2);
    assert!(
        proposal
            .checks
            .iter()
            .all(|c| matches!(c.level, hammer_agent::orchestrator::CheckLevel::Pass)),
        "todas las checks deben pasar"
    );

    // Verificación post-condición: los archivos en disco coinciden con lo esperado.
    let net = std::fs::read_to_string(prefix.join("etc/network.conf")).unwrap();
    assert_eq!(net, "USE=eth0\nIP=192.168.1.100\nMTU=1500\n");
    let notes = std::fs::read(prefix.join("var/lib/agent/notes.txt")).unwrap();
    assert_eq!(notes, b"datos finales\n");
}

#[test]
fn unknown_intent_surfaces_translator_error() {
    let d = tempfile::tempdir().unwrap();
    let orch = Orchestrator::new(
        MockTranslator::empty(),
        baseref(),
        ApplyTarget::Prefix { prefix: d.path().to_path_buf() },
        CompileMode::Skip,
    );
    let err = orch
        .run("intencion sin entrada en el catalogo")
        .unwrap_err()
        .to_string();
    assert!(err.contains("no reconocida"), "{err}");
}

#[test]
fn proposal_is_serializable_for_humans() {
    // Sanidad: el Proposal serializa a JSON. La CLI o un cliente externo lo puede mostrar.
    let d = tempfile::tempdir().unwrap();
    let prefix = d.path().join("prefix");
    std::fs::create_dir_all(prefix.join("etc")).unwrap();
    std::fs::write(prefix.join("etc/network.conf"), "DHCP=yes\n").unwrap();
    let swm = Swm {
        swm_version: 1,
        base: Base {
            distro_version: "2026-06-06".into(),
            pins: {
                let mut m = BTreeMap::new();
                m.insert("musl".into(), "deadbeef".into());
                m
            },
        },
        mutations: vec![Mutation::ConfigEdit {
            file: "/etc/network.conf".into(),
            inline_diff: "- DHCP=yes\n+ STATIC=1\n".into(),
        }],
        signature: None,
    };
    let translator = MockTranslator::empty().with_intent("x", swm);
    let orch = Orchestrator::new(
        translator,
        baseref(),
        ApplyTarget::Prefix { prefix },
        CompileMode::Skip,
    );
    let proposal = orch.run("x").unwrap();
    let json = serde_json::to_string(&proposal).unwrap();
    assert!(json.contains("\"applied\""));
    assert!(json.contains("\"checks\""));
}
