//! Dual-server tests where the Rust side uses the **real Axum router** (same nesting
//! as `src/bin/server.rs`: `/api/auth/*`) instead of `AthenaAuth::handle_request`.
//! The Node reference server and request bodies match [`dual_server_tests`](crate).

#![cfg(feature = "axum")]

mod compat;

use std::sync::Arc;
use std::time::Duration;

use athena_auth::handlers::AxumIntegration;
use axum::Router;
use serde_json::Value;
use tokio::net::TcpListener;

use compat::dual_http_compare::{compare_shapes, filter_known_diffs, json_shape};
use compat::helpers::{create_server_binary_equivalent_memory_auth_with_base_url, unique_email};
use compat::reference_node::{RefClient, SERIAL, ensure_reference_server};

struct AxumJoinGuard(tokio::task::JoinHandle<std::result::Result<(), std::io::Error>>);

impl Drop for AxumJoinGuard {
    fn drop(&mut self) {
        self.0.abort();
    }
}

struct RustHttpClient {
    client: reqwest::Client,
    api_base: String,
    origin: String,
}

impl RustHttpClient {
    fn new(api_base: String, origin: String) -> Self {
        Self {
            client: reqwest::Client::builder()
                .redirect(reqwest::redirect::Policy::none())
                .timeout(Duration::from_secs(10))
                .build()
                .unwrap(),
            api_base,
            origin,
        }
    }

    fn with_origin(&self, req: reqwest::RequestBuilder) -> reqwest::RequestBuilder {
        req.header("origin", &self.origin)
    }

    async fn post_json(&self, path: &str, body: &Value) -> (u16, Value) {
        let url = format!("{}{}", self.api_base, path);
        let resp = self
            .with_origin(
                self.client
                    .post(&url)
                    .json(body)
                    .header("content-type", "application/json"),
            )
            .send()
            .await
            .unwrap_or_else(|e| panic!("Rust Axum POST {url}: {e}"));
        let status = resp.status().as_u16();
        let json: Value = resp
            .json()
            .await
            .unwrap_or_else(|_| Value::String("non-json".into()));
        (status, json)
    }

    async fn post_json_bearer(&self, path: &str, body: &Value, token: &str) -> (u16, Value) {
        let url = format!("{}{}", self.api_base, path);
        let resp = self
            .with_origin(
                self.client
                    .post(&url)
                    .json(body)
                    .header("content-type", "application/json")
                    .header("authorization", format!("Bearer {token}")),
            )
            .send()
            .await
            .unwrap_or_else(|e| panic!("Rust Axum POST {url}: {e}"));
        let status = resp.status().as_u16();
        let json: Value = resp
            .json()
            .await
            .unwrap_or_else(|_| Value::String("non-json".into()));
        (status, json)
    }

    async fn get_bearer(&self, path: &str, token: &str) -> (u16, Value) {
        let url = format!("{}{}", self.api_base, path);
        let resp = self
            .with_origin(
                self.client
                    .get(&url)
                    .header("authorization", format!("Bearer {token}")),
            )
            .send()
            .await
            .unwrap_or_else(|e| panic!("Rust Axum GET {url}: {e}"));
        let status = resp.status().as_u16();
        let json: Value = resp
            .json()
            .await
            .unwrap_or_else(|_| Value::String("non-json".into()));
        (status, json)
    }

    async fn get_json(&self, path: &str) -> (u16, Value) {
        let url = format!("{}{}", self.api_base, path);
        let resp = self
            .with_origin(self.client.get(&url))
            .send()
            .await
            .unwrap_or_else(|e| panic!("Rust Axum GET {url}: {e}"));
        let status = resp.status().as_u16();
        let json: Value = resp
            .json()
            .await
            .unwrap_or_else(|_| Value::String("non-json".into()));
        (status, json)
    }
}

async fn setup_rust_axum_stack() -> (RustHttpClient, AxumJoinGuard) {
    let listener = TcpListener::bind("127.0.0.1:0")
        .await
        .expect("bind 127.0.0.1:0");
    let port = listener.local_addr().expect("local_addr").port();
    let origin = format!("http://127.0.0.1:{port}");
    let api_base = format!("{origin}/api/auth");

    let auth: Arc<athena_auth::AthenaAuth<athena_auth::MemoryDatabaseAdapter>> =
        Arc::new(create_server_binary_equivalent_memory_auth_with_base_url(&origin).await);
    let auth_router = auth.clone().axum_router();
    let app = Router::new()
        .nest("/api/auth", auth_router)
        .with_state(auth);

    let join: tokio::task::JoinHandle<Result<(), std::io::Error>> =
        tokio::spawn(async move { axum::serve(listener, app).await });
    tokio::time::sleep(Duration::from_millis(150)).await;

    (RustHttpClient::new(api_base, origin), AxumJoinGuard(join))
}

