//! The `[7f]` conformance gate (G-minor, `spec/PLAN_GMINOR_SCHEMA_MINOR.md`
//! §4, pin 11): an independent oracle over the manifest's carried schema
//! minor, built from **known, decodable, in-tree artifacts**.
//!
//! `epiphany-textproj` deliberately never decodes edit-barrier bytes (pin 8:
//! it has no `epiphany-layout-ir` dependency), so it cannot itself check
//! whether a manifest's carried `SchemaVersion` matches what its declared
//! edit barriers require. This module is the independent check that *can*:
//! `epiphany-testkit` depends on `epiphany-layout-ir`, so it can decode
//! `ExtensionDeclaration::edit_barriers`, walk every barrier's
//! `prohibited_operation_kinds`, and recompute the exact aggregate minor the
//! manifest should carry (`epiphany_layout_ir::barrier::edit_barriers_introduced_minor`).
//!
//! **What this gate is not.** It is not evidence that `epiphany-textproj`
//! validates arbitrary hand-edited documents — pin 11's ruled design (a) is
//! that `textproj` stays a *preserving* producer, carrying whatever
//! `SchemaVersion` a document declares verbatim, and a hand-edited document
//! whose barrier bytes changed while its carried version did not is
//! undetectable at that layer by construction. This gate validates a
//! separate, narrower claim: that *this crate's own fixtures*, built directly
//! against `epiphany-bundle`, are exactly and correctly stamped. An
//! undecodable barrier blob (a foreign extension's bytes, a corrupt encoding)
//! is reported as **not-checkable**, never silently counted as a pass.

use epiphany_bundle::{
    Bundle, DocumentId, ExtensionDeclaration, ExtensionId, FileUuid, Manifest, MemStore,
    SchemaVersion, SemVer,
};
use epiphany_layout_ir::{
    decode_edit_barriers, edit_barriers_introduced_minor, encode_edit_barriers, BarrierCondition,
    BarrierScope, EditBarrier,
};
use epiphany_ops::OperationKindTag;

/// The oracle's verdict for one manifest.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum GminorVerdict {
    /// Every barrier blob decoded; `expected` is the recomputed aggregate,
    /// `actual` is the carried superblock value, and `matches` is whether
    /// they are **exactly equal** — not `>=` (pin 11.1: equality alone
    /// catches over-stamping after a contributing barrier is removed).
    Checked {
        expected: SchemaVersion,
        actual: SchemaVersion,
        matches: bool,
    },
    /// At least one barrier blob failed to decode (a foreign extension, or
    /// deliberately corrupt bytes). The exact epoch cannot be established, so
    /// this is reported as not-checkable — **never** as a pass (pin 11.3).
    NotCheckable(String),
}

/// A single edit barrier naming `tags`, with an otherwise-trivial
/// whole-score, unconditional scope — the minimum shape needed to exercise
/// `prohibited_operation_kinds`.
fn barrier(tags: &[OperationKindTag]) -> EditBarrier {
    EditBarrier {
        scope: BarrierScope::WholeScore,
        affected_object_kinds: Vec::new(),
        prohibited_operation_kinds: tags.to_vec(),
        condition: BarrierCondition::Always,
    }
}

fn extension_declaration(id_byte: u8, barriers: &[EditBarrier]) -> ExtensionDeclaration {
    ExtensionDeclaration {
        extension_id: ExtensionId([id_byte; 16]),
        version: SemVer::new(1, 0, 0),
        required: false,
        preserved_chunk_roots: Vec::new(),
        affected_object_kinds: Vec::new(),
        edit_barriers: encode_edit_barriers(barriers),
    }
}

/// Builds a bundle whose manifest declares `extensions` and is stamped at
/// `stamped_version` — the two independent knobs every fixture below varies.
fn build_bundle(
    seed: u8,
    extensions: Vec<ExtensionDeclaration>,
    stamped_version: SchemaVersion,
) -> Bundle<MemStore> {
    let mut manifest = Manifest::empty(DocumentId([seed; 16]));
    manifest.extension_declarations = extensions;
    Bundle::create_versioned(
        MemStore::new(),
        FileUuid([seed; 16]),
        manifest,
        stamped_version,
    )
    .expect("fixture manifest is emittable")
}

