//! Gate enforcement: given a PR's `Size` and observable evidence
//! (which artifacts the round produced, which feature files the diff
//! touched), return the gates whose blocking == true *and* aren't
//! satisfied. Empty Vec means "ship it".
//!
//! The asymmetry in [`fail_open`] /[`fail_closed`] is deliberate:
//! Small PRs default to fail-open (CI absence ≠ block), Architectural
//! PRs default to fail-closed (CI absence ≠ ship). This mirrors the
//! cost-proportionate philosophy in `wiki/features/adaptive-workflow.md`.

use crate::gates::{gates_for, Gate};
use crate::size::Size;

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct EnforceInput<'a> {
    pub size: Size,
    /// Round-N artifact files produced by the current PR. Path
    /// fragments like `concept-no-stale-refs` are sufficient — only
    /// presence is checked, not content.
    pub artifacts: &'a [String],
    /// `wiki/features/<id>.md` files modified by the diff (basename
    /// without extension). Used to satisfy `koala.feature-sync`.
    pub feature_changes: &'a [String],
    /// Evidence that the PR is backed by an ADR, used to satisfy
    /// `koala.adr`. Two forms count: an `adr-<NNNN>` from a touched
    /// `wiki/decisions/*` file (the PR adds/supersedes an ADR), or an
    /// `adr-<NNNN>` resolved from an `ADR-NNNN` reference in a commit
    /// message or touched feature doc that points at an *accepted* ADR
    /// (the PR implements an existing one). Kept separate from
    /// `feature_changes` so a mere citation never satisfies
    /// `koala.feature-sync`.
    pub adr_evidence: &'a [String],
    /// Whether human review is recorded (e.g. PR has an `approved`
    /// review). Architectural PRs need this; everything else doesn't.
    pub human_review: bool,
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct EnforceFinding {
    pub gate: Gate,
    pub reason: String,
}

pub fn enforce(input: &EnforceInput<'_>) -> Vec<EnforceFinding> {
    let mut out = Vec::new();
    for g in gates_for(input.size) {
        if !g.blocking {
            continue;
        }
        match g.id {
            "koala.artifact-required" if input.artifacts.is_empty() => {
                out.push(EnforceFinding {
                    gate: g,
                    reason: "no `.review/round-N/<kind>-<name>.md` artifacts in PR; \
                             reviewer must record at least one"
                        .into(),
                });
            }
            "koala.feature-sync" if input.feature_changes.is_empty() => {
                out.push(EnforceFinding {
                    gate: g,
                    reason: "PR touched code but did not update any `wiki/features/*.md`".into(),
                });
            }
            "koala.adr" if input.adr_evidence.is_empty() => {
                out.push(EnforceFinding {
                    gate: g,
                    reason: "architectural PR must be backed by an accepted ADR: \
                             add/supersede one under `wiki/decisions/`, or cite an \
                             existing accepted `ADR-NNNN` in a commit message or a \
                             touched `wiki/features/*.md`"
                        .into(),
                });
            }
            _ => {
                // Other gates (cargo build/test, invariants, drift)
                // are run as separate steps; not enforced here.
            }
        }
    }
    if matches!(input.size, Size::Architectural) && !input.human_review {
        out.push(EnforceFinding {
            gate: Gate {
                id: "koala.human-review",
                intent: "architectural change requires explicit human approval",
                blocking: true,
            },
            reason: "PR size = architectural, but `human_review = false` \
                     (default fail-closed — see ADR-0006)"
                .into(),
        });
    }
    out
}

/// Whether a missing-CI signal blocks a PR at this size. Small/medium
/// fail-open (give the benefit of the doubt); large/architectural
/// fail-closed (CI silence = block).
pub fn fail_closed(size: Size) -> bool {
    matches!(size, Size::Large | Size::Architectural)
}

