//! Integration tests for deprecate-pending-validation:
//!
//! 1. Dependent unblocks when `.evaluate-X` passes (becomes terminal Done).
//! 2. Dependent stays blocked while `.evaluate-X` is in flight.
//! 3. No routine `wg done` lands in PendingValidation.
//! 4. Legacy PendingValidation tasks get migrated to Done on dispatcher tick.
//! 5. Auto-rescue chains are capped at `coordinator.max_verify_failures`.
//!
//! See: src/query.rs (`is_eval_gate_pending`),
//! src/commands/service/coordinator.rs (`migrate_pending_validation_tasks`),
//! src/commands/evaluate.rs (`check_eval_gate` rescue cap).

use std::path::{Path, PathBuf};
use std::process::{Command, Stdio};
use tempfile::TempDir;
use workgraph::graph::{LogEntry, Node, Status, Task, WorkGraph};
use workgraph::parser::{load_graph, save_graph};

fn wg_binary() -> PathBuf {
    let mut path = std::env::current_exe().expect("could not get current exe path");
    path.pop();
    if path.ends_with("deps") {
        path.pop();
    }
    path.push("wg");
    assert!(
        path.exists(),
        "wg binary not found at {:?}. Run `cargo build` first.",
        path
    );
    path
}

fn wg_cmd(wg_dir: &Path, args: &[&str]) -> std::process::Output {
    Command::new(wg_binary())
        .arg("--dir")
        .arg(wg_dir)
        .args(args)
        .env_remove("WG_AGENT_ID")
        .env_remove("WG_TASK_ID")
        .env("WG_SMOKE_AGENT_OVERRIDE", "1")
        .stdin(Stdio::null())
        .stdout(Stdio::piped())
        .stderr(Stdio::piped())
        .output()
        .unwrap_or_else(|e| panic!("Failed to run wg {:?}: {}", args, e))
}

fn make_task(id: &str, status: Status) -> Task {
    Task {
        id: id.to_string(),
        title: id.to_string(),
        status,
        ..Task::default()
    }
}

fn setup_workgraph(tmp: &TempDir, tasks: Vec<Task>) -> PathBuf {
    let wg_dir = tmp.path().join(".wg");
    std::fs::create_dir_all(&wg_dir).unwrap();
    let graph_path = wg_dir.join("graph.jsonl");
    let mut graph = WorkGraph::new();
    for task in tasks {
        graph.add_node(Node::Task(task));
    }
    save_graph(&graph, &graph_path).unwrap();
    wg_dir
}

// ---------------------------------------------------------------------------
// 1. Dependent unblocks when eval passes
// ---------------------------------------------------------------------------

#[test]
fn test_dependent_task_unblocks_when_eval_passes() {
    let tmp = TempDir::new().unwrap();

    // Task A is Done. .evaluate-A is Done (eval passed). B depends on A.
    let task_a = make_task("a", Status::Done);
    let mut eval_a = make_task(".evaluate-a", Status::Done);
    eval_a.after = vec!["a".to_string()];
    eval_a.tags = vec!["evaluation".to_string()];
    let mut task_b = make_task("b", Status::Open);
    task_b.after = vec!["a".to_string()];

    let wg_dir = setup_workgraph(&tmp, vec![task_a, eval_a, task_b]);

    let graph = load_graph(wg_dir.join("graph.jsonl")).unwrap();
    let ready = workgraph::query::ready_tasks(&graph);
    let ready_ids: Vec<&str> = ready.iter().map(|t| t.id.as_str()).collect();
    assert!(
        ready_ids.contains(&"b"),
        "B should be ready when A is Done AND .evaluate-A is Done; ready={:?}",
        ready_ids
    );
}

// ---------------------------------------------------------------------------
// 2. Dependent stays blocked while eval is in flight
// ---------------------------------------------------------------------------

#[test]
fn test_dependent_task_stays_blocked_when_eval_in_flight() {
    let tmp = TempDir::new().unwrap();

    let task_a = make_task("a", Status::Done);
    let mut eval_a = make_task(".evaluate-a", Status::InProgress);
    eval_a.after = vec!["a".to_string()];
    eval_a.tags = vec!["evaluation".to_string()];
    let mut task_b = make_task("b", Status::Open);
    task_b.after = vec!["a".to_string()];

    let wg_dir = setup_workgraph(&tmp, vec![task_a, eval_a, task_b]);

    let graph = load_graph(wg_dir.join("graph.jsonl")).unwrap();
    let ready = workgraph::query::ready_tasks(&graph);
    let ready_ids: Vec<&str> = ready.iter().map(|t| t.id.as_str()).collect();
    assert!(
        !ready_ids.contains(&"b"),
        "B should be blocked while .evaluate-A is InProgress; ready={:?}",
        ready_ids
    );

    // .evaluate-A itself should be ready/active (it isn't gated by anything).
    // But it's already InProgress, so it shouldn't appear in `ready_tasks`.
    assert!(
        !ready_ids.contains(&".evaluate-a"),
        ".evaluate-A is InProgress so should not be in ready list"
    );
}

