use super::*;
use crate::plugins::test_helpers;
use athena_auth_core::adapters::{PasskeyOps, UserOps, VerificationOps};
use athena_auth_core::entity::{AuthPasskey, AuthVerification};
use athena_auth_core::{CreatePasskey, CreateUser, CreateVerification, HttpMethod};
use base64::Engine;
use base64::engine::general_purpose::URL_SAFE_NO_PAD;
use chrono::{Duration, Utc};
use coset::{Algorithm, CborSerializable, CoseKey, KeyType, Label, iana};
use p256::SecretKey;
use p256::ecdsa::signature::Signer;
use p256::ecdsa::{SigningKey, VerifyingKey};
use p256::elliptic_curve::rand_core::OsRng;
use sha2::{Digest, Sha256};

fn encoded_client_data(challenge: &str, client_type: &str, origin: &str) -> String {
    let client_data = serde_json::json!({
        "type": client_type,
        "challenge": challenge,
        "origin": origin,
    });
    URL_SAFE_NO_PAD.encode(serde_json::to_vec(&client_data).unwrap())
}

fn make_cose_key(public_key: &VerifyingKey) -> Vec<u8> {
    let encoded = public_key.to_encoded_point(false);
    let x = encoded.x().unwrap().as_slice();
    let y = encoded.y().unwrap().as_slice();

    let key = CoseKey {
        kty: KeyType::Assigned(iana::KeyType::EC2),
        key_id: vec![],
        alg: Some(Algorithm::Assigned(iana::Algorithm::ES256)),
        key_ops: Default::default(),
        base_iv: vec![],
        params: vec![
            (Label::Int(-1), coset::cbor::value::Value::Integer(1.into())),
            (Label::Int(-2), coset::cbor::value::Value::Bytes(x.to_vec())),
            (Label::Int(-3), coset::cbor::value::Value::Bytes(y.to_vec())),
        ],
    };
    key.to_vec().unwrap()
}

fn make_auth_data(
    rp_id: &str,
    flags: u8,
    counter: u32,
    cred_id: Option<&[u8]>,
    public_key: Option<&[u8]>,
) -> Vec<u8> {
    let mut buf = Vec::new();
    let hash = Sha256::digest(rp_id.as_bytes());
    buf.extend_from_slice(&hash);
    buf.push(flags);
    buf.extend_from_slice(&counter.to_be_bytes());

    if let (Some(cid), Some(pk)) = (cred_id, public_key) {
        buf.extend_from_slice(&[0u8; 16]);
        buf.extend_from_slice(&(cid.len() as u16).to_be_bytes());
        buf.extend_from_slice(cid);
        buf.extend_from_slice(pk);
    }

    buf
}

fn make_attestation_object(auth_data: &[u8]) -> String {
    let map = ciborium::value::Value::Map(vec![
        (
            ciborium::value::Value::Text("fmt".to_string()),
            ciborium::value::Value::Text("none".to_string()),
        ),
        (
            ciborium::value::Value::Text("attStmt".to_string()),
            ciborium::value::Value::Map(vec![]),
        ),
        (
            ciborium::value::Value::Text("authData".to_string()),
            ciborium::value::Value::Bytes(auth_data.to_vec()),
        ),
    ]);

    let mut bytes = Vec::new();
    ciborium::ser::into_writer(&map, &mut bytes).unwrap();
    URL_SAFE_NO_PAD.encode(bytes)
}

fn build_registration_response(
    challenge: &str,
    origin: &str,
    rp_id: &str,
    credential_id_bytes: &[u8],
    secret_key: &SecretKey,
) -> serde_json::Value {
    let signing_key = SigningKey::from(secret_key.clone());
    let public_key = VerifyingKey::from(&signing_key);
    let cose_key = make_cose_key(&public_key);
    let auth_data = make_auth_data(rp_id, 0x41, 0, Some(credential_id_bytes), Some(&cose_key));
    let attestation_object = make_attestation_object(&auth_data);
    let credential_id = URL_SAFE_NO_PAD.encode(credential_id_bytes);

    serde_json::json!({
        "response": {
            "id": credential_id,
            "rawId": credential_id,
            "type": "public-key",
            "response": {
                "clientDataJSON": encoded_client_data(challenge, "webauthn.create", origin),
                "attestationObject": attestation_object
            }
        }
    })
}

