#[path = "support/mock_server.rs"]
mod mock_server;

use std::process::{Command, Output};
use std::{
    fs,
    time::{SystemTime, UNIX_EPOCH},
};

#[cfg(feature = "github")]
use mock_server::github_base_env;
#[cfg(not(feature = "github"))]
use mock_server::{MERGE_REQUEST_IID, base_env};
use mock_server::{MockGitLabServer, PROJECT_ID, borrow_env};

fn run_cli(subcommand: &str, extra_args: &[&str], envs: &[(&str, &str)]) -> Output {
    let mut command = Command::new(env!("CARGO_BIN_EXE_mr-milchick"));
    command.current_dir(env!("CARGO_MANIFEST_DIR"));
    command.arg(subcommand);
    command.args(extra_args);
    command.env_clear();
    let config_override = env_value(envs, "MR_MILCHICK_CONFIG_PATH")
        .or_else(|| env_value(envs, "MR_MILCHICK_FLAVOR_PATH"));
    let default_config = config_override
        .is_none()
        .then(|| write_temp_config(&build_test_config(envs)));

    for (key, value) in envs {
        if is_config_managed_env(key) || *key == "MR_MILCHICK_FLAVOR_PATH" {
            continue;
        }
        command.env(key, value);
    }
    if let Some(path) = config_override {
        command.env("MR_MILCHICK_CONFIG_PATH", path);
    } else if let Some(path) = &default_config {
        command.env("MR_MILCHICK_CONFIG_PATH", path);
    }

    let output = command.output().expect("CLI invocation should succeed");

    if let Some(path) = default_config {
        let _ = fs::remove_file(path);
    }

    output
}

fn write_temp_config(contents: &str) -> String {
    let unique = SystemTime::now()
        .duration_since(UNIX_EPOCH)
        .expect("clock should be after epoch")
        .as_nanos();
    let path = std::env::temp_dir().join(format!("mr-milchick-config-{unique}.toml"));
    fs::write(&path, contents).expect("temp config should be written");
    path.display().to_string()
}

fn build_test_config(envs: &[(&str, &str)]) -> String {
    let mut sections = vec![format!(
        "[platform]\nkind = \"{}\"\nbase_url = {}\n",
        compiled_platform_connector_kind(),
        toml_string(
            env_value(envs, "GITLAB_BASE_URL")
                .or_else(|| env_value(envs, "GITHUB_API_BASE_URL"))
                .unwrap_or(default_platform_base_url())
        )
    )];

    sections.push(format!(
        "[execution]\ndry_run = {}\nnotification_policy = {}\n",
        legacy_bool(env_value(envs, "MR_MILCHICK_DRY_RUN")).unwrap_or(false),
        toml_string(env_value(envs, "MR_MILCHICK_NOTIFICATION_POLICY").unwrap_or("always"))
    ));

    sections.push(build_reviewers_config(envs));
    sections.push(format!(
        "[codeowners]\nenabled = {}\n",
        legacy_bool(env_value(envs, "MR_MILCHICK_CODEOWNERS_ENABLED")).unwrap_or(true)
    ));

    if legacy_bool(env_value(envs, "MR_MILCHICK_SLACK_ENABLED")).unwrap_or(false) {
        sections.push(format!(
            "[notifications.slack_app]\nenabled = true\nchannel = {}\nbase_url = {}\n",
            toml_optional_string(env_value(envs, "MR_MILCHICK_SLACK_CHANNEL")),
            toml_string(
                env_value(envs, "MR_MILCHICK_SLACK_BASE_URL").unwrap_or("https://slack.com/api")
            )
        ));

        if let Some(user_map) = env_value(envs, "MR_MILCHICK_SLACK_USER_MAP") {
            sections.push(build_user_map_config(user_map));
        }
    }

    if legacy_bool(env_value(envs, "MR_MILCHICK_SLACK_ENABLED")).unwrap_or(false)
        && env_value(envs, "MR_MILCHICK_SLACK_WEBHOOK_URL").is_some()
    {
        sections.push(format!(
            "[notifications.slack_workflow]\nenabled = true\nchannel = {}\n",
            toml_optional_string(env_value(envs, "MR_MILCHICK_SLACK_CHANNEL"))
        ));
    }

    sections.join("\n")
}

