//! Formation isolation at the backend level: two `AutomergeBackend`s in
//! *different* formations (different `formation_id` + `base64_shared_key`)
//! must not connect or exchange documents.
//!
//! peat-mesh enforces this in the `CAP_AUTOMERGE_ALPN` formation-key
//! handshake (`MeshSyncTransport`): the connector compares the acceptor's
//! advertised `formation_id` against its own and bails on mismatch, and the
//! acceptor independently verifies the HMAC challenge-response keyed by the
//! shared secret. There were unit tests for the handshake primitives but no
//! end-to-end pin that two fully-built backends actually refuse to sync —
//! this closes that gap.
//!
//! Context: a peat-node integration test (`formation_isolation_test`,
//! peat-node#138) appeared to show a cross-formation document "leak." Root
//! cause was a flaky test harness (fixed ports + leaked servers), not a
//! product bug — this test is the library-level proof that isolation holds,
//! and a regression guard so a future handshake change can't silently open
//! the boundary.

#![cfg(feature = "automerge-backend")]

use std::collections::HashMap;
use std::net::{IpAddr, Ipv4Addr, SocketAddr};
use std::sync::Arc;
use std::time::Duration;

use peat_mesh::security::FormationKey;
use peat_mesh::sync::{
    AutomergeBackend, AutomergeBackendConfig, BackendConfig, DataSyncBackend, Document, SyncEngine,
};
use tempfile::TempDir;

/// Build an iroh-backed backend in formation `formation_id` with shared key
/// `shared_key`, bound to an OS-chosen v4 localhost port. The `TempDir` owns
/// on-disk state and must outlive the backend.
async fn build_backend(formation_id: &str, shared_key: &str) -> (Arc<AutomergeBackend>, TempDir) {
    let dir = TempDir::new().expect("tempdir");
    let mut cfg = AutomergeBackendConfig::default();
    cfg.data_dir = dir.path().to_path_buf();
    cfg.formation_id = formation_id.to_string();
    cfg.base64_shared_key = shared_key.to_string();
    cfg.iroh_bind_addr = Some(SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 0));
    let backend = AutomergeBackend::with_iroh(cfg).await.expect("with_iroh");
    backend
        .initialize(BackendConfig {
            app_id: formation_id.to_string(),
            persistence_dir: dir.path().to_path_buf(),
            shared_key: None,
            transport: peat_mesh::sync::TransportConfig::default(),
            extra: std::collections::HashMap::new(),
        })
        .await
        .expect("initialize");
    (backend, dir)
}

/// One peer's `(EndpointId, "ip:port")` for a direct dial.
fn dial_target(backend: &Arc<AutomergeBackend>) -> (String, String) {
    let addr = backend.blob_store().endpoint().addr();
    let id = backend.blob_store().endpoint().id().to_string();
    let ip = addr
        .addrs
        .iter()
        .find_map(|a| match a {
            iroh::TransportAddr::Ip(s) => Some(s.to_string()),
            _ => None,
        })
        .expect("backend has a bound IP address");
    (id, ip)
}

#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
async fn different_formations_neither_connect_nor_exchange() {
    // Distinct formations: different formation_id AND different shared key.
    let key_alpha = FormationKey::generate_secret();
    let key_bravo = FormationKey::generate_secret();
    assert_ne!(
        key_alpha, key_bravo,
        "test needs two distinct formation keys"
    );

    let (alpha, _da) = build_backend("alpha", &key_alpha).await;
    let (bravo, _db) = build_backend("bravo", &key_bravo).await;

    // Cross-populate address resolution so the dial reaches the peer (the
    // rejection we're testing is the formation handshake, not unreachability).
    let addr_alpha = alpha.blob_store().endpoint().addr();
    let addr_bravo = bravo.blob_store().endpoint().addr();
    alpha
        .blob_store()
        .memory_lookup()
        .add_endpoint_info(addr_bravo);
    bravo
        .blob_store()
        .memory_lookup()
        .add_endpoint_info(addr_alpha);

    alpha.start_sync().await.expect("start_sync alpha");
    bravo.start_sync().await.expect("start_sync bravo");

    // bravo dials alpha. Different formations → the handshake must reject.
    let (alpha_id, alpha_ip) = dial_target(&alpha);
    let connect = bravo.connect_to_peer(&alpha_id, &[alpha_ip]).await;
    assert!(
        connect.is_err(),
        "cross-formation connect must be rejected by the formation handshake, got {connect:?}"
    );
    let err = connect.unwrap_err().to_string().to_lowercase();
    assert!(
        err.contains("formation"),
        "rejection should be a formation-auth failure, got: {err}"
    );

    // Even so, write on alpha and confirm nothing reaches bravo.
    let mut fields = HashMap::new();
    fields.insert(
        "formation".to_string(),
        serde_json::Value::String("alpha".to_string()),
    );
    alpha
        .document_store()
        .upsert("isolation-test", Document::with_id("from-alpha", fields))
        .await
        .expect("upsert on alpha");

    // Give any (incorrect) cross-formation sync ample time to surface.
    tokio::time::sleep(Duration::from_secs(3)).await;

    let leaked = bravo
        .document_store()
        .get("isolation-test", &"from-alpha".to_string())
        .await
        .expect("get on bravo");
    assert!(
        leaked.is_none(),
        "document leaked across formations: bravo received alpha's document"
    );

    alpha.shutdown().await.expect("shutdown alpha");
    bravo.shutdown().await.expect("shutdown bravo");
}