fn build_authentication_response(
    challenge: &str,
    origin: &str,
    client_type: &str,
    rp_id: &str,
    credential_id_b64: &str,
    secret_key: &SecretKey,
    counter: u32,
    user_id: Option<&str>,
) -> serde_json::Value {
    let client_data_json = encoded_client_data(challenge, client_type, origin);
    let client_data_bytes = URL_SAFE_NO_PAD.decode(&client_data_json).unwrap();
    let auth_data = make_auth_data(rp_id, 0x01, counter, None, None);

    let client_data_hash = Sha256::digest(&client_data_bytes);
    let mut signed_data = Vec::new();
    signed_data.extend_from_slice(&auth_data);
    signed_data.extend_from_slice(&client_data_hash);

    let signing_key = SigningKey::from(secret_key.clone());
    let signature: p256::ecdsa::Signature = signing_key.sign(&signed_data);
    let signature_der = signature.to_der();

    let user_handle = user_id.map(|value| URL_SAFE_NO_PAD.encode(value.as_bytes()));

    serde_json::json!({
        "response": {
            "id": credential_id_b64,
            "rawId": credential_id_b64,
            "type": "public-key",
            "response": {
                "clientDataJSON": client_data_json,
                "authenticatorData": URL_SAFE_NO_PAD.encode(&auth_data),
                "signature": URL_SAFE_NO_PAD.encode(signature_der.as_bytes()),
                "userHandle": user_handle
            }
        }
    })
}

#[tokio::test]
async fn test_verify_registration_rejects_invalid_webauthn_payload() {
    let plugin = PasskeyPlugin::new();
    let (ctx, _user, session) = test_helpers::create_test_context_with_user(
        CreateUser::new()
            .with_email("passkey-test@example.com")
            .with_name("Passkey Tester"),
        Duration::hours(1),
    )
    .await;

    let body = serde_json::json!({
        "response": {
            "id": "cred-1",
            "response": {
                "clientDataJSON": encoded_client_data("challenge-1", "webauthn.create", "http://localhost:3000"),
            }
        }
    });

    let req = test_helpers::create_auth_json_request_no_query(
        HttpMethod::Post,
        "/passkey/verify-registration",
        Some(&session.token),
        Some(body),
    );

    let err = plugin
        .handle_verify_registration(&req, &ctx)
        .await
        .unwrap_err();
    assert_eq!(err.status_code(), 400);
}

#[tokio::test]
async fn test_verify_registration_consumes_state_and_prevents_replay() {
    let plugin = PasskeyPlugin::new();
    let (ctx, user, session) = test_helpers::create_test_context_with_user(
        CreateUser::new()
            .with_email("passkey-test@example.com")
            .with_name("Passkey Tester"),
        Duration::hours(1),
    )
    .await;

    let generate_req = test_helpers::create_auth_json_request_no_query(
        HttpMethod::Get,
        "/passkey/generate-register-options",
        Some(&session.token),
        None,
    );
    let generate_response = plugin
        .handle_generate_register_options(&generate_req, &ctx)
        .await
        .unwrap();
    let options: serde_json::Value = serde_json::from_slice(&generate_response.body).unwrap();
    let challenge = options["challenge"].as_str().unwrap();

    let credential_id_bytes = b"credential_reg_1";
    let secret_key = SecretKey::random(&mut OsRng);

    let wrong_body = build_registration_response(
        "wrong-challenge",
        "http://localhost:3000",
        "localhost",
        credential_id_bytes,
        &secret_key,
    );
    let wrong_req = test_helpers::create_auth_json_request_no_query(
        HttpMethod::Post,
        "/passkey/verify-registration",
        Some(&session.token),
        Some(wrong_body),
    );
    let err = plugin
        .handle_verify_registration(&wrong_req, &ctx)
        .await
        .unwrap_err();
    assert_eq!(err.status_code(), 400);

    let state_identifier = format!("passkey_state:reg:{}", user.id);
    let verification = ctx
        .database
        .get_verification_by_identifier(&state_identifier)
        .await
        .unwrap()
        .unwrap();
    assert!(
        verification
            .value()
            .contains(&format!("\"challenge\":\"{challenge}\""))
    );

    let ok_body = build_registration_response(
        challenge,
        "http://localhost:3000",
        "localhost",
        credential_id_bytes,
        &secret_key,
    );
    let ok_req = test_helpers::create_auth_json_request_no_query(
        HttpMethod::Post,
        "/passkey/verify-registration",
        Some(&session.token),
        Some(ok_body),
    );
    let response = plugin
        .handle_verify_registration(&ok_req, &ctx)
        .await
        .unwrap();
    assert_eq!(response.status, 200);

    assert!(
        ctx.database
            .get_verification_by_identifier(&state_identifier)
            .await
            .unwrap()
            .is_none()
    );

    assert!(
        ctx.database
            .get_passkey_by_credential_id(&URL_SAFE_NO_PAD.encode(credential_id_bytes))
            .await
            .unwrap()
            .is_some()
    );

    let replay_body = build_registration_response(
        challenge,
        "http://localhost:3000",
        "localhost",
        credential_id_bytes,
        &secret_key,
    );
    let replay_req = test_helpers::create_auth_json_request_no_query(
        HttpMethod::Post,
        "/passkey/verify-registration",
        Some(&session.token),
        Some(replay_body),
    );
    let err = plugin
        .handle_verify_registration(&replay_req, &ctx)
        .await
        .unwrap_err();
    assert_eq!(err.status_code(), 400);
}

