mod support;

use std::{fs, path::Path, process};

use serde_json::json;
use tempfile::TempDir;

use millrace_ai::contracts::{
    Plane, PlanningTerminalResult, ResultClass, StageName, StageResultEnvelope, TaskDocument,
    TerminalResult, Timestamp, WorkItemKind,
};
use millrace_ai::work_documents::render_task_document;
use millrace_ai::workspace::{
    RuntimeOwnershipLockOptions, acquire_runtime_ownership_lock_with_options, initialize_workspace,
    load_snapshot, run_workspace_doctor_for_paths, save_snapshot,
};

const NOW: &str = "2026-04-15T00:00:00Z";

fn timestamp(value: &str) -> Timestamp {
    Timestamp::parse("created_at", value).unwrap()
}

fn task_document(task_id: &str) -> TaskDocument {
    TaskDocument {
        task_id: task_id.to_owned(),
        title: format!("Task {task_id}"),
        summary: "doctor queue artifact".to_owned(),
        root_idea_id: Some("idea-001".to_owned()),
        root_spec_id: Some("spec-root-001".to_owned()),
        root_intake_kind: None,
        root_intake_id: None,
        spec_id: Some("spec-root-001".to_owned()),
        parent_task_id: None,
        incident_id: None,
        target_paths: vec!["src/workspace/doctor.rs".to_owned()],
        acceptance: vec!["doctor flags queue artifacts".to_owned()],
        required_checks: vec!["cargo test --test workspace_doctor".to_owned()],
        references: vec!["../millrace-py/src/millrace_ai/doctor.py".to_owned()],
        risk: vec!["queue drift".to_owned()],
        depends_on: Vec::new(),
        blocks: Vec::new(),
        tags: vec!["doctor".to_owned()],
        status_hint: None,
        created_at: timestamp(NOW),
        created_by: "tests".to_owned(),
        updated_at: None,
    }
}

fn has_error(report: &millrace_ai::DoctorReport, code: &str) -> bool {
    report.errors.iter().any(|issue| issue.code == code)
}

fn has_warning(report: &millrace_ai::DoctorReport, code: &str) -> bool {
    report.warnings.iter().any(|issue| issue.code == code)
}

fn lock_options(pid: u32, session_id: &str) -> RuntimeOwnershipLockOptions {
    RuntimeOwnershipLockOptions::new(pid, "test-host", session_id, NOW).unwrap()
}

fn blueprint_repair_failure_stage_result(run_id: &str, completed_at: &str) -> StageResultEnvelope {
    let mut metadata = serde_json::Map::new();
    for (key, value) in [
        (
            "runtime_effect_handler_id",
            "evaluator_blueprint_approved_to_task",
        ),
        ("runtime_effect_decision", "request_block_source"),
        ("runtime_effect_failure_class", "generated_task_invalid"),
        (
            "runtime_effect_failure_message",
            "generated_task.md failed schema validation",
        ),
        ("runtime_effect_mutation_phase", "pre_mutation"),
        (
            "runtime_effect_failure_policy_id",
            "blueprint_approval_pre_mutation_effect_validation",
        ),
        ("runtime_effect_recovery_action", "route_to_node"),
    ] {
        metadata.insert(key.to_owned(), json!(value));
    }
    StageResultEnvelope {
        schema_version: "1.0".to_owned(),
        kind: "stage_result".to_owned(),
        run_id: run_id.to_owned(),
        plane: Plane::Planning,
        stage: StageName::EvaluatorBlueprint,
        node_id: "evaluator_blueprint".to_owned(),
        stage_kind_id: "evaluator_blueprint".to_owned(),
        work_item_kind: WorkItemKind::BlueprintDraft,
        work_item_id: "draft-blueprint-001".to_owned(),
        terminal_result: TerminalResult::Planning(PlanningTerminalResult::BlueprintApproved),
        result_class: ResultClass::Success,
        summary_status_marker: "### BLUEPRINT_APPROVED".to_owned(),
        success: true,
        retryable: false,
        exit_code: 0,
        duration_seconds: 1.0,
        prompt_artifact: None,
        report_artifact: None,
        artifact_paths: Vec::new(),
        detected_marker: Some("### BLUEPRINT_APPROVED".to_owned()),
        stdout_path: None,
        stderr_path: None,
        runner_name: Some("fake".to_owned()),
        model_name: None,
        thinking_level: None,
        model_reasoning_effort: None,
        token_usage: None,
        notes: Vec::new(),
        metadata,
        started_at: timestamp(completed_at),
        completed_at: timestamp(completed_at),
    }
}

