//! The closed, compiled-in dispatch from a serialized node **type identity** to
//! its `aura-std` builder factory — the load-side counterpart to the type label
//! a blueprint serializes (`PrimitiveBuilder::label`). This is **not** a dynamic
//! registry (domain invariant 9): it is a `match` the crate that *owns* the
//! nodes writes, over its own closed, compiled-in vocabulary (C24 — nodes are
//! referenced "by compiled-in type identity", never a by-name marketplace). The
//! engine cannot hold this table (it does not depend on `aura-std`); a loader
//! takes it as an injected resolver, and a project `cdylib` later supplies its
//! own (C16) through the same seam.
//!
//! Scope (#155): only the **zero-argument** `Type::builder()` factories are
//! resolvable. Builders that take structural construction arguments
//! (`LinComb(arity)`, `CostSum(n_costs)`, `SimBroker(pip_size)`, `Session(..)`)
//! and the recording sinks (`Recorder`/`GatedRecorder`/`SeriesReducer`, which
//! capture an `mpsc::Sender`) are deliberately absent — an absent `type_id`
//! yields `None`, so the loader fails cleanly (`LoadError::UnknownNodeType`)
//! rather than guessing. Serialising structural-axis construction args is a
//! later, additive extension (#156/C20).

use crate::{
    Abs, Add, And, Bias, CarryCost, Const, ConstantCost, Delay, Div, Ema, EqConst, FixedStop, Gt,
    Latch, LongOnly, Max, Min, Mul, PositionManagement, Resample, RollingMax, RollingMin, Scale,
    SessionFrankfurt, Sizer, Sma, Sqrt, Sub, VolSlippageCost,
};
use aura_core::PrimitiveBuilder;

/// The single roster source (#160): one `"TypeId" => Type` line per zero-arg
/// node, expanded into BOTH the `std_vocabulary` match and the
/// `std_vocabulary_types` list — the two surfaces cannot drift apart, and
/// adding a zero-arg node to the vocabulary is exactly one line here (plus the
/// conscious count-pin bumps: the in-crate shape test and aura-cli's
/// cross-boundary `--vocabulary` count e2e). What this cannot guard: a new
/// zero-arg node never rostered at all — that fails safe (clean
/// `LoadError::UnknownNodeType` on load, merely absent from `--vocabulary`;
/// #160).
macro_rules! std_vocabulary_roster {
    ($($type_id:literal => $ty:ty),+ $(,)?) => {
        /// Resolve a serialized node type identity (the `PrimitiveBuilder::label`,
        /// e.g. `"SMA"`) to a fresh, unbound builder from the node's own factory.
        /// Returns `None` for any identity outside the zero-arg `aura-std`
        /// vocabulary.
        pub fn std_vocabulary(type_id: &str) -> Option<PrimitiveBuilder> {
            Some(match type_id {
                $($type_id => <$ty>::builder(),)+
                _ => return None,
            })
        }

        /// The enumerable companion to [`std_vocabulary`]: the closed set of
        /// zero-arg type identities the resolver answers. A `fn(&str)->Option<…>`
        /// resolver cannot be listed, so build-free vocabulary introspection
        /// (#157) reads this. Generated from the same roster as the match arms
        /// (#160) — the two surfaces agree by construction.
        pub fn std_vocabulary_types() -> &'static [&'static str] {
            &[$($type_id),+]
        }
    };
}

std_vocabulary_roster! {
    "Abs" => Abs,
    "Add" => Add,
    "And" => And,
    "Bias" => Bias,
    "CarryCost" => CarryCost,
    "Const" => Const,
    "ConstantCost" => ConstantCost,
    "Delay" => Delay,
    "Div" => Div,
    "EMA" => Ema,
    "EqConst" => EqConst,
    "FixedStop" => FixedStop,
    "Gt" => Gt,
    "Latch" => Latch,
    "LongOnly" => LongOnly,
    "Max" => Max,
    "Min" => Min,
    "Mul" => Mul,
    "PositionManagement" => PositionManagement,
    "Resample" => Resample,
    "RollingMax" => RollingMax,
    "RollingMin" => RollingMin,
    "Scale" => Scale,
    "SessionFrankfurt" => SessionFrankfurt,
    "Sizer" => Sizer,
    "SMA" => Sma,
    "Sqrt" => Sqrt,
    "Sub" => Sub,
    "VolSlippageCost" => VolSlippageCost,
}

#[cfg(test)]
mod tests {
    use super::{std_vocabulary, std_vocabulary_types};

    /// Every rostered key resolves to a builder that carries that exact type
    /// label back, and only the rostered keys do. The builder's own `label()`
    /// is the independent oracle: a typo'd roster key fails the lookup-label
    /// agreement even though the match arms and the list share the same
    /// (mistyped) literal; construction-arg nodes and sinks stay absent so the
    /// loader fails cleanly rather than guessing.
    #[test]
    fn std_vocabulary_resolves_known_and_rejects_unknown() {
        for &type_id in std_vocabulary_types() {
            let builder =
                std_vocabulary(type_id).unwrap_or_else(|| panic!("{type_id} is in the vocabulary"));
            assert_eq!(
                builder.label(),
                type_id,
                "resolver key must round-trip to the same type label"
            );
        }
        // an unknown id, a construction-arg node, and a sink are all absent
        assert!(std_vocabulary("nope").is_none());
        assert!(std_vocabulary("LinComb").is_none());
        assert!(std_vocabulary("Recorder").is_none());
    }

    /// List <-> resolver agreement holds BY CONSTRUCTION since the #160 roster
    /// macro (one source expands into both surfaces); what stays pinned here is
    /// the roster's SHAPE. The count pin is deliberate friction: any roster
    /// line added or dropped trips it, making a vocabulary change a conscious
    /// act. The residual #160 drift — a new zero-arg node never rostered at
    /// all — is not catchable here (no enumeration of zero-arg builders
    /// exists); it fails safe (clean `UnknownNodeType` on load, merely absent
    /// from `--vocabulary`).
    #[test]
    fn std_vocabulary_types_lists_exactly_the_resolvable_keys() {
        // known non-members stay out of the list
        assert!(!std_vocabulary_types().contains(&"LinComb")); // construction-arg node
        assert!(!std_vocabulary_types().contains(&"Recorder")); // sink
        assert!(!std_vocabulary_types().contains(&"nope"));
        // count guard: pins the roster at exactly 29 entries
        assert_eq!(std_vocabulary_types().len(), 29);
    }
}