#[tokio::test]
async fn test_verify_registration_accepts_wrapped_browser_response() {
    let plugin = PasskeyPlugin::new();
    let (ctx, _user, session) = test_helpers::create_test_context_with_user(
        CreateUser::new()
            .with_email("passkey-wrapped@example.com")
            .with_name("Passkey Wrapped"),
        Duration::hours(1),
    )
    .await;

    let generate_req = test_helpers::create_auth_json_request_no_query(
        HttpMethod::Get,
        "/passkey/generate-register-options",
        Some(&session.token),
        None,
    );
    let generate_response = plugin
        .handle_generate_register_options(&generate_req, &ctx)
        .await
        .unwrap();
    let options: serde_json::Value = serde_json::from_slice(&generate_response.body).unwrap();
    let challenge = options["challenge"].as_str().unwrap();

    let credential_id_bytes = b"credential_wrapped_1";
    let credential_id = URL_SAFE_NO_PAD.encode(credential_id_bytes);
    let secret_key = SecretKey::random(&mut OsRng);

    let browser_body = build_registration_response(
        challenge,
        "http://localhost:3000",
        "localhost",
        credential_id_bytes,
        &secret_key,
    );
    let wrapped_body = serde_json::json!({
        "response": browser_body,
        "name": "Wrapped browser key"
    });
    let req = test_helpers::create_auth_json_request_no_query(
        HttpMethod::Post,
        "/passkey/verify-registration",
        Some(&session.token),
        Some(wrapped_body),
    );

    let response = plugin.handle_verify_registration(&req, &ctx).await.unwrap();
    assert_eq!(response.status, 200);

    let passkey = ctx
        .database
        .get_passkey_by_credential_id(&credential_id)
        .await
        .unwrap()
        .unwrap();
    assert_eq!(passkey.name(), "Wrapped browser key");
}

