use fabro_client::ServerTarget;
use fabro_config::{Storage, envfile};
use fabro_test::{fabro_snapshot, test_context};
use fabro_util::dev_token;
use httpmock::MockServer;
use serde_json::Value;

use super::support::{
    local_dev_token, remote_run_summary_json, setup_seeded_completed_dry_run,
    setup_seeded_created_dry_run,
};
use crate::support::{fatal_error_line, seed_dev_token_auth, unique_run_id};

const TEST_DEV_TOKEN: &str =
    "fabro_dev_abababababababababababababababababababababababababababababababab";
const TEST_SESSION_SECRET: &str =
    "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef";

fn provision_local_server_auth(context: &fabro_test::TestContext, storage_dir: &std::path::Path) {
    context.ensure_home_server_auth_methods();
    let runtime_directory = Storage::new(storage_dir).runtime_directory();
    let server_env_path = runtime_directory.env_path();
    envfile::merge_env_file(&server_env_path, [
        ("FABRO_DEV_TOKEN", TEST_DEV_TOKEN),
        ("SESSION_SECRET", TEST_SESSION_SECRET),
    ])
    .expect("merging server auth into server.env");
    dev_token::write_dev_token(&runtime_directory.dev_token_path(), TEST_DEV_TOKEN)
        .expect("writing runtime dev-token");
}

