use serde::{Deserialize, Serialize};

use crate::deny_reason::DenyReason;

#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum AuthorityDecision {
    Allow,
    Deny,
    Diagnose,
}

#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum AuthorityDenyCode {
    MissingContract,
    LockedVault,
    MissingAgent,
    MissingRequiredSecret,
    BadWorkdir,
    TargetDenied,
    PathEscape,
    BlankScope,
    ProfileOverride,
    ContractOverride,
    RequestTimeWidening,
    NetworkUnenforced,
    AuditUnavailable,
    OutputCap,
    Timeout,
    HostSchemaUnstable,
    ConfigStale,
    ProofUnavailable,
    ParseError,
    InternalError,
}

impl From<DenyReason> for AuthorityDenyCode {
    fn from(reason: DenyReason) -> Self {
        match reason {
            DenyReason::TargetNotAllowed | DenyReason::TargetMissing => Self::TargetDenied,
            DenyReason::RequiredSecretNotFound | DenyReason::AllowedSecretNotFound => {
                Self::MissingRequiredSecret
            }
            DenyReason::DangerousEnvVariable => Self::RequestTimeWidening,
            DenyReason::VaultProfileNotFound
            | DenyReason::NamespaceMissing
            | DenyReason::AccessProfileViolation => Self::MissingRequiredSecret,
            DenyReason::NetworkPolicyViolation | DenyReason::NetworkUnenforced => {
                Self::NetworkUnenforced
            }
            DenyReason::InsufficientAuthority => Self::BlankScope,
            DenyReason::MissingContract => Self::MissingContract,
            DenyReason::LockedVault => Self::LockedVault,
            DenyReason::MissingAgent => Self::MissingAgent,
            DenyReason::BadWorkdir => Self::BadWorkdir,
            DenyReason::PathEscape => Self::PathEscape,
            DenyReason::BlankScope => Self::BlankScope,
            DenyReason::ProfileOverride => Self::ProfileOverride,
            DenyReason::ContractOverride => Self::ContractOverride,
            DenyReason::RequestTimeWidening => Self::RequestTimeWidening,
            DenyReason::AuditUnavailable => Self::AuditUnavailable,
            DenyReason::OutputCap => Self::OutputCap,
            DenyReason::Timeout => Self::Timeout,
            DenyReason::HostSchemaUnstable => Self::HostSchemaUnstable,
            DenyReason::ConfigStale => Self::ConfigStale,
            DenyReason::ProofUnavailable => Self::ProofUnavailable,
            DenyReason::ParseError => Self::ParseError,
            DenyReason::InternalError => Self::InternalError,
        }
    }
}

#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum AuthorityMode {
    BoundMcp,
}

#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
pub struct AuthorityMetadata {
    pub profile: String,
    pub contract: String,
    pub workdir: String,
    pub mode: AuthorityMode,
}

#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
pub struct AuthorityRefusal {
    pub summary: String,
    pub detail: String,
    pub next_actions: Vec<String>,
    pub code: AuthorityDenyCode,
    pub authority: AuthorityMetadata,
    pub receipt_id: String,
}

impl AuthorityRefusal {
    pub fn new(
        summary: impl Into<String>,
        detail: impl Into<String>,
        next_actions: Vec<String>,
        code: AuthorityDenyCode,
        authority: AuthorityMetadata,
        receipt_id: impl Into<String>,
    ) -> Self {
        Self {
            summary: summary.into(),
            detail: detail.into(),
            next_actions,
            code,
            authority,
            receipt_id: receipt_id.into(),
        }
    }
}

#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
pub struct AuthorityCommandIdentity {
    pub display: String,
    pub target: String,
    pub argv_hash: String,
}

