//! S1 — the Slack post-channel is CONFIG-driven, end-to-end over the real WS
//! provisioning path and into adapter construction.
//!
//! Before this fix the boot path read the post-channel from a keychain key
//! (`SLACK_POST_CHANNEL`) that NOTHING ever wrote → live Slack posted to
//! `channel:""` → `channel_not_found` → zero prompts. The post-channel is
//! configuration, not a secret: it now rides the host-gated
//! `messaging.config.set { channel: "slack", …, slack_channel }` call alongside
//! the tokens and persists into `messaging.json` (NOT the keychain).
//!
//! This gate is NOT mock-injected — it drives the REAL WS set path, then proves
//! (a) the persisted config carries `slack_channel_id` (and that bearer tokens
//! still never land on disk), and (b) the boot path reads the post-channel from
//! CONFIG, so the adapter constructed the way `spawn_channel_pollers` builds it
//! posts to that exact channel (the gap the mock-only gates hid).

mod slack_mock;
use slack_mock::MockSlackTransport;

use car_memgine::MemgineEngine;
use car_proto::{CreateHostApprovalRequest, HostApprovalStatus};
use car_secrets::{SecretRef, SecretStore, DEFAULT_SERVICE};
use car_server_core::host::HostState;
use car_server_core::messaging_config::{ChannelId, MessagingConfigStore};
use car_server_core::slack_adapter::{SLACK_APP_TOKEN_KEY, SLACK_BOT_TOKEN_KEY};
use car_server_core::{run_dispatch, ServerState, ServerStateConfig, SlackAdapter, SlackTransport};
use futures::{SinkExt, StreamExt};
use std::net::{Ipv4Addr, SocketAddr, SocketAddrV4};
use std::sync::Arc;
use tempfile::TempDir;
use tokio::net::TcpListener;
use tokio::sync::Mutex;
use tokio_tungstenite::{accept_async, connect_async, tungstenite::Message};

type Ws =
    tokio_tungstenite::WebSocketStream<tokio_tungstenite::MaybeTlsStream<tokio::net::TcpStream>>;

const HOST_TOKEN: &str = "test-host-token-ccccccccccccccccccccccccccc";
const BOT_BEARER: &str = "xoxb-test-bot-token-value";
const APP_BEARER: &str = "xapp-test-app-token-value";
const POST_CHANNEL: &str = "C0123POSTCH";
const SLACK_MEMBER: &str = "U012ABCDEF";

/// Serializes the process-global `HOME` / `CAR_SECRETS_FILE_DIR` env mutation
/// (one test per binary today; this pins the invariant if a second is added).
static ENV_MUTEX: std::sync::Mutex<()> = std::sync::Mutex::new(());

fn gated_state(journal_dir: std::path::PathBuf) -> Arc<ServerState> {
    let engine = Arc::new(Mutex::new(MemgineEngine::new(None)));
    let cfg = ServerStateConfig::new(journal_dir).with_shared_memgine(engine);
    let state = Arc::new(ServerState::with_config(cfg));
    state
        .install_host_token(HOST_TOKEN.to_string())
        .expect("install host token");
    state
}

async fn spawn_dispatcher(state: Arc<ServerState>) -> SocketAddr {
    let listener = TcpListener::bind(SocketAddr::V4(SocketAddrV4::new(
        Ipv4Addr::new(127, 0, 0, 1),
        0,
    )))
    .await
    .expect("bind loopback");
    let addr = listener.local_addr().expect("local_addr");
    tokio::spawn(async move {
        let (stream, peer) = listener.accept().await.expect("accept");
        let ws = accept_async(stream).await.expect("ws handshake");
        let (write, read) = ws.split();
        let _ = run_dispatch(read, Box::pin(write), peer.to_string(), state).await;
    });
    addr
}

async fn call(ws: &mut Ws, id: &str, method: &str, params: serde_json::Value) -> serde_json::Value {
    ws.send(Message::Text(
        serde_json::json!({ "jsonrpc": "2.0", "id": id, "method": method, "params": params })
            .to_string()
            .into(),
    ))
    .await
    .expect("send");
    let text = ws
        .next()
        .await
        .expect("frame")
        .expect("frame ok")
        .into_text()
        .expect("text")
        .to_string();
    serde_json::from_str(&text).expect("parse")
}

fn cleanup_secrets() {
    let store = SecretStore::new();
    let _ = store.delete(&SecretRef::new(DEFAULT_SERVICE, SLACK_BOT_TOKEN_KEY));
    let _ = store.delete(&SecretRef::new(DEFAULT_SERVICE, SLACK_APP_TOKEN_KEY));
}

