//! Tests for dynamic audit: dead-end-only error semantics (R3).
//!
//! The dynamic audit ERRORs ONLY on `DeadEnd` (dangling/unconnected matched branch).
//! Legitimate terminals (RanScript, OpenedUrl) are never flagged.

use alfrusco::simulator::{Severity, Simulator};

/// Build the menu example binary once (shared across tests).
fn menu_binary() -> String {
    let output = std::process::Command::new("cargo")
        .args(["build", "--example", "menu", "--message-format=json"])
        .output()
        .expect("failed to run cargo build");
    assert!(output.status.success(), "cargo build --example menu failed");
    let stdout = String::from_utf8(output.stdout).unwrap();
    for line in stdout.lines() {
        if let Ok(msg) = serde_json::from_str::<serde_json::Value>(line) {
            if msg.get("reason").and_then(|r| r.as_str()) == Some("compiler-artifact")
                && msg
                    .get("target")
                    .and_then(|t| t.get("name"))
                    .and_then(|n| n.as_str())
                    == Some("menu")
            {
                if let Some(exe) = msg.get("executable").and_then(|e| e.as_str()) {
                    return exe.to_string();
                }
            }
        }
    }
    panic!("could not find executable path for example 'menu' in cargo output");
}

#[test]
fn dynamic_audit_good_menu_is_clean() {
    let sim = Simulator::for_workflow_dir("examples/menu_workflow")
        .unwrap()
        .binary(menu_binary());

    let diagnostics = sim.dynamic_audit().unwrap();
    let errors: Vec<_> = diagnostics
        .iter()
        .filter(|d| d.severity >= Severity::Error)
        .collect();
    assert!(
        errors.is_empty(),
        "expected no errors on good menu, got: {errors:#?}"
    );
}

#[test]
fn dynamic_audit_act_and_exit_not_flagged() {
    // The "misrouted" fixture connects SF-MAIN → RUN-SCRIPT directly.
    // Navigation items routing to RanScript is a legitimate act-and-exit pattern.
    // Under the faithful semantics, this is NOT an error.
    let sim = Simulator::for_workflow_dir("tests/fixtures/menu_misrouted_workflow")
        .unwrap()
        .binary(menu_binary());

    let diagnostics = sim.dynamic_audit().unwrap();
    let errors: Vec<_> = diagnostics
        .iter()
        .filter(|d| d.severity >= Severity::Error)
        .collect();

    assert!(
        errors.is_empty(),
        "act-and-exit (nav→RanScript) should NOT be flagged, got: {errors:#?}"
    );
}

#[test]
fn dynamic_audit_dangling_loopback_is_dead_end() {
    // The dangling fixture has a conditional whose loopback branch points to a
    // non-existent UID. Items with arg="loopback" (matching the condition) hit
    // a dead-end — that IS the one real error.
    let sim = Simulator::for_workflow_dir("tests/fixtures/menu_dangling_workflow")
        .unwrap()
        .binary(menu_binary());

    let diagnostics = sim.dynamic_audit().unwrap();
    let errors: Vec<_> = diagnostics
        .iter()
        .filter(|d| d.severity >= Severity::Error)
        .collect();

    assert!(
        !errors.is_empty(),
        "expected dead-end errors for dangling loopback, got none"
    );

    // Verify the error mentions dead-end
    let has_dead_end_error = errors.iter().any(|d| d.message.contains("dead-end"));
    assert!(
        has_dead_end_error,
        "expected an error about dead-end, got: {errors:#?}"
    );
}

#[test]
fn invoke_script_filter_uses_scriptfile() {
    let sim = Simulator::for_workflow_dir("examples/menu_workflow")
        .unwrap()
        .binary(menu_binary());

    let screen = sim.invoke_script_filter("SF-MAIN-001", &[]).unwrap();
    assert!(!screen.is_empty(), "expected items from top-level menu");
    assert_eq!(screen.items()[0].title(), "Fruits");
}

#[test]
fn invoke_script_filter_sub_level() {
    let sim = Simulator::for_workflow_dir("examples/menu_workflow")
        .unwrap()
        .binary(menu_binary());

    let screen = sim.invoke_script_filter("SF-SUB-001", &["fruits"]).unwrap();
    assert!(!screen.is_empty());
    assert_eq!(screen.items()[0].title(), "Apple");
}
