//! Join DECLARED prototypes to the MEASURED register footprint, and judge each one against the binary.
//!
//! Gamedata says WHERE a function is; it never says what it takes. Types cannot be recovered from a
//! stripped binary, so they have to come from a declaration — and a declaration has to be checked before
//! anything calls through it, because a stale one produces a call that resolves, passes live validation,
//! and then loads the wrong registers. That check is the point of this module: the declared parameter
//! list is converted to a SysV register footprint and compared against the footprint `abi` measured in
//! the build being shipped.
//!
//! The declarations are STATIC input (`mappings/prototypes.json`) rather than rolling state — they are
//! never folded forward, so unlike the model they live in the repository and need no baseline mechanism.
//! What moves per build is the measurement they are judged against.

use crate::model;
use anyhow::{Context, Result};
use serde::Deserialize;
use std::collections::{BTreeMap, BTreeSet};
use std::path::Path;

// The provenance ids this module READS are the ones the pipeline STAMPS, imported rather than re-spelled:
// they are one fact — which evidence named the function — and two copies of it "kept in step" by a
// comment is an invariant nothing enforces. A drift there would silently stop matching, and a prototype
// that stops matching does not fail; it simply stops being claimed.
use crate::pipeline::{VALVE_CONCOMMAND, VALVE_DATADESC, VALVE_VSCRIPT};

/// What the manifest calls a prototype that came from how the ENGINE invokes the function rather than
/// from anyone's declaration of it. Declared here because only this module states it.
const ENGINE_CONTRACT: &str = "engine-contract";

/// The prototype the engine invokes EVERY entity-IO handler through. Kept in step with
/// `pipeline::IO_HANDLER_INT_ARGS` / `within_io_prototype`, which measure this same claim on every
/// derive as a standing oracle — two halves of one fact, one asserting it and one checking it.
const ENGINE_CONTRACT_PARAMS: [&str; 2] = ["CEntityInstance*", "InputData_t&"];

/// What the engine passes EVERY console-command callback, whatever form it takes.
const CONCOMMAND_PARAMS: [&str; 2] = ["CCommandContext*", "CCommand*"];

/// The engine's dispatch contract for a console command, or `None` if `source` is not one.
///
/// The second contract in this module, and it needs the FORM where the entity-IO one needs nothing: a
/// `direct` registration passes a plain function, while the two object forms dispatch through a
/// receiver, so they take one more integer register. Declaring them all the same way would be wrong in
/// whichever direction it erred — the 2-argument list makes every object form a `mismatch`, and the
/// 3-argument list is judged as a lower bound, so it would quietly VERIFY a receiver that a direct
/// handler does not have and hand a caller a prototype with a bogus leading argument.
///
/// Kept in step with `pipeline::concmd_int_args`, which measures this same claim on every derive as a
/// standing oracle — two halves of one fact, one asserting it and one checking it.
fn concommand_contract(source: &str) -> Option<Vec<String>> {
    let form = source
        .strip_prefix(VALVE_CONCOMMAND)
        .and_then(|r| r.strip_prefix(':'))?;
    let receiver = match form {
        "direct" => None,
        // The interface form's receiver is the callback interface itself; the member form's is whatever
        // object the registering constructor was building, which the binary does not name. `void*` says
        // "a receiver, type unknown" — the honest claim, and the one `most_specific` already ranks last.
        "interface" => Some("ICommandCallback*"),
        "member" => Some("void*"),
        // A form this build introduced and this code has never measured claims NOTHING.
        _ => return None,
    };
    Some(
        receiver
            .into_iter()
            .chain(CONCOMMAND_PARAMS)
            .map(str::to_string)
            .collect(),
    )
}

/// Read the DIRECTION of a footprint disagreement, and decide what it means.
///
/// Split out because it is the whole content of the `mismatch` verdict, and it has to be callable: the
/// tests used to re-implement this rule rather than call it, so the regression guard could only fail if
/// someone edited both copies the same wrong way. One definition, two callers.
///
/// Only an over-READ refutes a declaration. `declared_over` alone is the documented LOWER-BOUND case —
/// calling through it loads a register nobody reads, which is safe — while `measured_over` means the
/// callee reads a register the declaration never mentions, which is not. `both` stays a mismatch: a
/// class where the callee reads more is unsafe regardless of another class where it reads fewer. 81 of
/// CS2's 140 former mismatches were the safe direction, reported as "does not describe this build".
fn adjudicate_mismatch(
    chosen: &Candidate,
    s: &model::AbiShape,
    types: Option<&BTreeMap<String, model::TypeLayout>>,
) -> (model::AbiStatus, &'static str) {
    let (i, f) = footprint(chosen.params, types);
    // The direction has to be read through the SAME allowance the verdict was, or the invisible `this`
    // reads as an over-count on its own: `CGameEvent::GetFloat` is declared `(char const*, float)` and
    // measures `int=2 float=0`, where the extra integer register is the receiver and the only real
    // disagreement is the float.
    let i = if chosen.complete {
        i
    } else {
        (i..=i + 1)
            .min_by_key(|d| d.abs_diff(s.int as usize))
            .expect("the range always has two elements")
    };
    let (i, f) = (i.min(6), f.min(8));
    let measured_over = s.int as usize > i || s.float as usize > f;
    let declared_over = i > s.int as usize || f > s.float as usize;
    let status = if declared_over && !measured_over {
        model::AbiStatus::LowerBound
    } else {
        model::AbiStatus::Mismatch
    };
    let note = match (measured_over, declared_over) {
        (true, true) => {
            "measured and declared footprints disagree in BOTH directions, in different register \
             classes: the callee reads a register the declaration does not mention AND the declaration \
             passes one the callee never reads"
        }
        (false, true) => {
            "the declaration passes registers the callee never reads, and contradicts it in no register \
             class — the measured footprint is a documented LOWER bound, so this is expected rather than \
             evidence against the declaration"
        }
        (true, false) => {
            "measured footprint EXCEEDS declared: the callee reads a register the declaration does not \
             mention, so this declaration does not describe this build"
        }
        _ => "the footprints disagree in neither direction, which a mismatch cannot be",
    };
    (status, note)
}

