//! Tests for notification message formatting.
use super::*;

// ── stage_display_name ────────────────────────────────────────────────────

fn done_stage() -> Stage {
    Stage::from_dir("done").unwrap()
}
fn merge_stage() -> Stage {
    Stage::from_dir("merge").unwrap()
}

#[test]
fn stage_display_name_maps_all_known_stages() {
    assert_eq!(stage_display_name(&Stage::Backlog), "Backlog");
    assert_eq!(
        stage_display_name(&Stage::Coding {
            claim: None,
            plan: Default::default(),
            retries: 0,
        }),
        "Current"
    );
    assert_eq!(stage_display_name(&Stage::Qa), "QA");
    assert_eq!(stage_display_name(&merge_stage()), "Merge");
    assert_eq!(stage_display_name(&done_stage()), "Done");
    assert_eq!(
        stage_display_name(&Stage::from_dir("archived").unwrap()),
        "Archived"
    );
    assert_eq!(stage_display_name(&Stage::Upcoming), "Upcoming");
}

// ── format_stage_notification ─────────────────────────────────────────────

#[test]
fn format_notification_done_stage_includes_party_emoji() {
    let (plain, html) = format_stage_notification(
        "353_story_done",
        "Done Story",
        &merge_stage(),
        &done_stage(),
    );
    assert_eq!(
        plain,
        "\u{1f389} #353 Done Story \u{2014} Merge \u{2192} Done"
    );
    assert_eq!(
        html,
        "\u{1f389} <strong>#353</strong> <em>Done Story</em> \u{2014} Merge \u{2192} Done"
    );
}

#[test]
fn format_notification_non_done_stage_has_no_emoji() {
    let (plain, _html) = format_stage_notification(
        "42_story_thing",
        "Some Story",
        &Stage::Backlog,
        &Stage::Coding {
            claim: None,
            plan: Default::default(),
            retries: 0,
        },
    );
    assert!(!plain.contains("\u{1f389}"));
}

#[test]
fn format_notification_with_story_name() {
    let (plain, html) = format_stage_notification(
        "261_story_bot_notifications",
        "Bot notifications",
        &Stage::Upcoming,
        &Stage::Coding {
            claim: None,
            plan: Default::default(),
            retries: 0,
        },
    );
    assert_eq!(
        plain,
        "#261 Bot notifications \u{2014} Upcoming \u{2192} Current"
    );
    assert_eq!(
        html,
        "<strong>#261</strong> <em>Bot notifications</em> \u{2014} Upcoming \u{2192} Current"
    );
}

#[test]
fn format_stage_notification_without_story_name_falls_back_to_number() {
    let (plain, html) = format_stage_notification(
        "42_bug_fix_thing",
        "",
        &Stage::Coding {
            claim: None,
            plan: Default::default(),
            retries: 0,
        },
        &Stage::Qa,
    );
    assert_eq!(plain, "#42 \u{2014} Current \u{2192} QA");
    assert_eq!(html, "<strong>#42</strong> \u{2014} Current \u{2192} QA");
}

#[test]
fn format_notification_non_numeric_id_uses_full_id() {
    let (plain, _html) =
        format_stage_notification("abc_story_thing", "Some Story", &Stage::Qa, &merge_stage());
    assert_eq!(
        plain,
        "#abc_story_thing Some Story \u{2014} QA \u{2192} Merge"
    );
}

#[test]
fn format_stage_notification_long_name_is_preserved() {
    let long_name = "A".repeat(300);
    let (plain, _html) = format_stage_notification(
        "1_story_long",
        &long_name,
        &Stage::Coding {
            claim: None,
            plan: Default::default(),
            retries: 0,
        },
        &Stage::Qa,
    );
    assert!(plain.contains(&long_name));
}