#[test]
fn test_dependent_task_stays_blocked_when_eval_open_not_started() {
    let tmp = TempDir::new().unwrap();

    let task_a = make_task("a", Status::Done);
    let mut eval_a = make_task(".evaluate-a", Status::Open);
    eval_a.after = vec!["a".to_string()];
    eval_a.tags = vec!["evaluation".to_string()];
    let mut task_b = make_task("b", Status::Open);
    task_b.after = vec!["a".to_string()];

    let wg_dir = setup_workgraph(&tmp, vec![task_a, eval_a, task_b]);

    let graph = load_graph(wg_dir.join("graph.jsonl")).unwrap();
    let ready = workgraph::query::ready_tasks(&graph);
    let ready_ids: Vec<&str> = ready.iter().map(|t| t.id.as_str()).collect();
    assert!(
        !ready_ids.contains(&"b"),
        "B should be blocked while .evaluate-A is Open; ready={:?}",
        ready_ids
    );
    assert!(
        ready_ids.contains(&".evaluate-a"),
        ".evaluate-A should be ready (its only blocker A is Done); ready={:?}",
        ready_ids
    );
}

#[test]
fn test_dependent_unblocks_when_no_eval_task_exists() {
    // Backward-compat: if no `.evaluate-X` exists in the graph, the eval gate
    // is trivially satisfied. B unblocks as soon as A is terminal.
    let tmp = TempDir::new().unwrap();
    let task_a = make_task("a", Status::Done);
    let mut task_b = make_task("b", Status::Open);
    task_b.after = vec!["a".to_string()];
    let wg_dir = setup_workgraph(&tmp, vec![task_a, task_b]);

    let graph = load_graph(wg_dir.join("graph.jsonl")).unwrap();
    let ready = workgraph::query::ready_tasks(&graph);
    let ready_ids: Vec<&str> = ready.iter().map(|t| t.id.as_str()).collect();
    assert!(
        ready_ids.contains(&"b"),
        "B should be ready when no .evaluate-A exists; ready={:?}",
        ready_ids
    );
}

#[test]
fn test_system_dependents_skip_eval_gate() {
    // A `.flip-X` system task depends on X, but should NOT be gated on
    // `.evaluate-X` — that would deadlock the eval pipeline.
    let tmp = TempDir::new().unwrap();
    let task_a = make_task("a", Status::Done);
    let mut eval_a = make_task(".evaluate-a", Status::Open);
    eval_a.after = vec!["a".to_string()];
    eval_a.tags = vec!["evaluation".to_string()];
    let mut flip_a = make_task(".flip-a", Status::Open);
    flip_a.after = vec!["a".to_string()];
    flip_a.tags = vec!["flip".to_string()];

    let wg_dir = setup_workgraph(&tmp, vec![task_a, eval_a, flip_a]);

    let graph = load_graph(wg_dir.join("graph.jsonl")).unwrap();
    let ready = workgraph::query::ready_tasks(&graph);
    let ready_ids: Vec<&str> = ready.iter().map(|t| t.id.as_str()).collect();
    assert!(
        ready_ids.contains(&".flip-a"),
        ".flip-a should be ready even when .evaluate-a is open; ready={:?}",
        ready_ids
    );
}

// ---------------------------------------------------------------------------
// 3. No routine PendingValidation
// ---------------------------------------------------------------------------

#[test]
fn test_no_routine_pending_validation_state() {
    // A routine task with no `validation` set, no `verify` field, and no
    // `verify_mode=separate` config should land in `Done` after `wg done` —
    // never in `PendingValidation`. We seed the graph directly with an
    // InProgress task and run `wg done`.
    let tmp = TempDir::new().unwrap();
    let wg_dir = tmp.path().join(".wg");
    std::fs::create_dir_all(&wg_dir).unwrap();

    let mut foo = make_task("foo", Status::InProgress);
    foo.assigned = Some("test-agent".to_string());
    let graph_path = wg_dir.join("graph.jsonl");
    let mut graph = WorkGraph::new();
    graph.add_node(Node::Task(foo));
    save_graph(&graph, &graph_path).unwrap();

    let out = wg_cmd(
        &wg_dir,
        &["done", "foo", "--ignore-unmerged-worktree", "--skip-smoke"],
    );
    assert!(
        out.status.success(),
        "wg done failed: stderr={}\nstdout={}",
        String::from_utf8_lossy(&out.stderr),
        String::from_utf8_lossy(&out.stdout)
    );

    let graph = load_graph(&graph_path).unwrap();
    let task = graph.get_task("foo").unwrap();
    assert_eq!(
        task.status,
        Status::Done,
        "Routine `wg done foo` should land in Done (got {:?})",
        task.status
    );
    assert_ne!(
        task.status,
        Status::PendingValidation,
        "Routine task lifecycle should never produce PendingValidation"
    );
}

