//! Handler for the `backlog` command — shows only Stage::Backlog items.

use super::CommandContext;
use super::status::{story_short_label, unmet_deps_from_items};
use crate::pipeline_state::{PipelineItem, Stage};

pub(super) fn handle_backlog(_ctx: &CommandContext) -> Option<String> {
    Some(build_backlog_output())
}

/// Build the backlog listing text.
///
/// Reads all items from CRDT via `read_all_typed`, filters to `Stage::Backlog`,
/// and renders each with number, type, name, and dependency satisfaction status.
pub(super) fn build_backlog_output() -> String {
    let items = crate::pipeline_state::read_all_typed();
    build_backlog_from_items(&items)
}

/// Inner implementation that accepts pre-loaded items for testability.
pub(super) fn build_backlog_from_items(items: &[PipelineItem]) -> String {
    let mut backlog: Vec<&PipelineItem> = items
        .iter()
        .filter(|i| matches!(i.stage, Stage::Backlog))
        .collect();
    backlog.sort_by(|a, b| a.story_id.0.cmp(&b.story_id.0));

    let count = backlog.len();
    let mut out = format!("**Backlog** ({count})\n");

    if backlog.is_empty() {
        out.push_str("  *(none)*\n");
        return out;
    }

    for item in &backlog {
        out.push_str(&render_backlog_line(item, items));
    }
    out
}

/// Render a single backlog line: `⚪ <number> [type] — <name> *(waiting on: X, Y)*`
fn render_backlog_line(item: &PipelineItem, all_items: &[PipelineItem]) -> String {
    let story_id = &item.story_id.0;
    let name_opt = if item.name.is_empty() {
        None
    } else {
        Some(item.name.as_str())
    };
    let display = story_short_label(story_id, name_opt);
    let unmet = unmet_deps_from_items(item, all_items);
    let dep_suffix = if unmet.is_empty() {
        String::new()
    } else {
        let nums: Vec<String> = unmet.iter().map(|n| n.to_string()).collect();
        format!(" *(waiting on: {})*", nums.join(", "))
    };
    format!("  \u{26AA} {display}{dep_suffix}\n") // ⚪
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::pipeline_state::{Stage, StoryId};
    use chrono::Utc;

    fn make_item(id: &str, name: &str, stage: Stage) -> PipelineItem {
        PipelineItem {
            story_id: StoryId(id.to_string()),
            name: name.to_string(),
            stage,
            depends_on: Vec::new(),
        }
    }

    fn make_item_with_deps(id: &str, name: &str, stage: Stage, deps: Vec<u32>) -> PipelineItem {
        PipelineItem {
            story_id: StoryId(id.to_string()),
            name: name.to_string(),
            stage,
            depends_on: deps.iter().map(|n| StoryId(n.to_string())).collect(),
        }
    }

    // -- AC: shows only Stage::Backlog items ----------------------------------

    #[test]
    fn backlog_shows_only_backlog_stage_items() {
        let items = vec![
            make_item("10_story_in_backlog", "In Backlog", Stage::Backlog),
            make_item(
                "20_story_in_progress",
                "In Progress",
                Stage::Coding {
                    claim: None,
                    plan: Default::default(),
                    retries: 0,
                },
            ),
            make_item("30_story_in_qa", "In QA", Stage::Qa),
        ];
        let output = build_backlog_from_items(&items);
        assert!(
            output.contains("In Backlog"),
            "should show backlog item: {output}"
        );
        assert!(
            !output.contains("In Progress"),
            "should not show coding items: {output}"
        );
        assert!(
            !output.contains("In QA"),
            "should not show QA items: {output}"
        );
    }

    // -- AC: shows number, type, name -----------------------------------------

    #[test]
    fn backlog_shows_number_type_and_name() {
        let items = vec![make_item(
            "42_story_my_feature",
            "My Feature",
            Stage::Backlog,
        )];
        let output = build_backlog_from_items(&items);
        assert!(
            output.contains("42 [story] — My Feature"),
            "should show number, type, and name: {output}"
        );
    }

    // -- AC: shows depends_on with satisfaction status ------------------------

    #[test]
    fn backlog_shows_waiting_on_for_unmet_deps() {
        let items = vec![
            make_item_with_deps(
                "10_story_waiting",
                "Waiting Story",
                Stage::Backlog,
                vec![999],
            ),
            make_item("999_story_dep", "Dep Story", Stage::Backlog),
        ];
        let output = build_backlog_from_items(&items);
        assert!(
            output.contains("waiting on: 999"),
            "should show unmet dep: {output}"
        );
    }

    #[test]
    fn backlog_no_waiting_on_when_dep_is_done() {
        let items = vec![
            make_item_with_deps("10_story_ready", "Ready Story", Stage::Backlog, vec![999]),
            make_item(
                "999_story_done",
                "Done Dep",
                Stage::Done {
                    merged_at: Utc::now(),
                    merge_commit: crate::pipeline_state::GitSha("abc".to_string()),
                },
            ),
        ];
        let output = build_backlog_from_items(&items);
        assert!(
            !output.contains("waiting on"),
            "should not show waiting-on when dep is done: {output}"
        );
    }

    #[test]
    fn backlog_no_waiting_on_when_no_deps() {
        let items = vec![make_item("5_story_nodeps", "No Deps", Stage::Backlog)];
        let output = build_backlog_from_items(&items);
        assert!(
            !output.contains("waiting on"),
            "no dep suffix when no deps: {output}"
        );
    }

    // -- AC: command is registered in the registry ----------------------------

    #[test]
    fn backlog_command_in_registry() {
        let found = super::super::commands().iter().any(|c| c.name == "backlog");
        assert!(found, "backlog must be registered in commands()");
    }

    #[test]
    fn backlog_command_appears_in_help() {
        let result = super::super::tests::try_cmd_addressed(
            "Timmy",
            "@timmy:homeserver.local",
            "@timmy help",
        );
        let output = result.unwrap_or_default();
        assert!(
            output.contains("backlog"),
            "backlog should appear in help output: {output}"
        );
    }

    #[test]
    fn backlog_command_matches() {
        let result = super::super::tests::try_cmd_addressed(
            "Timmy",
            "@timmy:homeserver.local",
            "@timmy backlog",
        );
        assert!(
            result.is_some(),
            "backlog command should match and return Some"
        );
    }

    #[test]
    fn backlog_output_contains_backlog_header() {
        let result = super::super::tests::try_cmd_addressed(
            "Timmy",
            "@timmy:homeserver.local",
            "@timmy backlog",
        );
        let output = result.unwrap_or_default();
        assert!(
            output.contains("Backlog"),
            "backlog output should contain Backlog header: {output}"
        );
    }

    // -- empty backlog --------------------------------------------------------

    #[test]
    fn backlog_shows_none_when_empty() {
        let items = vec![make_item(
            "1_story_done",
            "Done",
            Stage::Coding {
                claim: None,
                plan: Default::default(),
                retries: 0,
            },
        )];
        let output = build_backlog_from_items(&items);
        assert!(
            output.contains("*(none)*"),
            "should show none when no backlog items: {output}"
        );
    }
}