fn build_reviewers_config(envs: &[(&str, &str)]) -> String {
    let max_reviewers = env_value(envs, "MR_MILCHICK_MAX_REVIEWERS").unwrap_or("2");
    let mut config = format!("[reviewers]\nmax_reviewers = {}\n", max_reviewers);

    let Some(raw) = env_value(envs, "MR_MILCHICK_REVIEWERS") else {
        return config;
    };

    let reviewers = serde_json::from_str::<Vec<serde_json::Value>>(raw)
        .expect("test reviewer config should be valid JSON");
    for reviewer in reviewers {
        let username = reviewer["username"]
            .as_str()
            .expect("reviewer username should be present");
        config.push_str("\n[[reviewers.definitions]]\n");
        config.push_str(&format!("username = {}\n", toml_string(username)));
        if let Some(areas) = reviewer["areas"].as_array() {
            let areas = areas
                .iter()
                .map(|area| toml_string(area.as_str().expect("area should be a string")))
                .collect::<Vec<_>>()
                .join(", ");
            config.push_str(&format!("areas = [{}]\n", areas));
        }
        if reviewer["fallback"].as_bool().unwrap_or(false) {
            config.push_str("fallback = true\n");
        }
        if reviewer["mandatory"].as_bool().unwrap_or(false) {
            config.push_str("mandatory = true\n");
        }
    }

    config
}

fn build_user_map_config(raw: &str) -> String {
    let user_map = serde_json::from_str::<serde_json::Map<String, serde_json::Value>>(raw)
        .expect("test Slack user map should be valid JSON");
    let mut config = String::from("[notifications.slack_app.user_map]\n");
    for (username, user_id) in user_map {
        config.push_str(&format!(
            "{} = {}\n",
            toml_string(&username),
            toml_string(user_id.as_str().expect("Slack user id should be a string"))
        ));
    }
    config
}

fn env_value<'a>(envs: &'a [(&str, &str)], key: &str) -> Option<&'a str> {
    envs.iter()
        .find(|(name, _)| *name == key)
        .map(|(_, value)| *value)
}

fn is_config_managed_env(key: &str) -> bool {
    matches!(
        key,
        "MR_MILCHICK_REVIEWERS"
            | "MR_MILCHICK_MAX_REVIEWERS"
            | "MR_MILCHICK_CODEOWNERS_ENABLED"
            | "MR_MILCHICK_CODEOWNERS_PATH"
            | "MR_MILCHICK_DRY_RUN"
            | "MR_MILCHICK_NOTIFICATION_POLICY"
            | "MR_MILCHICK_LLM_ENABLED"
            | "MR_MILCHICK_LLM_MODEL_PATH"
            | "MR_MILCHICK_LLM_TIMEOUT_MS"
            | "MR_MILCHICK_LLM_MAX_PATCH_BYTES"
            | "MR_MILCHICK_LLM_CONTEXT_TOKENS"
            | "MR_MILCHICK_LLM_TRACE"
            | "MR_MILCHICK_SLACK_ENABLED"
            | "MR_MILCHICK_SLACK_CHANNEL"
            | "MR_MILCHICK_SLACK_BASE_URL"
            | "MR_MILCHICK_SLACK_USER_MAP"
            | "GITLAB_BASE_URL"
            | "GITHUB_API_BASE_URL"
            | "MR_MILCHICK_CONFIG_PATH"
    )
}

fn legacy_bool(value: Option<&str>) -> Option<bool> {
    value.map(|value| matches!(value, "1" | "true" | "TRUE" | "yes" | "YES" | "on" | "ON"))
}

fn toml_string(value: &str) -> String {
    serde_json::to_string(value).expect("string should serialize")
}

fn toml_optional_string(value: Option<&str>) -> String {
    value.map(toml_string).unwrap_or_else(|| "\"\"".to_string())
}

fn default_platform_base_url() -> &'static str {
    #[cfg(feature = "github")]
    {
        "https://api.github.com"
    }
    #[cfg(not(feature = "github"))]
    {
        "https://gitlab.com/api/v4"
    }
}

fn compiled_platform_connector_kind() -> &'static str {
    #[cfg(feature = "github")]
    {
        "github"
    }
    #[cfg(not(feature = "github"))]
    {
        "gitlab"
    }
}

fn review_env(server: &MockGitLabServer) -> Vec<(&'static str, String)> {
    #[cfg(feature = "github")]
    {
        github_base_env(server)
    }
    #[cfg(not(feature = "github"))]
    {
        base_env(server)
    }
}

