//! Front-matter checks for story files: review holds, blocked state, and merge failures.

/// Read the optional `agent:` pin for a story.
///
/// After story 871 the agent assignment lives in the CRDT typed register
/// (`PipelineItemView.agent`), not the YAML front matter. We check the CRDT
/// first; falling back to legacy YAML parsing keeps behaviour intact for any
/// stories whose CRDT entry doesn't yet have the field set.
pub(super) fn read_story_front_matter_agent(story_id: &str) -> Option<String> {
    // Story 929: agent name comes from the CRDT register. The previous
    // YAML fallback is gone — post-891 every story has its CRDT entry,
    // and any story without one is treated as having no pinned agent.
    crate::crdt_state::read_item(story_id).and_then(|w| w.agent().map(|a| a.to_string()))
}

/// Return `true` if the story is in `Stage::ReviewHold`.
///
/// Story 945: `Stage::ReviewHold { resume_to, reason }` is the single source
/// of truth — the legacy `review_hold: bool` CRDT register has been deleted.
/// The auto-assigner uses this to keep human-QA items / spikes parked after
/// gates pass until a reviewer explicitly clears the hold (e.g. via
/// `tool_approve_qa`).
pub(super) fn has_review_hold(story_id: &str) -> bool {
    crate::crdt_state::read_item(story_id)
        .map(|w| matches!(w.stage(), crate::pipeline_state::Stage::ReviewHold { .. }))
        .unwrap_or(false)
}

/// Return `true` if the story is blocked via the typed `Stage::Blocked` or
/// `Stage::MergeFailure` variant (or the legacy `Archived(Blocked)` state).
///
/// The typed pipeline stage register is the only source consulted — the legacy
/// `blocked: true` YAML front-matter field is no longer checked.
pub(super) fn is_story_blocked(story_id: &str) -> bool {
    crate::pipeline_state::read_typed(story_id)
        .ok()
        .flatten()
        .map(|item| {
            matches!(
                item.stage,
                crate::pipeline_state::Stage::Blocked { .. }
                    | crate::pipeline_state::Stage::MergeFailure { .. }
                    | crate::pipeline_state::Stage::MergeFailureFinal { .. }
                    | crate::pipeline_state::Stage::Archived {
                        reason: crate::pipeline_state::ArchiveReason::Blocked { .. },
                        ..
                    }
            )
        })
        .unwrap_or(false)
}

/// Return `true` if the story has any `depends_on` entries that are not yet in
/// `5_done` or `6_archived`. Reads dependency state from the CRDT (story 929).
pub(super) fn has_unmet_dependencies(story_id: &str) -> bool {
    !crate::crdt_state::check_unmet_deps_crdt(story_id).is_empty()
}

/// Return the list of dependency story numbers that are in `6_archived` (satisfied
/// via archive rather than via a clean `5_done` completion). Reads from the CRDT
/// (story 929).
pub(super) fn check_archived_dependencies(story_id: &str) -> Vec<u32> {
    crate::crdt_state::check_archived_deps_crdt(story_id)
}

/// Return `true` if the story is in `Stage::Frozen`.
///
/// Story 945: `Stage::Frozen { resume_to }` is the single source of truth —
/// the legacy `frozen: bool` CRDT register has been deleted.  Frozen stories
/// are skipped by the auto-assigner until `Unfreeze` returns them to
/// `resume_to`.
pub(super) fn is_story_frozen(story_id: &str) -> bool {
    crate::crdt_state::read_item(story_id)
        .map(|view| matches!(view.stage(), crate::pipeline_state::Stage::Frozen { .. }))
        .unwrap_or(false)
}

