//! Runtime and gateway posture checks (WOR-1432).
//!
//! ToolHive and similar projects secure the MCP runtime. mcptest does not run a
//! gateway, but it can score whether a deployment has the runtime posture
//! production needs. This module normalizes a deployment descriptor (its own
//! schema, a ToolHive run profile, or a Kubernetes pod spec) and scores nine
//! posture dimensions, tagging each result observed, claimed, or unknown.
//!
//! The report folds into an evidence pack alongside coverage and security
//! sections. Managed policy, RBAC, and multi-deployment dashboards stay in
//! `mcptest-enterprise`; this is local validation, not a control plane.

mod assess;
mod manifest;
mod model;

pub use assess::assess;
pub use manifest::{DeploymentKind, DeploymentManifest, ManifestError};
pub use model::{Dimension, Evidence, PostureCheck, PostureReport, Status, POSTURE_SCHEMA_VERSION};

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

    #[test]
    fn bare_stdio_command_fails_isolation_egress_and_filesystem() {
        let manifest = DeploymentManifest::for_stdio_command("npx some-server");
        let report = assess(&manifest, "npx some-server");
        let by_dim = |d: Dimension| report.checks.iter().find(|c| c.dimension == d).unwrap();
        assert_eq!(by_dim(Dimension::Isolation).status, Status::Fail);
        assert_eq!(by_dim(Dimension::Isolation).evidence, Evidence::Observed);
        assert_eq!(by_dim(Dimension::NetworkEgress).status, Status::Fail);
        assert_eq!(by_dim(Dimension::FilesystemScope).status, Status::Fail);
        assert!(report.failed >= 3);
    }

    #[test]
    fn hardened_toolhive_profile_passes_isolation_and_egress() {
        let json = r#"{
            "transport": "stdio",
            "image": "ghcr.io/acme/server@sha256:abc",
            "oauth_config": {"issuer": "https://idp.example"},
            "permission_profile": {
                "read": ["/data"],
                "network": {"outbound": {"allow_host": ["api.example.com"]}}
            },
            "secrets": "vault",
            "telemetry": {"otel": true},
            "audit": true
        }"#;
        let manifest = DeploymentManifest::parse(json).unwrap();
        let report = assess(&manifest, "toolhive");
        let by_dim = |d: Dimension| report.checks.iter().find(|c| c.dimension == d).unwrap();
        assert_eq!(by_dim(Dimension::Isolation).status, Status::Pass);
        assert_eq!(by_dim(Dimension::NetworkEgress).status, Status::Pass);
        assert_eq!(by_dim(Dimension::FilesystemScope).status, Status::Pass);
        assert_eq!(by_dim(Dimension::AuthIdentity).status, Status::Pass);
        assert_eq!(by_dim(Dimension::RegistryProvenance).status, Status::Pass);
        assert_eq!(by_dim(Dimension::SecretHandling).status, Status::Pass);
        assert_eq!(by_dim(Dimension::AuditLogging).status, Status::Pass);
        assert!(report.failed == 0);
        assert!(report.score_pct() > 80.0);
    }

    #[test]
    fn kubernetes_security_context_maps_to_isolation() {
        let yaml = r#"
kind: Pod
spec:
  securityContext:
    runAsNonRoot: true
  containers:
    - image: ghcr.io/acme/server@sha256:deadbeef
      securityContext:
        readOnlyRootFilesystem: true
"#;
        let manifest = DeploymentManifest::parse(yaml).unwrap();
        assert_eq!(manifest.kind, DeploymentKind::Kubernetes);
        let report = assess(&manifest, "pod");
        let iso = report
            .checks
            .iter()
            .find(|c| c.dimension == Dimension::Isolation)
            .unwrap();
        assert_eq!(iso.status, Status::Pass);
        let prov = report
            .checks
            .iter()
            .find(|c| c.dimension == Dimension::RegistryProvenance)
            .unwrap();
        assert_eq!(
            prov.status,
            Status::Pass,
            "digest-pinned image passes provenance"
        );
    }

    #[test]
    fn unknown_dimensions_do_not_count_as_failures() {
        // A near-empty descriptor: only the kind is known.
        let manifest = DeploymentManifest {
            kind: DeploymentKind::Container,
            containerized: Some(true),
            ..Default::default()
        };
        let report = assess(&manifest, "minimal");
        assert!(report.unknown >= 4);
        assert_eq!(report.failed, 0);
    }

    #[test]
    fn report_serializes_with_schema_version() {
        let manifest = DeploymentManifest::for_stdio_command("x");
        let report = assess(&manifest, "x");
        let json = serde_json::to_string(&report).unwrap();
        assert!(json.contains("mcptest.sh/posture/v1"));
        assert!(json.contains("\"evidence\":\"observed\""));
    }
}