fn review_path() -> String {
    #[cfg(feature = "github")]
    {
        format!("/api/github/repos/ArthurianX/mr-milchick/pulls/3995")
    }
    #[cfg(not(feature = "github"))]
    {
        format!("/api/v4/projects/{PROJECT_ID}/merge_requests/{MERGE_REQUEST_IID}")
    }
}

fn review_files_path() -> String {
    #[cfg(feature = "github")]
    {
        format!("{}/files", review_path())
    }
    #[cfg(not(feature = "github"))]
    {
        format!("{}/changes", review_path())
    }
}

fn review_comments_path() -> String {
    #[cfg(feature = "github")]
    {
        "/api/github/repos/ArthurianX/mr-milchick/issues/3995/comments".to_string()
    }
    #[cfg(not(feature = "github"))]
    {
        format!("{}/notes", review_path())
    }
}

#[test]
fn observe_mode_skips_gitlab_for_non_merge_request_pipelines() {
    let output = run_cli(
        "observe",
        &[],
        &[
            ("CI_PROJECT_ID", PROJECT_ID),
            ("CI_PIPELINE_SOURCE", "push"),
            (
                "CI_MERGE_REQUEST_SOURCE_BRANCH_NAME",
                "feat/intentional-cleanup",
            ),
            ("CI_MERGE_REQUEST_TARGET_BRANCH_NAME", "develop"),
            ("CI_MERGE_REQUEST_LABELS", ""),
            ("RUST_LOG", "off"),
        ],
    );

    assert!(
        output.status.success(),
        "command failed: {}",
        String::from_utf8_lossy(&output.stderr)
    );

    let stdout = String::from_utf8_lossy(&output.stdout);
    assert!(stdout.contains("This pipeline does not currently present review responsibilities."));
}

#[cfg(feature = "github")]
#[test]
fn observe_mode_skips_github_for_non_review_pipelines() {
    let output = run_cli(
        "observe",
        &[],
        &[
            ("GITHUB_ACTIONS", "true"),
            ("GITHUB_EVENT_NAME", "push"),
            ("GITHUB_REPOSITORY", "ArthurianX/mr-milchick"),
            ("RUST_LOG", "off"),
        ],
    );

    assert!(
        output.status.success(),
        "command failed: {}",
        String::from_utf8_lossy(&output.stderr)
    );

    let stdout = String::from_utf8_lossy(&output.stdout);
    assert!(stdout.contains("This pipeline does not currently present review responsibilities."));
}

#[test]
fn observe_mode_reports_planned_actions_without_mutating_gitlab() {
    let server = MockGitLabServer::start();
    let envs = review_env(&server);
    let output = run_cli("observe", &[], &borrow_env(&envs));

    assert!(
        output.status.success(),
        "command failed: {}\n{}",
        String::from_utf8_lossy(&output.stderr),
        String::from_utf8_lossy(&output.stdout)
    );

    let stdout = String::from_utf8_lossy(&output.stdout);
    assert!(stdout.contains("No findings were produced."));
    assert!(stdout.contains("If you run `refine`, it would:"));
    assert!(stdout.contains("[AssignReviewers] principal-reviewer, bob"));

    assert_eq!(server.request_count("GET", &review_path()), 1);
    assert_eq!(server.request_count_prefix("GET", &review_files_path()), 1);
    assert_eq!(server.request_count("POST", &review_comments_path()), 0);
}

#[cfg(feature = "github")]
#[test]
fn refine_mode_runs_end_to_end_through_github_runtime_wiring() {
    let server = MockGitLabServer::start();
    let envs = github_base_env(&server);
    let output = run_cli("refine", &[], &borrow_env(&envs));

    assert!(
        output.status.success(),
        "refine failed: {}\n{}",
        String::from_utf8_lossy(&output.stderr),
        String::from_utf8_lossy(&output.stdout)
    );

    let stdout = String::from_utf8_lossy(&output.stdout);
    assert!(stdout.contains("Planned actions:"));
    assert!(stdout.contains("[AssignReviewers] principal-reviewer, bob"));
    assert!(stdout.contains("Execution report:"));
    assert!(stdout.contains("[ReviewersAssigned] principal-reviewer, bob"));
    assert!(stdout.contains("[CommentPosted] Mr. Milchick summary comment"));

    assert_eq!(
        server.assigned_reviewers(),
        vec!["principal-reviewer".to_string(), "bob".to_string()]
    );
    assert_eq!(server.note_bodies().len(), 1);
}

