//! `.snap` file format: render, parse, compare.
//!
//! ```text
//! # generated by `cargo conformance --bless`; do not edit by hand
//! # encoding.snap โ€” canonical encodings
//! nebu::Goldilocks@v1                tier=gamma  h1f3aโ€ฆc2d
//! ```
//!
//! ```text
//! nox::reduce@v1 :: "spec-add"       tier=gamma  h891cโ€ฆ233
//! ```
//!
//! Lines are sorted by key; the name column is padded to the longest key.
//! The file is a pure function of its entries, so `--check` can demand
//! byte equality and still explain any difference entry by entry.

use crate::{EncodingSnapshot, Fingerprint, Manifest, MechanismSnapshot, Tier};

/// First line of every generated file.
pub const GENERATED: &str = "# generated by `cargo conformance --bless`; do not edit by hand";

/// One parsed line, keyed for comparison.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Entry {
    /// `name` for encodings, `mechanism :: "scenario"` for mechanisms
    pub key: String,
    /// tier recorded on the line
    pub tier: Tier,
    /// fingerprint recorded on the line
    pub fingerprint: Fingerprint,
}

/// Everything the harness produces for one stack revision.
#[derive(Debug, Clone, Default)]
pub struct Snapshot {
    /// encoding.snap entries
    pub encodings: Vec<EncodingSnapshot>,
    /// mechanism.snap entries
    pub mechanisms: Vec<MechanismSnapshot>,
}

impl Snapshot {
    /// The workspace manifest over all entries.
    pub fn manifest(&self) -> Manifest {
        Manifest { encodings: self.encodings.clone(), mechanisms: self.mechanisms.clone() }
    }

    /// Entries of encoding.snap, sorted.
    pub fn encoding_entries(&self) -> Vec<Entry> {
        let mut v: Vec<Entry> = self
            .encodings
            .iter()
            .map(|e| Entry { key: e.name.clone(), tier: e.tier, fingerprint: e.fingerprint })
            .collect();
        v.sort_by(|a, b| a.key.cmp(&b.key));
        v
    }

    /// Entries of mechanism.snap, sorted.
    pub fn mechanism_entries(&self) -> Vec<Entry> {
        let mut v: Vec<Entry> = self
            .mechanisms
            .iter()
            .map(|m| Entry {
                key: format!("{} :: \"{}\"", m.mechanism, m.scenario),
                tier: m.tier,
                fingerprint: m.fingerprint,
            })
            .collect();
        v.sort_by(|a, b| a.key.cmp(&b.key));
        v
    }

    /// Duplicate keys make a snapshot ambiguous; reject them at generation.
    pub fn duplicate_keys(&self) -> Vec<String> {
        let mut out = Vec::new();
        for entries in [self.encoding_entries(), self.mechanism_entries()] {
            for w in entries.windows(2) {
                if w[0].key == w[1].key {
                    out.push(w[0].key.clone());
                }
            }
        }
        out
    }

    /// The three files, as `(file name, contents)`.
    pub fn render(&self) -> Vec<(&'static str, String)> {
        vec![
            ("encoding.snap", render(&self.encoding_entries(), "encoding.snap โ€” canonical encodings")),
            ("mechanism.snap", render(&self.mechanism_entries(), "mechanism.snap โ€” mechanism outputs on fixed scenarios")),
            ("manifest.snap", render_manifest(self.manifest().root(), self.encodings.len(), self.mechanisms.len())),
        ]
    }
}

/// Render entries (already sorted) as a `.snap` file.
pub fn render(entries: &[Entry], title: &str) -> String {
    let width = entries.iter().map(|e| e.key.len()).max().unwrap_or(0);
    let mut s = String::new();
    s.push_str(GENERATED);
    s.push('\n');
    s.push_str("# ");
    s.push_str(title);
    s.push('\n');
    for e in entries {
        s.push_str(&format!(
            "{:<width$}  tier={:<7}  {}\n",
            e.key,
            e.tier.name(),
            e.fingerprint.to_snap_string()
        ));
    }
    s
}

/// Render the manifest file: the protocol stability root.
pub fn render_manifest(root: Fingerprint, encodings: usize, mechanisms: usize) -> String {
    format!(
        "{GENERATED}\n# manifest.snap โ€” hemera root over every encoding and mechanism entry\n\
         encodings  {encodings}\nmechanisms {mechanisms}\nroot       {}\n",
        root.to_snap_string()
    )
}

/// Parse a `.snap` file back into entries. Comment and blank lines skip.
pub fn parse(src: &str) -> Result<Vec<Entry>, String> {
    let mut out = Vec::new();
    for (n, line) in src.lines().enumerate() {
        if line.trim().is_empty() || line.starts_with('#') {
            continue;
        }
        let bad = || format!("line {}: not a snapshot entry: {line}", n + 1);
        let fp_at = line.rfind(" h").ok_or_else(bad)?;
        let fingerprint = Fingerprint::from_snap_string(line[fp_at + 1..].trim()).ok_or_else(bad)?;
        let head = line[..fp_at].trim_end();
        let tier_at = head.rfind("tier=").ok_or_else(bad)?;
        let tier = Tier::parse(head[tier_at + 5..].trim()).ok_or_else(bad)?;
        let key = head[..tier_at].trim_end().to_string();
        if key.is_empty() {
            return Err(bad());
        }
        out.push(Entry { key, tier, fingerprint });
    }
    Ok(out)
}

