//! MC-6 (Slack side) — anti-injection: a config-mutation-shaped Slack inbound
//! event mutates NOTHING.
//!
//! The Slack analogue of sc6 `inbound_text_that_is_not_pairing_code_never_binds`.
//! A Slack DM whose body is literally a config-mutation-shaped payload (e.g.
//! `{"messaging.config.set":{"enabled":true}}` or `enable slack`) routes to the
//! pairing validator only — and with no matching pairing code it binds nothing,
//! flips no enabled flag, sets no allowlist. The Slack inbound has exactly two
//! shapes (button → resolve-by-id, DM → validate-and-consume-pairing) and NO
//! edge to any config/token setter.

mod slack_mock;
use slack_mock::MockSlackTransport;

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

const SLACK_MEMBER: &str = "U012ABCDEF";
const SLACK_CHANNEL: &str = "D024BE91L";

#[tokio::test]
async fn slack_config_mutation_shaped_payload_mutates_nothing() {
    let dir = TempDir::new().unwrap();
    let store = MessagingConfigStore::with_base_dir(dir.path());
    store.set_enabled_for(ChannelId::Slack, true).unwrap();
    // Empty Slack allowlist; NO active pairing code.
    let host = Arc::new(HostState::new());
    let transport = MockSlackTransport::new();
    let adapter = SlackAdapter::new(
        host,
        MessagingConfigStore::with_base_dir(dir.path()),
        transport.clone(),
        SLACK_CHANNEL,
    );

    // Config-mutation-shaped DM bodies — none are the (absent) pairing code.
    for payload in [
        r#"{"messaging.config.set":{"enabled":true}}"#,
        "enable slack",
        r#"{"messaging.config.set":{"allowlisted_handles":["U999"]}}"#,
        "add U999 to allowlist",
    ] {
        transport.inject_pairing_dm(SLACK_MEMBER, payload);
        let ev = transport.next_event().await.unwrap();
        adapter.handle_event(&ev).await;
    }

    // Nothing mutated: the Slack allowlist is still empty (no member bound),
    // and the inbound path never touched the enabled flag (it stays whatever the
    // host set — here `true`, set via the HOST-gated mutator, never via inbound).
    assert!(
        store.allowlist_for(ChannelId::Slack).unwrap().is_empty(),
        "a config-mutation-shaped Slack payload must bind no member"
    );

    // And with an active pairing code in flight, the same config-shaped text
    // STILL binds nothing (only the exact code would).
    let _code = store.mint_pairing_code_for(ChannelId::Slack).unwrap();
    transport.inject_pairing_dm(SLACK_MEMBER, "enable slack");
    let ev = transport.next_event().await.unwrap();
    adapter.handle_event(&ev).await;
    assert!(
        store.allowlist_for(ChannelId::Slack).unwrap().is_empty(),
        "non-matching inbound text must never bind, even with a pairing in flight"
    );
    assert!(
        store
            .active_pairing_code_for(ChannelId::Slack)
            .unwrap()
            .is_some(),
        "non-matching text must not consume the active code"
    );
}

/// MC-6 at the WIRE-PARSE layer (S3): route a config-mutation-shaped payload
/// through the REAL `parse_events_api`, not a pre-built `PairingDm`. The parser
/// is the closed-set boundary — a hostile events_api envelope can only ever
/// yield a `PairingDm` (a candidate `(member, code)` for the constant-time
/// match) or `None` (`Ignore`). It has NO arm that manufactures a config
/// mutation, so the "looks like config.set" body is just an opaque candidate
/// code the pairing validator then rejects.
#[test]
fn slack_config_mutation_envelope_parses_to_nothing_dangerous() {
    // (a) A config-mutation-shaped DM in a real `message.im` events_api
    //     envelope parses to a PairingDm whose `text` is the (harmless)
    //     candidate code — NEVER a config setter. There is no parser arm that
    //     could mutate config; the worst it can do is hand the validator a
    //     wrong code.
    let hostile_dm = serde_json::json!({
        "event": {
            "type": "message",
            "channel": "D024BE91L",
            "channel_type": "im",
            "user": "U012ABCDEF",
            "text": r#"{"messaging.config.set":{"enabled":true,"allowlisted_handles":["U999"]}}"#,
            "ts": "1355517523.000005"
        }
    });
    match parse_events_api(&hostile_dm) {
        Some(SlackInboundEvent::PairingDm { user, text }) => {
            assert_eq!(user, "U012ABCDEF");
            // The body is carried verbatim as a candidate code — it is NOT a
            // config instruction. The only downstream effect is a (failing)
            // constant-time pairing match.
            assert!(text.contains("messaging.config.set"));
        }
        other => panic!("a config-shaped im DM must parse to a PairingDm candidate, got {other:?}"),
    }

    // (b) A config-mutation-shaped message on a NON-im channel (channel-wide)
    //     parses to None ⇒ Ignore. This is the literal MC-6 boundary: a
    //     config-mutation-shaped payload that is NOT a valid pairing DM
    //     produces NOTHING.
    let hostile_channel = serde_json::json!({
        "event": {
            "type": "message",
            "channel": "C0123CHANNEL",
            "channel_type": "channel",
            "user": "U012ABCDEF",
            "text": r#"{"messaging.config.set":{"enabled":true}}"#,
            "ts": "1355517523.000005"
        }
    });
    assert_eq!(
        parse_events_api(&hostile_channel),
        None,
        "a non-im config-mutation-shaped message must parse to None (MC-6 boundary)"
    );

    // (c) The bot's own echo of a config-shaped string is suppressed.
    let echo = serde_json::json!({
        "event": {
            "type": "message",
            "channel": "D024BE91L",
            "channel_type": "im",
            "bot_id": "BB12033",
            "text": r#"{"messaging.config.set":{"enabled":true}}"#,
            "ts": "1355517523.000010"
        }
    });
    assert_eq!(
        parse_events_api(&echo),
        None,
        "a bot-echoed config-shaped string must parse to None (echo suppression)"
    );
}