#[tokio::test]
async fn slack_post_channel_is_config_driven_end_to_end() {
    let _env_guard = ENV_MUTEX.lock().unwrap_or_else(|e| e.into_inner());
    let journal = TempDir::new().unwrap();
    let fake_home = TempDir::new().unwrap();
    std::env::set_var("HOME", fake_home.path());
    let secrets_dir = TempDir::new().unwrap();
    std::env::set_var("CAR_SECRETS_FILE_DIR", secrets_dir.path());
    cleanup_secrets();

    let messaging_json = fake_home.path().join(".car").join("messaging.json");

    let state = gated_state(journal.path().to_path_buf());
    let addr = spawn_dispatcher(state).await;
    let (mut ws, _) = connect_async(format!("ws://{addr}"))
        .await
        .expect("connect");

    // Authenticate as host.
    let auth = call(
        &mut ws,
        "auth",
        "session.auth",
        serde_json::json!({ "host_token": HOST_TOKEN }),
    )
    .await;
    assert_eq!(auth["result"]["role"], "host", "host auth should succeed: {auth}");

    // Host-gated provisioning WITH the post-channel on the SAME call as the
    // tokens (mirrors the Swift saveSlackTokens()).
    let set = call(
        &mut ws,
        "h1",
        "messaging.config.set",
        serde_json::json!({
            "channel": "slack",
            "bot_token": BOT_BEARER,
            "app_token": APP_BEARER,
            "slack_channel": POST_CHANNEL,
        }),
    )
    .await;
    assert!(set.get("error").is_none(), "host slack provisioning should succeed: {set}");

    // (a) The persisted config carries the post-channel id IN messaging.json —
    //     and still NO bearer token (config, not secret).
    let on_disk = std::fs::read_to_string(&messaging_json)
        .expect("messaging.json must exist after provisioning");
    assert!(
        on_disk.contains("slack_channel_id") && on_disk.contains(POST_CHANNEL),
        "messaging.json must carry the slack_channel_id config: {on_disk}"
    );
    assert!(
        !on_disk.contains("xoxb") && !on_disk.contains("xapp"),
        "tokens must NOT be on disk: {on_disk}"
    );

    // The boot path reads the post-channel from this store method (NOT a
    // keychain key). Read it exactly as `spawn_channel_pollers` does.
    let boot_store = MessagingConfigStore::with_base_dir(fake_home.path().join(".car"));
    let post_channel = boot_store
        .slack_channel_id_for(ChannelId::Slack)
        .expect("read slack_channel_id")
        .expect("post-channel must be configured after provisioning");
    assert_eq!(
        post_channel, POST_CHANNEL,
        "boot reads the post-channel from CONFIG"
    );

    // (b) An adapter constructed the way the boot path builds it — with the
    //     CONFIG-resolved post-channel — posts to that exact channel. (Uses the
    //     mock transport to capture the outbound; the construction wiring is
    //     identical to spawn_channel_pollers's `SlackAdapter::new(.., post_channel)`.)
    boot_store.set_enabled_for(ChannelId::Slack, true).unwrap();
    boot_store
        .add_handle_for(ChannelId::Slack, SLACK_MEMBER)
        .unwrap();
    let host = Arc::new(HostState::new());
    let id = host
        .create_approval(
            None,
            CreateHostApprovalRequest {
                agent_id: None,
                action: "send wire transfer".to_string(),
                details: serde_json::Value::Null,
                options: vec![],
                system_level: true,
            },
        )
        .await
        .unwrap()
        .id;
    let transport = MockSlackTransport::new();
    let adapter = SlackAdapter::new(
        host.clone(),
        MessagingConfigStore::with_base_dir(fake_home.path().join(".car")),
        transport.clone(),
        post_channel.clone(),
    );
    adapter.post_prompt("send wire transfer", &id, "A0").await;
    let posts = transport.captured();
    assert_eq!(posts.len(), 1, "exactly one Block Kit post");
    assert_eq!(
        posts[0].channel, POST_CHANNEL,
        "the adapter posts to the CONFIG-driven post-channel (not channel:\"\")"
    );

    // And the inbound resolve still works (sanity).
    transport.inject_approve(&id, SLACK_MEMBER);
    let ev = transport.next_event().await.unwrap();
    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, "approve resolves in-process");

    cleanup_secrets();
}
