//! Typed result variants for `AuthorityDerivationToken` verification (T2-B21).
//!
//! Today, [`crate::verify_authority_derivation`] collapses every failure class
//! (structural-superset violation, missing role key, malformed base64, bad
//! point on curve, invalid signature, scope-policy rejection …) into a single
//! [`crate::error::CellosError::InvalidSpec(String)`] variant. That is fine for
//! end-user error reporting, but admission-side callers (supervisor, taudit
//! emitters) want to *pattern-match* on the failure class so they can:
//!
//! 1. Emit class-specific CloudEvents (`signature_invalid` vs.
//!    `authority_overclaim`) without parsing a free-form message.
//! 2. Distinguish *expired* tokens (operationally remediable — re-issue) from
//!    *forged* tokens (security incident — alert).
//! 3. Surface concrete numeric overclaim deltas (`declared=12, allowed=8`)
//!    rather than a sentence.
//!
//! This module is the **typed alternative return type** for the existing
//! `verify_authority_derivation` flow. It does NOT replace
//! [`crate::error::CellosError`] — both live side by side under the typed-
//! validator scaffold (PR #40 / T2.A). The legacy `Result<(), CellosError>`
//! function stays as the stable public surface; the new
//! [`validate_authority_derivation`] returns [`AuthorityValidationResult`] for
//! callers that need to match on the variant.
//!
//! # Doctrine
//!
//! * **D11 (no I/O in `cellos-core`):** every variant is a plain enum carrying
//!   already-formatted strings + numbers. No tokio, no syscalls.
//! * **Authority Model §9 (non-inflation):** the
//!   [`AuthorityValidationResult::ExceedsGrantorEgress`] and
//!   [`AuthorityValidationResult::ExceedsGrantorSecrets`] variants carry the
//!   *declared* and *allowed* counts so consumers can emit a precise overclaim
//!   delta without recomputing.

use crate::types::{AuthorityCapability, AuthorityDerivationToken, EgressRule, ExecutionCellSpec};

/// Outcome of validating an [`AuthorityDerivationToken`] against the spec it
/// accompanies and the operator's role-keys map.
///
/// Returned by [`validate_authority_derivation`]. See module docs for why this
/// exists alongside [`crate::verify_authority_derivation`].
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum AuthorityValidationResult {
    /// Token's leaf capability is a subset of the spec's declared authority,
    /// the role key resolved, the signature verified, and the scope policy
    /// (universal-vs-scoped) passed.
    Valid,

    /// Cryptographic signature failed verification, OR the role key /
    /// signature bytes were malformed (bad base64, wrong length, bad curve
    /// point). The `reason` is a human-readable diagnostic — callers MUST NOT
    /// parse it as a security boundary.
    InvalidSignature {
        /// Free-form diagnostic (e.g. "verifying key not valid base64",
        /// "signature must be 64 bytes (got 63)").
        reason: String,
    },

    /// Token's `leafCapability.egressRules` claims a rule the spec does NOT
    /// declare — i.e., the grantor-signed narrowed leaf exceeds what the
    /// spec declares (Authority Model §9 non-inflation: child ⊆ parent).
    ///
    /// Carries both counts so emitters can quote the overclaim delta directly
    /// (`leaf claims N rules; spec declares M`).
    ExceedsGrantorEgress {
        /// Number of distinct egress rules on `spec.authority.egressRules` —
        /// the authoritative declared surface.
        declared: usize,
        /// Number of distinct egress rules on
        /// `token.leafCapability.egressRules` — the grantor-signed leaf.
        /// `claimed > declared` (with at least one claimed rule not present
        /// in the declared set) is the non-inflation violation.
        claimed: usize,
    },

    /// Token's `leafCapability.secretRefs` claims a secret ref the spec does
    /// NOT declare. Mirror of [`Self::ExceedsGrantorEgress`].
    ExceedsGrantorSecrets {
        /// Number of distinct secret refs on `spec.authority.secretRefs`.
        declared: usize,
        /// Number of distinct secret refs on
        /// `token.leafCapability.secretRefs`.
        claimed: usize,
    },

    /// The spec's `authority.authorityDerivation` field was `None`. This is
    /// distinct from "token present but invalid" — emitters may treat it as
    /// "fail-open in MVP" or "fail-closed in 1.0" depending on the operator
    /// posture flag.
    MissingDerivationToken,

    /// The token's `notBefore` / `notAfter` window did not contain the
    /// current time at validation. RFC3339 strings preserved verbatim so
    /// emitters can quote the exact window in the CloudEvent.
    ///
    /// **Note:** the existing [`crate::verify_authority_derivation`] flow does
    /// not check temporal validity — [`AuthorityDerivationToken`] has no
    /// `not_before`/`not_after` fields today. This variant is reserved for
    /// the post-1.0 scope-policy expansion (Authority Model §14). It exists
    /// in the enum now so consumers can write exhaustive matches and the
    /// future field addition becomes a deny-by-default compile failure
    /// rather than a silent fall-through.
    ExpiredToken {
        /// RFC3339 timestamp — start of validity window.
        not_before: String,
        /// RFC3339 timestamp — end of validity window.
        not_after: String,
    },
}

