mod common;
mod golden;

use std::path::{Path, PathBuf};

use bone_jig::{RunStatus, Scenario, StepOutcome, run_scenario};
use common::{find_artifact, manifest_path, run_library_scenario};
use golden::check_golden;

#[test]
fn shell_smoke_matches_golden() {
    let dir = match tempfile::tempdir() {
        Ok(d) => d,
        Err(e) => panic!("tempdir: {e}"),
    };
    let result = run_library_scenario("smoke.ron", dir.path());
    assert_eq!(result.status, RunStatus::Passed, "result: {result:?}");
    let snapshot = find_artifact(dir.path(), "_final.png");
    check_golden(
        "smoke",
        &snapshot,
        &manifest_path("tests/goldens/smoke_final.png"),
        "BONE_UPDATE_JIG_SMOKE",
    );
}

fn run_text(text: &str, out_dir: &Path) -> bone_jig::RunResult {
    let scenario = match Scenario::parse(text) {
        Ok(s) => s,
        Err(e) => panic!("parse scenario: {e}"),
    };
    let mut sink = Vec::new();
    match run_scenario(&scenario, out_dir, &mut sink) {
        Ok(result) => result,
        Err(e) => panic!("run scenario: {e}"),
    }
}

fn read_bytes(path: &Path) -> Vec<u8> {
    match std::fs::read(path) {
        Ok(bytes) => bytes,
        Err(e) => panic!("read {}: {e}", path.display()),
    }
}

#[test]
fn step_import_lands_one_frame_after_open() {
    let dir = match tempfile::tempdir() {
        Ok(d) => d,
        Err(e) => panic!("tempdir: {e}"),
    };
    let fixture = manifest_path("../bone-interop/tests/goldens/cube.step");
    let Some(path) = fixture.to_str() else {
        panic!("fixture path is not utf-8: {}", fixture.display());
    };
    let text = format!(
        r#"Scenario(steps: [Advance(2), OpenDocument({path:?}), Advance(1), DumpTree("cube")])"#
    );
    let result = run_text(&text, dir.path());
    assert_eq!(result.status, RunStatus::Passed, "result: {result:?}");
    let tree = match std::fs::read_to_string(dir.path().join("003_cube.tree.json")) {
        Ok(t) => t,
        Err(e) => panic!("read tree: {e}"),
    };
    assert!(
        tree.contains("cube") && !tree.contains("Untitled"),
        "a synchronous open replaces the default document within one frame; tree: {tree}",
    );
}

#[test]
fn missing_step_document_fails_the_step_and_halts() {
    let dir = match tempfile::tempdir() {
        Ok(d) => d,
        Err(e) => panic!("tempdir: {e}"),
    };
    let result = run_text(
        r#"Scenario(steps: [OpenDocument("/nonexistent/clam.step"), Snapshot("after")])"#,
        dir.path(),
    );
    assert_eq!(
        result.status,
        RunStatus::Failed,
        "a missing import must fail the run",
    );
    assert_eq!(
        result.steps.len(),
        1,
        "the snapshot after a failed open must not run",
    );
    assert!(matches!(result.steps[0].outcome, StepOutcome::Failed(_)));
    assert!(!dir.path().join("001_after.png").exists());
}

#[test]
fn smoke_run_is_byte_identical_across_runs() {
    let first = match tempfile::tempdir() {
        Ok(d) => d,
        Err(e) => panic!("tempdir: {e}"),
    };
    let second = match tempfile::tempdir() {
        Ok(d) => d,
        Err(e) => panic!("tempdir: {e}"),
    };
    let a = run_library_scenario("smoke.ron", first.path());
    let b = run_library_scenario("smoke.ron", second.path());
    assert_eq!(a.status, RunStatus::Passed, "first run: {a:?}");
    assert_eq!(b.status, RunStatus::Passed, "second run: {b:?}");
    assert_eq!(
        read_bytes(&find_artifact(first.path(), "_final.png")),
        read_bytes(&find_artifact(second.path(), "_final.png")),
        "two runs of the same scenario must produce byte-identical pixels",
    );
    assert_eq!(
        read_bytes(&find_artifact(first.path(), "_final.tree.json")),
        read_bytes(&find_artifact(second.path(), "_final.tree.json")),
        "two runs must produce byte-identical trees",
    );
}

#[test]
fn every_library_scenario_runs_clean() {
    let dir = manifest_path("scenarios");
    let entries = match std::fs::read_dir(&dir) {
        Ok(it) => it,
        Err(e) => panic!("read {}: {e}", dir.display()),
    };
    let ran: Vec<PathBuf> = entries
        .filter_map(Result::ok)
        .map(|entry| entry.path())
        .filter(|path| path.extension().is_some_and(|ext| ext == "ron"))
        .inspect(|path| {
            let out = match tempfile::tempdir() {
                Ok(d) => d,
                Err(e) => panic!("tempdir: {e}"),
            };
            let Some(name) = path.file_name().and_then(|n| n.to_str()) else {
                panic!("scenario path is not utf-8: {}", path.display());
            };
            let result = run_library_scenario(name, out.path());
            assert_eq!(
                result.status,
                RunStatus::Passed,
                "{name} did not run clean: {result:?}",
            );
        })
        .collect();
    assert!(ran.len() >= 12, "library shrank: {ran:?}");
}