#[test]
fn format_stage_notification_empty_story_name_falls_back_to_number() {
    let (plain, html) = format_stage_notification(
        "42_story_empty",
        "",
        &Stage::Coding {
            claim: None,
            plan: Default::default(),
            retries: 0,
        },
        &Stage::Qa,
    );
    assert_eq!(plain, "#42 \u{2014} Current \u{2192} QA");
    assert_eq!(html, "<strong>#42</strong> \u{2014} Current \u{2192} QA");
}

#[test]
fn format_stage_notification_unicode_name() {
    let (plain, html) = format_stage_notification(
        "7_story_i18n",
        "Ünïcödé Ñämé 🎉",
        &Stage::Qa,
        &merge_stage(),
    );
    assert!(plain.contains("Ünïcödé Ñämé 🎉"));
    assert!(html.contains("Ünïcödé Ñämé 🎉"));
}

// ── format_error_notification ─────────────────────────────────────────────

#[test]
fn format_error_notification_with_story_name() {
    let (plain, html) = format_error_notification(
        "262_story_bot_errors",
        "Bot error notifications",
        "merge conflict in src/main.rs",
    );
    assert_eq!(
        plain,
        "\u{274c} #262 Bot error notifications \u{2014} merge conflict in src/main.rs"
    );
    assert_eq!(
        html,
        "\u{274c} <strong>#262</strong> <em>Bot error notifications</em> \u{2014} merge conflict in src/main.rs"
    );
}

#[test]
fn format_error_notification_without_story_name_falls_back_to_number() {
    let (plain, html) = format_error_notification("42_bug_fix_thing", "", "tests failed");
    assert_eq!(plain, "\u{274c} #42 \u{2014} tests failed");
    assert_eq!(html, "\u{274c} <strong>#42</strong> \u{2014} tests failed");
}

#[test]
fn format_error_notification_non_numeric_id_uses_full_id() {
    let (plain, _html) =
        format_error_notification("abc_story_thing", "Some Story", "clippy errors");
    assert_eq!(
        plain,
        "\u{274c} #abc_story_thing Some Story \u{2014} clippy errors"
    );
}

#[test]
fn format_error_notification_long_reason_preserved() {
    let long_reason = "x".repeat(500);
    let (plain, _html) = format_error_notification("1_story_foo", "", &long_reason);
    assert!(plain.contains(&long_reason));
}

#[test]
fn format_error_notification_unicode_reason() {
    let (plain, _html) = format_error_notification("5_story_foo", "Foo", "错误：合并冲突");
    assert!(plain.contains("错误：合并冲突"));
}

#[test]
fn format_error_notification_empty_story_name_falls_back_to_number() {
    let (plain, _html) = format_error_notification("42_bug_fix_thing", "", "tests failed");
    assert_eq!(plain, "\u{274c} #42 \u{2014} tests failed");
}

// ── format_blocked_notification ───────────────────────────────────────────

#[test]
fn format_blocked_notification_with_story_name() {
    let (plain, html) = format_blocked_notification(
        "425_story_blocking_reason",
        "Blocking Reason Story",
        "Retry limit exceeded (3/3) at coder stage",
    );
    assert_eq!(
        plain,
        "\u{1f6ab} #425 Blocking Reason Story \u{2014} BLOCKED: Retry limit exceeded (3/3) at coder stage"
    );
    assert_eq!(
        html,
        "\u{1f6ab} <strong>#425</strong> <em>Blocking Reason Story</em> \u{2014} BLOCKED: Retry limit exceeded (3/3) at coder stage"
    );
}

#[test]
fn format_blocked_notification_falls_back_to_number() {
    let (plain, html) = format_blocked_notification("42_story_thing", "", "empty diff");
    assert_eq!(plain, "\u{1f6ab} #42 \u{2014} BLOCKED: empty diff");
    assert_eq!(
        html,
        "\u{1f6ab} <strong>#42</strong> \u{2014} BLOCKED: empty diff"
    );
}