/// One declared prototype as the frozen input records it.
#[derive(Deserialize)]
struct Decl {
    /// Absent where the source declared a return type but no parameter list — a `CALL_VIRTUAL(RET, …)`
    /// site passes VALUES, not types, so it says what comes back and nothing about what goes in. Such a
    /// declaration contributes a return type and never a signature candidate.
    #[serde(default)]
    params: Option<Vec<String>>,
    /// The parameter list is the FULL register-visible argument list, receiver included — a real
    /// function-pointer type rather than a mangled symbol. Those arities are matched EXACTLY; see
    /// [`agrees`] for why the alternative has to allow ±1.
    #[serde(default)]
    complete: bool,
    #[serde(rename = "const")]
    is_const: bool,
    provenance: String,
    /// Present only where the source could supply one — Itanium mangling omits return types, so the
    /// macOS-symbol majority has none.
    #[serde(default)]
    ret: Option<String>,
}

#[derive(Deserialize)]
struct PrototypeDoc {
    prototypes: BTreeMap<String, Vec<Decl>>,
    /// Bare method names borne by exactly ONE qualified declaration — computed over the FULL declaration
    /// set, before pruning. It has to be: pruning removes declarations, so a name borne by dozens
    /// (`IAppSystem::GetTier`, `Reconnect`, `IsSingleton`) can look unique among what survives, and
    /// deriving uniqueness from the pruned map would re-open exactly the wrong-class matching the
    /// bare-name gate exists to prevent.
    #[serde(default)]
    bare_unique: BTreeSet<String>,
}

/// The by-value SysV cost of the few engine math types, for the case where the derived layouts are not
/// available (an offline run has no typed schema, so no `types` section).
///
/// This is a FALLBACK, not the source of truth. Every entry is reproduced exactly by the derived layouts
/// (`Vector` is 12 bytes and SSE, which is two registers), so the two paths agree on everything it
/// covers, and the derived path also answers the ~1,960 types it does not.
///
/// Measured: on the current declaration set NONE of these six ever reaches here, because every `Vector`
/// in a declared prototype is a `Vector const&` or a `Vector*` and the pointer/reference test above
/// catches it first. The table is kept anyway — it costs nothing, and a by-value math argument is exactly
/// the case whose misclassification manufactured false mismatches in an early pass.
const FALLBACK_SSE: &[(&str, usize)] = &[
    ("Vector", 2),
    ("QAngle", 2),
    ("Vector2D", 1),
    ("Vector4D", 2),
    ("Quaternion", 2),
    ("RadianEuler", 2),
];

/// SysV register cost of ONE declared parameter, as `(integer, float)`.
///
/// The classification is not lexical, which is the trap this encodes: a pointer or a reference travels in
/// an INTEGER register whatever it points at, while a small all-float aggregate travels in SSE registers —
/// `Vector` is 3 floats, so it costs TWO SSE registers by value but ONE integer register by reference.
/// Treating `Vector` as integer either way manufactures false mismatches.
///
/// Where the deriver's own type layouts are available they decide, because they answer this question for
/// EVERY type rather than the handful anyone thought to tabulate: a size settles the memory case, and the
/// derived SysV class settles the register case.
fn classify(ty: &str, types: Option<&BTreeMap<String, model::TypeLayout>>) -> (usize, usize) {
    let t = ty.replace("const", "");
    let t = t.trim();
    if t.contains('*') || t.contains('&') {
        return (1, 0);
    }
    let base = t.split('<').next().unwrap_or(t).trim();
    if base == "float" || base == "double" {
        return (0, 1);
    }
    if let Some(l) = types.and_then(|m| m.get(base)) {
        // Eightbyte count — SysV assigns a register per 8 bytes of an aggregate small enough to travel
        // in them.
        let regs = l.size.div_ceil(8);
        return match l.sysv {
            model::SysvClass::Sse => (0, regs),
            model::SysvClass::Integer => (regs, 0),
            // Above the register budget an argument is copied to the STACK and consumes no register at
            // all — which the footprint comparison should see as zero, not as one.
            model::SysvClass::Memory => (0, 0),
            // Size known, composition not. Fall through to the assumption below rather than inventing a
            // classification the data does not support.
            model::SysvClass::Unknown => (1, 0),
        };
    }
    if let Some((_, n)) = FALLBACK_SSE.iter().find(|(k, _)| *k == base) {
        return (0, *n);
    }
    (1, 0)
}

/// The declared parameter list's total register footprint.
fn footprint(
    params: &[String],
    types: Option<&BTreeMap<String, model::TypeLayout>>,
) -> (usize, usize) {
    params.iter().fold((0, 0), |(i, f), p| {
        let (a, b) = classify(p, types);
        (i + a, f + b)
    })
}

