use assert_cmd::Command;
use predicates::prelude::*;
use std::fs;
use tempfile::tempdir;

const MANIFEST: &str = r#"{
  "schema_version": 1,
  "tool": { "name": "test", "version": "0.0.1" },
  "image": {
    "name": "img",
    "tag": "local",
    "os": { "family": "linux", "distro": "ubuntu", "version": "22.04", "arch": "x64" }
  },
  "labels": ["self-hosted", "linux", "x64", "ubuntu-22.04"],
  "capabilities": ["docker"],
  "toolcache": { "root": "/opt/hostedtoolcache" },
  "tools": [
    { "name": "node", "version": "22.11.0", "arch": "x64",
      "path": "/opt/hostedtoolcache/node/22.11.0/x64",
      "aliases": ["22", "lts/jod"], "in_toolcache": true }
  ],
  "packages": [
    { "manager": "apt", "name": "docker.io", "version": "27.3.1" }
  ],
  "environment": { "RUNNER_OS": "Linux", "RUNNER_ARCH": "X64", "RUNNER_TOOL_CACHE": "/opt/hostedtoolcache" },
  "evidence": { "source_urls": ["https://example.test/runner.md"] }
}"#;

fn write_manifest(json: &str) -> (tempfile::TempDir, String) {
    let dir = tempdir().unwrap();
    let path = dir.path().join("manifest.json");
    fs::write(&path, json).unwrap();
    let path_str = path.to_str().unwrap().to_owned();
    (dir, path_str)
}

#[test]
fn check_command_passes_minimal_manifest() {
    let (_dir, path) = write_manifest(MANIFEST);
    let output = Command::cargo_bin("gha-runner-image-proof")
        .unwrap()
        .args(["check", "--manifest", &path, "--format", "json"])
        .assert()
        .success()
        .get_output()
        .clone();
    let stdout = String::from_utf8_lossy(&output.stdout);
    let parsed: serde_json::Value = serde_json::from_str(&stdout).unwrap();
    assert_eq!(parsed["mode"], "check");
    assert_eq!(parsed["summary"]["failed"].as_u64().unwrap(), 0);
    assert!(parsed["summary"]["passed"].as_u64().unwrap() >= 1);
    assert_eq!(parsed["check"]["image_name"], "img");
}

#[test]
fn check_command_fails_when_manifest_invalid() {
    let bad = r#"{
        "schema_version": 1,
        "image": { "name": "", "os": { "family": "linux", "arch": "x64" } }
    }"#;
    let (_dir, path) = write_manifest(bad);
    Command::cargo_bin("gha-runner-image-proof")
        .unwrap()
        .args(["check", "--manifest", &path, "--format", "json"])
        .assert()
        .failure()
        .stderr(predicate::str::contains("runner image proof failed"));
}

#[test]
fn check_command_text_output_default() {
    let (_dir, path) = write_manifest(MANIFEST);
    let output = Command::cargo_bin("gha-runner-image-proof")
        .unwrap()
        .args(["check", "--manifest", &path])
        .assert()
        .success()
        .get_output()
        .clone();
    let stdout = String::from_utf8_lossy(&output.stdout);
    assert!(stdout.contains("gha-runner-image-proof"));
    assert!(stdout.contains("(check)"));
    assert!(stdout.contains("summary: "));
}

#[test]
fn check_command_markdown_output() {
    let (_dir, path) = write_manifest(MANIFEST);
    let output = Command::cargo_bin("gha-runner-image-proof")
        .unwrap()
        .args(["check", "--manifest", &path, "--format", "markdown"])
        .assert()
        .success()
        .get_output()
        .clone();
    let stdout = String::from_utf8_lossy(&output.stdout);
    assert!(stdout.contains("# gha-runner-image-proof Receipt"));
    assert!(stdout.contains("## Manifest"));
}

#[test]
fn check_command_writes_output_file() {
    let (_dir, path) = write_manifest(MANIFEST);
    let out_dir = tempdir().unwrap();
    let out_path = out_dir.path().join("nested").join("receipt.json");
    Command::cargo_bin("gha-runner-image-proof")
        .unwrap()
        .args([
            "check",
            "--manifest",
            &path,
            "--format",
            "json",
            "--output",
            out_path.to_str().unwrap(),
        ])
        .assert()
        .success();
    assert!(out_path.exists());
    let raw = fs::read_to_string(out_path).unwrap();
    let parsed: serde_json::Value = serde_json::from_str(&raw).unwrap();
    assert_eq!(parsed["mode"], "check");
}

#[test]
fn check_missing_manifest_path_errors() {
    Command::cargo_bin("gha-runner-image-proof")
        .unwrap()
        .args(["check", "--manifest", "no-such-file.json"])
        .assert()
        .failure()
        .stderr(predicate::str::contains(
            "--manifest must point at an existing file",
        ));
}

#[test]
fn check_strict_treats_warnings_as_fail() {
    // No labels list = manifest.labels_empty warning, but valid otherwise.
    let warn_manifest = r#"{
        "schema_version": 1,
        "image": { "name": "img", "os": { "family": "linux", "arch": "x64" } }
    }"#;
    let (_dir, path) = write_manifest(warn_manifest);
    Command::cargo_bin("gha-runner-image-proof")
        .unwrap()
        .args(["check", "--manifest", &path, "--strict"])
        .assert()
        .failure();
}