/// The independent oracle (pin 11): recomputes the exact aggregate minor a
/// bundle's manifest should carry from its decodable edit barriers, and
/// compares it **by equality** to the carried superblock value.
pub fn check(bundle: &Bundle<MemStore>) -> GminorVerdict {
    let mut all_barriers = Vec::new();
    for declaration in &bundle.manifest().extension_declarations {
        match decode_edit_barriers(&declaration.edit_barriers) {
            Ok(mut decoded) => all_barriers.append(&mut decoded),
            Err(error) => {
                return GminorVerdict::NotCheckable(format!(
                    "extension {:?}: edit_barriers did not decode: {error:?}",
                    declaration.extension_id
                ))
            }
        }
    }
    let epoch_max = edit_barriers_introduced_minor(&all_barriers);
    let expected = SchemaVersion::for_major_at_epoch(0, epoch_max);
    let actual = bundle.superblock().manifest_schema_version;
    GminorVerdict::Checked {
        expected,
        actual,
        matches: expected == actual,
    }
}

/// Fixture: a manifest naming barrier tag 31 (`CreateInstrument`, epoch 8),
/// correctly stamped at exactly that epoch. Positive: the oracle must find
/// `matches: true`.
pub fn fixture_correctly_stamped_tag_31() -> Bundle<MemStore> {
    let declarations = vec![extension_declaration(
        1,
        &[barrier(&[OperationKindTag::CreateInstrument])],
    )];
    build_bundle(1, declarations, SchemaVersion::new(0, 8))
}

/// Fixture: a manifest naming only baseline tags, correctly stamped at the
/// baseline `{0, 1}`. Positive.
pub fn fixture_baseline_only() -> Bundle<MemStore> {
    let declarations = vec![extension_declaration(
        2,
        &[barrier(&[
            OperationKindTag::InsertEvent,
            OperationKindTag::DeleteEvent,
        ])],
    )];
    build_bundle(2, declarations, SchemaVersion::V0)
}

/// Negative fixture (pin 11.2, required #1): a blob naming tag 31 (epoch 8),
/// but the manifest carries the **baseline** version — under-stamped. The
/// oracle must find `matches: false`.
pub fn fixture_understamped() -> Bundle<MemStore> {
    let declarations = vec![extension_declaration(
        3,
        &[barrier(&[OperationKindTag::CreateInstrument])],
    )];
    build_bundle(3, declarations, SchemaVersion::V0)
}

/// Negative fixture (pin 11.2, required #2): two barriers, one naming tag 31
/// (epoch 8, the sole max contributor) and one baseline-only. The barrier
/// contributing the maximum is then **removed** (only the baseline barrier
/// remains), but the manifest retains the old aggregate `{0, 8}` — exactly
/// the over-stamp pin 6.5 warns "blindly retaining the previous aggregate"
/// produces. The oracle must find `matches: false`.
pub fn fixture_overstamped_after_barrier_removal() -> Bundle<MemStore> {
    let declarations = vec![extension_declaration(
        4,
        &[barrier(&[OperationKindTag::InsertEvent])],
    )];
    build_bundle(4, declarations, SchemaVersion::new(0, 8))
}

/// Fixture: a manifest declaring an extension whose `edit_barriers` blob is
/// deliberately corrupt (not a valid canonical `EditBarrier` set encoding).
/// The oracle must report `NotCheckable`, never a pass (pin 11.3).
pub fn fixture_undecodable_barrier_blob() -> Bundle<MemStore> {
    let declaration = ExtensionDeclaration {
        extension_id: ExtensionId([5; 16]),
        version: SemVer::new(1, 0, 0),
        required: false,
        preserved_chunk_roots: Vec::new(),
        affected_object_kinds: Vec::new(),
        // Not a canonical edit-barrier-set encoding: garbage bytes that a
        // real foreign/corrupt extension could plausibly carry.
        edit_barriers: vec![0xFF, 0x00, 0x13, 0x37, 0xAB],
    };
    build_bundle(5, vec![declaration], SchemaVersion::V0)
}