#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
pub struct AuthorityReceipt {
    pub receipt_id: String,
    pub run_id: String,
    pub audit_join_key: String,
    pub decision: AuthorityDecision,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub code: Option<AuthorityDenyCode>,
    pub command: AuthorityCommandIdentity,
    pub authority: AuthorityMetadata,
    pub started_at: String,
    pub finished_at: String,
    pub truncated_stdout: bool,
    pub truncated_stderr: bool,
    pub redaction_policy: String,
}

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

    #[test]
    fn serializes_authority_decision_code_and_mode_as_snake_case() {
        assert_eq!(
            serde_json::to_string(&AuthorityDecision::Allow).unwrap(),
            "\"allow\""
        );
        assert_eq!(
            serde_json::to_string(&AuthorityDecision::Deny).unwrap(),
            "\"deny\""
        );
        assert_eq!(
            serde_json::to_string(&AuthorityDecision::Diagnose).unwrap(),
            "\"diagnose\""
        );
        assert_eq!(
            serde_json::to_string(&AuthorityDenyCode::MissingContract).unwrap(),
            "\"missing_contract\""
        );
        assert_eq!(
            serde_json::to_string(&AuthorityDenyCode::LockedVault).unwrap(),
            "\"locked_vault\""
        );
        assert_eq!(
            serde_json::to_string(&AuthorityDenyCode::MissingAgent).unwrap(),
            "\"missing_agent\""
        );
        assert_eq!(
            serde_json::to_string(&AuthorityDenyCode::MissingRequiredSecret).unwrap(),
            "\"missing_required_secret\""
        );
        assert_eq!(
            serde_json::to_string(&AuthorityDenyCode::BadWorkdir).unwrap(),
            "\"bad_workdir\""
        );
        assert_eq!(
            serde_json::to_string(&AuthorityDenyCode::TargetDenied).unwrap(),
            "\"target_denied\""
        );
        assert_eq!(
            serde_json::to_string(&AuthorityDenyCode::PathEscape).unwrap(),
            "\"path_escape\""
        );
        assert_eq!(
            serde_json::to_string(&AuthorityDenyCode::BlankScope).unwrap(),
            "\"blank_scope\""
        );
        assert_eq!(
            serde_json::to_string(&AuthorityDenyCode::ProfileOverride).unwrap(),
            "\"profile_override\""
        );
        assert_eq!(
            serde_json::to_string(&AuthorityDenyCode::ContractOverride).unwrap(),
            "\"contract_override\""
        );
        assert_eq!(
            serde_json::to_string(&AuthorityDenyCode::RequestTimeWidening).unwrap(),
            "\"request_time_widening\""
        );
        assert_eq!(
            serde_json::to_string(&AuthorityDenyCode::NetworkUnenforced).unwrap(),
            "\"network_unenforced\""
        );
        assert_eq!(
            serde_json::to_string(&AuthorityDenyCode::AuditUnavailable).unwrap(),
            "\"audit_unavailable\""
        );
        assert_eq!(
            serde_json::to_string(&AuthorityDenyCode::OutputCap).unwrap(),
            "\"output_cap\""
        );
        assert_eq!(
            serde_json::to_string(&AuthorityDenyCode::Timeout).unwrap(),
            "\"timeout\""
        );
        assert_eq!(
            serde_json::to_string(&AuthorityDenyCode::HostSchemaUnstable).unwrap(),
            "\"host_schema_unstable\""
        );
        assert_eq!(
            serde_json::to_string(&AuthorityDenyCode::ConfigStale).unwrap(),
            "\"config_stale\""
        );
        assert_eq!(
            serde_json::to_string(&AuthorityDenyCode::ProofUnavailable).unwrap(),
            "\"proof_unavailable\""
        );
        assert_eq!(
            serde_json::to_string(&AuthorityDenyCode::ParseError).unwrap(),
            "\"parse_error\""
        );
        assert_eq!(
            serde_json::to_string(&AuthorityDenyCode::InternalError).unwrap(),
            "\"internal_error\""
        );
        assert_eq!(
            serde_json::to_string(&AuthorityMode::BoundMcp).unwrap(),
            "\"bound_mcp\""
        );
    }

    #[test]
    fn refusal_and_receipt_roundtrip_with_snake_case_fields() {
        let authority = AuthorityMetadata {
            profile: "profile-name".to_string(),
            contract: "contract-name".to_string(),
            workdir: "C:\\Users\\0ryant\\prj\\example".to_string(),
            mode: AuthorityMode::BoundMcp,
        };
        let refusal = AuthorityRefusal::new(
            "Short model-safe refusal sentence.",
            "Operator-actionable explanation without secret values.",
            vec!["Concrete remediation step.".to_string()],
            AuthorityDenyCode::TargetDenied,
            authority.clone(),
            "rcpt_123",
        );

        let refusal_json = serde_json::to_string(&refusal).unwrap();
        assert!(refusal_json.contains("\"next_actions\""));
        assert!(refusal_json.contains("\"receipt_id\""));
        assert!(refusal_json.contains("\"target_denied\""));
        let decoded_refusal: AuthorityRefusal = serde_json::from_str(&refusal_json).unwrap();
        assert_eq!(decoded_refusal, refusal);

        let receipt = AuthorityReceipt {
            receipt_id: "rcpt_123".to_string(),
            run_id: "run_123".to_string(),
            audit_join_key: "audit_123".to_string(),
            decision: AuthorityDecision::Deny,
            code: Some(AuthorityDenyCode::TargetDenied),
            command: AuthorityCommandIdentity {
                display: "cordance check".to_string(),
                target: "C:\\Tools\\cordance\\cordance.exe".to_string(),
                argv_hash:
                    "sha256:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
                        .to_string(),
            },
            authority,
            started_at: "2026-05-18T00:00:00Z".to_string(),
            finished_at: "2026-05-18T00:00:01Z".to_string(),
            truncated_stdout: false,
            truncated_stderr: false,
            redaction_policy: "mcp_boundary_redaction_v1".to_string(),
        };

        let receipt_json = serde_json::to_string(&receipt).unwrap();
        assert!(receipt_json.contains("\"audit_join_key\""));
        assert!(receipt_json.contains("\"argv_hash\""));
        assert!(receipt_json.contains("\"truncated_stdout\""));
        assert!(receipt_json.contains("\"truncated_stderr\""));
        assert!(receipt_json.contains("\"redaction_policy\""));
        let decoded_receipt: AuthorityReceipt = serde_json::from_str(&receipt_json).unwrap();
        assert_eq!(decoded_receipt, receipt);
    }

    #[test]
    fn receipt_omits_absent_code_to_match_schema() {
        let receipt = AuthorityReceipt {
            receipt_id: "rcpt_123".to_string(),
            run_id: "run_123".to_string(),
            audit_join_key: "audit_123".to_string(),
            decision: AuthorityDecision::Allow,
            code: None,
            command: AuthorityCommandIdentity {
                display: "cordance check".to_string(),
                target: "C:\\Tools\\cordance\\cordance.exe".to_string(),
                argv_hash:
                    "sha256:0000000000000000000000000000000000000000000000000000000000000000"
                        .to_string(),
            },
            authority: AuthorityMetadata {
                profile: "profile-name".to_string(),
                contract: "contract-name".to_string(),
                workdir: "C:\\Users\\0ryant\\prj\\example".to_string(),
                mode: AuthorityMode::BoundMcp,
            },
            started_at: "2026-05-18T00:00:00Z".to_string(),
            finished_at: "2026-05-18T00:00:01Z".to_string(),
            truncated_stdout: false,
            truncated_stderr: false,
            redaction_policy: "mcp_boundary_redaction_v1".to_string(),
        };

        let receipt_json = serde_json::to_string(&receipt).unwrap();
        assert!(!receipt_json.contains("\"code\""));
        let decoded_receipt: AuthorityReceipt = serde_json::from_str(&receipt_json).unwrap();
        assert_eq!(decoded_receipt, receipt);
    }

    #[test]
    fn maps_legacy_secret_denials_to_missing_required_secret() {
        assert_eq!(
            AuthorityDenyCode::from(DenyReason::RequiredSecretNotFound),
            AuthorityDenyCode::MissingRequiredSecret
        );
        assert_eq!(
            AuthorityDenyCode::from(DenyReason::AllowedSecretNotFound),
            AuthorityDenyCode::MissingRequiredSecret
        );
    }

    #[test]
    fn maps_legacy_target_denials_to_target_denied() {
        assert_eq!(
            AuthorityDenyCode::from(DenyReason::TargetNotAllowed),
            AuthorityDenyCode::TargetDenied
        );
        assert_eq!(
            AuthorityDenyCode::from(DenyReason::TargetMissing),
            AuthorityDenyCode::TargetDenied
        );
    }

    #[test]
    fn maps_legacy_network_denials_to_network_unenforced() {
        assert_eq!(
            AuthorityDenyCode::from(DenyReason::NetworkPolicyViolation),
            AuthorityDenyCode::NetworkUnenforced
        );
        assert_eq!(
            AuthorityDenyCode::from(DenyReason::NetworkUnenforced),
            AuthorityDenyCode::NetworkUnenforced
        );
    }
}
