//! Cross-endpoint consistency tests — verify user/session objects are
//! structurally identical across different API responses.

mod compat;

use athena_auth::{AuthUser, UserOps};
use chrono::{DateTime, Utc};
use compat::helpers::*;
use compat::shapes::compare_shapes;

/// Test the complete auth flow: signup -> signin -> get-session -> signout.
/// Validates that the user object is consistent across all responses.
#[tokio::test]
async fn test_auth_flow_user_object_consistency() {
    let auth = create_test_auth().await;

    // Step 1: Sign up
    let (_signup_token, signup_body) =
        signup_user(&auth, "flow@example.com", "password123", "Flow User").await;
    let signup_user_obj: &serde_json::Value = &signup_body["user"];

    // Step 2: Sign in
    let (signin_token, signin_body) = signin_user(&auth, "flow@example.com", "password123").await;
    let signin_user_obj = &signin_body["user"];
    let signin_session_obj = &signin_body["session"];

    // Step 3: Get session
    let (_, session_body) = send_request(&auth, get_with_auth("/get-session", &signin_token)).await;
    let session_user_obj = &session_body["user"];
    let get_session_obj = &session_body["session"];

    // The user object should have the SAME shape across all responses
    // Use sign-in/session as reference because those payloads include runtime-populated
    // fields such as `lastSignInAt` that may still be null immediately after sign-up.
    let shapes_to_compare = vec![
        ("signin vs signup", signin_user_obj, signup_user_obj),
        ("signin vs session", signin_user_obj, session_user_obj),
    ];

    for (label, a, b) in shapes_to_compare {
        let diffs = compare_shapes(a, b, "user", false);
        assert!(
            diffs.is_empty(),
            "User object shape mismatch between {}: {:?}",
            label,
            diffs
        );
    }

    // Verify the user ID is consistent
    assert_eq!(
        signup_user_obj["id"], signin_user_obj["id"],
        "User ID must be consistent: signup vs signin"
    );
    assert_eq!(
        signup_user_obj["id"], session_user_obj["id"],
        "User ID must be consistent: signup vs session"
    );

    // Verify the email is consistent
    assert_eq!(
        signup_user_obj["email"], signin_user_obj["email"],
        "Email must be consistent: signup vs signin"
    );
    assert!(
        signin_session_obj["serverVersion"].is_string(),
        "sign-in response session.serverVersion must be a string"
    );
    assert_eq!(
        signin_session_obj["serverVersion"], get_session_obj["serverVersion"],
        "session.serverVersion must stay consistent between sign-in and get-session"
    );
}

/// Test that duplicate signup returns proper error shape.
#[tokio::test]
async fn test_duplicate_signup_error_shape() {
    let auth = create_test_auth().await;

    // First signup succeeds
    signup_user(&auth, "dup@example.com", "password123", "Dup User").await;

    // Second signup with same email should fail
    let (status, body) = send_request(
        &auth,
        post_json(
            "/sign-up/email",
            serde_json::json!({
                "name": "Dup User 2",
                "email": "dup@example.com",
                "password": "password123"
            }),
        ),
    )
    .await;

    assert!(
        (400..500).contains(&status),
        "Duplicate signup should return 4xx, got {}",
        status
    );
    assert!(
        body["message"].is_string(),
        "Error response must have 'message' field, got: {}",
        body
    );
}

/// Test that the session object shape is consistent across endpoints.
#[tokio::test]
async fn test_session_object_consistency() {
    let auth = create_test_auth().await;
    let (token, _) = signup_user(&auth, "sess@example.com", "password123", "Sess User").await;

    // Get session from /get-session
    let (_, session_body) = send_request(&auth, get_with_auth("/get-session", &token)).await;
    let get_session_obj = &session_body["session"];

    // Get sessions from /list-sessions
    let (_, list_body) = send_request(&auth, get_with_auth("/list-sessions", &token)).await;
    let list_session_obj = list_body.as_array().and_then(|arr| arr.first());

    assert!(
        get_session_obj["serverVersion"].is_string(),
        "get-session session.serverVersion must be a string"
    );

    if let Some(list_session) = list_session_obj {
        let diffs = compare_shapes(get_session_obj, list_session, "session", false);
        // Note: /get-session wraps in {session, user}, /list-sessions returns array of session
        // The shapes may differ slightly because of this wrapping
        if !diffs.is_empty() {
            eprintln!("Session shape differences (get-session vs list-sessions):");
            for diff in &diffs {
                eprintln!("  {}", diff);
            }
        }
        assert_eq!(
            get_session_obj["serverVersion"], list_session["serverVersion"],
            "session.serverVersion must stay consistent between get-session and list-sessions"
        );
    }
}