impl AuthorityValidationResult {
    /// `true` iff the variant is [`AuthorityValidationResult::Valid`].
    pub fn is_valid(&self) -> bool {
        matches!(self, Self::Valid)
    }
}

/// Typed-result counterpart of [`crate::verify_authority_derivation`].
///
/// Performs the same checks in the same order:
///
/// 1. Structural superset: `token.leaf_capability ⊆ spec.authority` — when
///    violated, returns [`AuthorityValidationResult::ExceedsGrantorEgress`] or
///    [`AuthorityValidationResult::ExceedsGrantorSecrets`] with concrete
///    counts.
/// 2. Role-key lookup + signature verify — failures collapse to
///    [`AuthorityValidationResult::InvalidSignature`] with a free-form reason.
///
/// Unlike [`crate::verify_authority_derivation`], structural failures here are
/// distinguished **per dimension** so admission emitters can route egress
/// overclaim to one CloudEvent and secret overclaim to another. The
/// signature-class failures stay collapsed because callers that need to
/// distinguish "bad base64" from "bad point" can already inspect the `reason`
/// field, and splitting those further would expose verifier internals.
///
/// # Parameters
///
/// * `spec` — the cell spec carrying `spec.authority` (the declared claim).
/// * `token` — the [`AuthorityDerivationToken`] from
///   `spec.authority.authorityDerivation`. Note this function takes the token
///   *directly*; callers that want to handle the "no token at all" case
///   should match on
///   `spec.authority.authority_derivation.as_ref().map_or(MissingDerivationToken, …)`
///   themselves — see the example below.
/// * `role_keys` — map of `RoleId → base64-STANDARD-encoded ED25519 verifying
///   key`. Same shape as the legacy function.
///
/// # Example
///
/// ```no_run
/// use cellos_core::authority::{
///     validate_authority_derivation, AuthorityValidationResult,
/// };
/// use cellos_core::ExecutionCellSpec;
/// use std::collections::HashMap;
/// fn run(spec: &ExecutionCellSpec, role_keys: &HashMap<String, String>) {
///     let result = match spec.authority.authority_derivation.as_ref() {
///         Some(token) => validate_authority_derivation(spec, token, role_keys),
///         None => AuthorityValidationResult::MissingDerivationToken,
///     };
///     match result {
///         AuthorityValidationResult::Valid => { /* admit */ }
///         AuthorityValidationResult::ExceedsGrantorEgress { declared, claimed } => {
///             tracing::warn!(declared, claimed, "egress overclaim");
///         }
///         _ => { /* other failure handling */ }
///     }
/// }
/// ```
pub fn validate_authority_derivation(
    spec: &ExecutionCellSpec,
    token: &AuthorityDerivationToken,
    role_keys: &std::collections::HashMap<String, String>,
) -> AuthorityValidationResult {
    // Step 1: structural subset, but split into typed per-dimension variants
    // (the legacy function collapses both into one InvalidSpec(String)).
    let spec_capability = AuthorityCapability {
        egress_rules: spec.authority.egress_rules.clone().unwrap_or_default(),
        secret_refs: spec.authority.secret_refs.clone().unwrap_or_default(),
    };

    if !egress_subset(
        &token.leaf_capability.egress_rules,
        &spec_capability.egress_rules,
    ) {
        return AuthorityValidationResult::ExceedsGrantorEgress {
            declared: spec_capability.egress_rules.len(),
            claimed: token.leaf_capability.egress_rules.len(),
        };
    }

    if !secret_subset(
        &token.leaf_capability.secret_refs,
        &spec_capability.secret_refs,
    ) {
        return AuthorityValidationResult::ExceedsGrantorSecrets {
            declared: spec_capability.secret_refs.len(),
            claimed: token.leaf_capability.secret_refs.len(),
        };
    }

    // Step 2: defer to the legacy signature path; collapse any error into
    // InvalidSignature. We re-call the public function so we don't duplicate
    // the ED25519 wire-format code.
    match crate::verify_authority_derivation(spec, token, role_keys) {
        Ok(()) => AuthorityValidationResult::Valid,
        Err(e) => AuthorityValidationResult::InvalidSignature {
            reason: e.to_string(),
        },
    }
}

