//! MC-12 — Slack end-to-end via the mockable transport seam (NO live Slack).
//!
//! An eligible approval → the mock Slack transport captures a Block Kit post
//! with Approve/Deny buttons whose `value` carries the `approval_id`. Injecting
//! an Approve button interaction resolves the approval in-process (status
//! Resolved, exactly one `approval.resolved`). Injecting Deny denies. A BOGUS /
//! already-resolved `approval_id` no-ops: no panic, nothing resolves, no second
//! `approval.resolved`.

mod slack_mock;
use slack_mock::MockSlackTransport;

use car_proto::{CreateHostApprovalRequest, HostApprovalStatus};
use car_server_types::host::HostState;
use car_messaging::messaging_config::{ChannelId, MessagingConfigStore};
use car_messaging::slack_adapter::SlackAdapter;
use car_messaging::slack_adapter::SlackTransport; // for `next_event` on the mock
use std::sync::Arc;
use tempfile::TempDir;

/// A test Slack member id (the paired approver) and the channel to post into.
const SLACK_MEMBER: &str = "U012ABCDEF";
const SLACK_CHANNEL: &str = "D024BE91L";

/// Build an enabled-Slack harness: host, temp-rooted config store with Slack
/// enabled + the member paired, the mock transport, and the adapter.
struct SlackHarness {
    host: Arc<HostState>,
    transport: Arc<MockSlackTransport>,
    adapter: SlackAdapter,
    _dir: TempDir,
}

impl SlackHarness {
    fn enabled_paired() -> Self {
        let dir = TempDir::new().expect("tempdir");
        let store = MessagingConfigStore::with_base_dir(dir.path());
        store.set_enabled_for(ChannelId::Slack, true).unwrap();
        store.add_handle_for(ChannelId::Slack, SLACK_MEMBER).unwrap();
        let host = Arc::new(HostState::new());
        let transport = MockSlackTransport::new();
        let adapter = SlackAdapter::new(
            host.clone(),
            MessagingConfigStore::with_base_dir(dir.path()),
            transport.clone(),
            SLACK_CHANNEL,
        );
        Self {
            host,
            transport,
            adapter,
            _dir: dir,
        }
    }

    async fn seed_fire_and_return(&self, action: &str) -> String {
        let req = CreateHostApprovalRequest {
            agent_id: None,
            action: action.to_string(),
            details: serde_json::Value::Null,
            options: vec![],
            system_level: true,
        };
        self.host.create_approval(None, req).await.unwrap().id
    }

    async fn is_resolved(&self, id: &str) -> bool {
        self.host
            .approvals()
            .await
            .into_iter()
            .find(|a| a.id == id)
            .map(|a| a.status == HostApprovalStatus::Resolved)
            .unwrap_or(false)
    }

    async fn resolution(&self, id: &str) -> Option<String> {
        self.host
            .approvals()
            .await
            .into_iter()
            .find(|a| a.id == id)
            .and_then(|a| a.resolution)
    }

    async fn resolved_count(&self) -> usize {
        self.host
            .events(500)
            .await
            .into_iter()
            .filter(|e| e.kind == "approval.resolved")
            .count()
    }
}

#[tokio::test]
async fn slack_approve_button_resolves_approval() {
    let h = SlackHarness::enabled_paired();
    let id = h.seed_fire_and_return("send wire transfer").await;

    // Outbound: post the Block Kit prompt carrying the approval_id in the
    // button value. (The fan-out coordinator would supply a code; here we drive
    // the adapter's post directly with a stand-in shared code.)
    h.adapter.post_prompt("send wire transfer", &id, "A0").await;

    let posts = h.transport.captured();
    assert_eq!(posts.len(), 1, "exactly one Block Kit post, got {posts:?}");
    let post = &posts[0];
    assert_eq!(post.channel, SLACK_CHANNEL, "posted to the configured channel");
    assert!(
        post.has_approve_deny_buttons(),
        "the post carries Approve AND Deny buttons: {:?}",
        post.blocks
    );
    assert_eq!(
        post.button_value().as_deref(),
        Some(id.as_str()),
        "the button value carries the approval_id directly (no CodeMap)"
    );

    // Inbound: inject an Approve button interaction → resolve in-process.
    h.transport.inject_approve(&id, SLACK_MEMBER);
    let ev = h.transport.next_event().await.expect("an injected event");
    h.adapter.handle_event(&ev).await;

    assert!(h.is_resolved(&id).await, "the approval is Resolved");
    assert_eq!(
        h.resolution(&id).await.as_deref(),
        Some("approve"),
        "resolution string is exactly 'approve'"
    );
    assert_eq!(
        h.resolved_count().await,
        1,
        "approval.resolved fired exactly once"
    );
}