#[tokio::test]
async fn test_verify_authentication_checks_type_origin_and_prevents_replay() {
    let plugin = PasskeyPlugin::new();
    let (ctx, user, session) = test_helpers::create_test_context_with_user(
        CreateUser::new()
            .with_email("passkey-test@example.com")
            .with_name("Passkey Tester"),
        Duration::hours(1),
    )
    .await;

    let register_req = test_helpers::create_auth_json_request_no_query(
        HttpMethod::Get,
        "/passkey/generate-register-options",
        Some(&session.token),
        None,
    );
    let register_options = plugin
        .handle_generate_register_options(&register_req, &ctx)
        .await
        .unwrap();
    let register_body: serde_json::Value = serde_json::from_slice(&register_options.body).unwrap();
    let register_challenge = register_body["challenge"].as_str().unwrap();

    let credential_id_bytes = b"credential_auth_1";
    let credential_id_b64 = URL_SAFE_NO_PAD.encode(credential_id_bytes);
    let secret_key = SecretKey::random(&mut OsRng);

    let verify_register_body = build_registration_response(
        register_challenge,
        "http://localhost:3000",
        "localhost",
        credential_id_bytes,
        &secret_key,
    );
    let verify_register_req = test_helpers::create_auth_json_request_no_query(
        HttpMethod::Post,
        "/passkey/verify-registration",
        Some(&session.token),
        Some(verify_register_body),
    );
    let verify_register_response = plugin
        .handle_verify_registration(&verify_register_req, &ctx)
        .await
        .unwrap();
    assert_eq!(verify_register_response.status, 200);

    let generate_auth_req = test_helpers::create_auth_json_request_no_query(
        HttpMethod::Post,
        "/passkey/generate-authenticate-options",
        None,
        None,
    );
    let generate_auth_response = plugin
        .handle_generate_authenticate_options(&generate_auth_req, &ctx)
        .await
        .unwrap();
    let auth_options: serde_json::Value =
        serde_json::from_slice(&generate_auth_response.body).unwrap();
    let challenge = auth_options["challenge"].as_str().unwrap();

    let wrong_type_body = build_authentication_response(
        challenge,
        "http://localhost:3000",
        "webauthn.create",
        "localhost",
        &credential_id_b64,
        &secret_key,
        1,
        Some(&user.id),
    );
    let wrong_type_req = test_helpers::create_auth_json_request_no_query(
        HttpMethod::Post,
        "/passkey/verify-authentication",
        None,
        Some(wrong_type_body),
    );
    let err = plugin
        .handle_verify_authentication(&wrong_type_req, &ctx)
        .await
        .unwrap_err();
    assert_eq!(err.status_code(), 400);

    let wrong_origin_body = build_authentication_response(
        challenge,
        "http://evil.example",
        "webauthn.get",
        "localhost",
        &credential_id_b64,
        &secret_key,
        1,
        Some(&user.id),
    );
    let wrong_origin_req = test_helpers::create_auth_json_request_no_query(
        HttpMethod::Post,
        "/passkey/verify-authentication",
        None,
        Some(wrong_origin_body),
    );
    let err = plugin
        .handle_verify_authentication(&wrong_origin_req, &ctx)
        .await
        .unwrap_err();
    assert_eq!(err.status_code(), 400);

    let login_state_identifier = format!("passkey_state:login:{challenge}");
    assert!(
        ctx.database
            .get_verification_by_identifier(&login_state_identifier)
            .await
            .unwrap()
            .is_some()
    );

    let ok_body = build_authentication_response(
        challenge,
        "http://localhost:3000",
        "webauthn.get",
        "localhost",
        &credential_id_b64,
        &secret_key,
        1,
        Some(&user.id),
    );
    let ok_req = test_helpers::create_auth_json_request_no_query(
        HttpMethod::Post,
        "/passkey/verify-authentication",
        None,
        Some(ok_body.clone()),
    );
    let response = plugin
        .handle_verify_authentication(&ok_req, &ctx)
        .await
        .unwrap();
    assert_eq!(response.status, 200);

    assert!(
        ctx.database
            .get_verification_by_identifier(&login_state_identifier)
            .await
            .unwrap()
            .is_none()
    );

    let replay_req = test_helpers::create_auth_json_request_no_query(
        HttpMethod::Post,
        "/passkey/verify-authentication",
        None,
        Some(ok_body),
    );
    let err = plugin
        .handle_verify_authentication(&replay_req, &ctx)
        .await
        .unwrap_err();
    assert_eq!(err.status_code(), 400);

    let passkey = ctx
        .database
        .get_passkey_by_credential_id(&credential_id_b64)
        .await
        .unwrap()
        .unwrap();
    assert_eq!(passkey.counter(), 1);
}