#[test]
fn format_blocked_notification_empty_story_name_falls_back_to_number() {
    let (plain, _html) = format_blocked_notification("42_story_thing", "", "empty diff");
    assert_eq!(plain, "\u{1f6ab} #42 \u{2014} BLOCKED: empty diff");
}

#[test]
fn format_blocked_notification_unicode_reason() {
    let (plain, _html) = format_blocked_notification("3_story_x", "X", "理由：空の差分");
    assert!(plain.contains("BLOCKED: 理由：空の差分"));
}

// ── format_rate_limit_notification ────────────────────────────────────────

#[test]
fn format_rate_limit_notification_includes_agent_and_story() {
    let (plain, html) =
        format_rate_limit_notification("365_story_my_feature", "My Feature", "coder-2");
    assert_eq!(
        plain,
        "\u{26a0}\u{fe0f} #365 My Feature \u{2014} coder-2 hit an API rate limit"
    );
    assert_eq!(
        html,
        "\u{26a0}\u{fe0f} <strong>#365</strong> <em>My Feature</em> \u{2014} coder-2 hit an API rate limit"
    );
}

#[test]
fn format_rate_limit_notification_falls_back_to_number() {
    let (plain, html) = format_rate_limit_notification("42_story_thing", "", "coder-1");
    assert_eq!(
        plain,
        "\u{26a0}\u{fe0f} #42 \u{2014} coder-1 hit an API rate limit"
    );
    assert_eq!(
        html,
        "\u{26a0}\u{fe0f} <strong>#42</strong> \u{2014} coder-1 hit an API rate limit"
    );
}

#[test]
fn format_rate_limit_notification_empty_story_name_falls_back_to_number() {
    let (plain, _html) = format_rate_limit_notification("42_story_thing", "", "coder-1");
    assert_eq!(
        plain,
        "\u{26a0}\u{fe0f} #42 \u{2014} coder-1 hit an API rate limit"
    );
}

#[test]
fn format_rate_limit_notification_unicode_agent_name() {
    let (plain, _html) = format_rate_limit_notification("9_story_foo", "Foo", "агент-1");
    assert!(plain.contains("агент-1"));
    assert!(plain.contains("hit an API rate limit"));
}

// ── format_agent_started_notification ─────────────────────────────────────

#[test]
fn format_agent_started_notification_with_story_name() {
    let (plain, html) = format_agent_started_notification("42_story_foo", "My Feature", "coder-1");
    assert_eq!(plain, "\u{1F916} #42 My Feature \u{2014} coder-1 started");
    assert_eq!(
        html,
        "\u{1F916} <strong>#42</strong> <em>My Feature</em> \u{2014} coder-1 started"
    );
}

#[test]
fn format_agent_started_notification_falls_back_to_number() {
    let (plain, html) = format_agent_started_notification("42_story_foo", "", "coder-1");
    assert_eq!(plain, "\u{1F916} #42 \u{2014} coder-1 started");
    assert_eq!(
        html,
        "\u{1F916} <strong>#42</strong> \u{2014} coder-1 started"
    );
}

#[test]
fn format_agent_started_notification_empty_name_falls_back_to_number() {
    let (plain, _html) = format_agent_started_notification("42_story_foo", "", "coder-1");
    assert_eq!(plain, "\u{1F916} #42 \u{2014} coder-1 started");
}

// ── format_merge_auto_retry_notification ──────────────────────────────────

#[test]
fn format_merge_auto_retry_notification_with_story_name() {
    let (plain, html) = format_merge_auto_retry_notification("42_story_foo", "My Feature", 1, 3);
    assert_eq!(
        plain,
        "\u{1f504} #42 My Feature \u{2014} auto-retrying merge (attempt 1/3)"
    );
    assert_eq!(
        html,
        "\u{1f504} <strong>#42</strong> <em>My Feature</em> \u{2014} auto-retrying merge \
             (attempt 1/3)"
    );
}