/// Parse the root out of a manifest file.
pub fn parse_manifest_root(src: &str) -> Option<Fingerprint> {
    src.lines()
        .find_map(|l| l.strip_prefix("root"))
        .and_then(|r| Fingerprint::from_snap_string(r.trim()))
}

/// How one key differs between the committed and the current snapshot.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Change {
    /// present now, absent from the committed file
    Added(Entry),
    /// committed, no longer produced
    Removed(Entry),
    /// fingerprint moved (tier as committed)
    Drift {
        /// committed line
        committed: Entry,
        /// current fingerprint
        current: Fingerprint,
    },
    /// same fingerprint, tier changed
    Retier {
        /// committed line
        committed: Entry,
        /// tier the code now declares
        current: Tier,
    },
}

impl Change {
    /// Whether this change fails `--check`.
    ///
    /// Shape changes (added/removed entries) always fail: the stable surface
    /// moved and needs a bless. Fingerprint drift fails at enforcing tiers
    /// (gamma and above) and is reported only at alpha/beta. A tier change
    /// fails: promotion and demotion are bless ceremonies.
    pub fn fails(&self) -> bool {
        match self {
            Change::Added(_) | Change::Removed(_) | Change::Retier { .. } => true,
            Change::Drift { committed, .. } => committed.tier.enforces(),
        }
    }

    /// The tier whose ceremony this change needs on bless.
    pub fn ceremony(&self) -> Tier {
        match self {
            Change::Added(e) => e.tier,
            Change::Removed(e) => e.tier,
            Change::Drift { committed, .. } => committed.tier,
            Change::Retier { committed, current } => committed.tier.max(*current),
        }
    }

    /// One-line human report.
    pub fn describe(&self) -> String {
        match self {
            Change::Added(e) => format!("added    {}  tier={}  {}", e.key, e.tier.name(), e.fingerprint.to_snap_string()),
            Change::Removed(e) => format!("removed  {}  tier={}  {}", e.key, e.tier.name(), e.fingerprint.to_snap_string()),
            Change::Drift { committed, current } => format!(
                "drift    {}  tier={}  {} -> {}",
                committed.key,
                committed.tier.name(),
                committed.fingerprint.to_snap_string(),
                current.to_snap_string()
            ),
            Change::Retier { committed, current } => format!(
                "retier   {}  tier={} -> {}",
                committed.key,
                committed.tier.name(),
                current.name()
            ),
        }
    }
}

/// Entry-level difference, committed โ†’ current. Both inputs sorted or not.
pub fn diff(committed: &[Entry], current: &[Entry]) -> Vec<Change> {
    use std::collections::BTreeMap;
    let old: BTreeMap<&str, &Entry> = committed.iter().map(|e| (e.key.as_str(), e)).collect();
    let new: BTreeMap<&str, &Entry> = current.iter().map(|e| (e.key.as_str(), e)).collect();
    let mut out = Vec::new();
    for (k, o) in &old {
        match new.get(k) {
            None => out.push(Change::Removed((*o).clone())),
            Some(n) if n.fingerprint != o.fingerprint => {
                out.push(Change::Drift { committed: (*o).clone(), current: n.fingerprint })
            }
            Some(n) if n.tier != o.tier => out.push(Change::Retier { committed: (*o).clone(), current: n.tier }),
            Some(_) => {}
        }
    }
    for (k, n) in &new {
        if !old.contains_key(k) {
            out.push(Change::Added((*n).clone()));
        }
    }
    out
}

#[cfg(test)]
mod tests {
    use super::*;

    fn snap() -> Snapshot {
        Snapshot {
            encodings: vec![EncodingSnapshot { name: "a::T@v1".into(), tier: Tier::Gamma, fingerprint: Fingerprint::of(b"a") }],
            mechanisms: vec![
                MechanismSnapshot { mechanism: "m::f@v1".into(), scenario: "s 1".into(), tier: Tier::Beta, fingerprint: Fingerprint::of(b"m") },
                MechanismSnapshot { mechanism: "m::f@v1".into(), scenario: "s 0".into(), tier: Tier::Gamma, fingerprint: Fingerprint::of(b"n") },
            ],
        }
    }

    #[test]
    fn render_parse_roundtrip() {
        let s = snap();
        let files = s.render();
        assert_eq!(parse(&files[0].1).unwrap(), s.encoding_entries());
        assert_eq!(parse(&files[1].1).unwrap(), s.mechanism_entries());
        assert_eq!(parse_manifest_root(&files[2].1), Some(s.manifest().root()));
        // rendering is a pure function of the entries, independent of input order
        let mut r = s.clone();
        r.mechanisms.reverse();
        assert_eq!(r.render(), files);
    }

    #[test]
    fn diff_classifies() {
        let s = snap();
        let committed = s.mechanism_entries();
        let mut current = committed.clone();
        current[0].fingerprint = Fingerprint::of(b"other"); // "s 0", gamma
        current[1].fingerprint = Fingerprint::of(b"other"); // "s 1", beta
        let d = diff(&committed, &current);
        assert_eq!(d.len(), 2);
        assert!(d.iter().any(|c| c.fails()));
        assert!(d.iter().any(|c| !c.fails()), "beta drift reports, does not fail");
        current.pop();
        let d = diff(&committed, &current);
        assert!(d.iter().any(|c| matches!(c, Change::Removed(_)) && c.fails()));
    }

    #[test]
    fn parse_rejects_garbage() {
        assert!(parse("not an entry\n").is_err());
        assert!(parse("x  tier=omega  h00\n").is_err());
        assert!(parse("# only comments\n\n").unwrap().is_empty());
    }
}

Graph