//! v5.5 c6 — V2 fixture anchor parity check.
//! v5.6 c6 D4 — manifest-anchored: reads selftest-fixture/v2-anchors-manifest.yaml
//! as canonical source of truth, then verifies BOTH V2Area enum + V2BlackboxTests
//! anchor matrix agree with the manifest (3-way drift catcher instead of 2-way).
//!
//! Adding a new V2 screen:
//!   1. Add a new `V2<Area>Screen.swift` under Sources/Screens/V2/.
//!   2. Add a manifest entry to `selftest-fixture/v2-anchors-manifest.yaml`.
//!   3. Mirror to `V2RootScreen.swift` (enum case + content switch).
//!   4. Mirror to `UITests/V2BlackboxTests.swift` (anchor matrix row).
//!   5. `cargo test -p smix-sdk --test v2_anchor_parity` must stay green.
//!
//! Future codegen path (留 v5.7+ if needed): `scripts/codegen-v2-anchors.sh`
//! could regenerate steps 3+4 from the manifest. v5.6 stops at the parity
//! gate — manifest is canonical, production swift stays hand-written, drift
//! lands red at `cargo test` time instead of next XCUITest run.

use std::fs;
use std::path::PathBuf;

fn repo_root() -> PathBuf {
    PathBuf::from(env!("CARGO_MANIFEST_DIR"))
        .parent()
        .and_then(|p| p.parent())
        .expect("crates/<crate>/ → crates/ → repo root")
        .to_path_buf()
}

fn read(path: &str) -> String {
    let abs = repo_root().join(path);
    fs::read_to_string(&abs).unwrap_or_else(|e| {
        panic!("read {} failed: {e}", abs.display());
    })
}

/// Parses `case root, home, form, ...` lines out of the V2Area enum body
/// in V2RootScreen.swift. Returns the case identifiers in source order.
fn extract_v2_areas(swift: &str) -> Vec<String> {
    let enum_start = swift
        .find("enum V2Area")
        .expect("V2Area enum declaration must exist in V2RootScreen.swift");
    let tail = &swift[enum_start..];
    let case_line = tail
        .lines()
        .find(|l| l.trim_start().starts_with("case "))
        .expect("V2Area body must contain a `case` declaration line");
    case_line
        .trim_start()
        .strip_prefix("case ")
        .expect("checked")
        .split(',')
        .map(|t| t.trim().to_string())
        .filter(|t| !t.is_empty())
        .collect()
}

/// Parses `("v2-tab-<name>", ...)` tuples from the V2BlackboxTests
/// `cases` matrix and returns the `<name>` suffix per tab. Restricted to
/// lines whose `"v2-tab-..."` literal sits inside a `(...)` tuple — this
/// excludes the file's trailing "return to root" lookup
/// (`app.buttons["v2-tab-root"]`) which doesn't represent a matrix row.
fn extract_blackbox_tab_suffixes(swift: &str) -> Vec<String> {
    swift
        .lines()
        .filter_map(|l| {
            let idx = l.find("(\"v2-tab-")?;
            let after = &l[idx + "(\"v2-tab-".len()..];
            let end = after.find('"')?;
            Some(after[..end].to_string())
        })
        .collect()
}

/// v5.6 c6 D4 — A single anchor entry from `v2-anchors-manifest.yaml`.
#[derive(Debug, Clone, PartialEq)]
struct ManifestEntry {
    case: String,
    in_blackbox_matrix: bool,
}

/// Parse the manifest's `cases:` sequence. Hand-rolled minimal YAML reader
/// (no extra dep needed); matches the manifest's documented shape exactly.
fn parse_manifest(yaml: &str) -> Vec<ManifestEntry> {
    let mut entries = Vec::new();
    let mut current_case: Option<String> = None;
    let mut current_in_matrix: Option<bool> = None;
    let mut in_cases = false;

    for line in yaml.lines() {
        let trimmed = line.trim();
        if trimmed.starts_with('#') || trimmed.is_empty() {
            continue;
        }
        if trimmed == "cases:" {
            in_cases = true;
            continue;
        }
        if !in_cases {
            continue;
        }
        if let Some(case_val) = trimmed.strip_prefix("- case: ") {
            // commit previous entry
            if let Some(case) = current_case.take() {
                entries.push(ManifestEntry {
                    case,
                    in_blackbox_matrix: current_in_matrix.unwrap_or(false),
                });
                current_in_matrix = None;
            }
            current_case = Some(case_val.trim().to_string());
        } else if let Some(val) = trimmed.strip_prefix("in_blackbox_matrix: ") {
            current_in_matrix = Some(val.trim() == "true");
        }
    }
    // flush last entry
    if let Some(case) = current_case {
        entries.push(ManifestEntry {
            case,
            in_blackbox_matrix: current_in_matrix.unwrap_or(false),
        });
    }
    entries
}