#[test]
fn format_merge_auto_retry_notification_falls_back_to_number() {
    let (plain, html) = format_merge_auto_retry_notification("42_story_foo", "", 2, 3);
    assert_eq!(
        plain,
        "\u{1f504} #42 \u{2014} auto-retrying merge (attempt 2/3)"
    );
    assert_eq!(
        html,
        "\u{1f504} <strong>#42</strong> \u{2014} auto-retrying merge (attempt 2/3)"
    );
}

// ── project_display_name ──────────────────────────────────────────────────

#[test]
fn project_display_name_prefers_gateway_project() {
    let root = std::path::Path::new("/tmp/whatever-dir-name");
    assert_eq!(
        project_display_name(Some("my-configured-name"), root),
        "my-configured-name"
    );
}

#[test]
fn project_display_name_falls_back_to_dir_basename() {
    let root = std::path::Path::new("/tmp/my-project-dir");
    assert_eq!(project_display_name(None, root), "my-project-dir");
}

#[test]
fn project_display_name_falls_back_to_project_literal() {
    let root = std::path::Path::new("/");
    assert_eq!(project_display_name(None, root), "project");
}

// ── truncate_gate_output ──────────────────────────────────────────────────

#[test]
fn truncate_gate_output_short_output_returned_unchanged() {
    let output = "line1\nline2\nline3";
    assert_eq!(truncate_gate_output(output, 30), output);
}

#[test]
fn truncate_gate_output_exact_limit_returned_unchanged() {
    let lines: Vec<String> = (1..=30).map(|i| format!("line{i}")).collect();
    let output = lines.join("\n");
    assert_eq!(truncate_gate_output(&output, 30), output);
}

#[test]
fn truncate_gate_output_over_limit_prepends_marker() {
    let lines: Vec<String> = (1..=35).map(|i| format!("line{i}")).collect();
    let output = lines.join("\n");
    let result = truncate_gate_output(&output, 30);
    assert!(
        result.starts_with("[...output truncated, last 30 lines shown...]"),
        "must start with truncation marker; got: {result}"
    );
}

#[test]
fn truncate_gate_output_over_limit_contains_tail_lines() {
    let lines: Vec<String> = (1..=35).map(|i| format!("line{i}")).collect();
    let output = lines.join("\n");
    let result = truncate_gate_output(&output, 30);
    // Last 30 lines are line6..line35.
    assert!(result.contains("line35"), "must contain last line");
    assert!(result.contains("line6"), "must contain first tail line");
    assert!(!result.contains("line5"), "must not contain dropped line");
}

#[test]
fn truncate_gate_output_empty_input_returned_unchanged() {
    assert_eq!(truncate_gate_output("", 30), "");
}

#[test]
fn truncate_gate_output_single_line_returned_unchanged() {
    assert_eq!(truncate_gate_output("only one line", 30), "only one line");
}

#[test]
fn truncate_gate_output_marker_contains_configured_limit() {
    let lines: Vec<String> = (1..=10).map(|i| format!("x{i}")).collect();
    let output = lines.join("\n");
    let result = truncate_gate_output(&output, 5);
    assert!(
        result.contains("last 5 lines shown"),
        "marker must state configured limit; got: {result}"
    );
}

// ── format_agent_completed_notification ───────────────────────────────────

#[test]
fn format_agent_completed_notification_success_with_story_name() {
    let (plain, html) =
        format_agent_completed_notification("42_story_foo", "My Feature", "coder-1", true);
    assert_eq!(plain, "\u{2705} #42 My Feature \u{2014} coder-1 completed");
    assert_eq!(
        html,
        "\u{2705} <strong>#42</strong> <em>My Feature</em> \u{2014} coder-1 completed"
    );
}

#[test]
fn format_agent_completed_notification_failure_with_story_name() {
    let (plain, _html) =
        format_agent_completed_notification("42_story_foo", "My Feature", "coder-1", false);
    assert_eq!(plain, "\u{274C} #42 My Feature \u{2014} coder-1 failed");
}