fn blueprint_repair_apply_stage_result(run_id: &str, completed_at: &str) -> StageResultEnvelope {
    let mut metadata = serde_json::Map::new();
    for (key, value) in [
        (
            "runtime_effect_handler_id",
            "mechanic_blueprint_repair_apply",
        ),
        ("runtime_effect_decision", "request_complete_source"),
        (
            "runtime_effect_failure_message",
            "promoted blueprint to repaired task",
        ),
        ("runtime_effect_mutation_phase", "unknown"),
    ] {
        metadata.insert(key.to_owned(), json!(value));
    }
    StageResultEnvelope {
        schema_version: "1.0".to_owned(),
        kind: "stage_result".to_owned(),
        run_id: run_id.to_owned(),
        plane: Plane::Planning,
        stage: StageName::MechanicBlueprint,
        node_id: "mechanic_blueprint".to_owned(),
        stage_kind_id: "mechanic_blueprint".to_owned(),
        work_item_kind: WorkItemKind::BlueprintDraft,
        work_item_id: "draft-blueprint-001".to_owned(),
        terminal_result: TerminalResult::Planning(
            PlanningTerminalResult::MechanicBlueprintComplete,
        ),
        result_class: ResultClass::Success,
        summary_status_marker: "### MECHANIC_BLUEPRINT_COMPLETE".to_owned(),
        success: true,
        retryable: false,
        exit_code: 0,
        duration_seconds: 1.0,
        prompt_artifact: None,
        report_artifact: None,
        artifact_paths: Vec::new(),
        detected_marker: Some("### MECHANIC_BLUEPRINT_COMPLETE".to_owned()),
        stdout_path: None,
        stderr_path: None,
        runner_name: Some("fake".to_owned()),
        model_name: None,
        thinking_level: None,
        model_reasoning_effort: None,
        token_usage: None,
        notes: Vec::new(),
        metadata,
        started_at: timestamp(completed_at),
        completed_at: timestamp(completed_at),
    }
}

fn write_stage_result(
    root: &Path,
    run_id: &str,
    request_id: &str,
    stage_result: &StageResultEnvelope,
) -> String {
    let relative = format!("millrace-agents/runs/{run_id}/stage_results/{request_id}.json");
    let path = root.join(&relative);
    fs::create_dir_all(path.parent().unwrap()).unwrap();
    fs::write(
        path,
        serde_json::to_string_pretty(stage_result).unwrap() + "\n",
    )
    .unwrap();
    relative
}

#[test]
fn doctor_passes_for_healthy_initialized_workspace() {
    let temp_dir = TempDir::new().unwrap();
    let paths = initialize_workspace(temp_dir.path().join("workspace")).unwrap();

    let report = run_workspace_doctor_for_paths(&paths);

    assert!(report.ok, "unexpected doctor errors: {:#?}", report.errors);
    assert!(report.errors.is_empty());
}

#[test]
fn doctor_flags_missing_directories_and_files() {
    let temp_dir = TempDir::new().unwrap();
    let paths = initialize_workspace(temp_dir.path().join("workspace")).unwrap();
    fs::remove_dir(&paths.logs_dir).unwrap();
    fs::remove_file(&paths.outline_file).unwrap();

    let report = run_workspace_doctor_for_paths(&paths);

    assert!(!report.ok);
    assert!(has_error(&report, "missing_directory"));
    assert!(has_error(&report, "missing_file"));
}