#[test]
fn refine_mode_runs_end_to_end_through_runtime_wiring() {
    let server = MockGitLabServer::start();
    let envs = review_env(&server);
    let output = run_cli("refine", &[], &borrow_env(&envs));

    assert!(
        output.status.success(),
        "refine failed: {}\n{}",
        String::from_utf8_lossy(&output.stderr),
        String::from_utf8_lossy(&output.stdout)
    );

    let stdout = String::from_utf8_lossy(&output.stdout);
    assert!(stdout.contains("Planned actions:"));
    assert!(stdout.contains("[AssignReviewers] principal-reviewer, bob"));
    assert!(stdout.contains("Execution report:"));
    assert!(stdout.contains("[ReviewersAssigned] principal-reviewer, bob"));
    assert!(stdout.contains("[CommentPosted] Mr. Milchick summary comment"));
    assert!(!stdout.contains("[Notification "));

    assert_eq!(
        server.assigned_reviewers(),
        vec!["principal-reviewer".to_string(), "bob".to_string()]
    );
    assert_eq!(server.note_bodies().len(), 1);
}

#[cfg(feature = "slack-app")]
#[test]
fn refine_mode_always_policy_sends_summary_notifications_even_when_summary_is_unchanged() {
    let server = MockGitLabServer::start();
    let config_path = write_temp_config(&format!(
        r#"[platform]
kind = "{platform}"
base_url = {base_url}

[execution]
notification_policy = "always"

[reviewers]
max_reviewers = 2

[codeowners]
enabled = false

[notifications.slack_app]
enabled = true
channel = "C0ALY38CW3X"
base_url = {slack_base_url}
"#,
        platform = compiled_platform_connector_kind(),
        base_url = toml_string(server.api_base_url().as_str()),
        slack_base_url = toml_string(server.slack_api_base_url().as_str()),
    ));
    let mut envs = review_env(&server);
    envs.push(("MR_MILCHICK_CONFIG_PATH", config_path.clone()));
    envs.push(("MR_MILCHICK_SLACK_BOT_TOKEN", "xoxb-test".to_string()));

    let first = run_cli("refine", &[], &borrow_env(&envs));
    assert!(
        first.status.success(),
        "first refine failed: {}\n{}",
        String::from_utf8_lossy(&first.stderr),
        String::from_utf8_lossy(&first.stdout)
    );

    let first_stdout = String::from_utf8_lossy(&first.stdout);
    assert!(
        first_stdout.contains("[CommentPosted] Mr. Milchick summary comment"),
        "stdout was:\n{first_stdout}"
    );
    assert!(
        first_stdout.contains("[Notification SlackApp] delivered=true sent"),
        "stdout was:\n{first_stdout}"
    );

    let second = run_cli("refine", &[], &borrow_env(&envs));
    assert!(
        second.status.success(),
        "second refine failed: {}\n{}",
        String::from_utf8_lossy(&second.stderr),
        String::from_utf8_lossy(&second.stdout)
    );

    let second_stdout = String::from_utf8_lossy(&second.stdout);
    assert!(
        second_stdout.contains("[CommentSkippedAlreadyPresent] Mr. Milchick summary comment"),
        "stdout was:\n{second_stdout}"
    );
    assert!(
        second_stdout.contains("[Notification SlackApp] delivered=true sent"),
        "stdout was:\n{second_stdout}"
    );

    assert_eq!(server.request_count("POST", &review_comments_path()), 1);
    assert_eq!(server.note_bodies().len(), 1);
    assert_eq!(
        server.request_count("POST", "/slack/api/chat.postMessage"),
        3
    );
    let _ = fs::remove_file(config_path);
}