#[test]
fn format_agent_completed_notification_falls_back_to_number() {
    let (plain, html) = format_agent_completed_notification("42_story_foo", "", "coder-1", true);
    assert_eq!(plain, "\u{2705} #42 \u{2014} coder-1 completed");
    assert_eq!(
        html,
        "\u{2705} <strong>#42</strong> \u{2014} coder-1 completed"
    );
}

// ── format_new_item_notification ──────────────────────────────────────────

#[test]
fn format_new_item_notification_story() {
    let (plain, html) =
        format_new_item_notification("42_story_my_feature", "story", "My Feature", "user alice");
    assert_eq!(
        plain,
        "\u{1f4d6} New story #42 \u{2014} My Feature (via user alice)"
    );
    assert_eq!(
        html,
        "\u{1f4d6} New story <strong>#42</strong> \u{2014} My Feature (via <em>user alice</em>)"
    );
}

#[test]
fn format_new_item_notification_bug() {
    let (plain, html) = format_new_item_notification(
        "99_bug_login_crash",
        "bug",
        "Login Crash",
        "agent coder-1@story=42",
    );
    assert_eq!(
        plain,
        "\u{1f41b} New bug #99 \u{2014} Login Crash (via agent coder-1@story=42)"
    );
    assert_eq!(
        html,
        "\u{1f41b} New bug <strong>#99</strong> \u{2014} Login Crash \
             (via <em>agent coder-1@story=42</em>)"
    );
}

#[test]
fn format_new_item_notification_refactor() {
    let (plain, html) = format_new_item_notification(
        "1075_refactor_split_stage",
        "refactor",
        "Split Stage enum into Pipeline + Status",
        "chat-bot Timmy@!room:home",
    );
    assert_eq!(
        plain,
        "\u{1f4dd} New refactor #1075 \u{2014} Split Stage enum into Pipeline + Status \
             (via chat-bot Timmy@!room:home)"
    );
    assert_eq!(
        html,
        "\u{1f4dd} New refactor <strong>#1075</strong> \u{2014} Split Stage enum into \
             Pipeline + Status (via <em>chat-bot Timmy@!room:home</em>)"
    );
}

#[test]
fn format_new_item_notification_spike() {
    let (plain, html) = format_new_item_notification(
        "7_spike_encoder_comparison",
        "spike",
        "Compare Encoders",
        "user alice",
    );
    assert_eq!(
        plain,
        "\u{1f52c} New spike #7 \u{2014} Compare Encoders (via user alice)"
    );
    assert_eq!(
        html,
        "\u{1f52c} New spike <strong>#7</strong> \u{2014} Compare Encoders \
             (via <em>user alice</em>)"
    );
}

#[test]
fn format_new_item_notification_epic() {
    let (plain, _html) =
        format_new_item_notification("50_epic_big_thing", "epic", "Big Thing", "user alice");
    assert!(plain.contains("New epic #50"));
    assert!(plain.contains("Big Thing"));
}

#[test]
fn format_new_item_notification_non_numeric_id_uses_full_id() {
    let (plain, _html) =
        format_new_item_notification("abc_story_thing", "story", "Some Story", "user alice");
    assert_eq!(
        plain,
        "\u{1f4d6} New story #abc_story_thing \u{2014} Some Story (via user alice)"
    );
}

// ── origin rendering (AC2, AC5) ───────────────────────────────────────────

#[test]
fn format_new_item_notification_renders_user_origin() {
    let (plain, _html) = format_new_item_notification("1_story_a", "story", "A", "user alice");
    assert!(plain.contains("(via user alice)"));
}

#[test]
fn format_new_item_notification_renders_chat_bot_origin() {
    let (plain, _html) =
        format_new_item_notification("1_story_a", "story", "A", "chat-bot Timmy@!room:home");
    assert!(plain.contains("(via chat-bot Timmy@!room:home)"));
}

