use serde_json::Value;
use std::fs;
use std::path::Path;
use std::process::{Command, Stdio};

mod common;
use common::ChildInputExt;

#[test]
fn package_verify_outputs_fixture_report() {
    let manifest = common::repo_root().join("examples/protein-package/manifest.json");
    let observations = common::repo_root().join("examples/protein-package/observations.json");

    let output = Command::new(env!("CARGO_BIN_EXE_biors"))
        .arg("package")
        .arg("verify")
        .arg(manifest)
        .arg(observations)
        .output()
        .expect("run biors package verify");

    assert!(
        output.status.success(),
        "stderr: {}",
        String::from_utf8_lossy(&output.stderr)
    );

    let value: Value = serde_json::from_slice(&output.stdout).expect("valid JSON output");

    assert_eq!(value["data"]["package"], "protein-seed");
    assert_eq!(value["data"]["fixtures"], 1);
    assert_eq!(value["data"]["passed"], 1);
    assert_eq!(value["data"]["failed"], 0);
    assert_eq!(
        value["data"]["observation_issues"]
            .as_array()
            .expect("observation issues")
            .len(),
        0
    );
    assert_eq!(value["data"]["results"][0]["status"], "passed");
    assert_eq!(
        value["data"]["results"][0]["expected_output_path"],
        "fixtures/tiny.output.json"
    );
    assert_eq!(
        value["data"]["results"][0]["observed_output_path"],
        "observed/tiny.output.json"
    );
    assert_eq!(value["data"]["results"][0]["checksum_mismatch"], false);
    assert_eq!(value["data"]["results"][0]["content_mismatch"], false);
}

#[test]
fn package_verify_reports_duplicate_observation_code() {
    let manifest = common::repo_root().join("examples/protein-package/manifest.json");

    let output = Command::new(env!("CARGO_BIN_EXE_biors"))
        .arg("--json")
        .arg("package")
        .arg("verify")
        .arg(manifest)
        .arg("-")
        .stdin(Stdio::piped())
        .stdout(Stdio::piped())
        .stderr(Stdio::piped())
        .spawn()
        .expect("spawn biors package verify")
        .tap_stdin(
            r#"[
              {
                "name": "tiny-protein",
                "path": "observed/tiny.output.json"
              },
              {
                "name": "tiny-protein",
                "path": "observed/tiny.reordered.json"
              }
            ]"#,
        );

    assert_eq!(output.status.code(), Some(2));
    assert!(output.stderr.is_empty());

    let value: Value = serde_json::from_slice(&output.stdout).expect("valid JSON error");
    assert_eq!(value["error"]["code"], "package.duplicate_observation");
    assert_eq!(
        value["error"]["details"]["results"][0]["issue_code"],
        "duplicate_observation"
    );
}

#[test]
fn package_verify_reports_unexpected_observation_code() {
    let manifest = common::repo_root().join("examples/protein-package/manifest.json");

    let output = Command::new(env!("CARGO_BIN_EXE_biors"))
        .arg("--json")
        .arg("package")
        .arg("verify")
        .arg(manifest)
        .arg("-")
        .stdin(Stdio::piped())
        .stdout(Stdio::piped())
        .stderr(Stdio::piped())
        .spawn()
        .expect("spawn biors package verify")
        .tap_stdin(
            r#"[
              {
                "name": "tiny-protein",
                "path": "observed/tiny.output.json"
              },
              {
                "name": "stale-output",
                "path": "observed/stale.output.json"
              }
            ]"#,
        );

    assert_eq!(output.status.code(), Some(2));
    assert!(output.stderr.is_empty());

    let value: Value = serde_json::from_slice(&output.stdout).expect("valid JSON error");
    assert_eq!(value["error"]["code"], "package.unexpected_observation");
    assert_eq!(
        value["error"]["details"]["observation_issues"][0]["code"],
        "unexpected_observation"
    );
}

#[test]
fn package_verify_reports_missing_observed_output_code() {
    let manifest = common::repo_root().join("examples/protein-package/manifest.json");

    let output = Command::new(env!("CARGO_BIN_EXE_biors"))
        .arg("--json")
        .arg("package")
        .arg("verify")
        .arg(manifest)
        .arg("-")
        .stdin(Stdio::piped())
        .stdout(Stdio::piped())
        .stderr(Stdio::piped())
        .spawn()
        .expect("spawn biors package verify")
        .tap_stdin(
            r#"[
              {
                "name": "tiny-protein",
                "path": "observed/missing.json"
              }
            ]"#,
        );

    assert_eq!(output.status.code(), Some(2));
    assert!(output.stderr.is_empty());

    let value: Value = serde_json::from_slice(&output.stdout).expect("valid JSON error");
    assert_eq!(value["error"]["code"], "package.observed_output_missing");
    assert_eq!(value["error"]["location"], "fixtures");
    assert_eq!(
        value["error"]["details"]["results"][0]["issue_code"],
        "observed_output_read_failed"
    );
    assert!(!value["error"]["message"]
        .as_str()
        .expect("message")
        .starts_with('['));
}

