//! MC-12 (authz) — the Slack button-resolve path enforces BOTH trust gates the
//! iMessage inbound path enforces. These are the NEGATIVE cases the original
//! Slack adapter shipped UNCOVERED — which is why the gap shipped:
//!
//! 1. **Allowlist (SC-7):** a button click from a NON-allowlisted member (a
//!    member of the configured channel who was never paired) is a silent no-op
//!    — no resolve, no `approval.resolved`. Without this, the allowlist (the
//!    whole point of pairing) was inert on Slack: any channel member could
//!    resolve.
//! 2. **Eligibility (MC-7):** a crafted `block_actions` whose `value` names a
//!    `ws.method:`-prefixed blocking-gate row (`client_id: None`), or a
//!    session-owned (`client_id: Some`) row, resolves NOTHING — exactly like the
//!    iMessage path. Without this, a hostile payload could resolve a high-risk
//!    gate the user never acked (privilege escalation).
//!
//! The happy path (an allowlisted member clicking Approve on an eligible
//! approval STILL resolves) is asserted last so a too-aggressive gate cannot
//! pass this file silently.

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, SlackInboundEvent, APPROVE_ACTION_ID};
use std::sync::Arc;
use tempfile::TempDir;

/// The PAIRED (allowlisted) Slack member and the channel to post into.
const SLACK_MEMBER: &str = "U012ABCDEF";
/// A DIFFERENT member who is in the channel but was NEVER paired — not on the
/// allowlist. Their click must be a no-op.
const SLACK_INTRUDER: &str = "U999ZZZ000";
const SLACK_CHANNEL: &str = "D024BE91L";

/// Enabled-Slack harness with `SLACK_MEMBER` paired (allowlisted).
struct SlackHarness {
    host: Arc<HostState>,
    // Held so the transport (and its inbound channel) outlives the adapter; the
    // authz tests drive `handle_event` directly rather than via the socket, so
    // it is not read after construction.
    _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: transport,
            adapter,
            _dir: dir,
        }
    }

    /// Seed a system-level fire-and-return (eligible) approval and return its id.
    async fn seed_eligible(&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
    }

    /// Seed a blocking-gate (`ws.method:*`) row — `client_id: None`, system-level
    /// — which `is_eligible_pending` EXCLUDES (the v2-deferred high-risk-method
    /// gate). A channel transport must never resolve one.
    async fn seed_ws_method(&self, method: &str) -> String {
        let req = CreateHostApprovalRequest {
            agent_id: None,
            action: format!("ws.method:{method}"),
            details: serde_json::Value::Null,
            options: vec![],
            system_level: true,
        };
        self.host.create_approval(None, req).await.unwrap().id
    }

    /// Seed a SESSION-OWNED (`client_id: Some(...)`, `system_level: false`) row —
    /// owned by a WS session, which `is_eligible_pending` EXCLUDES (requires
    /// `client_id.is_none()`). A channel transport must never resolve one.
    async fn seed_session_owned(&self, action: &str) -> String {
        let req = CreateHostApprovalRequest {
            agent_id: None,
            action: action.to_string(),
            details: serde_json::Value::Null,
            options: vec![],
            system_level: false,
        };
        self.host
            .create_approval(Some("owning-ws-session"), 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 resolved_count(&self) -> usize {
        self.host
            .events(500)
            .await
            .into_iter()
            .filter(|e| e.kind == "approval.resolved")
            .count()
    }

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

    /// Dispatch an Approve click for `approval_id` from `user` through the FULL
    /// adapter path (`handle_event`), exactly as a live socket event would.
    async fn click_approve(&self, approval_id: &str, user: &str) {
        let ev = SlackInboundEvent::ButtonInteraction {
            action_id: APPROVE_ACTION_ID.to_string(),
            value: approval_id.to_string(),
            user: user.to_string(),
        };
        self.adapter.handle_event(&ev).await;
    }
}

/// Gate 1 (allowlist / SC-7): a NON-allowlisted member clicking Approve on an
/// otherwise-eligible approval resolves NOTHING — the approval stays Pending,
/// zero `approval.resolved`.
#[tokio::test]
async fn non_allowlisted_click_resolves_nothing() {
    let h = SlackHarness::enabled_paired();
    let id = h.seed_eligible("send wire transfer").await;

    // The intruder is in the channel but was never paired.
    h.click_approve(&id, SLACK_INTRUDER).await;

    assert!(
        !h.is_resolved(&id).await,
        "a non-allowlisted clicker cannot resolve — approval stays Pending"
    );
    assert_eq!(
        h.resolved_count().await,
        0,
        "a non-allowlisted click emits zero approval.resolved"
    );
    assert_eq!(
        h.resolution(&id).await,
        None,
        "no resolution stamped from a non-allowlisted click"
    );
}

/// Gate 2 (eligibility / MC-7): a crafted button whose `value` names a
/// `ws.method:`-prefixed blocking-gate row (`client_id: None`), from an
/// ALLOWLISTED user, resolves NOTHING — the eligibility gate excludes it.
#[tokio::test]
async fn ws_method_value_not_resolved_even_by_allowlisted_user() {
    let h = SlackHarness::enabled_paired();
    let gate_id = h.seed_ws_method("dangerous_method").await;

    // The clicker IS allowlisted — so this isolates the eligibility gate.
    h.click_approve(&gate_id, SLACK_MEMBER).await;

    assert!(
        !h.is_resolved(&gate_id).await,
        "a ws.method: blocking-gate row is NOT resolvable from a Slack button"
    );
    assert_eq!(
        h.resolved_count().await,
        0,
        "no approval.resolved for a ws.method: gate row"
    );
}

/// Gate 2 (eligibility / MC-7): a button whose `value` names a session-owned
/// (`client_id: Some`) row, from an ALLOWLISTED user, resolves NOTHING — the
/// eligibility gate requires `client_id.is_none()`.
#[tokio::test]
async fn session_owned_value_not_resolved_even_by_allowlisted_user() {
    let h = SlackHarness::enabled_paired();
    let owned_id = h.seed_session_owned("ship it").await;

    h.click_approve(&owned_id, SLACK_MEMBER).await;

    assert!(
        !h.is_resolved(&owned_id).await,
        "a session-owned (client_id: Some) row is NOT resolvable from a Slack button"
    );
    assert_eq!(
        h.resolved_count().await,
        0,
        "no approval.resolved for a session-owned row"
    );
}

/// Sanity / happy path: an ALLOWLISTED member clicking Approve on an ELIGIBLE
/// approval STILL resolves (the two gates must not break MC-12/MC-8). Guards
/// against a too-aggressive gate that no-ops everything.
#[tokio::test]
async fn allowlisted_click_on_eligible_still_resolves() {
    let h = SlackHarness::enabled_paired();
    let id = h.seed_eligible("send wire transfer").await;

    h.click_approve(&id, SLACK_MEMBER).await;

    assert!(
        h.is_resolved(&id).await,
        "an allowlisted member resolving an eligible approval STILL works"
    );
    assert_eq!(
        h.resolution(&id).await.as_deref(),
        Some("approve"),
        "resolution string is exactly 'approve'"
    );
    assert_eq!(
        h.resolved_count().await,
        1,
        "exactly one approval.resolved on the happy path"
    );
}