#[cfg(feature = "slack-app")]
#[test]
fn refine_mode_on_applied_action_policy_skips_notifications_when_summary_is_unchanged() {
    let server = MockGitLabServer::start();
    let config_path = write_temp_config(&format!(
        r#"[platform]
kind = "{platform}"
base_url = {base_url}

[execution]
notification_policy = "on-applied-action"

[reviewers]
max_reviewers = 2

[codeowners]
enabled = false

[notifications.slack_app]
enabled = true
channel = "C0ALY38CW3X"
base_url = {slack_base_url}
"#,
        platform = compiled_platform_connector_kind(),
        base_url = toml_string(server.api_base_url().as_str()),
        slack_base_url = toml_string(server.slack_api_base_url().as_str()),
    ));
    let mut envs = review_env(&server);
    envs.push(("MR_MILCHICK_CONFIG_PATH", config_path.clone()));
    envs.push(("MR_MILCHICK_SLACK_BOT_TOKEN", "xoxb-test".to_string()));

    let first = run_cli("refine", &[], &borrow_env(&envs));
    assert!(
        first.status.success(),
        "first refine failed: {}\n{}",
        String::from_utf8_lossy(&first.stderr),
        String::from_utf8_lossy(&first.stdout)
    );

    let first_stdout = String::from_utf8_lossy(&first.stdout);
    assert!(
        first_stdout.contains("[CommentPosted] Mr. Milchick summary comment"),
        "stdout was:\n{first_stdout}"
    );
    assert!(
        first_stdout.contains("[Notification SlackApp] delivered=true sent"),
        "stdout was:\n{first_stdout}"
    );

    let second = run_cli("refine", &[], &borrow_env(&envs));
    assert!(
        second.status.success(),
        "second refine failed: {}\n{}",
        String::from_utf8_lossy(&second.stderr),
        String::from_utf8_lossy(&second.stdout)
    );

    let second_stdout = String::from_utf8_lossy(&second.stdout);
    assert!(second_stdout.contains("[CommentSkippedAlreadyPresent] Mr. Milchick summary comment"));
    assert!(
        second_stdout
            .contains("[Notification SlackApp] delivered=false skipped because summary unchanged")
    );

    assert_eq!(server.request_count("POST", &review_comments_path()), 1);
    assert_eq!(server.note_bodies().len(), 1);
    assert_eq!(
        server.request_count("POST", "/slack/api/chat.postMessage"),
        2
    );
    let _ = fs::remove_file(config_path);
}

#[cfg(feature = "slack-app")]
#[test]
fn refine_mode_uses_partial_slack_template_override_from_config_file() {
    let server = MockGitLabServer::start();
    let config_body = r#"
[platform]
kind = "__PLATFORM__"
base_url = "__BASE_URL__"

[reviewers]
max_reviewers = 2

[notifications.slack_app]
enabled = true
channel = "C0ALY38CW3X"
base_url = "__SLACK_BASE_URL__"

[codeowners]
enabled = false

[templates.slack_app]
update_root = "Template override for {{mr_ref}}"
"#
    .replace("__PLATFORM__", compiled_platform_connector_kind())
    .replace("__BASE_URL__", &server.api_base_url())
    .replace("__SLACK_BASE_URL__", &server.slack_api_base_url());
    let config_path = write_temp_config(&config_body);

    let mut envs = review_env(&server);
    envs.push(("MR_MILCHICK_SLACK_BOT_TOKEN", "xoxb-test".to_string()));
    envs.push(("MR_MILCHICK_CONFIG_PATH", config_path.clone()));

    let output = run_cli("refine", &[], &borrow_env(&envs));
    assert!(
        output.status.success(),
        "refine failed: {}\n{}",
        String::from_utf8_lossy(&output.stderr),
        String::from_utf8_lossy(&output.stdout)
    );

    let bodies = server.request_bodies("POST", "/slack/api/chat.postMessage");
    assert_eq!(bodies.len(), 2);
    assert!(
        bodies
            .iter()
            .any(|body| body.contains("Template override for"))
    );
    assert!(
        bodies
            .iter()
            .any(|body| body.contains("No findings were produced."))
    );

    let _ = fs::remove_file(config_path);
}

#[cfg(feature = "slack-app")]
#[test]
fn refine_mode_invalid_template_override_falls_back_to_default_output() {
    let server = MockGitLabServer::start();
    let config_body = r#"
[platform]
kind = "__PLATFORM__"
base_url = "__BASE_URL__"

[reviewers]
max_reviewers = 2

[notifications.slack_app]
enabled = true
channel = "C0ALY38CW3X"
base_url = "__SLACK_BASE_URL__"

[codeowners]
enabled = false

[templates.slack_app]
update_root = "Template override for {{unknown_placeholder}}"
"#
    .replace("__PLATFORM__", compiled_platform_connector_kind())
    .replace("__BASE_URL__", &server.api_base_url())
    .replace("__SLACK_BASE_URL__", &server.slack_api_base_url());
    let config_path = write_temp_config(&config_body);

    let mut envs = review_env(&server);
    envs.push(("MR_MILCHICK_SLACK_BOT_TOKEN", "xoxb-test".to_string()));
    envs.push(("MR_MILCHICK_CONFIG_PATH", config_path.clone()));

    let output = run_cli("refine", &[], &borrow_env(&envs));
    assert!(
        output.status.success(),
        "refine failed: {}\n{}",
        String::from_utf8_lossy(&output.stderr),
        String::from_utf8_lossy(&output.stdout)
    );

    let bodies = server.request_bodies("POST", "/slack/api/chat.postMessage");
    assert_eq!(bodies.len(), 2);
    assert!(
        bodies
            .iter()
            .all(|body| !body.contains("unknown_placeholder"))
    );

    let _ = fs::remove_file(config_path);
}