#[test]
fn v2_anchor_matrix_covers_every_screen_tab() {
    let root_swift = read("selftest-fixture/Sources/Screens/V2/V2RootScreen.swift");
    let tests_swift = read("selftest-fixture/UITests/V2BlackboxTests.swift");

    let areas = extract_v2_areas(&root_swift);
    let covered = extract_blackbox_tab_suffixes(&tests_swift);

    assert!(
        !areas.is_empty(),
        "V2Area enum extraction yielded no cases — parser drift in v2_anchor_parity"
    );
    assert!(
        covered.len() >= 2,
        "V2BlackboxTests sweep extraction yielded < 2 tabs — parser drift in v2_anchor_parity"
    );

    let blackbox_set: std::collections::HashSet<&str> =
        covered.iter().map(String::as_str).collect();

    let missing: Vec<&str> = areas
        .iter()
        .map(String::as_str)
        .filter(|a| *a != "root" && !blackbox_set.contains(a))
        .collect();

    assert!(
        missing.is_empty(),
        "V2BlackboxTests is missing tab entries for V2Area cases {missing:?}; \
         either add a row to the swift `cases: [(tab, anchor, kind)]` matrix \
         or, if intentionally excluded, document the exemption in this test"
    );

    assert!(
        tests_swift.contains("v2-tab-root"),
        "V2BlackboxTests should reference v2-tab-root somewhere (return-to-root anchor)"
    );
}

/// v5.6 c6 D4 — manifest is canonical: V2Area enum case list MUST match
/// the manifest's case order 1:1.
#[test]
fn manifest_matches_v2_area_enum_order() {
    let manifest_yaml = read("selftest-fixture/v2-anchors-manifest.yaml");
    let manifest_cases: Vec<String> = parse_manifest(&manifest_yaml)
        .into_iter()
        .map(|e| e.case)
        .collect();

    let root_swift = read("selftest-fixture/Sources/Screens/V2/V2RootScreen.swift");
    let enum_cases = extract_v2_areas(&root_swift);

    assert_eq!(
        manifest_cases, enum_cases,
        "manifest v2-anchors-manifest.yaml case order vs swift V2Area enum drift — \
         update both to match"
    );
}

/// v5.6 c6 D4 — manifest is canonical: every `in_blackbox_matrix: true`
/// entry MUST appear in V2BlackboxTests.swift's `cases` matrix; every
/// `in_blackbox_matrix: false` entry MUST NOT appear (intentional exclusion).
#[test]
fn manifest_blackbox_membership_matches_swift_matrix() {
    let manifest_yaml = read("selftest-fixture/v2-anchors-manifest.yaml");
    let manifest = parse_manifest(&manifest_yaml);

    let tests_swift = read("selftest-fixture/UITests/V2BlackboxTests.swift");
    let blackbox_suffixes: std::collections::HashSet<String> =
        extract_blackbox_tab_suffixes(&tests_swift)
            .into_iter()
            .collect();

    for entry in &manifest {
        let listed = blackbox_suffixes.contains(&entry.case);
        if entry.in_blackbox_matrix {
            assert!(
                listed,
                "manifest claims `{}` in_blackbox_matrix=true but \
                 V2BlackboxTests.swift `cases` array doesn't list it",
                entry.case
            );
        } else {
            assert!(
                !listed,
                "manifest claims `{}` in_blackbox_matrix=false but \
                 V2BlackboxTests.swift `cases` array does list it — \
                 either flip the manifest flag or remove the swift row",
                entry.case
            );
        }
    }
}

#[test]
fn extract_v2_areas_parses_known_baseline() {
    // v5.7 c5 — baseline extended for auth screen.
    // v6.6 c1 — heavy expansion adds player/deepnav/stacked/heavylist/map/
    // camera/wizard (7 new). v6.9 c1 closes the snapshot drift.
    let root_swift = read("selftest-fixture/Sources/Screens/V2/V2RootScreen.swift");
    let areas = extract_v2_areas(&root_swift);
    let expected = vec![
        "root",
        "home",
        "form",
        "list",
        "modal",
        "perm",
        "rec",
        "loc",
        "orient",
        "kbd",
        "clip",
        "argv",
        "deeplink",
        "share",
        "push",
        "picker",
        "auth",
        "bigtabs",
        "localized",
        "ocr",
        "anchor",
        "webview",
        "player",
        "deepnav",
        "stacked",
        "heavylist",
        "map",
        "camera",
        "wizard",
    ];
    assert_eq!(
        areas, expected,
        "V2Area baseline drift — actual:{areas:?} expected:{expected:?}"
    );
}

/// v5.6 c6 D4 — manifest parser snapshot (forward-compat / readable diff
/// catcher): the inline mini-yaml reader must agree with the documented
/// manifest shape. If the manifest grows fields, this test surfaces it.
#[test]
fn manifest_parses_to_expected_baseline() {
    let manifest_yaml = read("selftest-fixture/v2-anchors-manifest.yaml");
    let parsed = parse_manifest(&manifest_yaml);
    let expected_cases = [
        ("root", false),
        ("home", true),
        ("form", true),
        ("list", true),
        ("modal", true),
        ("perm", true),
        ("rec", true),
        ("loc", true),
        ("orient", true),
        ("kbd", true),
        ("clip", true),
        ("argv", true),
        ("deeplink", true),
        ("share", true),
        ("push", true),
        ("picker", true),
        ("auth", true),
        ("bigtabs", true),
        ("localized", true),
        ("ocr", true),
        ("anchor", true),
        ("webview", true),
        // v6.6 c1 heavy expansion (v6.9 c1 snapshot sync)
        ("player", true),
        ("deepnav", true),
        ("stacked", true),
        ("heavylist", true),
        ("map", true),
        ("camera", true),
        ("wizard", true),
    ];
    let actual: Vec<(&str, bool)> = parsed
        .iter()
        .map(|e| (e.case.as_str(), e.in_blackbox_matrix))
        .collect();
    let expected: Vec<(&str, bool)> = expected_cases.iter().map(|t| (t.0, t.1)).collect();
    assert_eq!(
        actual, expected,
        "manifest parser drift — expected:{expected:?} actual:{actual:?}"
    );
}