#[tokio::test]
async fn test_generate_register_options_returns_challenge_and_stores_verification() {
    let plugin = PasskeyPlugin::new();
    let (ctx, user, session) = test_helpers::create_test_context_with_user(
        CreateUser::new()
            .with_email("passkey-test@example.com")
            .with_name("Passkey Tester"),
        Duration::hours(1),
    )
    .await;

    let req = test_helpers::create_auth_json_request_no_query(
        HttpMethod::Get,
        "/passkey/generate-register-options",
        Some(&session.token),
        None,
    );

    let response = plugin
        .handle_generate_register_options(&req, &ctx)
        .await
        .unwrap();
    assert_eq!(response.status, 200);

    let body: serde_json::Value = serde_json::from_slice(&response.body).unwrap();
    assert!(body["challenge"].is_string());
    assert_eq!(body["rp"]["id"], "localhost");
    assert_eq!(body["rp"]["name"], "Better Auth");
    assert!(body["user"]["id"].is_string());
    assert!(body["pubKeyCredParams"].is_array());
    assert!(body["excludeCredentials"].is_array());

    let challenge = body["challenge"].as_str().unwrap();
    let identifier = format!("passkey_state:reg:{}", user.id);
    let verification = ctx
        .database
        .get_verification_by_identifier(&identifier)
        .await
        .unwrap()
        .unwrap();
    let state_json: serde_json::Value = serde_json::from_str(verification.value()).unwrap();
    assert_eq!(state_json["challenge"], challenge);
    assert_eq!(state_json["user_id"], user.id);
}

#[tokio::test]
async fn test_generate_register_options_unauthenticated() {
    let plugin = PasskeyPlugin::new();
    let (ctx, _user, _session) = test_helpers::create_test_context_with_user(
        CreateUser::new()
            .with_email("passkey-test@example.com")
            .with_name("Passkey Tester"),
        Duration::hours(1),
    )
    .await;

    let req = test_helpers::create_auth_json_request_no_query(
        HttpMethod::Get,
        "/passkey/generate-register-options",
        None,
        None,
    );

    let err = plugin
        .handle_generate_register_options(&req, &ctx)
        .await
        .unwrap_err();
    assert_eq!(err.status_code(), 401);
}

#[tokio::test]
async fn test_generate_authenticate_options_returns_challenge() {
    let plugin = PasskeyPlugin::new();
    let (ctx, _user, _session) = test_helpers::create_test_context_with_user(
        CreateUser::new()
            .with_email("passkey-test@example.com")
            .with_name("Passkey Tester"),
        Duration::hours(1),
    )
    .await;

    // No auth required for this endpoint
    let req = test_helpers::create_auth_json_request_no_query(
        HttpMethod::Post,
        "/passkey/generate-authenticate-options",
        None,
        None,
    );

    let response = plugin
        .handle_generate_authenticate_options(&req, &ctx)
        .await
        .unwrap();
    assert_eq!(response.status, 200);

    let body: serde_json::Value = serde_json::from_slice(&response.body).unwrap();
    assert!(body["challenge"].is_string());
    assert_eq!(body["rpId"], "localhost");
    assert!(body["allowCredentials"].is_array());
    assert_eq!(body["allowCredentials"].as_array().unwrap().len(), 0);
}

#[tokio::test]
async fn test_generate_authenticate_options_with_auth_includes_credentials() {
    let plugin = PasskeyPlugin::new();
    let (ctx, user, session) = test_helpers::create_test_context_with_user(
        CreateUser::new()
            .with_email("passkey-test@example.com")
            .with_name("Passkey Tester"),
        Duration::hours(1),
    )
    .await;

    // Create a passkey for the user
    ctx.database
        .create_passkey(CreatePasskey {
            user_id: user.id.clone(),
            name: "Test Key".to_string(),
            credential_id: "cred-gen-auth-1".to_string(),
            public_key: "pk".to_string(),
            counter: 0,
            device_type: "singleDevice".to_string(),
            backed_up: false,
            transports: Some("[\"usb\"]".to_string()),
        })
        .await
        .unwrap();

    let req = test_helpers::create_auth_json_request_no_query(
        HttpMethod::Post,
        "/passkey/generate-authenticate-options",
        Some(&session.token),
        None,
    );

    let response = plugin
        .handle_generate_authenticate_options(&req, &ctx)
        .await
        .unwrap();
    assert_eq!(response.status, 200);

    let body: serde_json::Value = serde_json::from_slice(&response.body).unwrap();
    let allow = body["allowCredentials"].as_array().unwrap();
    assert_eq!(allow.len(), 1);
    assert_eq!(allow[0]["id"], "cred-gen-auth-1");
}

