//! SlackTransport — ChatTransport implementation for the Slack Bot API.

use async_trait::async_trait;
use serde::{Deserialize, Serialize};

use crate::chat::{ChatTransport, MessageId};
use crate::slog;

// ── Slack API base URL (overridable for tests) ──────────────────────────

const SLACK_API_BASE: &str = "https://slack.com/api";

// ── SlackTransport ──────────────────────────────────────────────────────

/// Slack Bot API transport.
///
/// Sends messages via `POST {SLACK_API_BASE}/chat.postMessage` and edits
/// via `POST {SLACK_API_BASE}/chat.update`.
pub struct SlackTransport {
    bot_token: String,
    client: reqwest::Client,
    /// Optional base URL override for tests.
    api_base: String,
}

impl SlackTransport {
    /// Creates a new `SlackTransport` authenticated with the given bot token.
    pub fn new(bot_token: String) -> Self {
        Self {
            bot_token,
            client: reqwest::Client::new(),
            api_base: SLACK_API_BASE.to_string(),
        }
    }

    #[cfg(test)]
    fn with_api_base(bot_token: String, api_base: String) -> Self {
        Self {
            bot_token,
            client: reqwest::Client::new(),
            api_base,
        }
    }
}

// ── Slack API response types ────────────────────────────────────────────

#[derive(Deserialize, Debug)]
struct SlackApiResponse {
    ok: bool,
    #[serde(default)]
    error: Option<String>,
    /// Message timestamp (acts as message ID in Slack).
    #[serde(default)]
    ts: Option<String>,
}

// ── Slack API request types ─────────────────────────────────────────────

#[derive(Serialize)]
struct PostMessageRequest<'a> {
    channel: &'a str,
    text: &'a str,
}

#[derive(Serialize)]
struct UpdateMessageRequest<'a> {
    channel: &'a str,
    ts: &'a str,
    text: &'a str,
}

#[async_trait]
impl ChatTransport for SlackTransport {
    async fn send_message(
        &self,
        channel: &str,
        plain: &str,
        _html: &str,
    ) -> Result<MessageId, String> {
        slog!("[slack] send_message to {channel}: {plain:.80}");
        let url = format!("{}/chat.postMessage", self.api_base);

        let payload = PostMessageRequest {
            channel,
            text: plain,
        };

        let resp = self
            .client
            .post(&url)
            .bearer_auth(&self.bot_token)
            .json(&payload)
            .send()
            .await
            .map_err(|e| format!("Slack API request failed: {e}"))?;

        let status = resp.status();
        let resp_text = resp
            .text()
            .await
            .unwrap_or_else(|_| "<no body>".to_string());

        if !status.is_success() {
            return Err(format!("Slack API returned {status}: {resp_text}"));
        }

        let parsed: SlackApiResponse = serde_json::from_str(&resp_text)
            .map_err(|e| format!("Failed to parse Slack API response: {e} — body: {resp_text}"))?;

        if !parsed.ok {
            return Err(format!(
                "Slack API error: {}",
                parsed.error.unwrap_or_else(|| "unknown".to_string())
            ));
        }

        Ok(parsed.ts.unwrap_or_default())
    }

    async fn edit_message(
        &self,
        channel: &str,
        original_message_id: &str,
        plain: &str,
        _html: &str,
    ) -> Result<(), String> {
        slog!("[slack] edit_message in {channel}: ts={original_message_id}");
        let url = format!("{}/chat.update", self.api_base);

        let payload = UpdateMessageRequest {
            channel,
            ts: original_message_id,
            text: plain,
        };

        let resp = self
            .client
            .post(&url)
            .bearer_auth(&self.bot_token)
            .json(&payload)
            .send()
            .await
            .map_err(|e| format!("Slack chat.update request failed: {e}"))?;

        let status = resp.status();
        let resp_text = resp
            .text()
            .await
            .unwrap_or_else(|_| "<no body>".to_string());

        if !status.is_success() {
            return Err(format!("Slack chat.update returned {status}: {resp_text}"));
        }

        let parsed: SlackApiResponse = serde_json::from_str(&resp_text).map_err(|e| {
            format!("Failed to parse Slack chat.update response: {e} — body: {resp_text}")
        })?;

        if !parsed.ok {
            return Err(format!(
                "Slack chat.update error: {}",
                parsed.error.unwrap_or_else(|| "unknown".to_string())
            ));
        }

        Ok(())
    }