/// Runs the whole `[7f]` gate: every fixture, checked against its expected
/// verdict shape. Returns `(checked, not_checkable)` — the two counts the
/// conformance suite reports (pin 11.4: describing the gate's actual reach
/// honestly, not implying it validates arbitrary edits).
pub fn run_gate() -> (usize, usize) {
    let mut checked = 0usize;
    let mut not_checkable = 0usize;

    let positive_correct = check(&fixture_correctly_stamped_tag_31());
    assert_eq!(
        positive_correct,
        GminorVerdict::Checked {
            expected: SchemaVersion::new(0, 8),
            actual: SchemaVersion::new(0, 8),
            matches: true,
        },
        "a manifest naming tag 31, correctly stamped at minor 8, must check as matching"
    );
    checked += 1;

    let positive_baseline = check(&fixture_baseline_only());
    assert_eq!(
        positive_baseline,
        GminorVerdict::Checked {
            expected: SchemaVersion::V0,
            actual: SchemaVersion::V0,
            matches: true,
        },
        "a manifest naming only baseline tags must check as matching its baseline stamp"
    );
    checked += 1;

    let negative_understamped = check(&fixture_understamped());
    assert_eq!(
        negative_understamped,
        GminorVerdict::Checked {
            expected: SchemaVersion::new(0, 8),
            actual: SchemaVersion::V0,
            matches: false,
        },
        "an under-stamped manifest (tag 31 present, baseline carried) must be caught"
    );
    checked += 1;

    let negative_overstamped = check(&fixture_overstamped_after_barrier_removal());
    assert_eq!(
        negative_overstamped,
        GminorVerdict::Checked {
            expected: SchemaVersion::V0,
            actual: SchemaVersion::new(0, 8),
            matches: false,
        },
        "an over-stamped manifest (max contributor removed, old aggregate retained) must be caught"
    );
    checked += 1;

    match check(&fixture_undecodable_barrier_blob()) {
        GminorVerdict::NotCheckable(_) => not_checkable += 1,
        other => panic!("an undecodable barrier blob must report NotCheckable, got {other:?}"),
    }

    (checked, not_checkable)
}

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

    #[test]
    fn s8_manifest_naming_tag_31_stamps_8_baseline_tags_stamp_baseline() {
        assert_eq!(
            check(&fixture_correctly_stamped_tag_31()),
            GminorVerdict::Checked {
                expected: SchemaVersion::new(0, 8),
                actual: SchemaVersion::new(0, 8),
                matches: true,
            }
        );
        assert_eq!(
            check(&fixture_baseline_only()),
            GminorVerdict::Checked {
                expected: SchemaVersion::V0,
                actual: SchemaVersion::V0,
                matches: true,
            }
        );
    }

    #[test]
    fn s15_the_gate_fails_the_understamped_fixture() {
        let verdict = check(&fixture_understamped());
        assert_eq!(
            verdict,
            GminorVerdict::Checked {
                expected: SchemaVersion::new(0, 8),
                actual: SchemaVersion::V0,
                matches: false,
            }
        );
    }

    #[test]
    fn s16_the_gate_fails_the_overstamped_fixture() {
        let verdict = check(&fixture_overstamped_after_barrier_removal());
        assert_eq!(
            verdict,
            GminorVerdict::Checked {
                expected: SchemaVersion::V0,
                actual: SchemaVersion::new(0, 8),
                matches: false,
            }
        );
    }

    #[test]
    fn s17_an_undecodable_blob_is_reported_not_checkable_never_a_pass() {
        assert!(matches!(
            check(&fixture_undecodable_barrier_blob()),
            GminorVerdict::NotCheckable(_)
        ));
    }

    #[test]
    fn the_gate_reports_four_checked_and_one_not_checkable() {
        assert_eq!(run_gate(), (4, 1));
    }

    #[test]
    fn correctly_stamped_fixtures_match() {
        assert!(matches!(
            check(&fixture_correctly_stamped_tag_31()),
            GminorVerdict::Checked { matches: true, .. }
        ));
        assert!(matches!(
            check(&fixture_baseline_only()),
            GminorVerdict::Checked { matches: true, .. }
        ));
    }

    #[test]
    fn understamped_fixture_is_caught() {
        assert!(matches!(
            check(&fixture_understamped()),
            GminorVerdict::Checked { matches: false, .. }
        ));
    }

    #[test]
    fn overstamped_fixture_is_caught() {
        assert!(matches!(
            check(&fixture_overstamped_after_barrier_removal()),
            GminorVerdict::Checked { matches: false, .. }
        ));
    }

    #[test]
    fn undecodable_barrier_blob_is_not_checkable_not_a_pass() {
        assert!(matches!(
            check(&fixture_undecodable_barrier_blob()),
            GminorVerdict::NotCheckable(_)
        ));
    }
}