#[tokio::test]
async fn slack_deny_button_denies_approval() {
    let h = SlackHarness::enabled_paired();
    let id = h.seed_fire_and_return("delete prod database").await;
    h.adapter.post_prompt("delete prod database", &id, "A0").await;

    h.transport.inject_deny(&id, SLACK_MEMBER);
    let ev = h.transport.next_event().await.expect("an injected event");
    h.adapter.handle_event(&ev).await;

    assert!(h.is_resolved(&id).await, "the approval is Resolved (denied)");
    assert_eq!(
        h.resolution(&id).await.as_deref(),
        Some("deny"),
        "resolution string is exactly 'deny'"
    );
}

#[tokio::test]
async fn slack_bogus_approval_id_resolves_nothing_no_panic() {
    let h = SlackHarness::enabled_paired();
    let id = h.seed_fire_and_return("post to twitter").await;
    h.adapter.post_prompt("post to twitter", &id, "A0").await;

    // (1) A BOGUS approval_id button click → no panic, nothing resolves.
    h.transport.inject_approve("does-not-exist-id", SLACK_MEMBER);
    let ev = h.transport.next_event().await.unwrap();
    h.adapter.handle_event(&ev).await;
    assert!(!h.is_resolved(&id).await, "the real approval is untouched");
    assert_eq!(h.resolved_count().await, 0, "no resolve from a bogus id");

    // (2) Resolve the real one (Approve), then click again → idempotent no-op,
    // NO second approval.resolved.
    h.transport.inject_approve(&id, SLACK_MEMBER);
    let ev = h.transport.next_event().await.unwrap();
    h.adapter.handle_event(&ev).await;
    assert!(h.is_resolved(&id).await, "the real approval resolved");
    assert_eq!(h.resolved_count().await, 1, "exactly one resolve");

    // A second click on the ALREADY-RESOLVED approval → no panic, no 2nd event.
    h.transport.inject_deny(&id, SLACK_MEMBER);
    let ev = h.transport.next_event().await.unwrap();
    h.adapter.handle_event(&ev).await;
    assert_eq!(
        h.resolved_count().await,
        1,
        "a click on an already-resolved approval emits no second approval.resolved"
    );
    assert_eq!(
        h.resolution(&id).await.as_deref(),
        Some("approve"),
        "the first resolution stands; the later deny does not overwrite it"
    );
}

#[tokio::test]
async fn slack_disabled_channel_does_not_post_or_resolve() {
    // Feature OFF on Slack ⇒ no outbound post and inbound is a no-op.
    let dir = TempDir::new().unwrap();
    // Slack left DISABLED (default) — the store under this dir is implicitly all-off.
    let host = Arc::new(HostState::new());
    let transport = MockSlackTransport::new();
    let adapter = SlackAdapter::new(
        host.clone(),
        MessagingConfigStore::with_base_dir(dir.path()),
        transport.clone(),
        SLACK_CHANNEL,
    );
    let req = CreateHostApprovalRequest {
        agent_id: None,
        action: "dangerous".to_string(),
        details: serde_json::Value::Null,
        options: vec![],
        system_level: true,
    };
    let id = host.create_approval(None, req).await.unwrap().id;

    adapter.post_prompt("dangerous", &id, "A0").await;
    assert_eq!(transport.post_count(), 0, "feature OFF ⇒ zero posts");

    // Even an injected Approve does nothing while disabled.
    let ev = car_messaging::slack_adapter::SlackInboundEvent::ButtonInteraction {
        action_id: car_messaging::slack_adapter::APPROVE_ACTION_ID.to_string(),
        value: id.clone(),
        user: SLACK_MEMBER.to_string(),
    };
    adapter.handle_event(&ev).await;
    let resolved = host
        .approvals()
        .await
        .into_iter()
        .find(|a| a.id == id)
        .map(|a| a.status == HostApprovalStatus::Resolved)
        .unwrap_or(false);
    assert!(!resolved, "feature OFF ⇒ inbound resolve is a no-op");
}