#[test]
fn package_verify_reports_content_mismatch_code() {
    let manifest = common::repo_root().join("examples/protein-package/manifest.json");
    let observations =
        common::repo_root().join("examples/protein-package/observations.mismatch.json");

    let output = Command::new(env!("CARGO_BIN_EXE_biors"))
        .arg("--json")
        .arg("package")
        .arg("verify")
        .arg(manifest)
        .arg(observations)
        .output()
        .expect("run biors package verify");

    assert_eq!(output.status.code(), Some(2));
    assert!(output.stderr.is_empty());

    let value: Value = serde_json::from_slice(&output.stdout).expect("valid JSON error");
    assert_eq!(value["error"]["code"], "package.output_content_mismatch");
    assert_eq!(value["error"]["location"], "fixtures");
    assert_eq!(
        value["error"]["details"]["results"][0]["content_mismatch"],
        true
    );
}

#[test]
fn package_verify_rejects_manifest_missing_v1_metadata_before_fixture_compare() {
    let package = copy_example_package("verify-missing-metadata");
    let manifest_path = package.path().join("manifest.json");
    let observations_path = package.path().join("observations.json");
    let mut manifest: Value =
        serde_json::from_str(&fs::read_to_string(&manifest_path).expect("read copied manifest"))
            .expect("parse manifest");
    manifest
        .as_object_mut()
        .expect("manifest object")
        .remove("metadata");
    fs::write(
        &manifest_path,
        serde_json::to_string_pretty(&manifest).expect("serialize manifest"),
    )
    .expect("write invalid manifest");

    let output = Command::new(env!("CARGO_BIN_EXE_biors"))
        .arg("--json")
        .arg("package")
        .arg("verify")
        .arg(manifest_path)
        .arg(observations_path)
        .output()
        .expect("run biors package verify");

    assert_eq!(output.status.code(), Some(2));
    assert!(output.stderr.is_empty());

    let value: Value = serde_json::from_slice(&output.stdout).expect("valid JSON error");
    assert_eq!(value["error"]["code"], "package.validation_failed");
    assert_eq!(value["error"]["location"], "manifest");
    assert_eq!(
        value["error"]["details"]["validation"]["structured_issues"][0]["field"],
        "metadata"
    );
    assert_eq!(
        value["error"]["details"]["validation"]["structured_issues"][0]["code"],
        "required_field"
    );
    assert!(value["error"]["details"]["results"].is_null());
}

#[test]
fn package_verify_rejects_invalid_layout_before_fixture_compare() {
    let package = copy_example_package("verify-invalid-layout");
    let manifest_path = package.path().join("manifest.json");
    let observations_path = package.path().join("observations.json");
    fs::create_dir_all(package.path().join("artifacts")).expect("create artifacts dir");
    fs::copy(
        package.path().join("models/protein-seed.onnx"),
        package.path().join("artifacts/protein-seed.onnx"),
    )
    .expect("copy model into invalid layout path");

    let mut manifest: Value =
        serde_json::from_str(&fs::read_to_string(&manifest_path).expect("read copied manifest"))
            .expect("parse manifest");
    manifest["model"]["path"] = Value::String("artifacts/protein-seed.onnx".to_string());
    fs::write(
        &manifest_path,
        serde_json::to_string_pretty(&manifest).expect("serialize manifest"),
    )
    .expect("write invalid manifest");

    let output = Command::new(env!("CARGO_BIN_EXE_biors"))
        .arg("--json")
        .arg("package")
        .arg("verify")
        .arg(manifest_path)
        .arg(observations_path)
        .output()
        .expect("run biors package verify");

    assert_eq!(output.status.code(), Some(2));
    assert!(output.stderr.is_empty());

    let value: Value = serde_json::from_slice(&output.stdout).expect("valid JSON error");
    assert_eq!(value["error"]["code"], "package.layout_mismatch");
    assert_eq!(value["error"]["location"], "manifest");
    assert_eq!(
        value["error"]["details"]["validation"]["structured_issues"][0]["code"],
        "layout_mismatch"
    );
    assert_eq!(
        value["error"]["details"]["validation"]["structured_issues"][0]["field"],
        "model.path"
    );
}

fn copy_example_package(name: &str) -> common::TempDir {
    let package = common::TempDir::new(name);
    copy_dir(
        &common::repo_root().join("examples/protein-package"),
        package.path(),
    );
    package
}

fn copy_dir(from: &Path, to: &Path) {
    fs::create_dir_all(to).expect("create target dir");
    for entry in fs::read_dir(from).expect("read source dir") {
        let entry = entry.expect("read source entry");
        let source = entry.path();
        let target = to.join(entry.file_name());
        if source.is_dir() {
            copy_dir(&source, &target);
        } else {
            fs::copy(&source, &target).expect("copy file");
        }
    }
}
