//! Dual-server comparison testing infrastructure.
//!
//! Sends identical requests to both the Rust `better-auth-rs` server and the
//! reference Node.js `better-auth` server, then compares response *shapes*
//! (not exact values, since IDs / tokens will differ).
//!
//! ## How it works
//!
//! 1. The test harness **automatically starts** the reference Node.js server
//!    (`compat-tests/reference-server/server.mjs`) as a child process.
//! 2. It waits for the `/__health` endpoint to respond (up to 10 seconds).
//! 3. Each test sends the same request to both servers and compares the JSON
//!    response *shape* (field names + value types, not values).
//! 4. The child process is killed when the RAII guard is dropped.
//!
//! ## Prerequisites
//!
//! The reference server dependencies must be installed before running:
//!
//! ```bash
//! cd compat-tests/reference-server && npm install
//! ```
//!
//! If `node_modules` is missing, all tests are skipped with a diagnostic
//! message — they never fail CI.
//!
//! ## Cookie-based auth
//!
//! The TypeScript better-auth uses cookie-based session auth
//! (`set-cookie: better-auth.session_token=...`), NOT Bearer tokens.
//! The `RefClient` in this module captures and forwards cookies to match
//! the real TS behavior.

mod compat;

use compat::dual_http_compare::{compare_shapes, filter_known_diffs, json_shape};
use compat::helpers::*;
use compat::reference_node::{RefClient, SERIAL, ensure_reference_server};
use serde_json::Value;

// ---------------------------------------------------------------------------
// Dual-server comparison tests
// ---------------------------------------------------------------------------

/// Compare sign-up response shapes between Rust and reference servers.
#[tokio::test]
async fn dual_server_signup_shape_comparison() {
    let _lock = SERIAL.lock().await;
    if !ensure_reference_server().await {
        eprintln!("[dual-server] SKIPPED: reference server not available");
        return;
    }

    let auth = create_server_binary_equivalent_memory_auth().await;
    let mut ref_client = RefClient::new();

    let email = unique_email("dual_signup");
    let signup_body = serde_json::json!({
        "name": "Dual Test User",
        "email": email,
        "password": "password123"
    });

    // Rust server
    let (rust_status, rust_body) =
        send_request(&auth, post_json("/sign-up/email", signup_body.clone())).await;

    // Reference server (also captures session cookie)
    let (ref_status, ref_body) = ref_client
        .post("/sign-up/email", &signup_body)
        .await
        .expect("Reference server request failed");

    eprintln!("[dual-server] POST /sign-up/email");
    eprintln!("  Rust status:  {}", rust_status);
    eprintln!("  Ref  status:  {}", ref_status);

    let all_diffs = compare_shapes(&json_shape(&rust_body), &json_shape(&ref_body), "");
    let unexpected = filter_known_diffs(&all_diffs);

    if all_diffs.is_empty() {
        eprintln!("  Shape: EXACT MATCH");
    } else {
        eprintln!(
            "  Shape: {} total diffs ({} known, {} unexpected)",
            all_diffs.len(),
            all_diffs.len() - unexpected.len(),
            unexpected.len()
        );
        for d in &all_diffs {
            eprintln!("    {}", d);
        }
    }

    assert!(
        unexpected.is_empty(),
        "Unexpected signup shape differences:\n{}",
        unexpected.join("\n")
    );
}

