//! `VerifyError` + `VerifyResult` — the structured result types
//! returned by every verification pass.

use std::path::PathBuf;

use serde::Serialize;

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

/// Structured verification error codes for programmatic handling.
#[derive(Debug, Clone, Serialize)]
#[serde(tag = "code", content = "detail")]
pub enum VerifyError {
    /// A file exists in the bundle but is not listed in SHA256SUMS
    UnexpectedFile(String),
    /// HMAC signature verification failed
    HmacFailure,
    /// A hash in SHA256SUMS does not match the actual file hash
    HashMismatch {
        /// Bundle-relative path whose hash disagreed.
        file: String,
        /// Hash recorded in `SHA256SUMS`.
        expected: String,
        /// Hash computed at verify time.
        actual: String,
    },
    /// A file listed in SHA256SUMS is missing from the bundle
    MissingHashedFile(String),
    /// content_hash in index.json doesn't match SHA256SUMS hash
    ContentHashMismatch {
        /// Value recorded in `index.json.content_hash`.
        index_hash: String,
        /// `SHA-256(SHA256SUMS)` computed at verify time.
        actual_hash: String,
    },
    /// A path in SHA256SUMS or trace_outputs contains path traversal
    /// (`..`, absolute path, or Windows drive prefix)
    UnsafePath(String),
    /// A field in index.json has an invalid format
    FormatError {
        /// Field name that failed the format check.
        field: String,
        /// Description of the expected shape.
        expected: String,
        /// Actual value found in the bundle.
        actual: String,
    },
    /// A field in env.json is inconsistent with the same field in index.json
    CrossFileInconsistency {
        /// Field name that disagreed between `env.json` and `index.json`.
        field: String,
        /// Value read from `index.json`.
        index_value: String,
        /// Value read from `env.json`.
        env_value: String,
    },
    /// `deterministic_hash` in `index.json` does not match the actual
    /// SHA-256 of `deterministic-manifest.json`.
    DeterministicHashMismatch {
        /// Value recorded in `index.json.deterministic_hash`.
        index_hash: String,
        /// `SHA-256(deterministic-manifest.json)` computed at verify time.
        actual_hash: String,
    },
    /// Re-projecting `env.json`'s `DeterministicManifest` subset
    /// does not byte-equal the committed `deterministic-manifest.json`.
    /// Indicates tampering or a CLI bug that let the two drift apart
    /// at generation time.
    ManifestProjectionDrift {
        /// Short description — byte lengths, parse failure, or
        /// serialization failure.
        detail: String,
    },
    /// A trace output path in `index.json.trace_outputs` is not
    /// listed in `SHA256SUMS`. Every generated trace matrix must be
    /// in the content layer; an index-only reference overclaims
    /// coverage because the referenced file would not be integrity-
    /// checked.
    TraceOutputNotHashed(String),
    /// `index.json.test_summary` disagrees with a re-parse of the
    /// captured `tests/cargo_test_stdout.txt` — either the summary
    /// was tampered or the generator's parser drifted from the
    /// verifier's.
    TestSummaryMismatch {
        /// Which counter disagreed (`total` / `passed` / `failed` /
        /// `ignored` / `filtered_out` / `parse`).
        field: &'static str,
        /// Value recorded in `index.json.test_summary`.
        index_value: String,
        /// Value obtained by re-parsing captured stdout.
        parsed_value: String,
    },
    /// `index.json.dal_map[crate]` disagrees with
    /// `compliance/<crate>.json.dal` for the same crate.
    DalMapMismatch {
        /// Crate whose DAL differs across the two files.
        crate_name: String,
        /// DAL level recorded in `index.json.dal_map`.
        index_value: String,
        /// DAL level recorded in the compliance report.
        compliance_value: String,
    },
    /// `compliance/<crate>.json` is present in the bundle but its
    /// crate name is not referenced in `index.json.dal_map`, or
    /// vice versa.
    DalMapOrphan {
        /// Crate whose references don't match.
        crate_name: String,
        /// Which direction of the mismatch fired.
        detail: String,
    },
    /// Bundle's `env.json` reports `tool_prerelease = true` — the
    /// engine that produced this bundle was a pre-release build
    /// (semver-`-` suffix on `CARGO_PKG_VERSION`). The library
    /// pushes this error on every profile; the CLI downgrades to
    /// Warning severity on `Profile::Dev` so dev iteration is
    /// unblocked, keeps Error on Cert/Record (→ `VERIFY_FAIL`
    /// terminal, exit 2). See SYS-017 / HLR-049.
    PrereleaseToolDetected {
        /// Profile read from `index.json` — drives the CLI's
        /// Dev-vs-Cert/Record severity partition.
        profile: String,
        /// Full version string from `index.json.engine_crate_version`
        /// for the reviewer-visible context.
        engine_crate_version: String,
    },
    /// `index.json.bundle_complete == true` but
    /// `tool_command_failures` is non-empty. Tamper signal or
    /// builder bug — these two fields are wired strict-
    /// consistent at generate time (bundle_complete is DERIVED
    /// from tool_command_failures.is_empty()), so a bundle with
    /// the two disagreeing was hand-edited or produced by a
    /// broken generator.
    BundleIncompletelyClaimed {
        /// How many recorded failures the tampered bundle
        /// carried — surfaces the count so an auditor can
        /// cross-reference `index.json.tool_command_failures`.
        failure_count: usize,
    },
    /// Bundle is `cert` / `record` profile but carries recorded
    /// captured-subprocess failures. Cert/record bundles must
    /// not ship with any `tool_command_failures` entries: by
    /// definition they are provably-incomplete evidence and
    /// cannot be cert-grade.
    ToolCommandsFailedSilently {
        /// Profile read from `index.json` — always `"cert"` or
        /// `"record"` when this error fires (dev profile with
        /// failures is a Warning at the CLI layer, not a
        /// VerifyError).
        profile: String,
        /// The failing commands, in the order
        /// `tool_command_failures` recorded them. Agent-visible
        /// so a tooling consumer can show which subprocesses
        /// broke without re-reading the index.
        commands: Vec<String>,
    },
    /// `tool_command_failures` contains a cargo-test entry but
    /// `index.json.test_summary` is absent. Either the failure
    /// happened before any parseable stdout (compile-fail → no
    /// `test result:` line) but the builder should still have
    /// recorded an empty summary, OR the test_summary field was
    /// hand-edited out. Sanity check that the two fields stay
    /// wired together.
    TestSummaryAbsentOnFailedRun {
        /// Command name from the `tool_command_failures` entry
        /// that triggered this invariant check.
        command_name: String,
    },
    /// An LLR in `bundle/trace/llr.toml` declares
    /// `verification_methods` including `"test"` + has a non-
    /// `None` `uid`, but that uid is missing from every
    /// `tests/test_outcomes.jsonl` record's `requirement_uids`.
    /// Either: (a) the LLR's `test_selectors` (via a TestEntry
    /// pointing to this LLR) didn't resolve to any real
    /// `#[test] fn` — the selector is dangling; (b) the test
    /// existed but didn't run (filtered, in a disabled crate);
    /// (c) no TestEntry traces to this LLR at all. All three
    /// are reverse-direction traceability holes.
    LlrTestSelectorUnresolved {
        /// UUID of the unresolved LLR.
        llr_uid: String,
        /// Human-readable LLR id for the reviewer.
        llr_id: String,
    },
    /// `index.json.boundary_policy` claims `forbid_build_rs` or
    /// `forbid_proc_macros` was enforced at generate time, but the
    /// bundle is missing `cargo_metadata.json` — the verify-time
    /// recheck cannot replay the policy. Either the bundle predates
    /// LLR-072 (older tool, before this artifact existed) or the
    /// file was deleted between generate and verify.
    BoundaryVerifyMetadataMissing,
    /// Verify-time recheck against `cargo_metadata.json` found one
    /// or more in-scope crates with `kind = ["custom-build"]`.
    /// Mirrors the generate-time `BOUNDARY_FORBIDDEN_BUILD_RS`
    /// emit so the diagnostic shape is identical at both ends of
    /// the pipeline.
    BoundaryVerifyForbiddenBuildRs {
        /// Human-readable list of `crate (links = "...")` entries.
        details: String,
    },
    /// Verify-time recheck found one or more in-scope crates with
    /// `kind = ["proc-macro"]`.
    BoundaryVerifyForbiddenProcMacro {
        /// Comma-separated list of offender crate names.
        details: String,
    },
}