#[test]
fn help() {
    let context = test_context!();
    let mut cmd = context.command();
    cmd.args(["ps", "--help"]);
    fabro_snapshot!(context.filters(), cmd, @"
    success: true
    exit_code: 0
    ----- stdout -----
    List workflow runs

    Usage: fabro ps [OPTIONS]

    Options:
          --json                 Output as JSON [env: FABRO_JSON=]
          --server <SERVER>      Fabro server target: http(s) URL or absolute Unix socket path [env: FABRO_SERVER=]
          --before <BEFORE>      Only include runs started before this date (YYYY-MM-DD prefix match)
          --debug                Enable DEBUG-level logging (default is INFO) [env: FABRO_DEBUG=]
          --no-upgrade-check     Disable automatic upgrade check [env: FABRO_NO_UPGRADE_CHECK=true]
          --workflow <WORKFLOW>  Filter by workflow name (substring match)
          --label <KEY=VALUE>    Filter by label (KEY=VALUE, repeatable, AND semantics)
          --orphans              Include orphan directories (no matching durable run)
          --verbose              Enable verbose output [env: FABRO_VERBOSE=]
      -a, --all                  Show all runs, not just running (like docker ps -a)
      -q, --quiet                Only display run IDs
          --parent <RUN>         Only display runs linked to this orchestration parent
      -h, --help                 Print help
    ----- stderr -----
    ");
}

#[test]
fn ps_explicit_local_tcp_server_target_ignores_env_dev_token() {
    let context = test_context!();
    let storage_root = tempfile::tempdir_in("/tmp").unwrap();
    let storage_dir = storage_root.path().join("storage");
    std::fs::create_dir_all(&storage_dir).unwrap();
    provision_local_server_auth(&context, &storage_dir);

    context
        .command()
        .env("FABRO_STORAGE_DIR", &storage_dir)
        .args(["server", "start", "--bind", "127.0.0.1"])
        .assert()
        .success();

    let status_output = context
        .command()
        .env("FABRO_STORAGE_DIR", &storage_dir)
        .args(["server", "status", "--json"])
        .assert()
        .success()
        .get_output()
        .stdout
        .clone();
    let status_json: Value = serde_json::from_slice(&status_output).unwrap();
    let bind = status_json["bind"]
        .as_str()
        .expect("bind should be present");

    let output = context
        .command()
        .env("FABRO_STORAGE_DIR", &storage_dir)
        .env("FABRO_DEV_TOKEN", TEST_DEV_TOKEN)
        .args(["ps", "-a", "--json", "--server", &format!("http://{bind}")])
        .output()
        .expect("ps should run");

    context
        .command()
        .env("FABRO_STORAGE_DIR", &storage_dir)
        .args(["server", "stop"])
        .assert()
        .success();

    assert!(
        !output.status.success(),
        "ps against an explicit local TCP target should ignore FABRO_DEV_TOKEN and require persisted auth:\nstdout:\n{}\nstderr:\n{}",
        String::from_utf8_lossy(&output.stdout),
        String::from_utf8_lossy(&output.stderr)
    );
    assert_eq!(output.status.code(), Some(4));
    assert_eq!(fatal_error_line(&output.stderr), "Authentication required.");
}

#[test]
fn ps_explicit_local_tcp_server_target_accepts_explicit_dev_token() {
    let context = test_context!();
    let storage_root = tempfile::tempdir_in("/tmp").unwrap();
    let storage_dir = storage_root.path().join("storage");
    std::fs::create_dir_all(&storage_dir).unwrap();
    provision_local_server_auth(&context, &storage_dir);

    context
        .command()
        .env("FABRO_STORAGE_DIR", &storage_dir)
        .args(["server", "start", "--bind", "127.0.0.1"])
        .assert()
        .success();

    let status_output = context
        .command()
        .env("FABRO_STORAGE_DIR", &storage_dir)
        .args(["server", "status", "--json"])
        .assert()
        .success()
        .get_output()
        .stdout
        .clone();
    let status_json: Value = serde_json::from_slice(&status_output).unwrap();
    let bind = status_json["bind"]
        .as_str()
        .expect("bind should be present");
    let token = local_dev_token(&storage_dir).expect("local dev token should exist");
    let target = ServerTarget::http_url(format!("http://{bind}")).expect("bind should parse");
    seed_dev_token_auth(&context.home_dir, &target, &token);

    let output = context
        .command()
        .args(["ps", "-a", "--json", "--server", &format!("http://{bind}")])
        .output()
        .expect("ps should run");

    context
        .command()
        .env("FABRO_STORAGE_DIR", &storage_dir)
        .args(["server", "stop"])
        .assert()
        .success();

    assert!(
        output.status.success(),
        "ps against local TCP target with explicit auth failed:\nstdout:\n{}\nstderr:\n{}",
        String::from_utf8_lossy(&output.stdout),
        String::from_utf8_lossy(&output.stderr)
    );
    let runs: Vec<Value> = serde_json::from_slice(&output.stdout).unwrap();
    assert!(runs.is_empty(), "new local TCP server should have no runs");
}

#[test]
fn ps_default_excludes_non_running_runs() {
    let context = test_context!();
    setup_seeded_completed_dry_run(&context);
    let mut cmd = context.ps();
    cmd.args(["--label", &context.test_case_label()]);

    fabro_snapshot!(context.filters(), cmd, @"
    success: true
    exit_code: 0
    ----- stdout -----
    ----- stderr -----
    No running processes found. Use -a to show all runs (including archived).
    ");
}

#[test]
fn ps_all_json_lists_created_and_completed_runs() {
    let context = test_context!();
    setup_seeded_completed_dry_run(&context);
    setup_seeded_created_dry_run(&context);
    let output = context
        .ps()
        .args(["-a", "--json", "--label", &context.test_case_label()])
        .output()
        .expect("ps should run");

    assert!(output.status.success(), "ps should succeed");
    let runs: Vec<Value> = serde_json::from_slice(&output.stdout).expect("ps JSON should parse");
    assert_eq!(runs.len(), 2, "expected submitted + completed runs");
    assert!(
        runs.iter()
            .all(|run| run["workflow_name"].is_null() && run["workflow_graph_name"] == "Simple"),
        "all runs should expose Simple as the graph name, not a workflow name: {runs:#?}"
    );
    assert!(
        runs.iter()
            .all(|run| run["labels"]["fabro_test_case"] == context.test_case_id()),
        "all runs should be scoped to the current test case: {runs:#?}"
    );
    assert!(
        runs.iter()
            .all(|run| run["labels"]["fabro_test_run"] == context.test_run_id()),
        "all runs should be scoped to the current test session: {runs:#?}"
    );
    assert!(
        runs.iter().any(|run| run["status"]["kind"] == "submitted"),
        "ps should include the created run: {runs:#?}"
    );
    assert!(
        runs.iter().any(|run| {
            run["status"]["kind"] == "succeeded" && run["status"]["reason"] == "completed"
        }),
        "ps should include the completed run: {runs:#?}"
    );
}

#[test]
fn setup_seeded_run_helpers_preserve_handle_when_another_run_exists() {
    let context = test_context!();

    let created = setup_seeded_created_dry_run(&context);
    let completed = setup_seeded_completed_dry_run(&context);

    assert_ne!(created.run_id, completed.run_id);
    assert_ne!(created.run_dir, completed.run_dir);
    assert!(created.run_dir.exists(), "created run dir should exist");
    assert!(completed.run_dir.exists(), "completed run dir should exist");
}

#[test]
fn ps_quiet_outputs_run_ids_only() {
    let context = test_context!();
    setup_seeded_completed_dry_run(&context);
    setup_seeded_created_dry_run(&context);
    let mut cmd = context.ps();
    cmd.args(["-a", "--quiet", "--label", &context.test_case_label()]);

    fabro_snapshot!(context.filters(), cmd, @r###"
    success: true
    exit_code: 0
    ----- stdout -----
    [ULID]
    [ULID]
    ----- stderr -----
    "###);
}

#[test]
fn ps_filters_by_workflow_and_label() {
    let context = test_context!();
    let simple = context.temp_dir.join("simple.fabro");
    let branching = context.temp_dir.join("branching.fabro");
    context.write_temp(
        "simple.fabro",
        r#"digraph Simple {
  start [shape=Mdiamond]
  exit [shape=Msquare]
  run [shape=parallelogram, script="true"]
  start -> run -> exit
}
"#,
    );
    context.write_temp(
        "branching.fabro",
        r#"digraph Branching {
  start [shape=Mdiamond]
  exit [shape=Msquare]
  run [shape=parallelogram, script="true"]
  start -> run -> exit
}
"#,
    );

    context
        .run_cmd()
        .args(["--dry-run", "--auto-approve", "--label", "suite=alpha"])
        .arg(&simple)
        .assert()
        .success();
    context
        .create_cmd()
        .args(["--dry-run", "--auto-approve", "--label", "suite=beta"])
        .arg(&branching)
        .assert()
        .success();

    let output = context
        .ps()
        .args([
            "-a",
            "--json",
            "--workflow",
            "Simple",
            "--label",
            "suite=alpha",
            "--label",
            &context.test_case_label(),
        ])
        .output()
        .expect("ps should run");

    assert!(output.status.success(), "ps should succeed");
    let runs: Vec<Value> = serde_json::from_slice(&output.stdout).expect("ps JSON should parse");
    assert_eq!(
        runs.len(),
        1,
        "workflow+label filter should isolate one run"
    );
    let run = &runs[0];
    assert!(run["workflow_name"].is_null());
    assert_eq!(run["workflow_graph_name"], "Simple");
    assert_eq!(run["status"]["kind"], "succeeded");
    assert_eq!(run["status"]["reason"], "completed");
    assert_eq!(run["labels"]["suite"], "alpha");
    assert_eq!(run["labels"]["fabro_test_case"], context.test_case_id());
    assert_eq!(run["labels"]["fabro_test_run"], context.test_run_id());
}

#[test]
fn ps_uses_configured_server_target_without_server_flag() {
    let context = test_context!();
    let server = MockServer::start();
    let run_id = unique_run_id();
    let mut summary = remote_run_summary_json(
        &run_id,
        "Remote Workflow",
        "remote-workflow",
        "Remote goal",
        &serde_json::json!({
            "kind": "succeeded",
            "reason": "completed"
        }),
        "2026-04-05T12:00:00Z",
    );
    summary["labels"] = serde_json::json!({
        "suite": "remote"
    });
    let mock = server.mock(|when, then| {
        when.method("GET").path("/api/v1/runs");
        then.status(200)
            .header("Content-Type", "application/json")
            .body(
                serde_json::json!({
                    "data": [summary],
                    "meta": { "has_more": false }
                })
                .to_string(),
            );
    });
    context.set_http_target(&server.base_url());

    let output = context
        .ps()
        .args(["-a", "--json"])
        .output()
        .expect("ps should execute");

    assert!(output.status.success(), "ps should succeed");
    let runs: Vec<Value> = serde_json::from_slice(&output.stdout).expect("ps JSON should parse");
    mock.assert();
    assert_eq!(runs.len(), 1);
    assert_eq!(runs[0]["workflow_name"], "Remote Workflow");
    assert_eq!(runs[0]["source_directory"], "/srv/repo");
}

#[test]
fn ps_parent_resolves_parent_and_filters_on_server() {
    let context = test_context!();
    let server = MockServer::start();
    let child_id = unique_run_id();
    let parent_id = unique_run_id();
    let resolve_mock = super::support::mock_resolved_run(&server, "nightly-parent", &parent_id);
    let mut summary = remote_run_summary_json(
        &child_id,
        "Child Workflow",
        "child-workflow",
        "Child goal",
        &serde_json::json!({
            "kind": "succeeded",
            "reason": "completed"
        }),
        "2026-04-20T12:00:00Z",
    );
    summary["parent_id"] = serde_json::json!(parent_id);
    let list_mock = server.mock(|when, then| {
        when.method("GET")
            .path("/api/v1/runs")
            .query_param("parent_id", parent_id.as_str());
        then.status(200)
            .header("Content-Type", "application/json")
            .body(
                serde_json::json!({
                    "data": [summary],
                    "meta": { "has_more": false }
                })
                .to_string(),
            );
    });

    let output = context
        .ps()
        .args([
            "-a",
            "--json",
            "--server",
            &format!("{}/api/v1", server.base_url()),
            "--parent",
            "nightly-parent",
        ])
        .output()
        .expect("ps should execute");

    assert!(output.status.success(), "ps should succeed");
    let runs: Vec<Value> = serde_json::from_slice(&output.stdout).expect("ps JSON should parse");
    resolve_mock.assert();
    list_mock.assert();
    assert_eq!(runs.len(), 1);
    assert_eq!(runs[0]["run_id"], child_id);
    assert_eq!(runs[0]["parent_id"], parent_id);
}

#[test]
fn ps_table_adds_parent_column_for_unfiltered_child_runs() {
    let context = test_context!();
    let server = MockServer::start();
    let child_id = unique_run_id();
    let parent_id = unique_run_id();
    let mut summary = remote_run_summary_json(
        &child_id,
        "Child Workflow",
        "child-workflow",
        "Child goal",
        &serde_json::json!({
            "kind": "succeeded",
            "reason": "completed"
        }),
        "2026-04-20T12:00:00Z",
    );
    summary["parent_id"] = serde_json::json!(parent_id);
    let list_mock = server.mock(|when, then| {
        when.method("GET").path("/api/v1/runs");
        then.status(200)
            .header("Content-Type", "application/json")
            .body(
                serde_json::json!({
                    "data": [summary],
                    "meta": { "has_more": false }
                })
                .to_string(),
            );
    });

    let output = context
        .ps()
        .args(["-a", "--server", &format!("{}/api/v1", server.base_url())])
        .output()
        .expect("ps should execute");

    assert!(output.status.success(), "ps should succeed");
    let stdout = String::from_utf8_lossy(&output.stdout);
    list_mock.assert();
    assert!(
        stdout.contains("PARENT"),
        "table should include parent column:\n{stdout}"
    );
    assert!(
        stdout.contains(&child_id[..12]),
        "table should include child run id:\n{stdout}"
    );
    assert!(
        stdout.contains(&parent_id[..12]),
        "table should include parent run id:\n{stdout}"
    );
}

#[test]
fn ps_explicit_remote_target_ignores_broken_local_storage_settings() {
    let context = test_context!();
    let server = MockServer::start();
    let run_id = unique_run_id();
    let summary = remote_run_summary_json(
        &run_id,
        "Explicit Remote",
        "explicit-remote",
        "Remote goal",
        &serde_json::json!({
            "kind": "succeeded",
            "reason": "completed"
        }),
        "2026-04-20T12:00:00Z",
    );
    let mock = server.mock(|when, then| {
        when.method("GET").path("/api/v1/runs");
        then.status(200)
            .header("Content-Type", "application/json")
            .body(
                serde_json::json!({
                    "data": [summary],
                    "meta": { "has_more": false }
                })
                .to_string(),
            );
    });
    context.write_home(
        ".fabro/settings.toml",
        "_version = 1\n\n[server.storage]\nroot = \"${FABRO_MISSING_STORAGE_ROOT}\"\n",
    );

    let output = context
        .command()
        .env_remove("FABRO_STORAGE_DIR")
        .args([
            "ps",
            "-a",
            "--json",
            "--server",
            &format!("{}/api/v1", server.base_url()),
        ])
        .output()
        .expect("ps should execute");

    assert!(
        output.status.success(),
        "explicit remote ps should ignore broken local storage settings\nstdout:\n{}\nstderr:\n{}",
        String::from_utf8_lossy(&output.stdout),
        String::from_utf8_lossy(&output.stderr)
    );
    mock.assert();
    let runs: Vec<Value> = serde_json::from_slice(&output.stdout).expect("ps JSON should parse");
    assert_eq!(runs.len(), 1);
    assert_eq!(runs[0]["workflow_name"], "Explicit Remote");
}
