//! `Dal` — DO-178C Design Assurance Level + per-crate `DalConfig`.
//!
//! `Dal::D` is the `Default` on purpose: a missing `[dal]` section in
//! `boundary.toml` must never silently *lower* cert requirements
//! below what was intended. A crate that inherits D (the least
//! stringent) on a misconfigured run is obvious; a crate that
//! inherits A on a misconfigured run would silently demand evidence
//! nobody knows to produce. The `#[derive(Ord)]` sort order (D < C <
//! B < A) matches this — later variants are *more* stringent, so
//! `max()` over a `dal_map` gives the highest required rigor.

use serde::{Deserialize, Serialize};
use std::collections::BTreeMap;

use crate::diagnostic::{DiagnosticCode, Severity};

/// Design Assurance Level per DO-178C.
/// A is most stringent, D is least. Default is D (safest: missing config
/// never accidentally lowers requirements below what was intended).
#[derive(
    Debug, Clone, Copy, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize,
)]
pub enum Dal {
    /// Lowest-rigor DO-178C level; default when no boundary config
    /// is present so an empty config never downgrades cert requirements.
    #[default]
    D,
    /// DO-178C Level C.
    C,
    /// DO-178C Level B.
    B,
    /// Highest-rigor DO-178C level — most objectives required with
    /// independence.
    A,
}

impl std::fmt::Display for Dal {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Dal::A => write!(f, "A"),
            Dal::B => write!(f, "B"),
            Dal::C => write!(f, "C"),
            Dal::D => write!(f, "D"),
        }
    }
}

/// Error type for parsing a [`Dal`] from a string.
#[derive(Debug, thiserror::Error)]
pub enum ParseDalError {
    /// Input didn't match any of `A` / `B` / `C` / `D` (case-insensitive).
    #[error("unknown DAL '{0}'; expected one of: A, B, C, D")]
    Unknown(String),
}

impl DiagnosticCode for ParseDalError {
    fn code(&self) -> &'static str {
        match self {
            ParseDalError::Unknown(_) => "POLICY_UNKNOWN_DAL",
        }
    }

    fn severity(&self) -> Severity {
        Severity::Error
    }
}

impl std::str::FromStr for Dal {
    type Err = ParseDalError;
    fn from_str(s: &str) -> Result<Self, Self::Err> {
        match s.to_uppercase().as_str() {
            "A" => Ok(Dal::A),
            "B" => Ok(Dal::B),
            "C" => Ok(Dal::C),
            "D" => Ok(Dal::D),
            _ => Err(ParseDalError::Unknown(s.to_string())),
        }
    }
}

/// Minimum structural-coverage percentages per DO-178C Annex A
/// Table A-7 Obj-5 (statement) + Obj-6 (decision/branch). A
/// `None` means the dimension is not required at this DAL.
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
pub struct DalCoverageThresholds {
    /// Minimum statement coverage percentage (Obj-5). Required
    /// at DAL ≥ C; absent at D.
    pub statement_percent: Option<u8>,
    /// Minimum branch coverage percentage (approximation of
    /// Obj-6 decision coverage — LLVM branch coverage, not
    /// MC/DC). Required at DAL ≥ B; absent at C and D.
    pub branch_percent: Option<u8>,
}

impl Dal {
    /// DO-178C-mandated minimum coverage percentages at this DAL.
    ///
    /// These are hardcoded DO-178C defaults. A downstream
    /// project can override via `cert/boundary.toml.[dal.coverage]`
    /// in a future schema extension; today the defaults are the
    /// single source of truth.
    ///
    /// - D: no thresholds (info-only).
    /// - C: statement ≥ 90% (Obj-5).
    /// - B: statement ≥ 95% + branch ≥ 85% (Obj-5 + 6).
    /// - A: statement ≥ 95% + branch ≥ 90% (Obj-5 + 6, plus
    ///   MC/DC handled by an auxiliary tool — see
    ///   `cert/QUALIFICATION.md`).
    pub fn coverage_thresholds(self) -> DalCoverageThresholds {
        match self {
            Dal::D => DalCoverageThresholds {
                statement_percent: None,
                branch_percent: None,
            },
            Dal::C => DalCoverageThresholds {
                statement_percent: Some(90),
                branch_percent: None,
            },
            Dal::B => DalCoverageThresholds {
                statement_percent: Some(95),
                branch_percent: Some(85),
            },
            Dal::A => DalCoverageThresholds {
                statement_percent: Some(95),
                branch_percent: Some(90),
            },
        }
    }
}

