//! Mock Slack transport for the Slack gate tests (MC-12 / MC-13 / MC-8).
//!
//! Drives the [`SlackTransport`] seam with NO live Slack: it CAPTURES every
//! outbound `chat.postMessage` (so a test can assert the Block Kit buttons
//! carry the `approval_id`) and lets a test INJECT inbound events (button
//! clicks, pairing DMs) one at a time. Mirrors the `SpySender`/`MessageSender`
//! pattern the iMessage gates use.
#![allow(dead_code)]

use std::sync::{Arc, Mutex};

use async_trait::async_trait;
use car_server_core::slack_adapter::{
    build_block_kit_message, SlackInboundEvent, SlackTransport, APPROVE_ACTION_ID, DENY_ACTION_ID,
};

/// A captured outbound post: the channel, the human text, and the rendered
/// Block Kit JSON (so a test can read the button `value`s = approval_id).
#[derive(Debug, Clone)]
pub struct CapturedPost {
    pub channel: String,
    pub text: String,
    pub blocks: serde_json::Value,
}

impl CapturedPost {
    /// Extract the `value` carried by the Approve/Deny buttons (both carry the
    /// same `approval_id`). Returns the first button's value.
    pub fn button_value(&self) -> Option<String> {
        let elements = self.blocks["blocks"]
            .as_array()?
            .iter()
            .find(|b| b["type"] == "actions")?["elements"]
            .as_array()?;
        elements.first()?["value"].as_str().map(|s| s.to_string())
    }

    /// True iff this post carries BOTH an Approve and a Deny button (Block Kit
    /// actions block), each with the action_id the adapter expects.
    pub fn has_approve_deny_buttons(&self) -> bool {
        let Some(elements) = self.blocks["blocks"]
            .as_array()
            .and_then(|bs| bs.iter().find(|b| b["type"] == "actions"))
            .and_then(|a| a["elements"].as_array())
        else {
            return false;
        };
        let has_approve = elements
            .iter()
            .any(|e| e["action_id"] == APPROVE_ACTION_ID);
        let has_deny = elements.iter().any(|e| e["action_id"] == DENY_ACTION_ID);
        has_approve && has_deny
    }
}

/// A mock Slack transport. Captures outbound posts and serves injected inbound
/// events from an internal queue.
pub struct MockSlackTransport {
    pub posts: Mutex<Vec<CapturedPost>>,
    inbound: tokio::sync::Mutex<tokio::sync::mpsc::UnboundedReceiver<SlackInboundEvent>>,
    // `Option` so a test can DROP the only sender (`close_inbound`) to exhaust
    // the inbound source ⇒ `next_event` yields `None` (the `run()` close path).
    inbound_tx: Mutex<Option<tokio::sync::mpsc::UnboundedSender<SlackInboundEvent>>>,
}

impl MockSlackTransport {
    pub fn new() -> Arc<Self> {
        let (inbound_tx, inbound) = tokio::sync::mpsc::unbounded_channel();
        Arc::new(Self {
            posts: Mutex::new(Vec::new()),
            inbound: tokio::sync::Mutex::new(inbound),
            inbound_tx: Mutex::new(Some(inbound_tx)),
        })
    }

    /// Snapshot of every captured outbound post.
    pub fn captured(&self) -> Vec<CapturedPost> {
        self.posts.lock().unwrap().clone()
    }

    /// How many outbound posts were captured.
    pub fn post_count(&self) -> usize {
        self.posts.lock().unwrap().len()
    }

    /// Inject an inbound event the adapter will pull via `next_event`.
    pub fn inject(&self, event: SlackInboundEvent) {
        self.inbound_tx
            .lock()
            .unwrap()
            .as_ref()
            .expect("inbound channel open")
            .send(event)
            .expect("inbound channel open");
    }

    /// Drop the only inbound sender, exhausting the inbound source so the
    /// adapter's `next_event` resolves to `None` (the `run()` loop's close
    /// path). Idempotent.
    pub fn close_inbound(&self) {
        *self.inbound_tx.lock().unwrap() = None;
    }

    /// Inject an Approve button click for `approval_id` (the value the button
    /// carries), clicked by `user`.
    pub fn inject_approve(&self, approval_id: &str, user: &str) {
        self.inject(SlackInboundEvent::ButtonInteraction {
            action_id: APPROVE_ACTION_ID.to_string(),
            value: approval_id.to_string(),
            user: user.to_string(),
        });
    }

    /// Inject a Deny button click for `approval_id`.
    pub fn inject_deny(&self, approval_id: &str, user: &str) {
        self.inject(SlackInboundEvent::ButtonInteraction {
            action_id: DENY_ACTION_ID.to_string(),
            value: approval_id.to_string(),
            user: user.to_string(),
        });
    }

    /// Inject a pairing DM (`member` is the Slack member id, `code` the text).
    pub fn inject_pairing_dm(&self, member: &str, code: &str) {
        self.inject(SlackInboundEvent::PairingDm {
            user: member.to_string(),
            text: code.to_string(),
        });
    }
}

#[async_trait]
impl SlackTransport for MockSlackTransport {
    async fn post_message(
        &self,
        channel: &str,
        text: &str,
        approval_id: &str,
    ) -> Result<String, String> {
        let blocks = build_block_kit_message(text, approval_id);
        self.posts.lock().unwrap().push(CapturedPost {
            channel: channel.to_string(),
            text: text.to_string(),
            blocks,
        });
        // A deterministic fake `ts`.
        Ok(format!("1700000000.{:06}", self.post_count()))
    }

    async fn next_event(&self) -> Option<SlackInboundEvent> {
        self.inbound.lock().await.recv().await
    }
}
