//! Runtime and gateway posture model (WOR-1432).
//!
//! A posture report scores whether an MCP deployment has the runtime controls
//! production needs: isolation, filesystem scope, network egress, secret
//! handling, identity, registry provenance, policy enforcement, telemetry, and
//! audit logging. mcptest does not operate a gateway; it validates one.
//!
//! Every check carries an [`Evidence`] tag so a reader can tell an observed
//! fact (mcptest confirmed it) from a configured claim (the manifest asserts it
//! but mcptest did not exercise it) from an unknown. That distinction is the
//! point: a posture report is honest about what it actually verified.

use serde::{Deserialize, Serialize};

/// Schema version stamped on every posture report.
pub const POSTURE_SCHEMA_VERSION: &str = "mcptest.sh/posture/v1";

/// The nine posture dimensions a report scores.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum Dimension {
    /// Process or container isolation.
    Isolation,
    /// Filesystem scope: how far the server can read and write.
    FilesystemScope,
    /// Network egress controls.
    NetworkEgress,
    /// Secret handling (env, file, vault, none).
    SecretHandling,
    /// Authentication and identity enforcement.
    AuthIdentity,
    /// Registry provenance: where the artifact came from and whether it is pinned.
    RegistryProvenance,
    /// Policy enforcement (an external engine gating tool calls).
    PolicyEnforcement,
    /// OpenTelemetry / Prometheus readiness.
    Telemetry,
    /// Audit logging.
    AuditLogging,
}

impl Dimension {
    /// A stable, human-readable label.
    pub fn label(self) -> &'static str {
        match self {
            Dimension::Isolation => "isolation",
            Dimension::FilesystemScope => "filesystem-scope",
            Dimension::NetworkEgress => "network-egress",
            Dimension::SecretHandling => "secret-handling",
            Dimension::AuthIdentity => "auth-identity",
            Dimension::RegistryProvenance => "registry-provenance",
            Dimension::PolicyEnforcement => "policy-enforcement",
            Dimension::Telemetry => "telemetry",
            Dimension::AuditLogging => "audit-logging",
        }
    }
}

/// How strongly a check is grounded.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum Evidence {
    /// mcptest confirmed this from the deployment shape (a fact).
    Observed,
    /// The manifest configures this, but mcptest did not exercise it (a claim).
    Claimed,
    /// Neither observed nor claimed.
    Unknown,
}

/// The scored outcome of one check.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum Status {
    /// The control is present and adequate.
    Pass,
    /// The control is partial or weakly grounded.
    Warn,
    /// The control is absent or unsafe.
    Fail,
    /// Not enough information to score.
    Unknown,
}

impl Status {
    /// A short glyph for the pretty table.
    pub fn glyph(self) -> &'static str {
        match self {
            Status::Pass => "PASS",
            Status::Warn => "WARN",
            Status::Fail => "FAIL",
            Status::Unknown => "????",
        }
    }
}

/// One scored posture dimension.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct PostureCheck {
    /// The dimension scored.
    pub dimension: Dimension,
    /// The outcome.
    pub status: Status,
    /// How the outcome was grounded.
    pub evidence: Evidence,
    /// A one-line explanation.
    pub detail: String,
    /// A remediation pointer, when the check did not pass.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub remediation: Option<String>,
}

impl PostureCheck {
    /// Build a check with no remediation.
    pub fn new(dimension: Dimension, status: Status, evidence: Evidence, detail: &str) -> Self {
        PostureCheck {
            dimension,
            status,
            evidence,
            detail: detail.to_string(),
            remediation: None,
        }
    }

    /// Attach a remediation pointer (chainable).
    pub fn with_remediation(mut self, remediation: &str) -> Self {
        self.remediation = Some(remediation.to_string());
        self
    }
}

/// A full posture report over one deployment.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct PostureReport {
    /// Schema version (see [`POSTURE_SCHEMA_VERSION`]).
    pub schema_version: String,
    /// The deployment kind that was assessed.
    pub deployment_kind: String,
    /// A label for the assessed target (a manifest path or a command).
    pub target: String,
    /// The scored checks, in dimension order.
    pub checks: Vec<PostureCheck>,
    /// Count of passing checks.
    pub passed: usize,
    /// Count of failing checks.
    pub failed: usize,
    /// Count of warnings.
    pub warned: usize,
    /// Count of unknowns.
    pub unknown: usize,
}

impl PostureReport {
    /// Tally a set of checks into a report.
    pub fn from_checks(deployment_kind: &str, target: &str, checks: Vec<PostureCheck>) -> Self {
        let mut passed = 0;
        let mut failed = 0;
        let mut warned = 0;
        let mut unknown = 0;
        for check in &checks {
            match check.status {
                Status::Pass => passed += 1,
                Status::Fail => failed += 1,
                Status::Warn => warned += 1,
                Status::Unknown => unknown += 1,
            }
        }
        PostureReport {
            schema_version: POSTURE_SCHEMA_VERSION.to_string(),
            deployment_kind: deployment_kind.to_string(),
            target: target.to_string(),
            checks,
            passed,
            failed,
            warned,
            unknown,
        }
    }

    /// The share of scored (non-unknown) checks that pass, 0 to 100.
    pub fn score_pct(&self) -> f64 {
        let scored = self.passed + self.failed + self.warned;
        if scored == 0 {
            return 0.0;
        }
        (self.passed as f64 / scored as f64) * 100.0
    }
}