/// Compare sign-in response shapes.
#[tokio::test]
async fn dual_server_signin_shape_comparison() {
    let _lock = SERIAL.lock().await;
    if !ensure_reference_server().await {
        eprintln!("[dual-server] SKIPPED: reference server not available");
        return;
    }

    let auth = create_server_binary_equivalent_memory_auth().await;
    let mut ref_client = RefClient::new();

    let email = unique_email("dual_signin");
    let signup_body = serde_json::json!({
        "name": "Dual Signin User",
        "email": email,
        "password": "password123"
    });

    // Sign up on both servers
    send_request(&auth, post_json("/sign-up/email", signup_body.clone())).await;
    let _ = ref_client.post("/sign-up/email", &signup_body).await;

    let signin_body = serde_json::json!({
        "email": email,
        "password": "password123"
    });

    // Rust server
    let (rust_status, rust_body) =
        send_request(&auth, post_json("/sign-in/email", signin_body.clone())).await;

    // Reference server (captures session cookie)
    let (ref_status, ref_body) = ref_client
        .post("/sign-in/email", &signin_body)
        .await
        .expect("Reference server request failed");

    eprintln!("[dual-server] POST /sign-in/email");
    eprintln!("  Rust status:  {}", rust_status);
    eprintln!("  Ref  status:  {}", ref_status);

    let all_diffs = compare_shapes(&json_shape(&rust_body), &json_shape(&ref_body), "");
    let unexpected = filter_known_diffs(&all_diffs);

    if all_diffs.is_empty() {
        eprintln!("  Shape: EXACT MATCH");
    } else {
        eprintln!(
            "  Shape: {} total diffs ({} known, {} unexpected)",
            all_diffs.len(),
            all_diffs.len() - unexpected.len(),
            unexpected.len()
        );
        for d in &all_diffs {
            eprintln!("    {}", d);
        }
    }

    assert!(
        unexpected.is_empty(),
        "Unexpected signin shape differences:\n{}",
        unexpected.join("\n")
    );
}

/// Compare error response shapes (invalid credentials).
#[tokio::test]
async fn dual_server_error_shape_comparison() {
    let _lock = SERIAL.lock().await;
    if !ensure_reference_server().await {
        eprintln!("[dual-server] SKIPPED: reference server not available");
        return;
    }

    let auth = create_server_binary_equivalent_memory_auth().await;
    let mut ref_client = RefClient::new();

    let email = unique_email("nonexistent_dual");
    let signin_body = serde_json::json!({
        "email": email,
        "password": "password123"
    });

    // Rust server
    let (rust_status, rust_body) =
        send_request(&auth, post_json("/sign-in/email", signin_body.clone())).await;

    // Reference server
    let (ref_status, ref_body) = ref_client
        .post("/sign-in/email", &signin_body)
        .await
        .expect("Reference server request failed");

    eprintln!("[dual-server] POST /sign-in/email (error case)");
    eprintln!("  Rust status:  {}", rust_status);
    eprintln!("  Ref  status:  {}", ref_status);

    let all_diffs = compare_shapes(&json_shape(&rust_body), &json_shape(&ref_body), "");
    let unexpected = filter_known_diffs(&all_diffs);

    if all_diffs.is_empty() {
        eprintln!("  Shape: EXACT MATCH");
    } else {
        eprintln!(
            "  Shape: {} total diffs ({} known, {} unexpected)",
            all_diffs.len(),
            all_diffs.len() - unexpected.len(),
            unexpected.len()
        );
        for d in &all_diffs {
            eprintln!("    {}", d);
        }
    }

    assert!(
        unexpected.is_empty(),
        "Unexpected error shape differences:\n{}",
        unexpected.join("\n")
    );
}