impl DiagnosticCode for VerifyError {
    // `#[rustfmt::skip]` keeps every arm on one line — the
    // `diagnostic_codes_locked` walker matches `=> "CODE"`
    // directly and doesn't follow `=> { "CODE" }` block forms.
    // Long variant names wrap into block form by default, so
    // the attribute pins single-line form for every arm.
    #[rustfmt::skip]
    fn code(&self) -> &'static str {
        // Exhaustive match: adding a new `VerifyError` variant without
        // a stable code here fails compilation — Schema Rule 3.
        match self {
            VerifyError::UnexpectedFile(_)                  => "VERIFY_UNEXPECTED_FILE",
            VerifyError::HmacFailure                        => "VERIFY_HMAC_FAILURE",
            VerifyError::HashMismatch { .. }                => "VERIFY_HASH_MISMATCH",
            VerifyError::MissingHashedFile(_)               => "VERIFY_MISSING_HASHED_FILE",
            VerifyError::ContentHashMismatch { .. }         => "VERIFY_CONTENT_HASH_MISMATCH",
            VerifyError::UnsafePath(_)                      => "VERIFY_UNSAFE_PATH",
            VerifyError::FormatError { .. }                 => "VERIFY_INVALID_FORMAT",
            VerifyError::CrossFileInconsistency { .. }      => "VERIFY_CROSS_FILE_INCONSISTENCY",
            VerifyError::DeterministicHashMismatch { .. }   => "VERIFY_DETERMINISTIC_HASH_MISMATCH",
            VerifyError::ManifestProjectionDrift { .. }     => "VERIFY_MANIFEST_PROJECTION_DRIFT",
            VerifyError::TraceOutputNotHashed(_)            => "VERIFY_TRACE_OUTPUT_NOT_HASHED",
            VerifyError::TestSummaryMismatch { .. }         => "VERIFY_TEST_SUMMARY_MISMATCH",
            VerifyError::DalMapMismatch { .. }              => "VERIFY_DAL_MAP_MISMATCH",
            VerifyError::DalMapOrphan { .. }                => "VERIFY_DAL_MAP_ORPHAN",
            VerifyError::PrereleaseToolDetected { .. }      => "VERIFY_PRERELEASE_TOOL",
            VerifyError::BundleIncompletelyClaimed { .. }   => "VERIFY_BUNDLE_INCOMPLETELY_CLAIMED",
            VerifyError::ToolCommandsFailedSilently { .. }  => "VERIFY_TOOL_COMMANDS_FAILED_SILENTLY",
            VerifyError::TestSummaryAbsentOnFailedRun { .. }=> "VERIFY_TEST_SUMMARY_ABSENT_ON_FAILED_RUN",
            VerifyError::LlrTestSelectorUnresolved { .. }   => "VERIFY_LLR_TEST_SELECTOR_UNRESOLVED",
            VerifyError::BoundaryVerifyMetadataMissing      => "BOUNDARY_VERIFY_METADATA_MISSING",
            VerifyError::BoundaryVerifyForbiddenBuildRs { .. }   => "VERIFY_BOUNDARY_BUILD_RS_DETECTED",
            VerifyError::BoundaryVerifyForbiddenProcMacro { .. } => "VERIFY_BOUNDARY_PROC_MACRO_DETECTED",
        }
    }

    fn severity(&self) -> Severity {
        // Every VerifyError is a finding, not a progress event.
        Severity::Error
    }

    fn location(&self) -> Option<Location> {
        // Populate Location.file from bundle-relative paths the error
        // already carries. Agents can match on toml_path when the
        // underlying emitter has enough structure, but VerifyError
        // lives one layer above TOML — bundle-file paths are the
        // strongest locators available. `line`/`col` stay None
        // because verify operates on binary/JSON content, not
        // human-edited source.
        let file_path = match self {
            VerifyError::UnexpectedFile(p)
            | VerifyError::MissingHashedFile(p)
            | VerifyError::UnsafePath(p)
            | VerifyError::TraceOutputNotHashed(p) => Some(PathBuf::from(p)),
            VerifyError::HashMismatch { file, .. } => Some(PathBuf::from(file)),
            VerifyError::DalMapMismatch { crate_name, .. }
            | VerifyError::DalMapOrphan { crate_name, .. } => {
                // The DAL-map mismatches point at a compliance file
                // keyed by crate name. Surface that path so an agent
                // can jump straight to the offending report.
                Some(PathBuf::from(format!("compliance/{}.json", crate_name)))
            }
            VerifyError::BoundaryVerifyMetadataMissing => {
                Some(PathBuf::from("cargo_metadata.json"))
            }
            // The remaining variants are bundle-wide invariants; no
            // single file "owns" the failure.
            VerifyError::HmacFailure
            | VerifyError::ContentHashMismatch { .. }
            | VerifyError::FormatError { .. }
            | VerifyError::CrossFileInconsistency { .. }
            | VerifyError::DeterministicHashMismatch { .. }
            | VerifyError::ManifestProjectionDrift { .. }
            | VerifyError::TestSummaryMismatch { .. }
            | VerifyError::PrereleaseToolDetected { .. }
            | VerifyError::BundleIncompletelyClaimed { .. }
            | VerifyError::ToolCommandsFailedSilently { .. }
            | VerifyError::TestSummaryAbsentOnFailedRun { .. }
            | VerifyError::LlrTestSelectorUnresolved { .. }
            | VerifyError::BoundaryVerifyForbiddenBuildRs { .. }
            | VerifyError::BoundaryVerifyForbiddenProcMacro { .. } => None,
        };

        file_path.map(|file| Location {
            file: Some(file),
            ..Location::default()
        })
    }
}