fn log_shape_diffs(tag: &str, all_diffs: &[String], unexpected: &[String]) {
    eprintln!("[{tag}] shape compare");
    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}");
        }
    }
}

#[tokio::test]
async fn server_http_dual_signup_shape_matches_reference() {
    let _lock = SERIAL.lock().await;
    if !ensure_reference_server().await {
        eprintln!("[server-http-dual] SKIPPED: reference server not available");
        return;
    }

    let (rust_http, _guard) = setup_rust_axum_stack().await;
    let mut ref_client = RefClient::new();

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

    let (rust_status, rust_body) = rust_http.post_json("/sign-up/email", &signup_body).await;
    let (ref_status, ref_body) = ref_client
        .post("/sign-up/email", &signup_body)
        .await
        .expect("Reference server request failed");

    eprintln!("[server-http-dual] 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);
    log_shape_diffs("signup", &all_diffs, &unexpected);

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

#[tokio::test]
async fn server_http_dual_signin_shape_matches_reference() {
    let _lock = SERIAL.lock().await;
    if !ensure_reference_server().await {
        eprintln!("[server-http-dual] SKIPPED: reference server not available");
        return;
    }

    let (rust_http, _guard) = setup_rust_axum_stack().await;
    let mut ref_client = RefClient::new();

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

    rust_http.post_json("/sign-up/email", &signup_body).await;
    let _ = ref_client.post("/sign-up/email", &signup_body).await;

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

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

    eprintln!("[server-http-dual] 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);
    log_shape_diffs("signin", &all_diffs, &unexpected);

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

#[tokio::test]
async fn server_http_dual_signin_error_shape_matches_reference() {
    let _lock = SERIAL.lock().await;
    if !ensure_reference_server().await {
        eprintln!("[server-http-dual] SKIPPED: reference server not available");
        return;
    }

    let (rust_http, _guard) = setup_rust_axum_stack().await;
    let mut ref_client = RefClient::new();

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

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

    eprintln!("[server-http-dual] 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);
    log_shape_diffs("error", &all_diffs, &unexpected);

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

#[tokio::test]
async fn server_http_dual_comprehensive_shape_matches_reference() {
    let _lock = SERIAL.lock().await;
    if !ensure_reference_server().await {
        eprintln!("[server-http-dual] SKIPPED: reference server not available");
        return;
    }

    let (rust_http, _guard) = setup_rust_axum_stack().await;
    let mut ref_client = RefClient::new();

    let email = unique_email("http_dual_comp");
    let signup_body = serde_json::json!({
        "name": "HTTP Dual Comprehensive",
        "email": email,
        "password": "password123"
    });

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

    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 (_, rust_signup) = rust_http.post_json("/sign-up/email", &signup_body).await;
    let rust_token = rust_signup["token"].as_str().unwrap_or("").to_string();

    let _ = ref_client.post("/sign-up/email", &signup_body).await;

    let signin_body = serde_json::json!({
        "email": email,
        "password": "password123"
    });
    let _ = ref_client.post("/sign-in/email", &signin_body).await;

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

    {
        let (rs, rb) = rust_http.get_bearer("/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);
        }
    }

    {
        let (rs, rb) = rust_http.get_bearer("/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);
        }
    }

    {
        let chg_body = serde_json::json!({
            "currentPassword": "password123",
            "newPassword": "newpassword456",
            "revokeOtherSessions": false
        });
        let (rs, rb) = rust_http
            .post_json_bearer("/change-password", &chg_body, &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);
        }
    }

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

    eprintln!("\n=== Server HTTP Dual Comparison Report ===\n");
    let mut total_unexpected = 0;
    for r in &results {
        let icon = if r.unexpected_diffs.is_empty() {
            "PASS"
        } else {
            "FAIL"
        };
        eprintln!(
            "[{icon}] {} (Rust={}, Ref={})",
            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 {total_unexpected} unexpected shape differences across {} endpoints (Axum HTTP vs reference)",
        results.len()
    );
}