/// Run a broad comparison across multiple endpoints and produce a summary
/// report.  Uses cookie-based auth for the reference server.
#[tokio::test]
async fn dual_server_comprehensive_comparison() {
    let _lock = SERIAL.lock().await;
    if !ensure_reference_server().await {
        eprintln!("[dual-server] SKIPPED: reference server not available");
        return;
    }

    let auth = create_server_binary_equivalent_memory_auth().await;
    let mut ref_client = RefClient::new();

    // Sign up on both servers
    let email = unique_email("dual_comp");

    /// Compare a single endpoint between Rust and reference servers,
    /// returning an `EndpointCheck` with shape diff results.
    fn compare_endpoint(
        name: &'static str,
        rust_status: u16,
        rust_body: &Value,
        ref_result: Result<(u16, Value), String>,
    ) -> Option<EndpointCheck> {
        match ref_result {
            Ok((ref_status, ref_body)) => {
                let all_diffs = compare_shapes(&json_shape(rust_body), &json_shape(&ref_body), "");
                let unexpected_diffs = filter_known_diffs(&all_diffs);
                Some(EndpointCheck {
                    name,
                    all_diffs,
                    unexpected_diffs,
                    rust_status,
                    ref_status,
                })
            }
            Err(_) => None,
        }
    }
    let signup_body = serde_json::json!({
        "name": "Dual Comprehensive",
        "email": email,
        "password": "password123"
    });

    let (_, rust_signup) =
        send_request(&auth, post_json("/sign-up/email", signup_body.clone())).await;
    let rust_token = rust_signup["token"].as_str().unwrap_or("").to_string();

    // This also captures the session cookie in ref_client
    let _ = ref_client.post("/sign-up/email", &signup_body).await;

    // Sign in on reference server to get a fresh session cookie
    let signin_body = serde_json::json!({
        "email": email,
        "password": "password123"
    });
    let _ = ref_client.post("/sign-in/email", &signin_body).await;

    // Endpoints to compare
    struct EndpointCheck {
        name: &'static str,
        all_diffs: Vec<String>,
        unexpected_diffs: Vec<String>,
        rust_status: u16,
        ref_status: u16,
    }

    let mut results: Vec<EndpointCheck> = Vec::new();

    // GET /get-session
    {
        let (rs, rb) = send_request(&auth, get_with_auth("/get-session", &rust_token)).await;
        if let Some(check) = compare_endpoint(
            "GET /get-session",
            rs,
            &rb,
            ref_client.get("/get-session").await,
        ) {
            results.push(check);
        }
    }

    // GET /list-sessions
    {
        let (rs, rb) = send_request(&auth, get_with_auth("/list-sessions", &rust_token)).await;
        if let Some(check) = compare_endpoint(
            "GET /list-sessions",
            rs,
            &rb,
            ref_client.get("/list-sessions").await,
        ) {
            results.push(check);
        }
    }

    // POST /change-password (use boolean for revokeOtherSessions — TS validates strictly)
    {
        let chg_body = serde_json::json!({
            "currentPassword": "password123",
            "newPassword": "newpassword456",
            "revokeOtherSessions": false
        });
        let (rs, rb) = send_request(
            &auth,
            post_json_with_auth("/change-password", chg_body.clone(), &rust_token),
        )
        .await;
        if let Some(check) = compare_endpoint(
            "POST /change-password",
            rs,
            &rb,
            ref_client.post("/change-password", &chg_body).await,
        ) {
            results.push(check);
        }
    }

    // GET /ok
    {
        let (rs, rb) = send_request(&auth, get_request("/ok")).await;
        if let Some(check) = compare_endpoint("GET /ok", rs, &rb, ref_client.get("/ok").await) {
            results.push(check);
        }
    }

    // Print summary report
    eprintln!("\n=== Dual-Server Comparison Report ===\n");
    let mut total_unexpected = 0;
    for r in &results {
        let icon = if r.unexpected_diffs.is_empty() {
            "PASS"
        } else {
            "FAIL"
        };
        eprintln!(
            "[{}] {} (Rust={}, Ref={})",
            icon, r.name, r.rust_status, r.ref_status
        );
        if !r.all_diffs.is_empty() {
            for d in &r.all_diffs {
                let is_known = !r.unexpected_diffs.contains(d);
                let marker = if is_known { "known" } else { "UNEXPECTED" };
                eprintln!("      [{}] {}", marker, d);
            }
        }
        total_unexpected += r.unexpected_diffs.len();
    }
    eprintln!(
        "\nEndpoints compared: {}, Unexpected differences: {}",
        results.len(),
        total_unexpected
    );
    eprintln!("=====================================\n");

    assert_eq!(
        total_unexpected,
        0,
        "Found {} unexpected shape differences across {} endpoints",
        total_unexpected,
        results.len()
    );
}
