//! SC-6 (config-gate half) over a real `run_dispatch` WebSocket session.
//!
//! The iMessage approval-transport config surface (`messaging.config.*` /
//! `messaging.pairing.*`, Unit 3) is the ONLY allowlist/config-mutation
//! path in the system. It is gated on the SAME trust root that guards all
//! config mutation today (`require_approval_authority`): when a host token
//! is configured, a connection must be `session.is_host` (presenting the
//! per-launch host token via `session.auth { host_token }`) to mutate or
//! read the config. An inbound iMessage carries neither credential.
//!
//! This test proves: with a host token configured, a NON-host connection
//! is REJECTED on `messaging.config.set` (and the other three methods);
//! after authenticating as host, the same call SUCCEEDS and actually
//! mutates the store. We point `HOME` at a temp dir so the handlers'
//! `~/.car/messaging.json` lands in the fixture, not the developer's home.

use car_server_core::{run_dispatch, ServerState, ServerStateConfig};
use car_memgine::MemgineEngine;
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-aaaaaaaaaaaaaaaaaaaaaaaaaaa";

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));
    // Installing a host token activates `require_approval_authority`:
    // now non-host connections are rejected on config mutation. Without
    // it, the daemon is in dev/embedder mode (the developer is the
    // authority) and the gate is a no-op.
    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")
}

/// SC-6 (config-gate half): the `messaging.config.*` / `messaging.pairing.*`
/// handlers REJECT a caller that is neither `session.is_host` nor presenting
/// the per-launch local auth token; after authenticating as host the same
/// call succeeds and mutates the store.
#[tokio::test]
async fn config_mutation_requires_host_or_local_auth() {
    let journal = TempDir::new().unwrap();
    // Redirect `~/.car/` to a temp HOME so the handlers don't touch the
    // developer's real config. SAFETY: tests in this file are the only
    // thing in this process; setting HOME here is fine for the test bin.
    let fake_home = TempDir::new().unwrap();
    std::env::set_var("HOME", fake_home.path());

    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");

    // 1. NON-host connection: every messaging.* method is rejected.
    for (i, (method, params)) in [
        ("messaging.config.get", serde_json::json!({})),
        (
            "messaging.config.set",
            serde_json::json!({ "add_handles": ["+15551234567"] }),
        ),
        ("messaging.pairing.start", serde_json::json!({})),
        ("messaging.pairing.status", serde_json::json!({})),
    ]
    .into_iter()
    .enumerate()
    {
        let resp = call(&mut ws, &format!("n{i}"), method, params).await;
        assert!(
            resp.get("error").is_some(),
            "{method} must REJECT a non-host caller, got: {resp}"
        );
        assert!(
            resp.get("result").is_none(),
            "{method} must not return a result to a non-host caller"
        );
    }

    // 2. Authenticate as the host-management role (per-launch host token).
    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}");

    // 3. As host, the same mutating call SUCCEEDS and actually mutates.
    let set = call(
        &mut ws,
        "h1",
        "messaging.config.set",
        serde_json::json!({ "enabled": true, "add_handles": ["+15551234567"] }),
    )
    .await;
    assert!(set.get("error").is_none(), "host config.set should succeed: {set}");
    assert_eq!(set["result"]["enabled"], true);
    assert_eq!(
        set["result"]["allowlisted_handles"]
            .as_array()
            .unwrap()
            .len(),
        1
    );

    // 4. As host, get reflects the mutation.
    let get = call(&mut ws, "h2", "messaging.config.get", serde_json::json!({})).await;
    assert_eq!(get["result"]["enabled"], true);
    assert_eq!(get["result"]["allowlisted_handles"][0], "+15551234567");

    // 5. As host, pairing.start mints a code and pairing.status surfaces it.
    let start = call(&mut ws, "h3", "messaging.pairing.start", serde_json::json!({})).await;
    let code = start["result"]["pairing_code"].as_str().unwrap().to_string();
    assert_eq!(code.len(), 43, "high-entropy base64url-no-pad code");
    assert_eq!(start["result"]["config"]["pairing_active"], true);

    let status = call(&mut ws, "h4", "messaging.pairing.status", serde_json::json!({})).await;
    assert_eq!(status["result"]["pairing_active"], true);
    assert_eq!(status["result"]["pairing_code"], code);
}