#[test]
fn setup_node_exact_succeeds() {
    let (_dir, path) = write_manifest(MANIFEST);
    let output = Command::cargo_bin("gha-runner-image-proof")
        .unwrap()
        .args([
            "setup-node",
            "--manifest",
            &path,
            "--version",
            "22.11.0",
            "--arch",
            "x64",
            "--format",
            "json",
        ])
        .assert()
        .success()
        .get_output()
        .clone();
    let parsed: serde_json::Value = serde_json::from_slice(&output.stdout).unwrap();
    assert_eq!(parsed["setup_node"]["classification"], "exact");
    assert_eq!(parsed["setup_node"]["resolved_version"], "22.11.0");
}

#[test]
fn setup_node_broad_spec_without_alias_fails() {
    let (_dir, path) = write_manifest(MANIFEST);
    Command::cargo_bin("gha-runner-image-proof")
        .unwrap()
        .args([
            "setup-node",
            "--manifest",
            &path,
            "--version",
            "lts/*",
            "--arch",
            "x64",
        ])
        .assert()
        .failure()
        .stdout(predicate::str::contains(
            "setup-node.version_resolution_requires_catalog",
        ));
}

#[test]
fn setup_node_arch_mismatch_fails() {
    let (_dir, path) = write_manifest(MANIFEST);
    Command::cargo_bin("gha-runner-image-proof")
        .unwrap()
        .args([
            "setup-node",
            "--manifest",
            &path,
            "--version",
            "22.11.0",
            "--arch",
            "arm64",
        ])
        .assert()
        .failure();
}

#[test]
fn setup_node_check_latest_emits_warning() {
    let (_dir, path) = write_manifest(MANIFEST);
    let output = Command::cargo_bin("gha-runner-image-proof")
        .unwrap()
        .args([
            "setup-node",
            "--manifest",
            &path,
            "--version",
            "22.11.0",
            "--arch",
            "x64",
            "--check-latest",
            "--format",
            "json",
        ])
        .assert()
        .success()
        .get_output()
        .clone();
    let parsed: serde_json::Value = serde_json::from_slice(&output.stdout).unwrap();
    let checks = parsed["setup_node"]["checks"].as_array().unwrap();
    assert!(
        checks
            .iter()
            .any(|c| c["id"] == "setup-node.check_latest_requires_online")
    );
}

#[test]
fn probe_host_records_request_against_manifest() {
    // Probe a tool that the manifest does not declare; we should record the failure
    // without touching the OS.
    let (_dir, path) = write_manifest(MANIFEST);
    let output = Command::cargo_bin("gha-runner-image-proof")
        .unwrap()
        .args([
            "probe-host",
            "--manifest",
            &path,
            "--tool",
            "totally-fake-tool-name",
            "--format",
            "json",
        ])
        .assert()
        .failure()
        .get_output()
        .clone();
    let parsed: serde_json::Value = serde_json::from_slice(&output.stdout).unwrap();
    assert_eq!(parsed["mode"], "probe-host");
    let checks = parsed["probe_host"]["checks"].as_array().unwrap();
    assert!(
        checks
            .iter()
            .any(|c| c["id"] == "probe.tool_not_in_manifest")
    );
}

#[test]
fn probe_docker_skips_when_docker_cli_absent_or_runs_when_present() {
    // Behaviour is environment-dependent. If docker is on PATH, we still expect a
    // non-zero exit because the image probably doesn't exist; if it isn't, the
    // probe should skip and exit zero with no failures, but the requested
    // manifest mismatch may still fail. Just assert that the receipt is emitted
    // and is well-formed JSON.
    let (_dir, path) = write_manifest(MANIFEST);
    let assert = Command::cargo_bin("gha-runner-image-proof")
        .unwrap()
        .args([
            "probe-docker",
            "--manifest",
            &path,
            "--image",
            "ci-forge/runner-image-proof-test:does-not-exist",
            "--format",
            "json",
        ])
        .assert();
    let output = assert.get_output().clone();
    let parsed: serde_json::Value = serde_json::from_slice(&output.stdout).unwrap();
    assert_eq!(parsed["mode"], "probe-docker");
}

#[test]
fn probe_docker_empty_image_rejected() {
    let (_dir, path) = write_manifest(MANIFEST);
    Command::cargo_bin("gha-runner-image-proof")
        .unwrap()
        .args(["probe-docker", "--manifest", &path, "--image", ""])
        .assert()
        .failure()
        .stderr(predicate::str::contains("--image must not be empty"));
}

#[test]
fn check_help_lists_all_subcommands() {
    let output = Command::cargo_bin("gha-runner-image-proof")
        .unwrap()
        .arg("--help")
        .assert()
        .success()
        .get_output()
        .clone();
    let stdout = String::from_utf8_lossy(&output.stdout);
    for sub in ["check", "probe-host", "probe-docker", "setup-node"] {
        assert!(stdout.contains(sub), "help missing subcommand {sub}");
    }
}

#[test]
fn example_ubuntu_manifest_check_passes() {
    // Use the repo's bundled example to validate the schema is real-world plausible.
    let path = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
        .join("examples")
        .join("manifests")
        .join("ubuntu-22.04.json");
    let path_str = path.to_str().unwrap().to_owned();
    Command::cargo_bin("gha-runner-image-proof")
        .unwrap()
        .args(["check", "--manifest", &path_str, "--format", "json"])
        .assert()
        .success();
}