#[test]
fn doctor_flags_invalid_status_json_and_baseline_state() {
    let temp_dir = TempDir::new().unwrap();
    let paths = initialize_workspace(temp_dir.path().join("workspace")).unwrap();
    fs::write(&paths.execution_status_file, "RUNNING\n").unwrap();
    fs::write(&paths.runtime_snapshot_file, "{not-valid-json").unwrap();
    fs::write(
        &paths.baseline_manifest_file,
        serde_json::json!({
            "schema_version": "2.0",
            "manifest_id": "bad",
            "seed_package_version": "0.0.0",
            "entries": []
        })
        .to_string()
            + "\n",
    )
    .unwrap();

    let report = run_workspace_doctor_for_paths(&paths);

    assert!(!report.ok);
    assert!(has_error(&report, "execution_status_invalid"));
    assert!(has_error(&report, "snapshot_invalid"));
    assert!(has_error(&report, "baseline_manifest_invalid"));
}

#[test]
fn doctor_flags_missing_manifest_tracked_managed_assets() {
    let temp_dir = TempDir::new().unwrap();
    let paths = initialize_workspace(temp_dir.path().join("workspace")).unwrap();
    fs::remove_file(paths.runtime_root.join("entrypoints/execution/builder.md")).unwrap();

    let report = run_workspace_doctor_for_paths(&paths);

    assert!(!report.ok);
    assert!(has_error(&report, "baseline_manifest_managed_file_missing"));
}

#[test]
fn doctor_flags_unparseable_queue_artifacts_and_filename_id_mismatches() {
    let temp_dir = TempDir::new().unwrap();
    let paths = initialize_workspace(temp_dir.path().join("workspace")).unwrap();
    fs::write(
        paths.tasks_queue_dir.join("bad.md"),
        "# Bad task\nnot a valid task document\n",
    )
    .unwrap();
    fs::write(
        paths.tasks_queue_dir.join("task-alias.md"),
        render_task_document(&task_document("task-mismatch")),
    )
    .unwrap();

    let report = run_workspace_doctor_for_paths(&paths);

    assert!(!report.ok);
    let queue_errors: Vec<_> = report
        .errors
        .iter()
        .filter(|issue| issue.code == "queue_artifact_invalid")
        .collect();
    assert_eq!(queue_errors.len(), 2);
    assert!(queue_errors.iter().any(|issue| {
        issue
            .message
            .contains("filename stem does not match task_id")
    }));
}

#[test]
fn doctor_flags_invalid_blueprint_artifacts() {
    let temp_dir = TempDir::new().unwrap();
    let paths = initialize_workspace(temp_dir.path().join("workspace")).unwrap();
    let draft_dir = paths.runtime_root.join("blueprints/drafts/queue");
    fs::create_dir_all(&draft_dir).unwrap();
    fs::write(
        draft_dir.join("draft-invalid.json"),
        serde_json::json!({
            "schema_version": "1.0",
            "kind": "blueprint_draft",
            "draft_id": "draft-other"
        })
        .to_string()
            + "\n",
    )
    .unwrap();

    let report = run_workspace_doctor_for_paths(&paths);

    assert!(!report.ok);
    let issue = report
        .errors
        .iter()
        .find(|issue| issue.code == "blueprint_artifact_invalid")
        .expect("blueprint artifact diagnostic");
    assert!(issue.message.contains("blueprint_draft"));
    assert_eq!(
        issue.path.as_ref(),
        Some(&draft_dir.join("draft-invalid.json"))
    );
}