#[test]
fn observe_mode_supports_fixture_without_ci_env_without_notification_sinks() {
    let output = run_cli(
        "observe",
        &["--fixture", "fixtures/first-notification.toml"],
        &[("RUST_LOG", "off")],
    );

    assert!(
        output.status.success(),
        "observe fixture failed: {}\n{}",
        String::from_utf8_lossy(&output.stderr),
        String::from_utf8_lossy(&output.stdout)
    );

    let stdout = String::from_utf8_lossy(&output.stdout);
    assert!(stdout.contains("If you run `refine`, it would:"));
    assert!(stdout.contains("No notification previews were produced."));
}

#[cfg(feature = "slack-app")]
#[test]
fn observe_mode_fixture_variant_override_can_force_update_preview() {
    let config_path = write_temp_config(&format!(
        r#"[platform]
kind = "{platform}"
base_url = {base_url}

[notifications.slack_app]
enabled = true
channel = "C0ALY38CW3X"
"#,
        platform = compiled_platform_connector_kind(),
        base_url = toml_string(default_platform_base_url()),
    ));
    let output = run_cli(
        "observe",
        &[
            "--fixture",
            "fixtures/first-notification.toml",
            "--fixture-variant",
            "update",
        ],
        &[
            ("MR_MILCHICK_CONFIG_PATH", config_path.as_str()),
            ("RUST_LOG", "off"),
        ],
    );

    assert!(
        output.status.success(),
        "observe fixture failed: {}\n{}",
        String::from_utf8_lossy(&output.stderr),
        String::from_utf8_lossy(&output.stdout)
    );

    let stdout = String::from_utf8_lossy(&output.stdout);
    assert!(stdout.contains("Notification previews:"));
    assert!(stdout.contains("Mr. Milchick - updates"));
    assert!(!stdout.contains("took a first look at"));

    let _ = fs::remove_file(config_path);
}

#[cfg(feature = "slack-app")]
#[test]
fn observe_mode_supports_fixture_without_ci_env_and_prints_notification_preview() {
    let config_path = write_temp_config(&format!(
        r#"[platform]
kind = "{platform}"
base_url = {base_url}

[notifications.slack_app]
enabled = true
channel = "C0ALY38CW3X"
"#,
        platform = compiled_platform_connector_kind(),
        base_url = toml_string(default_platform_base_url()),
    ));
    let output = run_cli(
        "observe",
        &["--fixture", "fixtures/first-notification.toml"],
        &[
            ("MR_MILCHICK_CONFIG_PATH", config_path.as_str()),
            ("RUST_LOG", "off"),
        ],
    );

    assert!(
        output.status.success(),
        "observe fixture failed: {}\n{}",
        String::from_utf8_lossy(&output.stderr),
        String::from_utf8_lossy(&output.stdout)
    );

    let stdout = String::from_utf8_lossy(&output.stdout);
    assert!(stdout.contains("Notification previews:"));
    assert!(stdout.contains("SlackApp"));
    assert!(stdout.contains("took a first look at"));

    let _ = fs::remove_file(config_path);
}