/// Does a declared parameter list agree with the footprint measured in the binary?
///
/// One allowance is unconditional and is a property of the ABI rather than slack: only six integer
/// argument registers exist, so a declared arity above six is compared as `min(n, 6)`.
///
/// The second is conditional, and that condition matters. Where the declaration came from a mangled
/// symbol, `this` is invisible — a non-static member function and a static one mangle identically — so
/// both `n` and `n + 1` have to be accepted, which is why some entries verify "only as static". A
/// COMPLETE declaration is a function-pointer type that already names its receiver, so the same allowance
/// there is pure slack that hides real staleness: `IScriptVM::CreateVM` is declared with one argument and
/// measures two, and `SoundOpGameSystem::StopSoundEvent` is declared with two and measures three. Both
/// would pass under `n + 1` while being exactly the case this manifest exists to catch.
///
/// The third case inverts the question. The engine's own dispatch contract cannot be stale, so equality
/// is the wrong test for it: the measured footprint is a documented LOWER bound (a handler that ignores
/// its `InputData_t&` reads one register, a forwarding thunk none), and 49 of CS2's 205 contract-only
/// handlers measure fewer than the two the engine always passes. Only an over-count refutes it, which is
/// the direction that would mean a caller loads a register the callee never reads.
fn agrees(
    c: &Candidate,
    sh: &model::AbiShape,
    types: Option<&BTreeMap<String, model::TypeLayout>>,
) -> bool {
    let (i, f) = footprint(c.params, types);
    if c.contract {
        // A by-value return is the one over-count the register counts cannot show: the caller passes a
        // hidden output pointer as argument 0 and every other argument shifts, which a `void` contract
        // says does not happen. `pipeline::within_io_prototype` rejects it for the same reason, so
        // checking it here keeps the manifest and the standing oracle from ever disagreeing. Empty on
        // both games today — the oracle reports 715/715 and 624/624 — which is why it is a guard.
        return sh.ret != "ret=byval"
            && i.min(6) >= sh.int as usize
            && f.min(8) >= sh.float as usize;
    }
    if f.min(8) != sh.float as usize {
        return false;
    }
    if c.complete {
        return i.min(6) == sh.int as usize;
    }
    [1usize, 0]
        .iter()
        .any(|t| (i + t).min(6) == sh.int as usize)
}

/// One signature the declarations offer, and the convention it is written in.
#[derive(Clone, Copy)]
struct Candidate<'a> {
    params: &'a Vec<String>,
    complete: bool,
    /// Carried from the declaration that offered this signature, so the chosen one's own const-ness
    /// travels with it rather than being taken from whichever declaration happened to be listed first.
    is_const: bool,
    /// Likewise the return type. Taking it from "the first declaration that has one" would pair the
    /// ACCEPTED parameter list with a REJECTED declaration's return — two sources describing one
    /// function, reported as though they were one description.
    ret: Option<&'a String>,
    /// This is the ENGINE'S dispatch contract rather than something a source declared, and both of its
    /// consequences follow from that one fact — it describes how the function is INVOKED, not what
    /// somebody believed about it. It cannot go stale, so [`agrees`] judges it as a lower bound; and it
    /// is reported as `matched_by: engine-contract`, so a consumer can tell "the engine calls it this
    /// way" from "someone wrote this down".
    contract: bool,
}

/// Every DISTINCT signature a declaration set offers. Two declarations that write the same parameter
/// list in the same convention are one candidate, not an overload.
fn collect_candidates<'a>(decls: &'a [Decl], cands: &mut Vec<Candidate<'a>>) {
    for d in decls {
        if let Some(p) = d.params.as_ref()
            && !cands
                .iter()
                .any(|c| c.params == p && c.complete == d.complete)
        {
            cands.push(Candidate {
                params: p,
                complete: d.complete,
                is_const: d.is_const,
                ret: d.ret.as_ref(),
                contract: false,
            });
        }
    }
}

/// Pick between signatures that the measurement cannot separate: prefer the one that names its receiver,
/// then the one that says the most (`void*` is the least specific thing a declaration can write), then
/// alphabetically so the artifact is stable.
fn most_specific<'a>(cands: &[Candidate<'a>]) -> Candidate<'a> {
    *cands
        .iter()
        .min_by_key(|c| {
            (
                !c.complete,
                c.params
                    .iter()
                    .filter(|p| p.replace(' ', "") == "void*")
                    .count(),
                c.params.clone(),
            )
        })
        .expect("caller guarantees a non-empty set")
}