/// Result of a verification operation.
#[derive(Debug, Clone)]
pub enum VerifyResult {
    /// Verification passed
    Pass,
    /// Verification failed with structured error(s)
    Fail(Vec<VerifyError>),
    /// Verification skipped with a reason
    Skipped(String),
}

impl VerifyResult {
    /// Check if verification passed.
    pub fn is_pass(&self) -> bool {
        matches!(self, VerifyResult::Pass)
    }

    /// Check if verification failed.
    pub fn is_fail(&self) -> bool {
        matches!(self, VerifyResult::Fail(_))
    }

    /// Human-readable summary of any failure reasons.
    pub fn summary(&self) -> String {
        match self {
            VerifyResult::Pass => "PASS".to_string(),
            VerifyResult::Fail(errors) => errors
                .iter()
                .map(|e| e.to_string())
                .collect::<Vec<_>>()
                .join("; "),
            VerifyResult::Skipped(reason) => format!("SKIPPED: {}", reason),
        }
    }
}

#[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_verify_result_methods() {
        assert!(VerifyResult::Pass.is_pass());
        assert!(!VerifyResult::Pass.is_fail());

        assert!(VerifyResult::Fail(vec![VerifyError::HmacFailure]).is_fail());
        assert!(!VerifyResult::Fail(vec![VerifyError::HmacFailure]).is_pass());

        assert!(!VerifyResult::Skipped("reason".to_string()).is_pass());
        assert!(!VerifyResult::Skipped("reason".to_string()).is_fail());
    }
}