// ── Tests ──────────────────────────────────────────────────────────────────

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

    // ── has_review_hold ───────────────────────────────────────────────────────

    #[test]
    fn has_review_hold_returns_true_when_flag_set() {
        crate::crdt_state::init_for_test();
        crate::db::ensure_content_store();
        // Story 945: review_hold is now a typed Stage variant, seeded via
        // the wire-form stage register directly.
        crate::crdt_state::write_item_str(
            "890_spike_held",
            "review_hold",
            Some("Held Spike"),
            None,
            None,
            None,
        );
        assert!(has_review_hold("890_spike_held"));
    }

    #[test]
    fn has_review_hold_returns_false_when_flag_unset() {
        crate::crdt_state::init_for_test();
        crate::db::ensure_content_store();
        crate::crdt_state::write_item_str(
            "890_spike_active_qa",
            "3_qa",
            Some("Active QA Spike"),
            None,
            None,
            None,
        );
        assert!(!has_review_hold("890_spike_active_qa"));
    }

    #[test]
    fn has_review_hold_returns_false_when_story_unknown() {
        assert!(!has_review_hold("99_spike_missing"));
    }

    // ── is_story_blocked — regression: typed stage is sole authority ──────────

    #[test]
    fn is_story_blocked_set_via_typed_stage_returns_true() {
        crate::crdt_state::init_for_test();
        crate::db::ensure_content_store();
        crate::db::write_item_with_content(
            "890_story_blocked_set",
            "2_blocked",
            "---\nname: Blocked Story\n---\n",
            crate::db::ItemMeta::named("Blocked Story"),
        );
        assert!(is_story_blocked("890_story_blocked_set"));
    }

    #[test]
    fn is_story_blocked_cleared_via_typed_stage_returns_false() {
        crate::crdt_state::init_for_test();
        crate::db::ensure_content_store();
        // First set to blocked.
        crate::db::write_item_with_content(
            "890_story_blocked_clear",
            "2_blocked",
            "---\nname: Clearable Story\n---\n",
            crate::db::ItemMeta::named("Clearable Story"),
        );
        // Then clear by transitioning to an active stage.
        crate::db::write_item_with_content(
            "890_story_blocked_clear",
            "2_current",
            "---\nname: Clearable Story\n---\n",
            crate::db::ItemMeta::named("Clearable Story"),
        );
        assert!(!is_story_blocked("890_story_blocked_clear"));
    }

    #[test]
    fn is_story_blocked_stale_yaml_is_ignored() {
        crate::crdt_state::init_for_test();
        crate::db::ensure_content_store();
        // YAML front matter says `blocked: true`, but the typed CRDT stage is backlog.
        // After removing the YAML fallback, the function must return false.
        crate::db::write_item_with_content(
            "890_story_stale_yaml",
            "1_backlog",
            "---\nname: Stale\nblocked: true\n---\n",
            crate::db::ItemMeta::named("Stale"),
        );
        assert!(
            !is_story_blocked("890_story_stale_yaml"),
            "stale YAML `blocked: true` must not be reported as blocked when typed stage is Backlog"
        );
    }

    // ── has_unmet_dependencies ────────────────────────────────────────────────

    #[test]
    fn has_unmet_dependencies_returns_true_when_dep_not_done() {
        crate::crdt_state::init_for_test();
        crate::crdt_state::write_item_str(
            "10_story_blocked",
            "2_current",
            Some("Blocked"),
            None,
            Some("[999]"),
            None,
        );
        assert!(has_unmet_dependencies("10_story_blocked"));
    }

    #[test]
    fn has_unmet_dependencies_returns_false_when_dep_done() {
        crate::crdt_state::init_for_test();
        crate::crdt_state::write_item_str("999_story_dep", "5_done", Some("Dep"), None, None, None);
        crate::crdt_state::write_item_str(
            "10_story_ok",
            "2_current",
            Some("Ok"),
            None,
            Some("[999]"),
            None,
        );
        assert!(!has_unmet_dependencies("10_story_ok"));
    }

    #[test]
    fn has_unmet_dependencies_returns_false_when_no_deps() {
        crate::crdt_state::init_for_test();
        crate::crdt_state::write_item_str(
            "5_story_free",
            "2_current",
            Some("Free"),
            None,
            None,
            None,
        );
        assert!(!has_unmet_dependencies("5_story_free"));
    }

    // ── Bug 503: archived-dep visibility ─────────────────────────────────────

    /// check_archived_dependencies returns dep IDs that are in 6_archived.
    #[test]
    fn check_archived_dependencies_returns_archived_ids() {
        crate::crdt_state::init_for_test();
        crate::crdt_state::write_item_str(
            "500_spike_crdt",
            "6_archived",
            Some("CRDT Spike"),
            None,
            None,
            None,
        );
        crate::crdt_state::write_item_str(
            "503_story_dependent",
            "1_backlog",
            Some("Dependent"),
            None,
            Some("[500]"),
            None,
        );
        let archived_deps = check_archived_dependencies("503_story_dependent");
        assert_eq!(archived_deps, vec![500]);
    }

    /// check_archived_dependencies returns empty when dep is in 5_done (not archived).
    #[test]
    fn check_archived_dependencies_empty_when_dep_in_done() {
        crate::crdt_state::init_for_test();
        crate::crdt_state::write_item_str(
            "490_story_done",
            "5_done",
            Some("Done"),
            None,
            None,
            None,
        );
        crate::crdt_state::write_item_str(
            "503_story_waiting",
            "1_backlog",
            Some("Waiting"),
            None,
            Some("[490]"),
            None,
        );
        let archived_deps = check_archived_dependencies("503_story_waiting");
        assert!(archived_deps.is_empty());
    }
}