/// Returns `true` when every rule in `child` matches some rule in `parent`
/// (host case-insensitive, port and protocol exact). Mirrors
/// [`AuthorityCapability::is_superset_of`]'s egress half — we re-implement it
/// here only because we need to call it on the *opposite* direction (token
/// leaf ⊆ spec authority) without taking a full `AuthorityCapability` clone.
fn egress_subset(child: &[EgressRule], parent: &[EgressRule]) -> bool {
    child.iter().all(|cr| {
        parent.iter().any(|pr| {
            pr.host.eq_ignore_ascii_case(&cr.host)
                && pr.port == cr.port
                && pr.protocol == cr.protocol
        })
    })
}

fn secret_subset(child: &[String], parent: &[String]) -> bool {
    child.iter().all(|cs| parent.iter().any(|ps| ps == cs))
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::types::{AuthorityBundle, AuthoritySignature, EgressRule, Lifetime, RoleId};

    fn egress(host: &str, port: u16, proto: Option<&str>) -> EgressRule {
        EgressRule {
            host: host.to_string(),
            port,
            protocol: proto.map(str::to_string),
            dns_egress_justification: None,
        }
    }

    fn spec_with(egress_rules: Vec<EgressRule>, secret_refs: Vec<String>) -> ExecutionCellSpec {
        // Mirror of `derivation_spec_with_authority` in spec_validation tests —
        // direct struct construction avoids the JSON-fixture pitfall where
        // `id` (and other required fields) need to be present for deserialize.
        ExecutionCellSpec {
            id: "deriv-test".into(),
            correlation: None,
            ingress: None,
            environment: None,
            placement: None,
            policy: None,
            identity: None,
            run: None,
            authority: AuthorityBundle {
                filesystem: None,
                network: None,
                egress_rules: Some(egress_rules),
                secret_refs: Some(secret_refs),
                authority_derivation: None,
                dns_authority: None,
                cdn_authority: None,
            },
            lifetime: Lifetime { ttl_seconds: 60 },
            export: None,
            telemetry: None,
        }
    }

    fn token_with(leaf: AuthorityCapability) -> AuthorityDerivationToken {
        AuthorityDerivationToken {
            role_root: RoleId("test-role".into()),
            parent_run_id: Some("run-0".into()),
            derivation_steps: vec![],
            leaf_capability: leaf,
            grantor_signature: AuthoritySignature {
                algorithm: "ed25519".into(),
                // 64 zero bytes base64 — deliberately invalid; signature step
                // is tested separately. These unit tests focus on structural
                // pre-signature variants.
                bytes: "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA".into(),
            },
        }
    }

    #[test]
    fn is_valid_helper_matches_variant() {
        assert!(AuthorityValidationResult::Valid.is_valid());
        assert!(!AuthorityValidationResult::MissingDerivationToken.is_valid());
        assert!(!AuthorityValidationResult::ExceedsGrantorEgress {
            declared: 2,
            claimed: 3,
        }
        .is_valid());
    }

    #[test]
    fn exceeds_egress_when_leaf_claims_rule_spec_does_not_declare() {
        // Spec declares one host; leaf claims a DIFFERENT host. Leaf is not a
        // subset of spec — non-inflation violation.
        let spec = spec_with(vec![egress("api.example.com", 443, Some("tcp"))], vec![]);
        let token = token_with(AuthorityCapability {
            egress_rules: vec![egress("evil.example.com", 443, Some("tcp"))],
            secret_refs: vec![],
        });
        let role_keys = std::collections::HashMap::new();
        let result = validate_authority_derivation(&spec, &token, &role_keys);
        assert_eq!(
            result,
            AuthorityValidationResult::ExceedsGrantorEgress {
                declared: 1,
                claimed: 1,
            }
        );
    }

    #[test]
    fn exceeds_secrets_when_leaf_claims_ref_spec_does_not_declare() {
        let spec = spec_with(vec![], vec!["api-key".into()]);
        let token = token_with(AuthorityCapability {
            egress_rules: vec![],
            secret_refs: vec!["root-token".into()],
        });
        let role_keys = std::collections::HashMap::new();
        let result = validate_authority_derivation(&spec, &token, &role_keys);
        assert_eq!(
            result,
            AuthorityValidationResult::ExceedsGrantorSecrets {
                declared: 1,
                claimed: 1,
            }
        );
    }

    #[test]
    fn leaf_narrower_than_spec_is_structurally_valid() {
        // Spec declares 2 rules; leaf claims a subset of 1. This is the
        // canonical "narrowing" — must pass the structural check (and then
        // hit InvalidSignature because the fixture signature is junk).
        let spec = spec_with(
            vec![
                egress("api.example.com", 443, Some("tcp")),
                egress("db.example.com", 5432, Some("tcp")),
            ],
            vec!["k1".into(), "k2".into()],
        );
        let token = token_with(AuthorityCapability {
            egress_rules: vec![egress("api.example.com", 443, Some("tcp"))],
            secret_refs: vec!["k1".into()],
        });
        let role_keys = std::collections::HashMap::new();
        let result = validate_authority_derivation(&spec, &token, &role_keys);
        assert!(
            matches!(result, AuthorityValidationResult::InvalidSignature { .. }),
            "expected InvalidSignature (structural narrowing OK), got {result:?}"
        );
    }

    #[test]
    fn structural_subset_then_signature_failure_collapses_to_invalid_signature() {
        // Token leaf == spec — structural pass; signature is junk so we land
        // in InvalidSignature.
        let rule = egress("api.example.com", 443, Some("tcp"));
        let spec = spec_with(vec![rule.clone()], vec!["k".into()]);
        let token = token_with(AuthorityCapability {
            egress_rules: vec![rule],
            secret_refs: vec!["k".into()],
        });
        let role_keys = std::collections::HashMap::new();
        let result = validate_authority_derivation(&spec, &token, &role_keys);
        match result {
            AuthorityValidationResult::InvalidSignature { reason } => {
                assert!(!reason.is_empty(), "reason should carry diagnostic");
            }
            other => panic!("expected InvalidSignature, got {other:?}"),
        }
    }

    #[test]
    fn missing_derivation_token_variant_is_inert() {
        // Just construct it — the variant exists for callers who detect the
        // None case before calling validate_authority_derivation.
        let v = AuthorityValidationResult::MissingDerivationToken;
        assert!(!v.is_valid());
    }

    #[test]
    fn expired_token_variant_preserves_window() {
        let v = AuthorityValidationResult::ExpiredToken {
            not_before: "2026-01-01T00:00:00Z".into(),
            not_after: "2026-02-01T00:00:00Z".into(),
        };
        match v {
            AuthorityValidationResult::ExpiredToken {
                not_before,
                not_after,
            } => {
                assert!(not_before.contains("2026-01-01"));
                assert!(not_after.contains("2026-02-01"));
            }
            _ => panic!("variant mismatch"),
        }
    }

    #[test]
    fn host_match_is_case_insensitive_for_egress_subset() {
        let spec = spec_with(vec![egress("API.example.com", 443, Some("tcp"))], vec![]);
        let token = token_with(AuthorityCapability {
            egress_rules: vec![egress("api.example.com", 443, Some("tcp"))],
            secret_refs: vec![],
        });
        let role_keys = std::collections::HashMap::new();
        // Structural step passes; we land in InvalidSignature (junk sig).
        let result = validate_authority_derivation(&spec, &token, &role_keys);
        assert!(matches!(
            result,
            AuthorityValidationResult::InvalidSignature { .. }
        ));
    }
}