// ---------------------------------------------------------------------------
// 4. Legacy PendingValidation migration
// ---------------------------------------------------------------------------

#[test]
fn test_legacy_pending_validation_migrated() {
    use workgraph::graph::WorkGraph;

    // Build a graph in memory that contains a PendingValidation task and an
    // unrelated task. After the migration sweep, the PendingValidation task
    // should be Done with a migration log entry.
    let mut graph = WorkGraph::new();
    let mut stuck = make_task("stuck", Status::PendingValidation);
    stuck.completed_at = Some("2026-04-01T00:00:00+00:00".to_string());
    graph.add_node(Node::Task(stuck));
    graph.add_node(Node::Task(make_task("other", Status::Open)));

    // Run the migration directly. This tests the helper in isolation; the
    // dispatcher invokes it on every tick, which is exercised separately by
    // the live coordinator harness.
    let migrated = workgraph::lifecycle::migrate_pending_validation_tasks(&mut graph);
    let modified = !migrated.is_empty();
    assert!(modified, "migration should have run");

    let stuck = graph.get_task("stuck").expect("task still present");
    assert_eq!(
        stuck.status,
        Status::Done,
        "stuck PendingValidation task should now be Done"
    );
    let migration_log = stuck
        .log
        .iter()
        .any(|e| e.message.contains("Migrated PendingValidation"));
    assert!(
        migration_log,
        "migration log entry not found: {:?}",
        stuck.log
    );

    // Other task is unaffected
    assert_eq!(graph.get_task("other").unwrap().status, Status::Open);
}

#[test]
fn test_migration_idempotent() {
    use workgraph::graph::WorkGraph;

    let mut graph = WorkGraph::new();
    graph.add_node(Node::Task(make_task("stuck", Status::PendingValidation)));

    let first = workgraph::lifecycle::migrate_pending_validation_tasks(&mut graph);
    assert_eq!(first.len(), 1);
    let second = workgraph::lifecycle::migrate_pending_validation_tasks(&mut graph);
    assert!(
        second.is_empty(),
        "second sweep should be a no-op (no PendingValidation tasks left)"
    );
}

#[test]
fn test_migration_skips_human_review_opt_in() {
    use workgraph::graph::WorkGraph;

    let mut graph = WorkGraph::new();
    let mut human = make_task("opt-in", Status::PendingValidation);
    human.tags = vec!["human-review".to_string()];
    graph.add_node(Node::Task(human));

    let migrated = workgraph::lifecycle::migrate_pending_validation_tasks(&mut graph);
    let modified = !migrated.is_empty();
    assert!(
        !modified,
        "human-review tasks should be exempt from migration"
    );
    assert_eq!(
        graph.get_task("opt-in").unwrap().status,
        Status::PendingValidation
    );
}

// ---------------------------------------------------------------------------
// 5. Max eval rescues caps loops
// ---------------------------------------------------------------------------

#[test]
fn test_rescue_count_field_persists() {
    use workgraph::graph::WorkGraph;
    use workgraph::parser::{load_graph, save_graph};

    let tmp = TempDir::new().unwrap();
    let wg_dir = tmp.path().join(".wg");
    std::fs::create_dir_all(&wg_dir).unwrap();
    let graph_path = wg_dir.join("graph.jsonl");
    let mut graph = WorkGraph::new();
    let mut task = make_task("rescued", Status::Failed);
    task.rescue_count = 2;
    task.log.push(LogEntry {
        timestamp: "2026-04-26T00:00:00+00:00".to_string(),
        actor: None,
        user: None,
        message: "Auto-rescue cap reached (2/2); no further rescue spawned".to_string(),
    });
    graph.add_node(Node::Task(task));
    save_graph(&graph, &graph_path).unwrap();

    let reloaded = load_graph(&graph_path).unwrap();
    let task = reloaded.get_task("rescued").unwrap();
    assert_eq!(task.rescue_count, 2);
    assert_eq!(task.status, Status::Failed);
}

#[test]
fn test_max_eval_rescues_caps_loops() {
    // Simulate the cap logic: a task with rescue_count >= max_verify_failures
    // should NOT be auto-rescued. We can't trigger the LLM eval path in a
    // unit test, so we verify the field is read correctly and the cap branch
    // logs the cap message.
    //
    // The actual rescue spawning is exercised by integration_agency_loop and
    // covered there. Here we assert the data shape.
    use workgraph::config::Config;

    let mut config = Config::default();
    config.coordinator.max_verify_failures = 3;
    assert_eq!(config.coordinator.max_verify_failures, 3);

    // Forward-compat alias: max_eval_rescues = 3 should deserialize the same way.
    let toml_with_alias = r#"
[coordinator]
max_eval_rescues = 5
"#;
    let parsed: Config = toml::from_str(toml_with_alias).expect("parse");
    assert_eq!(
        parsed.coordinator.max_verify_failures, 5,
        "max_eval_rescues alias should populate max_verify_failures"
    );
}