/// Build the prototype manifest for one build: every shipped function that a declaration names, with the
/// verdict its own binary gives that declaration.
pub fn build_manifest(
    prototypes: &Path,
    mono: &model::Monolith,
    types: Option<&BTreeMap<String, model::TypeLayout>>,
    vscript_ret: Option<&BTreeMap<String, String>>,
) -> Result<model::AbiManifest> {
    let doc: PrototypeDoc = serde_json::from_str(
        &std::fs::read_to_string(prototypes)
            .with_context(|| format!("read {}", prototypes.display()))?,
    )
    .context("parse prototypes json")?;

    // Bare method name -> the qualified names declaring it, among the names present here. Uniqueness is
    // NOT decided from this map — see `bare_unique`.
    let mut by_bare: BTreeMap<&str, BTreeSet<&str>> = BTreeMap::new();
    for name in doc.prototypes.keys() {
        by_bare
            .entry(name.rsplit("::").next().unwrap_or(name))
            .or_default()
            .insert(name.as_str());
    }

    // Bare method name -> how many SHIPPED functions bear it. The declaration side alone cannot gate
    // bare-name matching: uniqueness there says only that one declaration offers the name, never that
    // one function ANSWERS to it, and 46 bare names are borne by several shipped functions at once.
    // Without this, a single declaration is handed to every one of them — measured, and it shipped
    // `CTakeDamageInfo::Constructor` as `verified` taking a `CCSGameRules*`, because both footprints
    // are one pointer. The name has to be unique on BOTH sides or nothing can say which function the
    // declaration describes.
    let mut shipped_bare: BTreeMap<&str, usize> = BTreeMap::new();
    for name in mono.core.keys().chain(mono.high_confidence.keys()) {
        *shipped_bare
            .entry(name.rsplit("::").next().unwrap_or(name))
            .or_default() += 1;
    }

    // Measurements come from every tier: an experimental entry's shape is still a fact about the binary.
    let shapes: BTreeMap<&str, &model::AbiShape> =
        [&mono.core, &mono.high_confidence, &mono.experimental]
            .into_iter()
            .flat_map(|m| m.iter())
            .filter_map(|(n, e)| e.abi.as_ref().map(|a| (n.as_str(), a)))
            .collect();

    let mut functions: BTreeMap<String, model::AbiEntry> = BTreeMap::new();
    let mut counts: BTreeMap<String, usize> = BTreeMap::new();
    let mut bump = |k: &str| *counts.entry(k.to_string()).or_default() += 1;

    for (tier, section) in [
        ("core", &mono.core),
        ("high_confidence", &mono.high_confidence),
    ] {
        for name in section.keys() {
            let sh = shapes.get(name.as_str()).copied();
            let exact = doc.prototypes.get(name);
            // A bare name is claimed ONLY when exactly one qualified declaration bears it, exactly one
            // SHIPPED function bears it, and the binary can arbitrate. `SetAbsAngles` exists on many
            // classes; matching by bare name without a measurement to adjudicate is how an early pass
            // invented most of its mismatches, and matching without the shipped-side count is how one
            // declaration gets handed to a dozen unrelated functions.
            // NOT for a console command: `ConCommand::status` has no C++ method called `status`, so the
            // tail is a console name that merely looks like one. Matching it would hand an unrelated
            // declaration to a command handler on a pure spelling coincidence — and the uniqueness gate
            // cannot catch it, because the command IS the only shipped bearer of that bare name.
            let bare = name.rsplit("::").next().unwrap_or(name);
            let by_bare_hit = by_bare
                .get(bare)
                .filter(|_| !name.starts_with("ConCommand::"))
                .filter(|h| {
                    h.len() == 1
                        && doc.bare_unique.contains(bare)
                        && shipped_bare.get(bare) == Some(&1)
                        && sh.is_some()
                })
                .map(|h| &doc.prototypes[*h.iter().next().unwrap()]);

            // The ENGINE'S OWN dispatch contract, which is a declaration and a stronger one than any
            // third-party header: an entity-IO handler is only ever invoked through
            // `void(CEntityInstance*, InputData_t&)`. It states the WHOLE prototype, and both halves
            // matter for the same reason — nobody has to have written this function down for it to be
            // known, because the engine's dispatch settles it.
            //
            // The return is where the alternative is weakest: the measured register class is wrong
            // about known-void functions roughly seven times in eight, because a callee cannot tell
            // whether its caller reads RAX and scratch use reads back as `ret=int`. The parameters are
            // where the COVERAGE is: 205 CS2 handlers that no declaration names get a real, checkable
            // signature instead of a return and a shrug. The deriver already establishes which names
            // came from the datadesc; this is that fact reaching the manifest.
            //
            // Two contracts reach this point now: the entity-IO one above, and the console-command one
            // (see `concommand_contract`), which the engine states just as firmly and which covers 755
            // more CS2 names that no declaration anywhere describes.
            let src = section
                .get(name)
                .and_then(|e| e.provenance.source.as_deref());
            let contract_params: Vec<String> = match src {
                Some(VALVE_DATADESC) => ENGINE_CONTRACT_PARAMS.map(str::to_string).to_vec(),
                Some(s) => concommand_contract(s).unwrap_or_default(),
                None => Vec::new(),
            };
            let is_contract = !contract_params.is_empty();
            let contract_ret = is_contract.then(|| "void".to_string());

            // The slot a vtable-offset locator resolves through, and ONLY when the live oracle
            // confirmed it (`OffVerdict::Live`). Recorded on every verdict rather than only the ones
            // that pass the emitters' gate, because it describes the LOCATOR, not the declaration —
            // and it is the one piece of evidence that settles a receiver the footprint cannot see.
            // See `model::AbiEntry::vtable` for why validation is part of the condition.
            let vtable = section
                .get(name)
                .filter(|e| e.validated == Some(true))
                .and_then(|e| e.locator.offset);

            let decls: &[Decl] = exact.or(by_bare_hit).map_or(&[][..], |v| v.as_slice());

            // The SCRIPT VM'S OWN declared return, for a name the registry states. It ranks above the
            // measured register class for the reason spelled out below: a callee cannot tell whether its
            // caller reads RAX, so measurement is wrong about known-void functions roughly seven times
            // in eight — and `void` is what the registry declares for 849 of Dota's bindings, which is
            // exactly the population measurement gets wrong. It ranks BELOW a real declaration only to
            // keep "a source wrote this down" ahead of anything derived; in practice the two never
            // compete, because no VScript name is also a declared name (measured: zero overlap).
            //
            // Read BEFORE the gate below, not after: a registry-declared return is on its own enough to
            // have something to say about a function, so a name carrying one must not be skipped for
            // having no parameter list. That is precisely the `return-only` case.
            let vs_ret = vscript_ret.and_then(|m| m.get(name)).cloned();
            let has_vs_ret = vs_ret.is_some();

            if decls.is_empty() && !is_contract && !has_vs_ret {
                bump(&format!("{tier}:none"));
                continue;
            }

            // Any DECLARED return type on offer, used only where the chosen signature carries none of
            // its own (a return-only declaration has no signature to choose). A source declaration is
            // preferred over the engine contract only because it is the more specific claim; they
            // disagree on exactly one function in the current set. The measured register class is the
            // last resort — a much weaker statement, labelled as such in the artifact's own docs.
            let any_ret = decls.iter().find_map(|d| d.ret.clone());
            let contract = contract_ret.clone();
            let fallback_ret = move || {
                any_ret
                    .clone()
                    .or(contract)
                    .or(vs_ret)
                    .or_else(|| sh.map(|s| s.ret.clone()))
            };
            let mut provenance: Vec<String> = decls
                .iter()
                .map(|d| d.provenance.clone())
                .collect::<BTreeSet<_>>()
                .into_iter()
                .collect();
            if is_contract {
                provenance.push(ENGINE_CONTRACT.to_string());
            }
            if has_vs_ret {
                provenance.push(VALVE_VSCRIPT.to_string());
            }

            // The contract goes in FIRST, so that where it and a declaration both fit the measurement,
            // `most_specific` reports the one that names its receiver — which the contract always does
            // and a mangled symbol never can. Nothing is lost by that: `overloads` lists every
            // signature that was on offer, including the more specific class a source may have named.
            let mut cands: Vec<Candidate> = Vec::new();
            if is_contract {
                cands.push(Candidate {
                    params: &contract_params,
                    complete: true,
                    is_const: false,
                    ret: None,
                    contract: true,
                });
            }
            collect_candidates(decls, &mut cands);
            // `bare-name` is a CLAIM — "one declaration bears this method name and the measurement could
            // adjudicate" — so it must not be the fallback for an entry that was never name-matched at
            // all. A registry-declared return with no declaration behind it is neither exact nor
            // bare-name; it is the script VM stating its own contract, and it says so.
            let matched_by = if exact.is_some() {
                "exact"
            } else if decls.is_empty() && has_vs_ret {
                VALVE_VSCRIPT
            } else {
                "bare-name"
            };
            // Nothing here declares a parameter list — the source said what comes back and stayed silent
            // about what goes in. There is no arity claim, so there is nothing for the binary to confirm
            // or refute, and saying "verified" or "mismatch" would claim a check that never happened.
            //
            // NOT attempted: reaching for a bare-name declaration's parameter list to fill the gap. It
            // cannot help, and the reason is structural — the exact declaration is itself a bearer of
            // that bare name, so the gate's uniqueness test can only pass when the bare-name owner IS
            // the exact name, which yields these same declarations again. `CBaseEntity::GetEyePosition`
            // is the case: it stays `return-only` because the only parameter list on offer belongs to
            // `IBody::GetEyePosition`, a different class.
            if cands.is_empty() {
                bump(&format!("{tier}:return-only"));
                bump(&format!("status:{}", model::AbiStatus::ReturnOnly.as_str()));
                functions.insert(
                    name.clone(),
                    model::AbiEntry {
                        tier: tier.to_string(),
                        matched_by: matched_by.to_string(),
                        status: model::AbiStatus::ReturnOnly,
                        ret: fallback_ret(),
                        provenance,
                        derived: sh.cloned(),
                        vtable,
                        ..model::AbiEntry::blank()
                    },
                );
                continue;
            }

            // The signatures on offer, deduped by SPELLING: the same list written in both conventions is
            // one thing a reader has to choose between, not two.
            let all_sigs: Vec<Vec<String>> = cands
                .iter()
                .map(|c| c.params.clone())
                .collect::<BTreeSet<_>>()
                .into_iter()
                .collect();
            let mut note = None;
            let chosen: Option<Candidate> = if cands.len() == 1 {
                Some(cands[0])
            } else if let Some(s) = sh {
                // Signatures the declarations alone cannot separate: let the measurement pick.
                let fits: Vec<Candidate> = cands
                    .iter()
                    .copied()
                    .filter(|c| agrees(c, s, types))
                    .collect();
                match fits.len() {
                    // NONE of them agrees. That is not an ambiguity — it is the same verdict for every
                    // candidate, so whichever is reported the answer is "no declaration on offer
                    // describes this build", which is precisely what `mismatch` says and what a caller
                    // needs to know. Calling it `ambiguous` would report a doubt that does not exist.
                    0 => Some(most_specific(&cands)),
                    1 => {
                        note = Some("overload resolved by measured footprint".to_string());
                        Some(fits[0])
                    }
                    // Every survivor agrees with the binary, so the FOOTPRINT is settled and only the type
                    // spellings differ — two sources naming the same argument `void*` and
                    // `CTakeDamageResult*`. Reporting that as `ambiguous` would understate what is known.
                    n => {
                        note = Some(format!(
                            "{n} of {} declarations agree with the measured footprint and differ only \
                             in the types they name; the most specific of those is reported, and \
                             `overloads` lists every signature that was on offer, agreeing or not",
                            cands.len()
                        ));
                        Some(most_specific(&fits))
                    }
                }
            } else {
                None
            };

            // Reachable only with NO measurement and more than one signature on offer: nothing can
            // separate them, which is the one thing `ambiguous` is for.
            let Some(chosen) = chosen else {
                bump(&format!("{tier}:overloaded"));
                // Counted like every other verdict. Omitting it left `meta.counts` — documented as the
                // verdict tally — silently missing a status that entries in the file actually carry.
                bump(&format!("status:{}", model::AbiStatus::Ambiguous.as_str()));
                functions.insert(
                    name.clone(),
                    model::AbiEntry {
                        tier: tier.to_string(),
                        matched_by: matched_by.to_string(),
                        status: model::AbiStatus::Ambiguous,
                        // The ambiguity is about the PARAMETER list; a declared return type that every
                        // candidate agrees on is not in doubt and is not dropped with them.
                        ret: fallback_ret(),
                        provenance,
                        derived: sh.cloned(),
                        overloads: Some(all_sigs),
                        vtable,
                        ..model::AbiEntry::blank()
                    },
                );
                continue;
            };

            let mut status = match sh {
                None => model::AbiStatus::Unverified,
                Some(s) if agrees(&chosen, s, types) => model::AbiStatus::Verified,
                Some(_) => model::AbiStatus::Mismatch,
            };
            // A mismatch has two directions and they mean opposite things. Declared ABOVE measured is the
            // documented lower-bound case — a callee that ignores an argument, or a thunk that reads none
            // of its own — and calling through it merely loads a register nobody reads. Declared BELOW
            // measured is the dangerous one: the callee reads an argument the declaration never mentions.
            //
            // And a declaration can be wrong in BOTH directions at once, in different register classes,
            // which an either/or test reports as whichever it happens to check first. `FindUseEntity` is
            // the case: declared `(CCSPlayer_UseServices*, float)` and measured `int=3 float=0`, so it
            // passes a float the callee never reads AND leaves two integer registers the callee DOES read
            // unset. That is the dangerous shape, and it was being described as the harmless one.
            // Split the disagreement by DIRECTION before reporting it, because the two directions are
            // not two flavours of the same verdict. Declared-above-measured is the documented
            // lower-bound case and calling through it loads a register nobody reads;
            // measured-above-declared leaves a register the callee DOES read unset. 81 of CS2's 140
            // former mismatches were the former, reported as "does not describe this build".
            if status == model::AbiStatus::Mismatch {
                let s = sh.expect("a mismatch is only reachable with a measurement");
                let (verdict, why) = adjudicate_mismatch(&chosen, s, types);
                status = verdict;
                note = Some(why.to_string());
            }
            // A BARE-NAME claim that the measurement CONTRADICTS is withdrawn, not reported. The gate
            // admits a bare name only when a measurement exists to adjudicate it — and adjudicating
            // means rejecting when the answer is no. `CWorldRendererMgr::LockForRead` takes the empty
            // parameter list of some other class's `LockForRead` and measures four integer arguments:
            // that is evidence the JOIN is wrong, not that this function's own declaration went stale,
            // and reporting `mismatch` would attribute a prototype to a function nothing connects it to.
            // A LOWER-BOUND disagreement is not a contradiction and is kept. 4 on CS2.
            if matched_by == "bare-name" && status == model::AbiStatus::Mismatch {
                bump(&format!("{tier}:none"));
                bump("bare-name:withdrawn");
                continue;
            }
            bump(&format!("status:{}", status.as_str()));
            bump(&format!("{tier}:resolved"));
            // Where the reported signature IS the contract, say so: "the engine invokes it this way"
            // and "somebody declared it this way" are different claims and a consumer weighs them
            // differently.
            let matched_by = if chosen.contract {
                ENGINE_CONTRACT
            } else {
                matched_by
            };
            functions.insert(
                name.clone(),
                model::AbiEntry {
                    tier: tier.to_string(),
                    matched_by: matched_by.to_string(),
                    status,
                    params: Some(chosen.params.clone()),
                    params_complete: chosen.complete.then_some(true),
                    is_const: Some(chosen.is_const),
                    ret: chosen.ret.cloned().or_else(fallback_ret),
                    provenance,
                    derived: sh.cloned(),
                    note,
                    overloads: (cands.len() > 1).then_some(all_sigs),
                    vtable,
                    // Prose belongs to the function record, not to a prototype; the merge attaches it
                    // there and `Rosetta::abi_manifest` joins it back on for the emitters.
                    doc: None,
                },
            );
        }
    }

    // Counted from the file rather than tallied as entries are built: the other counts are verdicts,
    // reached once per name, while this one describes entries that four different branches can create.
    let n_vtable = functions.values().filter(|e| e.vtable.is_some()).count();
    if n_vtable > 0 {
        counts.insert("locator:vtable".to_string(), n_vtable);
    }

    Ok(model::AbiManifest {
        meta: model::AbiMeta {
            game_key: mono.meta.game_key.clone(),
            source_build: mono.meta.source_build.clone(),
            counts,
        },
        functions,
    })
}

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

    fn shape(int: u8, float: u8) -> model::AbiShape {
        model::AbiShape {
            int,
            float,
            stack: false,
            ret: "ret=?".to_string(),
        }
    }
    fn p(v: &[&str]) -> Vec<String> {
        v.iter().map(|s| (*s).to_string()).collect()
    }
    fn classify_t(t: &str) -> (usize, usize) {
        classify(t, None)
    }
    /// A candidate as a mangled symbol writes one: `this` invisible, nothing authoritative.
    fn cand(params: &Vec<String>, complete: bool) -> Candidate<'_> {
        Candidate {
            params,
            complete,
            is_const: false,
            ret: None,
            contract: false,
        }
    }
    fn agrees_t(params: &Vec<String>, sh: &model::AbiShape) -> bool {
        agrees(&cand(params, false), sh, None)
    }

    #[test]
    fn sysv_classification_is_not_lexical() {
        // A `Vector` BY VALUE is 3 floats in two SSE registers…
        assert_eq!(classify_t("Vector"), (0, 2));
        // …but by reference it is one INTEGER register, whatever it points at. Getting this wrong is
        // what manufactured most of an early pass's false mismatches.
        assert_eq!(classify_t("Vector const&"), (1, 0));
        assert_eq!(classify_t("Vector*"), (1, 0));
        assert_eq!(classify_t("float"), (0, 1));
        assert_eq!(classify_t("int"), (1, 0));
        // A template is classified by its base, not its arguments.
        assert_eq!(classify_t("CUtlVector<float>"), (1, 0));
    }

    #[test]
    fn this_is_invisible_in_the_mangling_so_both_arities_are_accepted() {
        // `void Foo(int)` declares one parameter; as a MEMBER function the call also passes `this`.
        // The mangling cannot tell the two apart, so a measured 1 and a measured 2 both agree.
        assert!(agrees_t(&p(&["int"]), &shape(1, 0)));
        assert!(agrees_t(&p(&["int"]), &shape(2, 0)));
        assert!(!agrees_t(&p(&["int"]), &shape(3, 0)));
    }

    #[test]
    fn arity_above_the_register_budget_is_compared_capped() {
        // Only six integer argument registers exist, so a 9-parameter declaration cannot be
        // distinguished from a 7-parameter one by the footprint alone.
        let nine = p(&["int"; 9]);
        assert!(agrees_t(&nine, &shape(6, 0)));
    }

    #[test]
    fn a_complete_declaration_gets_no_this_allowance() {
        // The ±1 above exists only because a mangled symbol cannot say whether `this` is passed. A
        // function-pointer type already names its receiver, so allowing it there would let a declaration
        // that is short by exactly one argument pass — which is `IScriptVM::CreateVM`, declared with one
        // and measuring two.
        let one = p(&["IScriptVM*"]);
        assert!(agrees(&cand(&one, true), &shape(1, 0), None));
        assert!(!agrees(&cand(&one, true), &shape(2, 0), None));
        assert!(agrees(&cand(&one, false), &shape(2, 0), None));
    }

    #[test]
    fn the_most_specific_spelling_wins_when_the_binary_cannot_choose() {
        // Two sources declaring the same function with the same footprint: one says `void*` where the
        // other names the type. The measurement separates neither, so the informative one is reported.
        let vague = p(&["CBaseEntity*", "CTakeDamageInfo*", "void*"]);
        let named = p(&["CBaseEntity*", "CTakeDamageInfo*", "CTakeDamageResult*"]);
        let cands = [cand(&vague, true), cand(&named, true)];
        assert_eq!(most_specific(&cands).params, &named);
        // A receiver-bearing list outranks one that hides `this`, whatever else it says.
        let mangled = p(&["CTakeDamageInfo*"]);
        let cands = [cand(&mangled, false), cand(&vague, true)];
        assert_eq!(most_specific(&cands).params, &vague);
    }

    #[test]
    fn the_engine_contract_is_judged_as_a_lower_bound_not_an_equality() {
        let io = p(&ENGINE_CONTRACT_PARAMS);
        let contract = Candidate {
            contract: true,
            ..cand(&io, true)
        };
        // What the engine passes, exactly: the common case, 156 of CS2's 205.
        assert!(agrees(&contract, &shape(2, 0), None));
        // A handler that ignores its `InputData_t&`, and a forwarding thunk that reads neither
        // register. Both are real and neither refutes how the engine invokes them — 49 of the 205.
        assert!(agrees(&contract, &shape(1, 0), None));
        assert!(agrees(&contract, &shape(0, 0), None));
        // An OVER-count is the one direction that refutes it: the callee reads a register the
        // dispatch never fills, so either the reader invented an argument or this is not a handler.
        assert!(!agrees(&contract, &shape(3, 0), None));
        assert!(!agrees(&contract, &shape(2, 1), None));
        // …and so does an sret return, which the register counts cannot show: it would mean argument 0
        // is a hidden output pointer and every other argument sits one register along.
        let byval = model::AbiShape {
            ret: "ret=byval".to_string(),
            ..shape(2, 0)
        };
        assert!(!agrees(&contract, &byval, None));
        // The same list from a THIRD PARTY gets no such licence — a declaration can go stale, and
        // catching that is what the manifest is for.
        assert!(!agrees(&cand(&io, true), &shape(1, 0), None));
    }

    #[test]
    fn a_console_command_contract_carries_a_receiver_only_where_the_form_dispatches_through_one() {
        // The whole point of keying on the form: a direct registration passes a plain function, so its
        // contract is the two arguments the engine supplies and nothing else.
        assert_eq!(
            concommand_contract("valve-concommand:direct").unwrap(),
            vec!["CCommandContext*", "CCommand*"]
        );
        // The object forms dispatch through a receiver, so they take one more integer register. The
        // member form's receiver is whatever object the registering constructor was building, which the
        // binary does not name — `void*` says "a receiver, type unknown" rather than inventing a class.
        assert_eq!(
            concommand_contract("valve-concommand:interface").unwrap(),
            vec!["ICommandCallback*", "CCommandContext*", "CCommand*"]
        );
        assert_eq!(
            concommand_contract("valve-concommand:member").unwrap(),
            vec!["void*", "CCommandContext*", "CCommand*"]
        );
        // A form this code has never measured claims NOTHING — it does not fall back to a guess.
        assert!(concommand_contract("valve-concommand:something-new").is_none());
        assert!(concommand_contract("valve-concommand").is_none());
        // …and no other provenance is mistaken for one, including the prefix as a bare word.
        assert!(concommand_contract("valve-datadesc").is_none());
        assert!(concommand_contract("catalogue").is_none());
        assert!(concommand_contract("valve-concommandering:direct").is_none());

        // Judged as a lower bound like the entity-IO contract, and for the same reason: a handler that
        // ignores its arguments reads fewer registers, and only an OVER-count refutes the dispatch.
        let direct = p(&["CCommandContext*", "CCommand*"]);
        let c = Candidate {
            contract: true,
            ..cand(&direct, true)
        };
        assert!(agrees(&c, &shape(2, 0), None));
        assert!(agrees(&c, &shape(0, 0), None));
        // Three integers is what a RECEIVER form measures, and it refutes the direct contract — which
        // is exactly why the form has to be carried rather than assumed.
        assert!(!agrees(&c, &shape(3, 0), None));
        // A console callback returns void, so a by-value return would mean argument 0 is a hidden
        // output pointer and every other argument has shifted.
        let byval = model::AbiShape {
            ret: "ret=byval".to_string(),
            ..shape(2, 0)
        };
        assert!(!agrees(&c, &byval, None));
    }

    /// The verdict AND the note, for one declaration against one measurement.
    /// CALLS the shipped rule rather than restating it. It used to re-implement `build_manifest`'s
    /// direction logic, which made the assertions below unfalsifiable: only an edit that changed both
    /// copies the same wrong way could fail them, and that is the one edit nobody makes by accident.
    /// The direction is read back out of the shipped note text, so the mapping from direction to prose
    /// is under test too.
    fn judge(params: &[&str], sh: &model::AbiShape, complete: bool) -> (model::AbiStatus, String) {
        let ps = p(params);
        let c = cand(&ps, complete);
        if agrees(&c, sh, None) {
            return (model::AbiStatus::Verified, String::new());
        }
        let (status, note) = adjudicate_mismatch(&c, sh, None);
        let direction = if note.starts_with("measured and declared") {
            "both"
        } else if note.starts_with("measured footprint EXCEEDS") {
            "measured-exceeds"
        } else if note.starts_with("the declaration passes") {
            "declared-exceeds"
        } else {
            "neither"
        };
        (status, direction.to_string())
    }

    #[test]
    fn a_mismatch_in_both_directions_is_not_reported_as_the_harmless_one() {
        // `FindUseEntity`: declared `(CCSPlayer_UseServices*, float)`, measured `int=3 float=0`. It
        // passes a float the callee never reads AND leaves two integer registers the callee does read
        // unset. An either/or test finds the float side first and calls the whole thing benign.
        assert_eq!(judge(&["void*", "float"], &shape(3, 0), true).1, "both");
        // The two single-direction cases still read as themselves.
        assert_eq!(judge(&["void*"], &shape(3, 0), true).1, "measured-exceeds");
        assert_eq!(
            judge(&["void*", "void*", "void*"], &shape(1, 0), true).1,
            "declared-exceeds"
        );
        // …and only the OVER-read is a mismatch. The declaration that passes a register nobody reads is
        // consistent with a footprint that is a lower bound, and calling through it is harmless.
        assert_eq!(
            judge(&["void*"], &shape(3, 0), true).0,
            model::AbiStatus::Mismatch
        );
        assert_eq!(
            judge(&["void*", "float"], &shape(3, 0), true).0,
            model::AbiStatus::Mismatch
        );
        assert_eq!(
            judge(&["void*", "void*", "void*"], &shape(1, 0), true).0,
            model::AbiStatus::LowerBound
        );
        // …and the invisible `this` is not one of them. `CGameEvent::GetFloat` is declared
        // `(char const*, float)` from a mangled symbol and measures `int=2 float=0`: the extra integer
        // register IS the receiver, so the only real disagreement is the float the callee never reads.
        assert_eq!(
            judge(&["char const*", "float"], &shape(2, 0), false).1,
            "declared-exceeds"
        );
    }

    #[test]
    fn a_float_disagreement_is_decisive() {
        // The integer side has the `this` allowance; the float side has none, so a declared float
        // count that differs from the measurement is a real mismatch.
        assert!(!agrees_t(&p(&["Vector"]), &shape(1, 0)));
        assert!(agrees_t(&p(&["Vector"]), &shape(1, 2)));
    }
}