#[cfg(feature = "slack-app")]
#[test]
fn refine_mode_fixture_sends_slack_notifications_without_gitlab_env() {
    let server = MockGitLabServer::start();
    let slack_base_url = server.slack_api_base_url();
    let config_path = write_temp_config(&format!(
        r#"
[platform]
kind = "{platform}"
base_url = {base_url}

[notifications.slack_app]
enabled = true
channel = "C0ALY38CW3X"
base_url = {slack_base_url}
"#,
        platform = compiled_platform_connector_kind(),
        base_url = toml_string(default_platform_base_url()),
        slack_base_url = toml_string(slack_base_url.as_str()),
    ));

    let output = run_cli(
        "refine",
        &[
            "--fixture",
            "fixtures/first-notification.toml",
            "--send-notifications",
        ],
        &[
            ("MR_MILCHICK_CONFIG_PATH", config_path.as_str()),
            ("MR_MILCHICK_SLACK_BOT_TOKEN", "xoxb-test"),
            ("RUST_LOG", "off"),
        ],
    );

    assert!(
        output.status.success(),
        "refine fixture failed: {}\n{}",
        String::from_utf8_lossy(&output.stderr),
        String::from_utf8_lossy(&output.stdout)
    );

    let stdout = String::from_utf8_lossy(&output.stdout);
    assert!(stdout.contains("[Notification SlackApp] delivered=true sent"));
    assert_eq!(
        server.request_count("POST", "/slack/api/chat.postMessage"),
        2
    );
    assert_eq!(
        server.request_count_prefix("GET", "/api/v4/projects/"),
        0,
        "fixture mode should not talk to GitLab"
    );
    assert_eq!(
        server.request_count_prefix("GET", "/api/github/repos/"),
        0,
        "fixture mode should not talk to GitHub"
    );

    let _ = fs::remove_file(config_path);
}

#[cfg(feature = "slack-app")]
#[test]
fn refine_mode_fixture_update_reuses_existing_slack_thread_for_same_mr() {
    let server = MockGitLabServer::start();
    let slack_base_url = server.slack_api_base_url();
    let config_path = write_temp_config(&format!(
        r#"
[platform]
kind = "{platform}"
base_url = {base_url}

[notifications.slack_app]
enabled = true
channel = "C0ALY38CW3X"
base_url = {slack_base_url}
"#,
        platform = compiled_platform_connector_kind(),
        base_url = toml_string(default_platform_base_url()),
        slack_base_url = toml_string(slack_base_url.as_str()),
    ));

    let common_env = [
        ("MR_MILCHICK_CONFIG_PATH", config_path.as_str()),
        ("MR_MILCHICK_SLACK_BOT_TOKEN", "xoxb-test"),
        ("RUST_LOG", "off"),
    ];

    let first = run_cli(
        "refine",
        &[
            "--fixture",
            "fixtures/threaded-first-notification.toml",
            "--send-notifications",
        ],
        &common_env,
    );
    assert!(
        first.status.success(),
        "first threaded fixture failed: {}\n{}",
        String::from_utf8_lossy(&first.stderr),
        String::from_utf8_lossy(&first.stdout)
    );

    let update = run_cli(
        "refine",
        &[
            "--fixture",
            "fixtures/threaded-update-notification.toml",
            "--send-notifications",
        ],
        &common_env,
    );
    assert!(
        update.status.success(),
        "update threaded fixture failed: {}\n{}",
        String::from_utf8_lossy(&update.stderr),
        String::from_utf8_lossy(&update.stdout)
    );

    assert_eq!(
        server.request_count_prefix("GET", "/slack/api/conversations.history"),
        1
    );

    let bodies = server.request_bodies("POST", "/slack/api/chat.postMessage");
    assert_eq!(bodies.len(), 3);

    let update_payload: serde_json::Value =
        serde_json::from_str(&bodies[2]).expect("update Slack payload should parse");
    assert_eq!(
        update_payload["thread_ts"],
        serde_json::json!("1700000000.000001")
    );

    let update_text = update_payload["text"]
        .as_str()
        .expect("update text should be a string");
    assert!(update_text.contains("Mr. Milchick - updates on"));
    assert!(update_text.contains("MR #3997"));
    assert!(update_text.contains("Button spacing changed near the CTA"));

    let _ = fs::remove_file(config_path);
}

#[test]
fn send_notifications_requires_fixture() {
    let output = run_cli("refine", &["--send-notifications"], &[("RUST_LOG", "off")]);

    assert!(!output.status.success());
    assert!(String::from_utf8_lossy(&output.stderr).contains("--send-notifications"));
}

#[test]
fn fixture_variant_requires_fixture() {
    let output = run_cli(
        "observe",
        &["--fixture-variant", "first"],
        &[("RUST_LOG", "off")],
    );

    assert!(!output.status.success());
    assert!(String::from_utf8_lossy(&output.stderr).contains("--fixture-variant"));
}