    async fn send_typing(&self, _channel: &str, _typing: bool) -> Result<(), String> {
        // Slack Bot API does not expose typing indicators for bots.
        Ok(())
    }
}

// ── Tests ───────────────────────────────────────────────────────────────

#[cfg(test)]
mod tests {
    use super::*;
    use std::sync::Arc;

    #[tokio::test]
    async fn transport_send_message_calls_slack_api() {
        let mut server = mockito::Server::new_async().await;
        let mock = server
            .mock("POST", "/chat.postMessage")
            .match_header("authorization", "Bearer xoxb-test-token")
            .with_body(r#"{"ok": true, "ts": "1234567890.123456"}"#)
            .create_async()
            .await;

        let transport = SlackTransport::with_api_base("xoxb-test-token".to_string(), server.url());

        let result = transport
            .send_message("C01ABCDEF", "hello", "<p>hello</p>")
            .await;
        assert!(result.is_ok());
        assert_eq!(result.unwrap(), "1234567890.123456");
        mock.assert_async().await;
    }

    #[tokio::test]
    async fn transport_send_message_handles_api_error() {
        let mut server = mockito::Server::new_async().await;
        server
            .mock("POST", "/chat.postMessage")
            .with_body(r#"{"ok": false, "error": "channel_not_found"}"#)
            .create_async()
            .await;

        let transport = SlackTransport::with_api_base("xoxb-test-token".to_string(), server.url());

        let result = transport.send_message("C_INVALID", "hello", "").await;
        assert!(result.is_err());
        assert!(
            result.unwrap_err().contains("channel_not_found"),
            "error should contain the Slack error code"
        );
    }

    #[tokio::test]
    async fn transport_edit_message_calls_chat_update() {
        let mut server = mockito::Server::new_async().await;
        let mock = server
            .mock("POST", "/chat.update")
            .match_header("authorization", "Bearer xoxb-test-token")
            .with_body(r#"{"ok": true, "ts": "1234567890.123456"}"#)
            .create_async()
            .await;

        let transport = SlackTransport::with_api_base("xoxb-test-token".to_string(), server.url());

        let result = transport
            .edit_message("C01ABCDEF", "1234567890.123456", "updated", "")
            .await;
        assert!(result.is_ok());
        mock.assert_async().await;
    }

    #[tokio::test]
    async fn transport_edit_message_handles_error() {
        let mut server = mockito::Server::new_async().await;
        server
            .mock("POST", "/chat.update")
            .with_body(r#"{"ok": false, "error": "message_not_found"}"#)
            .create_async()
            .await;

        let transport = SlackTransport::with_api_base("xoxb-test-token".to_string(), server.url());

        let result = transport
            .edit_message("C01ABCDEF", "bad-ts", "updated", "")
            .await;
        assert!(result.is_err());
        assert!(result.unwrap_err().contains("message_not_found"));
    }

    #[tokio::test]
    async fn transport_send_typing_succeeds() {
        let transport = SlackTransport::new("xoxb-test".to_string());
        assert!(transport.send_typing("C01ABCDEF", true).await.is_ok());
        assert!(transport.send_typing("C01ABCDEF", false).await.is_ok());
    }

    #[tokio::test]
    async fn transport_handles_http_error() {
        let mut server = mockito::Server::new_async().await;
        server
            .mock("POST", "/chat.postMessage")
            .with_status(500)
            .with_body("Internal Server Error")
            .create_async()
            .await;

        let transport = SlackTransport::with_api_base("xoxb-test-token".to_string(), server.url());

        let result = transport.send_message("C01ABCDEF", "hello", "").await;
        assert!(result.is_err());
        assert!(result.unwrap_err().contains("500"));
    }

    // ── ChatTransport trait satisfaction ─────────────────────────────────

    #[test]
    fn slack_transport_satisfies_trait() {
        fn assert_transport<T: ChatTransport>() {}
        assert_transport::<SlackTransport>();

        let _: Arc<dyn ChatTransport> = Arc::new(SlackTransport::new("xoxb-test".to_string()));
    }
}