#[tokio::test]
async fn test_list_user_passkeys() {
    let plugin = PasskeyPlugin::new();
    let (ctx, user, session) = test_helpers::create_test_context_with_user(
        CreateUser::new()
            .with_email("passkey-test@example.com")
            .with_name("Passkey Tester"),
        Duration::hours(1),
    )
    .await;

    // No passkeys yet
    let req = test_helpers::create_auth_json_request_no_query(
        HttpMethod::Get,
        "/passkey/list-user-passkeys",
        Some(&session.token),
        None,
    );
    let response = plugin.handle_list_user_passkeys(&req, &ctx).await.unwrap();
    assert_eq!(response.status, 200);
    let body: Vec<serde_json::Value> = serde_json::from_slice(&response.body).unwrap();
    assert_eq!(body.len(), 0);

    // Create a passkey
    ctx.database
        .create_passkey(CreatePasskey {
            user_id: user.id.clone(),
            name: "My Key".to_string(),
            credential_id: "cred-list-1".to_string(),
            public_key: "pk".to_string(),
            counter: 0,
            device_type: "singleDevice".to_string(),
            backed_up: false,
            transports: None,
        })
        .await
        .unwrap();

    let response = plugin.handle_list_user_passkeys(&req, &ctx).await.unwrap();
    assert_eq!(response.status, 200);
    let body: Vec<serde_json::Value> = serde_json::from_slice(&response.body).unwrap();
    assert_eq!(body.len(), 1);
    assert_eq!(body[0]["name"], "My Key");
    assert_eq!(body[0]["credentialID"], "cred-list-1");
}

#[tokio::test]
async fn test_list_user_passkeys_unauthenticated() {
    let plugin = PasskeyPlugin::new();
    let (ctx, _user, _session) = test_helpers::create_test_context_with_user(
        CreateUser::new()
            .with_email("passkey-test@example.com")
            .with_name("Passkey Tester"),
        Duration::hours(1),
    )
    .await;

    let req = test_helpers::create_auth_json_request_no_query(
        HttpMethod::Get,
        "/passkey/list-user-passkeys",
        None,
        None,
    );
    let err = plugin
        .handle_list_user_passkeys(&req, &ctx)
        .await
        .unwrap_err();
    assert_eq!(err.status_code(), 401);
}

#[tokio::test]
async fn test_delete_passkey_success() {
    let plugin = PasskeyPlugin::new();
    let (ctx, user, session) = test_helpers::create_test_context_with_user(
        CreateUser::new()
            .with_email("passkey-test@example.com")
            .with_name("Passkey Tester"),
        Duration::hours(1),
    )
    .await;

    let passkey = ctx
        .database
        .create_passkey(CreatePasskey {
            user_id: user.id.clone(),
            name: "To Delete".to_string(),
            credential_id: "cred-del-1".to_string(),
            public_key: "pk".to_string(),
            counter: 0,
            device_type: "singleDevice".to_string(),
            backed_up: false,
            transports: None,
        })
        .await
        .unwrap();

    let body = serde_json::json!({ "id": passkey.id });
    let req = test_helpers::create_auth_json_request_no_query(
        HttpMethod::Post,
        "/passkey/delete-passkey",
        Some(&session.token),
        Some(body),
    );

    let response = plugin.handle_delete_passkey(&req, &ctx).await.unwrap();
    assert_eq!(response.status, 200);

    // Verify deleted
    let result = ctx.database.get_passkey_by_id(&passkey.id).await.unwrap();
    assert!(result.is_none());
}