#[test]
fn doctor_flags_duplicate_task_lifecycle_state_with_workspace_relative_paths() {
    let temp_dir = TempDir::new().unwrap();
    let paths = initialize_workspace(temp_dir.path().join("workspace")).unwrap();
    let document = task_document("task-duplicate");
    fs::write(
        paths.tasks_done_dir.join("task-duplicate.md"),
        render_task_document(&document),
    )
    .unwrap();
    let mut blocked = document.clone();
    blocked.summary = "stale blocked predecessor".to_owned();
    fs::write(
        paths.tasks_blocked_dir.join("task-duplicate.md"),
        render_task_document(&blocked),
    )
    .unwrap();

    let report = run_workspace_doctor_for_paths(&paths);

    assert!(!report.ok);
    let duplicate = report
        .errors
        .iter()
        .find(|issue| issue.code == "duplicate_task_lifecycle_state")
        .expect("duplicate task lifecycle diagnostic");
    assert_eq!(
        duplicate.message,
        "task task-duplicate appears in multiple lifecycle states: done:millrace-agents/tasks/done/task-duplicate.md, blocked:millrace-agents/tasks/blocked/task-duplicate.md"
    );
    assert_eq!(
        duplicate.path.as_ref(),
        Some(&paths.tasks_done_dir.join("task-duplicate.md"))
    );
}

#[test]
fn doctor_reports_runtime_ownership_lock_health() {
    let temp_dir = TempDir::new().unwrap();
    let paths = initialize_workspace(temp_dir.path().join("workspace")).unwrap();

    acquire_runtime_ownership_lock_with_options(
        &paths,
        lock_options(process::id(), "active-session"),
    )
    .unwrap();
    let active = run_workspace_doctor_for_paths(&paths);
    assert!(active.ok);
    assert!(has_warning(&active, "runtime_ownership_lock_active"));

    fs::remove_file(&paths.runtime_lock_file).unwrap();
    acquire_runtime_ownership_lock_with_options(&paths, lock_options(999_999_999, "stale-session"))
        .unwrap();
    let stale = run_workspace_doctor_for_paths(&paths);
    assert!(!stale.ok);
    assert!(has_error(&stale, "runtime_ownership_lock_stale"));

    fs::write(&paths.runtime_lock_file, "{not-valid-json").unwrap();
    let invalid = run_workspace_doctor_for_paths(&paths);
    assert!(!invalid.ok);
    assert!(has_error(&invalid, "runtime_ownership_lock_invalid"));
}

#[test]
fn doctor_warns_on_blueprint_repair_runtime_effect_context_after_apply() {
    let temp_dir = TempDir::new().unwrap();
    let root = temp_dir.path().join("workspace");
    let paths = initialize_workspace(&root).unwrap();
    let run_id = "run-blueprint-repair-doctor";
    write_stage_result(
        &root,
        run_id,
        "request-repair-failure",
        &blueprint_repair_failure_stage_result(run_id, "2026-05-21T07:11:00Z"),
    );
    let repair_path = write_stage_result(
        &root,
        run_id,
        "request-repair-apply",
        &blueprint_repair_apply_stage_result(run_id, "2026-05-21T07:12:00Z"),
    );
    let mut snapshot = load_snapshot(&paths).unwrap();
    snapshot.last_stage_result_path = Some(repair_path);
    save_snapshot(&paths, &snapshot).unwrap();

    let report = run_workspace_doctor_for_paths(&paths);

    assert!(report.ok, "unexpected doctor errors: {:#?}", report.errors);
    let warning = report
        .warnings
        .iter()
        .find(|issue| issue.code == "blueprint_runtime_effect_recovery_context")
        .expect("blueprint repair recovery context warning");
    assert!(
        warning
            .message
            .contains("failed_handler=evaluator_blueprint_approved_to_task")
    );
    assert!(
        warning
            .message
            .contains("failure_class=generated_task_invalid")
    );
    assert!(
        warning
            .message
            .contains("action=apply_repaired_generated_task")
    );
    assert!(
        warning
            .message
            .contains("blueprint_candidate_duplicate_conflict")
    );
    assert!(
        warning
            .message
            .contains("repaired_blueprint_artifact.md ignored")
    );
    assert!(
        warning
            .message
            .contains("mechanic writes repair artifacts only")
    );
    assert_eq!(
        warning.path.as_ref(),
        Some(&root.join(
            "millrace-agents/runs/run-blueprint-repair-doctor/stage_results/request-repair-failure.json"
        ))
    );
}
