//! Drift-guard for the spec/field-enum label convention (issue #204).
//!
//! Scans this crate's `src/` for every `pub enum`, subtracts a documented
//! skip-list, and fails if any remaining enum has neither
//! `dvb_common::impl_spec_display!(Name)` nor a hand-written `Display` impl.
//!
//! Because the project-wide `Display` impl delegates to an inherent
//! `name() -> &'static str`, a present `Display` transitively guarantees
//! `name()` exists (it would not compile otherwise) — so this single coverage
//! check enforces the whole convention and catches the one thing the compiler
//! cannot: a brand-new `pub enum` that nobody labelled.

use std::collections::BTreeSet;
use std::fs;
use std::path::Path;

/// Enums that are intentionally **not** spec/field labels. Each is one of:
/// a structured error; a dispatch/registry wrapper or tag enum (the `declare_*`
/// system, `Any*`); a section-kind discriminant; or a data-carrying ADT whose
/// variants hold payloads (a static label would be lossy and add nothing — use
/// the typed variant instead).
const SKIP: &[&str] = &[
    // errors + dispatch wrappers / tag enums
    "Error",
    "AnyDescriptor",
    "AnyTableSection",
    "DescriptorTag",
    "TableId",
    "ExtensionTag",
    "SatTableId",
    // data-carrying / wrapper / body ADTs
    "BiopMessage",
    "CarouselObject",
    "TaggedProfile",
    "ObjectKind",
    "UnMessage",
    "CollectError",
    "CompletedEit",
    "SatBody",
    "ProtectionMessageBody",
    "FontInfo",
    "DvbLocatorService",
    "DvbLocatorIdentifier",
    "ExtIterItem",
    "RegisteredExtension",
    "ExtensionBody",
    "TargetId",
    "LinkageData",
    "FormatSpecifier",
    "IdSelector",
    "CridLocation",
    "DepthRangeBody",
    "ImageIconBody",
    "IconLocation",
    "RegionCodes",
    "CellLinkage",
    "VbiService",
    "ShModulationMode",
    "ShInterleaver",
    "PositionSystem",
    "BeamhoppingMode",
    // internal section-kind discriminants + config (not wire-field labels)
    "EitKind",
    "NitKind",
    "SdtKind",
    "PacketStride",
];

fn read_rs(dir: &Path, out: &mut Vec<String>) {
    for entry in fs::read_dir(dir).expect("read src dir") {
        let path = entry.expect("dir entry").path();
        if path.is_dir() {
            read_rs(&path, out);
        } else if path.extension().is_some_and(|x| x == "rs") {
            out.push(fs::read_to_string(&path).expect("read .rs"));
        }
    }
}

/// True if `name` appears after `prefix` with an identifier boundary, i.e. the
/// match is the whole enum name and not a longer one sharing the prefix.
fn has_impl(all: &str, prefix: &str, name: &str) -> bool {
    let needle = format!("{prefix}{name}");
    let mut start = 0;
    while let Some(idx) = all[start..].find(&needle) {
        let end = start + idx + needle.len();
        let next = all[end..].chars().next();
        if !matches!(next, Some(c) if c.is_alphanumeric() || c == '_') {
            return true;
        }
        start = end;
    }
    false
}

#[test]
fn every_public_spec_enum_has_a_display_impl() {
    let src = Path::new(env!("CARGO_MANIFEST_DIR")).join("src");
    let mut files = Vec::new();
    read_rs(&src, &mut files);
    let all = files.join("\n");

    let mut enums = BTreeSet::new();
    for line in all.lines() {
        if let Some(rest) = line.trim_start().strip_prefix("pub enum ") {
            let name: String = rest
                .chars()
                .take_while(|c| c.is_alphanumeric() || *c == '_')
                .collect();
            if !name.is_empty() {
                enums.insert(name);
            }
        }
    }

    let missing: Vec<_> = enums
        .iter()
        .filter(|e| !SKIP.contains(&e.as_str()))
        .filter(|e| !has_impl(&all, "impl_spec_display!(", e) && !has_impl(&all, "Display for ", e))
        .cloned()
        .collect();

    assert!(
        missing.is_empty(),
        "pub enum(s) missing a Display impl (issue #204 convention): {missing:?}\n\
         Add `dvb_common::impl_spec_display!(Name)` plus an inherent `name()`, \
         or add the enum to SKIP if it is not a spec/field label."
    );
}