#[tokio::test]
async fn test_delete_passkey_non_owner_rejected() {
    let plugin = PasskeyPlugin::new();
    let (ctx, _user, session) = test_helpers::create_test_context_with_user(
        CreateUser::new()
            .with_email("passkey-test@example.com")
            .with_name("Passkey Tester"),
        Duration::hours(1),
    )
    .await;

    // Create another user's passkey
    let other_user = ctx
        .database
        .create_user(
            CreateUser::new()
                .with_email("other@example.com")
                .with_name("Other User"),
        )
        .await
        .unwrap();

    let passkey = ctx
        .database
        .create_passkey(CreatePasskey {
            user_id: other_user.id.clone(),
            name: "Other's Key".to_string(),
            credential_id: "cred-other-del".to_string(),
            public_key: "pk".to_string(),
            counter: 0,
            device_type: "singleDevice".to_string(),
            backed_up: false,
            transports: None,
        })
        .await
        .unwrap();

    let body = serde_json::json!({ "id": passkey.id });
    let req = test_helpers::create_auth_json_request_no_query(
        HttpMethod::Post,
        "/passkey/delete-passkey",
        Some(&session.token),
        Some(body),
    );

    let err = plugin.handle_delete_passkey(&req, &ctx).await.unwrap_err();
    assert_eq!(err.status_code(), 404);

    // Verify NOT deleted
    let result = ctx.database.get_passkey_by_id(&passkey.id).await.unwrap();
    assert!(result.is_some());
}

#[tokio::test]
async fn test_update_passkey_success() {
    let plugin = PasskeyPlugin::new();
    let (ctx, user, session) = test_helpers::create_test_context_with_user(
        CreateUser::new()
            .with_email("passkey-test@example.com")
            .with_name("Passkey Tester"),
        Duration::hours(1),
    )
    .await;

    let passkey = ctx
        .database
        .create_passkey(CreatePasskey {
            user_id: user.id.clone(),
            name: "Old Name".to_string(),
            credential_id: "cred-upd-1".to_string(),
            public_key: "pk".to_string(),
            counter: 0,
            device_type: "singleDevice".to_string(),
            backed_up: false,
            transports: None,
        })
        .await
        .unwrap();

    let body = serde_json::json!({ "id": passkey.id, "name": "New Name" });
    let req = test_helpers::create_auth_json_request_no_query(
        HttpMethod::Post,
        "/passkey/update-passkey",
        Some(&session.token),
        Some(body),
    );

    let response = plugin.handle_update_passkey(&req, &ctx).await.unwrap();
    assert_eq!(response.status, 200);

    let resp_body: serde_json::Value = serde_json::from_slice(&response.body).unwrap();
    assert_eq!(resp_body["passkey"]["name"], "New Name");

    // Verify persisted
    let updated = ctx
        .database
        .get_passkey_by_id(&passkey.id)
        .await
        .unwrap()
        .unwrap();
    assert_eq!(updated.name(), "New Name");
}

#[tokio::test]
async fn test_update_passkey_non_owner_rejected() {
    let plugin = PasskeyPlugin::new();
    let (ctx, _user, session) = test_helpers::create_test_context_with_user(
        CreateUser::new()
            .with_email("passkey-test@example.com")
            .with_name("Passkey Tester"),
        Duration::hours(1),
    )
    .await;

    let other_user = ctx
        .database
        .create_user(
            CreateUser::new()
                .with_email("other-upd@example.com")
                .with_name("Other"),
        )
        .await
        .unwrap();

    let passkey = ctx
        .database
        .create_passkey(CreatePasskey {
            user_id: other_user.id.clone(),
            name: "Other's Key".to_string(),
            credential_id: "cred-other-upd".to_string(),
            public_key: "pk".to_string(),
            counter: 0,
            device_type: "singleDevice".to_string(),
            backed_up: false,
            transports: None,
        })
        .await
        .unwrap();

    let body = serde_json::json!({ "id": passkey.id, "name": "Hijacked" });
    let req = test_helpers::create_auth_json_request_no_query(
        HttpMethod::Post,
        "/passkey/update-passkey",
        Some(&session.token),
        Some(body),
    );

    let err = plugin.handle_update_passkey(&req, &ctx).await.unwrap_err();
    assert_eq!(err.status_code(), 404);

    // Verify unchanged
    let unchanged = ctx
        .database
        .get_passkey_by_id(&passkey.id)
        .await
        .unwrap()
        .unwrap();
    assert_eq!(unchanged.name(), "Other's Key");
}

