// Conformance test for the A2UI basic catalog.
//
// Loads the shared envelope corpus at tests/conformance-corpus.json and
// asserts every envelope:
//   1. Parses cleanly (validate_payload — size + JSON shape).
//   2. Validates as a typed envelope (validate_envelope — surface-id
//      checks, supported component types, data-model size limits, etc.).
//   3. Applies cleanly to a fresh A2uiSurfaceStore (so multi-envelope
//      sequences round-trip through the store the same way the real
//      runtime applies them).
//
// Per docs/proposals/a2ui-renderer-parity.md, this is the gating test
// for catalog parity. Adding a component to BASIC_CATALOG_V0_9 in
// lib.rs without adding a corpus case here fails the build (the
// "every component in the catalog has a corpus case" assertion at the
// bottom catches it).
//
// Renderer-side snapshot tests (web @a2ui/lit + SwiftUI A2uiRenderer)
// load the same corpus file and check their rendered output. See
// tests/README-conformance.md for the cross-renderer test approach.

use car_a2ui::{envelopes_from_value, A2uiSurfaceStore, A2uiValidator, BASIC_CATALOG_V0_9};
use serde_json::Value;
use std::collections::BTreeSet;

const CORPUS: &str = include_str!("conformance-corpus.json");

#[derive(serde::Deserialize)]
struct Corpus {
    version: String,
    #[serde(rename = "catalogId")]
    catalog_id: String,
    cases: Vec<Case>,
}

#[derive(serde::Deserialize)]
struct Case {
    name: String,
    envelopes: Vec<Value>,
}

fn load_corpus() -> Corpus {
    serde_json::from_str(CORPUS).expect("conformance-corpus.json must be valid JSON")
}

#[test]
fn corpus_metadata_matches_lib() {
    let corpus = load_corpus();
    assert_eq!(corpus.version, car_a2ui::A2UI_VERSION_V0_9);
    assert_eq!(corpus.catalog_id, BASIC_CATALOG_V0_9);
}

#[test]
fn every_envelope_validates() {
    let corpus = load_corpus();
    let validator = A2uiValidator::default();

    for case in &corpus.cases {
        for (i, envelope_value) in case.envelopes.iter().enumerate() {
            // Step 1: payload-level validation (size + JSON shape).
            validator
                .validate_payload(envelope_value)
                .unwrap_or_else(|e| {
                    panic!(
                        "case `{}` envelope {} failed validate_payload: {}",
                        case.name, i, e
                    )
                });

            // Step 2: envelope-level validation (surface ids, components,
            // catalog id, data-model size). Goes through envelopes_from_value
            // so we exercise the same path the WS handler does.
            let envelopes = envelopes_from_value(envelope_value).unwrap_or_else(|e| {
                panic!(
                    "case `{}` envelope {} failed envelopes_from_value: {}",
                    case.name, i, e
                )
            });
            assert_eq!(
                envelopes.len(),
                1,
                "case `{}` envelope {} should yield exactly one envelope",
                case.name,
                i
            );
            validator
                .validate_envelope(&envelopes[0])
                .unwrap_or_else(|e| {
                    panic!(
                        "case `{}` envelope {} failed validate_envelope: {}",
                        case.name, i, e
                    )
                });
        }
    }
}

#[tokio::test]
async fn every_case_applies_to_store() {
    let corpus = load_corpus();

    for case in &corpus.cases {
        // Each case gets its own store so cases don't bleed into each other.
        let store = A2uiSurfaceStore::default();
        for (i, envelope_value) in case.envelopes.iter().enumerate() {
            let envelopes = envelopes_from_value(envelope_value).unwrap_or_else(|e| {
                panic!(
                    "case `{}` envelope {} could not be parsed for store apply: {}",
                    case.name, i, e
                )
            });
            for envelope in envelopes {
                store.apply(envelope).await.unwrap_or_else(|e| {
                    panic!(
                        "case `{}` envelope {} failed store.apply: {}",
                        case.name, i, e
                    )
                });
            }
        }
        // Every case should leave at least one surface alive in the store
        // (no case currently exercises deleteSurface as part of its scenario).
        let surfaces = store.list().await;
        assert!(
            !surfaces.is_empty(),
            "case `{}` left no surfaces in the store",
            case.name
        );
    }
}

#[test]
fn corpus_covers_every_basic_catalog_component() {
    // Mirrors the BASIC_CATALOG_V0_9 component list at car-a2ui::supported_components.
    // Kept in lockstep manually — if you add a component there, add a corpus
    // case here, then update this list. The failing assertion below is the
    // signal.
    let expected: BTreeSet<&str> = [
        "Text",
        "Row",
        "Column",
        "List",
        "Card",
        "Divider",
        "Button",
        "TextField",
        "CheckBox",
        "ChoicePicker",
        "Select",
        "Slider",
        "Image",
        "Icon",
        "Video",
        "AudioPlayer",
        "DateTimeInput",
        "Chart",
        "File",
        "FilePicker",
        "Spacer",
        "Badge",
        "Tabs",
        "Modal",
    ]
    .into_iter()
    .collect();

    let corpus = load_corpus();
    let mut covered = BTreeSet::new();
    for case in &corpus.cases {
        for envelope_value in &case.envelopes {
            // We only walk updateComponents — that's where component types
            // are declared. createSurface/updateDataModel/deleteSurface
            // don't introduce types.
            let Some(update) = envelope_value
                .get("updateComponents")
                .and_then(|v| v.get("components"))
                .and_then(|v| v.as_array())
            else {
                continue;
            };
            for component in update {
                if let Some(name) = component.get("component").and_then(|v| v.as_str()) {
                    covered.insert(name.to_string());
                }
            }
        }
    }

    let covered_refs: BTreeSet<&str> = covered.iter().map(String::as_str).collect();
    let missing: Vec<&&str> = expected.difference(&covered_refs).collect();
    assert!(
        missing.is_empty(),
        "corpus is missing cases for these basic catalog components: {missing:?}.\n\
         Add a case to tests/conformance-corpus.json for each."
    );
}