/// DAL configuration section in boundary.toml.
#[derive(Debug, Deserialize, Serialize, Clone)]
pub struct DalConfig {
    /// Default DAL for all in-scope crates without explicit override.
    #[serde(default)]
    pub default_dal: Dal,
    /// Per-crate DAL overrides. Key is crate name.
    #[serde(default)]
    pub crate_overrides: BTreeMap<String, Dal>,
    /// Reference to an auxiliary qualified MC/DC tool whose evidence
    /// the project records by reference. Required at DAL-A (DO-178C
    /// Table A-7 Obj-7) because stable Rust cannot currently emit
    /// MC/DC instrumentation — the unstable `-Zcoverage-options=mcdc`
    /// flag was removed by rust-lang/rust#144999 (merged 2025-08-08)
    /// and tracking issue rust-lang/rust#124144 has no active
    /// reimplementation.
    ///
    /// Absent ⇒ this project produces no MC/DC evidence in-band.
    /// Present ⇒ the project asserts MC/DC is satisfied via the
    /// named auxiliary tool (LDRA, VectorCAST, Rapita RVS, etc.).
    /// The tool's qualification ID and report path live in the
    /// nested struct so an auditor can cross-reference both at
    /// review time. Free-form `name` is a reviewer-readable label
    /// (e.g. `"LDRA TBvision"`); `report` is the bundle-relative
    /// path the auxiliary report is recorded under (the bundle
    /// pipeline does not validate the file's content, only its
    /// presence + hash). See HLR-066 / LLR-073.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub auxiliary_mcdc_tool: Option<AuxiliaryMcdcTool>,
}

/// Reference to an external qualified MC/DC tool whose evidence is
/// recorded by reference rather than measured in-band. See
/// [`DalConfig::auxiliary_mcdc_tool`].
#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq)]
pub struct AuxiliaryMcdcTool {
    /// Reviewer-readable label, e.g. `"LDRA TBvision"`.
    pub name: String,
    /// Tool qualification ID assigned by the auxiliary vendor /
    /// project. Free-form so projects can fold in their own
    /// internal tracking ID. Absent ⇒ this is treated as an
    /// undocumented reference and the auditor must resolve it
    /// out-of-band.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub qualification_id: Option<String>,
    /// Bundle-relative path the auxiliary report is recorded
    /// under. Absent today ⇒ the project asserts MC/DC was
    /// measured externally but does not bind a specific report
    /// into the bundle. A future schema extension may make this
    /// required when DAL-A is in scope.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub report: Option<String>,
}

impl Default for DalConfig {
    fn default() -> Self {
        Self {
            default_dal: Dal::D,
            crate_overrides: BTreeMap::new(),
            auxiliary_mcdc_tool: None,
        }
    }
}

#[cfg(test)]
#[allow(
    clippy::unwrap_used,
    clippy::expect_used,
    clippy::panic,
    reason = "test setup failures should panic immediately"
)]
mod tests {
    use super::*;

    #[test]
    fn test_dal_display_parse() {
        for dal in [Dal::A, Dal::B, Dal::C, Dal::D] {
            let s = dal.to_string();
            let parsed: Dal = s.parse().unwrap();
            assert_eq!(parsed, dal);
        }
        assert!("E".parse::<Dal>().is_err());
        assert!("".parse::<Dal>().is_err());
    }

    #[test]
    fn test_dal_ordering() {
        assert!(Dal::A > Dal::B);
        assert!(Dal::B > Dal::C);
        assert!(Dal::C > Dal::D);
    }

    #[test]
    fn test_dal_default_is_d() {
        assert_eq!(Dal::default(), Dal::D);
    }

    #[test]
    fn test_dal_config_default() {
        let config = DalConfig::default();
        assert_eq!(config.default_dal, Dal::D);
        assert!(config.crate_overrides.is_empty());
    }

    /// Pins DO-178C A-7 Obj-5/6 thresholds. Changing a number
    /// here is a cert-contract change; bump the `COMPLIANCE`
    /// schema version in the same PR.
    #[test]
    fn coverage_thresholds_by_dal() {
        assert_eq!(
            Dal::D.coverage_thresholds(),
            DalCoverageThresholds {
                statement_percent: None,
                branch_percent: None,
            }
        );
        assert_eq!(
            Dal::C.coverage_thresholds(),
            DalCoverageThresholds {
                statement_percent: Some(90),
                branch_percent: None,
            }
        );
        assert_eq!(
            Dal::B.coverage_thresholds(),
            DalCoverageThresholds {
                statement_percent: Some(95),
                branch_percent: Some(85),
            }
        );
        assert_eq!(
            Dal::A.coverage_thresholds(),
            DalCoverageThresholds {
                statement_percent: Some(95),
                branch_percent: Some(90),
            }
        );
    }
}