#[test]
fn format_new_item_notification_renders_agent_origin() {
    let (plain, _html) =
        format_new_item_notification("1_story_a", "story", "A", "agent coder-1@story=42");
    assert!(plain.contains("(via agent coder-1@story=42)"));
}

// ── format_new_items_notification ─────────────────────────────────────────

fn item(item_id: &str, item_type: &str, name: &str, origin_label: &str) -> NewItemInfo {
    NewItemInfo {
        item_id: item_id.to_string(),
        item_type: item_type.to_string(),
        name: name.to_string(),
        origin_label: origin_label.to_string(),
    }
}

#[test]
fn format_new_items_notification_single_item_matches_single_formatter() {
    let items = vec![item("42_story_a", "story", "A", "user alice")];
    let (plain, html) = format_new_items_notification(&items);
    let (expected_plain, expected_html) =
        format_new_item_notification("42_story_a", "story", "A", "user alice");
    assert_eq!(plain, expected_plain);
    assert_eq!(html, expected_html);
}

#[test]
fn format_new_items_notification_multiple_items_lists_all() {
    let items = vec![
        item("42_story_a", "story", "A Feature", "user alice"),
        item("43_bug_b", "bug", "B Crash", "agent coder-1@story=42"),
        item("44_refactor_c", "refactor", "C Cleanup", "user alice"),
    ];
    let (plain, html) = format_new_items_notification(&items);
    assert!(plain.starts_with("\u{1f4da} 3 new items created:"));
    assert!(plain.contains("#42"), "missing item 1: {plain}");
    assert!(plain.contains("A Feature"), "missing item 1 name: {plain}");
    assert!(plain.contains("#43"), "missing item 2: {plain}");
    assert!(plain.contains("B Crash"), "missing item 2 name: {plain}");
    assert!(plain.contains("#44"), "missing item 3: {plain}");
    assert!(plain.contains("C Cleanup"), "missing item 3 name: {plain}");
    assert!(html.contains("A Feature"));
    assert!(html.contains("B Crash"));
    assert!(html.contains("C Cleanup"));
}

#[test]
fn format_agent_completed_notification_empty_name_falls_back_to_number() {
    let (plain, _html) = format_agent_completed_notification("42_story_foo", "", "coder-1", false);
    assert_eq!(plain, "\u{274C} #42 \u{2014} coder-1 failed");
}

// ── format_disk_warning_notification ──────────────────────────────────────

#[test]
fn format_disk_warning_notification_warn_level_includes_content() {
    let (plain, html) = format_disk_warning_notification(
        "warn",
        "sled-a",
        45_000_000_000,
        30_000_000_000,
        10_000_000_000,
    );
    assert!(plain.contains("sled-a"));
    assert!(plain.contains("45.0GB free"));
    assert!(plain.contains("target/ 30.0GB"));
    assert!(plain.contains("worktrees/ 10.0GB"));
    assert!(plain.contains("`gc` tool"));
    assert!(html.contains("<code>gc</code> tool"));
}

#[test]
fn format_disk_warning_notification_critical_uses_distinct_emoji() {
    let (plain, _html) =
        format_disk_warning_notification("critical", "sled-b", 5_000_000_000, 0, 0);
    assert!(plain.starts_with("\u{1f6a8}"));
}

#[test]
fn format_disk_warning_notification_warn_uses_warning_emoji() {
    let (plain, _html) = format_disk_warning_notification("warn", "sled-b", 45_000_000_000, 0, 0);
    assert!(plain.starts_with("\u{26a0}\u{fe0f}"));
}

// ── format_disk_recovery_notification ─────────────────────────────────────

#[test]
fn format_disk_recovery_notification_includes_host_and_free_space() {
    let (plain, html) = format_disk_recovery_notification("sled-a", 60_000_000_000);
    assert_eq!(
        plain,
        "\u{2705} Disk space recovered on sled-a: 60.0GB free"
    );
    assert!(html.contains("<strong>sled-a</strong>"));
    assert!(html.contains("60.0GB free"));
}