pub fn fail_open(size: Size) -> bool {
    !fail_closed(size)
}

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

    #[test]
    fn small_fails_open_arch_fails_closed() {
        assert!(fail_open(Size::Small));
        assert!(fail_open(Size::Medium));
        assert!(fail_closed(Size::Large));
        assert!(fail_closed(Size::Architectural));
    }

    #[test]
    fn small_with_no_artifacts_is_clean() {
        let f = enforce(&EnforceInput {
            size: Size::Small,
            artifacts: &[],
            feature_changes: &[],
            adr_evidence: &[],
            human_review: false,
        });
        assert!(f.is_empty(), "{f:?}");
    }

    #[test]
    fn medium_without_artifact_blocks() {
        let f = enforce(&EnforceInput {
            size: Size::Medium,
            artifacts: &[],
            feature_changes: &[],
            adr_evidence: &[],
            human_review: false,
        });
        assert!(
            f.iter().any(|x| x.gate.id == "koala.artifact-required"),
            "expected artifact-required block, got {f:?}"
        );
    }

    #[test]
    fn medium_with_artifact_passes() {
        let f = enforce(&EnforceInput {
            size: Size::Medium,
            artifacts: &["concept-no-stale-refs".into()],
            feature_changes: &[],
            adr_evidence: &[],
            human_review: false,
        });
        assert!(
            f.is_empty(),
            "medium with one artifact should pass, got {f:?}"
        );
    }

    #[test]
    fn large_without_feature_sync_blocks() {
        let f = enforce(&EnforceInput {
            size: Size::Large,
            artifacts: &["quality-clippy".into()],
            feature_changes: &[],
            adr_evidence: &[],
            human_review: false,
        });
        assert!(
            f.iter().any(|x| x.gate.id == "koala.feature-sync"),
            "expected feature-sync block, got {f:?}"
        );
    }

    #[test]
    fn architectural_without_adr_or_human_blocks_two_gates() {
        let f = enforce(&EnforceInput {
            size: Size::Architectural,
            artifacts: &["concept-x".into()],
            feature_changes: &["drift-detector".into()],
            adr_evidence: &[],
            human_review: false,
        });
        let ids: Vec<&str> = f.iter().map(|x| x.gate.id).collect();
        assert!(ids.contains(&"koala.adr"), "{ids:?}");
        assert!(ids.contains(&"koala.human-review"), "{ids:?}");
    }

    #[test]
    fn architectural_with_adr_and_human_passes() {
        let f = enforce(&EnforceInput {
            size: Size::Architectural,
            artifacts: &["concept-x".into()],
            feature_changes: &["drift-detector".into()],
            adr_evidence: &["adr-0015-tier4-overlay".into()],
            human_review: true,
        });
        assert!(f.is_empty(), "{f:?}");
    }

    #[test]
    fn architectural_adr_satisfied_by_reference_only() {
        // No ADR file touched (feature_changes carries no adr-*), but a
        // resolved reference is supplied: koala.adr must pass while
        // feature-sync still sees a real feature doc.
        let f = enforce(&EnforceInput {
            size: Size::Architectural,
            artifacts: &["concept-x".into()],
            feature_changes: &["drift-detector".into()],
            adr_evidence: &["adr-0006".into()],
            human_review: true,
        });
        assert!(f.is_empty(), "{f:?}");
    }

    #[test]
    fn architectural_adr_reference_does_not_satisfy_feature_sync() {
        // A citation with no touched feature doc must still block on
        // feature-sync — adr_evidence is not feature evidence.
        let f = enforce(&EnforceInput {
            size: Size::Architectural,
            artifacts: &["concept-x".into()],
            feature_changes: &[],
            adr_evidence: &["adr-0006".into()],
            human_review: true,
        });
        let ids: Vec<&str> = f.iter().map(|x| x.gate.id).collect();
        assert!(ids.contains(&"koala.feature-sync"), "{ids:?}");
        assert!(!ids.contains(&"koala.adr"), "{ids:?}");
    }
}