#[tokio::test]
async fn test_expired_challenge_rejected() {
    let plugin = PasskeyPlugin::new();
    let (ctx, user, session) = test_helpers::create_test_context_with_user(
        CreateUser::new()
            .with_email("passkey-test@example.com")
            .with_name("Passkey Tester"),
        Duration::hours(1),
    )
    .await;

    let generate_req = test_helpers::create_auth_json_request_no_query(
        HttpMethod::Get,
        "/passkey/generate-register-options",
        Some(&session.token),
        None,
    );
    let generate_response = plugin
        .handle_generate_register_options(&generate_req, &ctx)
        .await
        .unwrap();
    let options: serde_json::Value = serde_json::from_slice(&generate_response.body).unwrap();
    let challenge = options["challenge"].as_str().unwrap();

    let state_identifier = format!("passkey_state:reg:{}", user.id);
    let state = ctx
        .database
        .get_verification_by_identifier(&state_identifier)
        .await
        .unwrap()
        .unwrap();

    ctx.database.delete_verification(state.id()).await.unwrap();
    ctx.database
        .create_verification(CreateVerification {
            identifier: state.identifier().to_string(),
            value: state.value().to_string(),
            expires_at: Utc::now() - Duration::seconds(1),
        })
        .await
        .unwrap();

    let secret_key = SecretKey::random(&mut OsRng);
    let body = build_registration_response(
        challenge,
        "http://localhost:3000",
        "localhost",
        b"credential_expired_1",
        &secret_key,
    );
    let req = test_helpers::create_auth_json_request_no_query(
        HttpMethod::Post,
        "/passkey/verify-registration",
        Some(&session.token),
        Some(body),
    );

    let err = plugin
        .handle_verify_registration(&req, &ctx)
        .await
        .unwrap_err();
    assert_eq!(err.status_code(), 400);
}

#[tokio::test]
async fn test_verify_authentication_rejects_invalid_webauthn_payload() {
    let plugin = PasskeyPlugin::new();
    let (ctx, _user, _session) = test_helpers::create_test_context_with_user(
        CreateUser::new()
            .with_email("passkey-test@example.com")
            .with_name("Passkey Tester"),
        Duration::hours(1),
    )
    .await;

    let body = serde_json::json!({
        "response": {
            "id": "cred-1",
            "response": {
                "clientDataJSON": encoded_client_data("c", "webauthn.get", "http://localhost:3000"),
            }
        }
    });

    let req = test_helpers::create_auth_json_request_no_query(
        HttpMethod::Post,
        "/passkey/verify-authentication",
        None,
        Some(body),
    );

    let err = plugin
        .handle_verify_authentication(&req, &ctx)
        .await
        .unwrap_err();
    assert_eq!(err.status_code(), 400);
}

#[tokio::test]
async fn test_memory_passkey_list_is_sorted_by_created_at_desc() {
    let (ctx, user, _session) = test_helpers::create_test_context_with_user(
        CreateUser::new()
            .with_email("passkey-test@example.com")
            .with_name("Passkey Tester"),
        Duration::hours(1),
    )
    .await;

    let first = ctx
        .database
        .create_passkey(CreatePasskey {
            user_id: user.id.clone(),
            name: "first".to_string(),
            credential_id: "cred-sort-1".to_string(),
            public_key: "pk-1".to_string(),
            counter: 0,
            device_type: "singleDevice".to_string(),
            backed_up: false,
            transports: None,
        })
        .await
        .unwrap();

    tokio::time::sleep(std::time::Duration::from_millis(2)).await;

    let second = ctx
        .database
        .create_passkey(CreatePasskey {
            user_id: user.id.clone(),
            name: "second".to_string(),
            credential_id: "cred-sort-2".to_string(),
            public_key: "pk-2".to_string(),
            counter: 0,
            device_type: "singleDevice".to_string(),
            backed_up: false,
            transports: None,
        })
        .await
        .unwrap();

    let listed = ctx.database.list_passkeys_by_user(&user.id).await.unwrap();
    assert_eq!(listed.len(), 2);
    assert_eq!(listed[0].id(), second.id());
    assert_eq!(listed[1].id(), first.id());
}