#[tokio::test]
async fn test_login_last_sign_in_db_and_local_session_no_drift() {
    let auth = create_test_auth().await;
    let email = unique_email("signin_drift");

    signup_user(&auth, &email, "password123", "Signin Drift").await;

    let (signin_token, signin_body) = signin_user(&auth, &email, "password123").await;
    let (_, session_body) = send_request(&auth, get_with_auth("/get-session", &signin_token)).await;

    let user_id = signin_body["user"]["id"]
        .as_str()
        .expect("sign-in response missing user.id");
    let db_user = auth
        .database()
        .get_user_by_id(user_id)
        .await
        .expect("db get_user_by_id failed")
        .expect("db user missing after sign-in");

    let db_last_sign_in = db_user
        .last_sign_in_at()
        .expect("db user.last_sign_in_at should be set after sign-in");
    let signin_last_sign_in = parse_rfc3339_utc(
        signin_body["user"]["lastSignInAt"]
            .as_str()
            .expect("sign-in response missing user.lastSignInAt"),
    );
    let local_session_last_sign_in = parse_rfc3339_utc(
        session_body["user"]["lastSignInAt"]
            .as_str()
            .expect("get-session response missing user.lastSignInAt"),
    );

    // Drift detector: runtime/login response and local-session projection should match DB.
    let signin_drift_ms = (db_last_sign_in - signin_last_sign_in)
        .num_milliseconds()
        .abs();
    let session_drift_ms = (db_last_sign_in - local_session_last_sign_in)
        .num_milliseconds()
        .abs();
    let response_drift_ms = (signin_last_sign_in - local_session_last_sign_in)
        .num_milliseconds()
        .abs();

    assert!(
        signin_drift_ms <= 1,
        "drift detected: sign-in user.lastSignInAt deviates from DB by {}ms (db={}, signin={})",
        signin_drift_ms,
        db_last_sign_in.to_rfc3339(),
        signin_last_sign_in.to_rfc3339()
    );
    assert!(
        session_drift_ms <= 1,
        "drift detected: get-session user.lastSignInAt deviates from DB by {}ms (db={}, session={})",
        session_drift_ms,
        db_last_sign_in.to_rfc3339(),
        local_session_last_sign_in.to_rfc3339()
    );
    assert!(
        response_drift_ms <= 1,
        "drift detected: sign-in and get-session user.lastSignInAt deviate by {}ms (signin={}, session={})",
        response_drift_ms,
        signin_last_sign_in.to_rfc3339(),
        local_session_last_sign_in.to_rfc3339()
    );
}

#[tokio::test]
async fn test_role_consistency_after_direct_db_promotion() {
    let auth = create_test_auth().await;
    let email = unique_email("role_consistency");

    let (initial_token, signup_body) =
        signup_user(&auth, &email, "password123", "Role Consistency").await;
    let user_id = signup_body["user"]["id"]
        .as_str()
        .expect("signup response missing user.id");

    // Warm the initial cached user projection while the role is still null.
    let (_, warmed_session_body) =
        send_request(&auth, get_with_auth("/get-session", &initial_token)).await;
    assert!(
        warmed_session_body["user"]["role"].is_null(),
        "newly signed-up user should not have a role yet"
    );

    auth.database()
        .update_user(
            user_id,
            athena_auth::types::UpdateUser {
                role: Some("admin".to_string()),
                ..Default::default()
            },
        )
        .await
        .expect("db update_user should succeed");

    let (signin_token, signin_body) = signin_user(&auth, &email, "password123").await;
    let (_, session_body) = send_request(&auth, get_with_auth("/get-session", &signin_token)).await;

    assert_eq!(
        signin_body["user"]["role"], "admin",
        "sign-in should reflect the promoted role even after a stale get-session cache entry was warmed"
    );
    assert_eq!(
        session_body["user"]["role"], "admin",
        "get-session should reflect the promoted role even after a stale get-session cache entry was warmed"
    );
    assert_eq!(
        signin_body["user"]["role"], session_body["user"]["role"],
        "sign-in and get-session should agree on the promoted role"
    );
}

fn parse_rfc3339_utc(value: &str) -> DateTime<Utc> {
    chrono::DateTime::parse_from_rfc3339(value)
        .unwrap_or_else(|error| panic!("invalid RFC3339 timestamp '{value}': {error}"))
        .with_timezone(&Utc)
}
