// SPDX-License-Identifier: AGPL-3.0-or-later
// Copyright (C) 2024-2026 SuperNovae Studio <contact@supernovae.studio>
//
#![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
// Pack integrity — the embedded snapshot must match its own manifest.
//
// The manifest (generated by the spec's showcase-projector) carries a
// sha256_16 over the LEAN text of every workflow. Re-hashing the
// embedded bytes here means a tampered, truncated or drifted vendored
// pack fails `cargo test` — the binary can never silently ship a pack
// that disagrees with the spec version it claims.

use sha2::{Digest, Sha256};

fn sha256_16(text: &str) -> String {
    // the manifest hashes lean + trailing newline (python lean() keeps it)
    let mut h = Sha256::new();
    h.update(text.as_bytes());
    h.update(b"\n");
    let digest = h.finalize();
    let mut out = String::with_capacity(16);
    for b in digest.iter().take(8) {
        use std::fmt::Write as _;
        let _ = write!(out, "{b:02x}");
    }
    out
}

/// Parse `file:` / `sha256_16:` pairs out of the manifest.
fn manifest_entries() -> Vec<(String, String)> {
    let mut out = Vec::new();
    let mut current: Option<String> = None;
    for line in nika_pack::manifest().lines() {
        let line = line.trim();
        if let Some(f) = line.strip_prefix("- file: ") {
            current = Some(f.trim().to_owned());
        } else if let Some(h) = line.strip_prefix("sha256_16: ")
            && let Some(f) = current.take()
        {
            out.push((f, h.trim().to_owned()));
        }
    }
    out
}

#[test]
fn version_is_nonempty_and_matches_manifest() {
    let v = nika_pack::pack_version();
    assert!(!v.is_empty());
    let line = format!("pack_version: {v}");
    assert!(
        nika_pack::manifest().contains(&line),
        "manifest pack_version must equal the embedded VERSION ({v})"
    );
}

#[test]
fn every_manifest_entry_resolves_and_hashes_clean() {
    let entries = manifest_entries();
    assert!(
        entries.len() >= 27,
        "expected the full pack · got {}",
        entries.len()
    );
    for (file, expected) in entries {
        let slug = file
            .strip_prefix("examples/")
            .map(str::to_owned)
            .or_else(|| {
                file.strip_prefix("templates/")
                    .map(|s| format!("templates::{s}"))
            });
        let text = match slug {
            Some(s) if s.starts_with("templates::") => {
                nika_pack::template(s.trim_start_matches("templates::"))
            }
            Some(s) => nika_pack::example(&s),
            None => None,
        };
        let text = text.unwrap_or_else(|| panic!("manifest entry {file} not embedded"));
        let lean = nika_pack::lean(text);
        assert_eq!(
            sha256_16(lean),
            expected,
            "embedded {file} drifted from its manifest hash"
        );
    }
}

#[test]
fn surface_counts_hold() {
    assert_eq!(nika_pack::template_names().len(), 6);
    assert!(nika_pack::example_slugs().len() >= 27);
    assert!(nika_pack::doc_paths().len() >= 12, "spec + stdlib pages");
    assert!(nika_pack::schema_json().contains("\"$schema\""));
    assert!(nika_pack::canon().contains("schema_version:"));
    // content assertions (not just non-empty) — mutation-hardened
    assert!(nika_pack::quickstart().contains("nika"));
    let verbs = nika_pack::doc("spec/02-verbs.md").expect("verbs page embedded");
    assert!(verbs.contains("infer"));
    assert!(nika_pack::doc("stdlib/builtins-v0.1.md").is_some());
    assert!(nika_pack::doc("no/such/page.md").is_none());
}

#[test]
fn manifest_and_embedded_sets_are_bijective() {
    // bidirectional · a file added to pack/ without a manifest regen (or
    // dropped from the manifest) fails here — drift cannot ride along.
    let entries = manifest_entries().len();
    let embedded = nika_pack::example_slugs().len() + nika_pack::template_names().len();
    assert_eq!(
        entries, embedded,
        "manifest entries vs embedded workflows+templates"
    );
}

#[test]
fn lean_strips_the_banner_and_nothing_else() {
    let text = "# SPDX\n# banner\nnika: v1\nworkflow: x\n";
    assert_eq!(nika_pack::lean(text), "nika: v1\nworkflow: x");
    // no banner → identity
    assert_eq!(nika_pack::lean("nika: v1\n"), "nika: v1");
    // the hashing invariant · lean() trims ALL trailing newlines and the
    // hasher re-appends exactly one — mirrors the spec projector's lean()
    // (python keeps one trailing \n). Multi-newline tails normalize.
    assert_eq!(nika_pack::lean("nika: v1\n\n"), "nika: v1");
}

#[test]
fn error_codes_registry_parses_typed_and_complete() {
    // THE one parser for the canon error_codes rows — every consumer
    // (explain's teach surface · the emitted⊆registered ratchet) reads
    // through this accessor, so its contract is pinned at the seam:
    let rows = nika_pack::error_codes();
    // ≥ the v0.1 normative floor (49 at vendoring · never hand-pin the
    // live count — the canon's own count field is the cross-check).
    assert!(
        rows.len() >= 30,
        "registry parse broke ({} rows)",
        rows.len()
    );
    let declared: usize = nika_pack::canon()
        .lines()
        .skip_while(|l| !l.starts_with("error_codes:"))
        .take_while(|l| {
            l.starts_with([' ', '#']) || l.starts_with("error_codes:") || l.trim().is_empty()
        })
        .find_map(|l| {
            l.trim()
                .strip_prefix("count:")
                .and_then(|v| v.trim().parse().ok())
        })
        .expect("canon declares error_codes.count");
    assert_eq!(rows.len(), declared, "parsed rows vs the canon's own count");

    // Spot-pin one row fully (the beginner's #1 error).
    let dag3 = rows
        .iter()
        .find(|r| r.code == "NIKA-DAG-003")
        .expect("NIKA-DAG-003 row");
    assert_eq!(dag3.category, "validation_error");
    assert_eq!(dag3.transient, "false");
    assert!(dag3.failure.len() > 10);

    // Every row is well-formed + the escape-free invariant the anchored
    // parse relies on (an escaped quote would silently truncate).
    for r in &rows {
        assert!(r.code.starts_with("NIKA-"), "{r:?}");
        assert!(!r.category.is_empty() && !r.failure.is_empty(), "{r:?}");
        assert!(!r.failure.contains('\\'), "escape in failure text: {r:?}");
    }

    // Anchoring: codes are unique (a same-shaped row from another canon
    // table leaking in would collide or inflate the count).
    let mut codes: Vec<_> = rows.iter().map(|r| r.code).collect();
    codes.sort_unstable();
    codes.dedup();
    assert_eq!(codes.len(), rows.len(), "duplicate codes in the registry");
}
