mod common;
mod helpers;

use base64::{Engine as _, engine::general_purpose::URL_SAFE_NO_PAD};
use chrono::Utc;
use common::{base_url, client};
use helpers::verify_new_account;
use reqwest::StatusCode;
use serde_json::{Value, json};
use sha2::{Digest, Sha256};
use wiremock::matchers::{method, path};
use wiremock::{Mock, MockServer, ResponseTemplate};

fn generate_pkce() -> (String, String) {
    let verifier_bytes: [u8; 32] = rand::random();
    let code_verifier = URL_SAFE_NO_PAD.encode(verifier_bytes);
    let mut hasher = Sha256::new();
    hasher.update(code_verifier.as_bytes());
    let hash = hasher.finalize();
    let code_challenge = URL_SAFE_NO_PAD.encode(hash);
    (code_verifier, code_challenge)
}

async fn setup_mock_client_metadata(redirect_uri: &str) -> MockServer {
    let mock_server = MockServer::start().await;
    let client_id = mock_server.uri();
    let metadata = json!({
        "client_id": client_id,
        "client_name": "Test OAuth Client",
        "redirect_uris": [redirect_uri],
        "grant_types": ["authorization_code", "refresh_token"],
        "response_types": ["code"],
        "token_endpoint_auth_method": "none",
        "dpop_bound_access_tokens": false
    });
    Mock::given(method("GET"))
        .and(path("/"))
        .respond_with(ResponseTemplate::new(200).set_body_json(metadata))
        .mount(&mock_server)
        .await;
    mock_server
}

struct OAuthSession {
    access_token: String,
    refresh_token: String,
    did: String,
    client_id: String,
}

async fn create_user_and_oauth_session(
    handle_prefix: &str,
    redirect_uri: &str,
) -> (OAuthSession, MockServer) {
    let url = base_url().await;
    let http_client = client();
    let suffix = &uuid::Uuid::new_v4().simple().to_string()[..4];
    let handle = format!("{}{}", handle_prefix, suffix);
    let email = format!("{}{}@example.com", handle_prefix, suffix);
    let password = format!("{}Pass123!", handle_prefix);
    let create_res = http_client
        .post(format!("{}/xrpc/com.atproto.server.createAccount", url))
        .json(&json!({
            "handle": handle,
            "email": email,
            "password": password
        }))
        .send()
        .await
        .expect("Account creation failed");
    assert_eq!(create_res.status(), StatusCode::OK);
    let account: Value = create_res.json().await.unwrap();
    let user_did = account["did"].as_str().unwrap().to_string();
    let _ = verify_new_account(&http_client, &user_did).await;
    let mock_client = setup_mock_client_metadata(redirect_uri).await;
    let client_id = mock_client.uri();
    let (code_verifier, code_challenge) = generate_pkce();
    let par_res = http_client
        .post(format!("{}/oauth/par", url))
        .form(&[
            ("response_type", "code"),
            ("client_id", &client_id),
            ("redirect_uri", redirect_uri),
            ("code_challenge", &code_challenge),
            ("code_challenge_method", "S256"),
            ("scope", "atproto transition:generic"),
        ])
        .send()
        .await
        .expect("PAR failed");
    assert!(
        par_res.status() == StatusCode::OK || par_res.status() == StatusCode::CREATED,
        "PAR should succeed with 200 or 201, got {}",
        par_res.status()
    );
    let par_body: Value = par_res.json().await.unwrap();
    let request_uri = par_body["request_uri"].as_str().unwrap();
    let auth_res = http_client
        .post(format!("{}/oauth/authorize", url))
        .header("Content-Type", "application/json")
        .header("Accept", "application/json")
        .json(&json!({
            "request_uri": request_uri,
            "username": &handle,
            "password": &password,
            "remember_device": false
        }))
        .send()
        .await
        .expect("Authorize failed");
    assert_eq!(
        auth_res.status(),
        StatusCode::OK,
        "Authorize should return OK"
    );
    let auth_body: Value = auth_res.json().await.unwrap();
    let mut location = auth_body["redirect_uri"]
        .as_str()
        .expect("Expected redirect_uri")
        .to_string();
    if location.contains("/oauth/consent") {
        let consent_res = http_client
            .post(format!("{}/oauth/authorize/consent", url))
            .header("Content-Type", "application/json")
            .json(&json!({"request_uri": request_uri, "approved_scopes": ["atproto", "transition:generic"], "remember": false}))
            .send().await.expect("Consent request failed");
        assert_eq!(
            consent_res.status(),
            StatusCode::OK,
            "Consent should succeed"
        );
        let consent_body: Value = consent_res.json().await.unwrap();
        location = consent_body["redirect_uri"]
            .as_str()
            .expect("Expected redirect_uri from consent")
            .to_string();
    }
    let code = location
        .split("code=")
        .nth(1)
        .unwrap()
        .split('&')
        .next()
        .unwrap();
    let token_res = http_client
        .post(format!("{}/oauth/token", url))
        .form(&[
            ("grant_type", "authorization_code"),
            ("code", code),
            ("redirect_uri", redirect_uri),
            ("code_verifier", &code_verifier),
            ("client_id", &client_id),
        ])
        .send()
        .await
        .expect("Token request failed");
    assert_eq!(token_res.status(), StatusCode::OK);
    let token_body: Value = token_res.json().await.unwrap();
    let session = OAuthSession {
        access_token: token_body["access_token"].as_str().unwrap().to_string(),
        refresh_token: token_body["refresh_token"].as_str().unwrap().to_string(),
        did: user_did,
        client_id,
    };
    (session, mock_client)
}

#[tokio::test]
async fn test_oauth_token_can_create_and_read_records() {
    let url = base_url().await;
    let http_client = client();
    let (session, _mock) =
        create_user_and_oauth_session("oauth-records", "https://example.com/callback").await;
    let collection = "app.bsky.feed.post";
    let post_text = "Hello from OAuth! This post was created with an OAuth access token.";
    let create_res = http_client
        .post(format!("{}/xrpc/com.atproto.repo.createRecord", url))
        .bearer_auth(&session.access_token)
        .json(&json!({
            "repo": session.did,
            "collection": collection,
            "record": {
                "$type": collection,
                "text": post_text,
                "createdAt": Utc::now().to_rfc3339()
            }
        }))
        .send()
        .await
        .expect("createRecord failed");
    assert_eq!(
        create_res.status(),
        StatusCode::OK,
        "Should create record with OAuth token"
    );
    let create_body: Value = create_res.json().await.unwrap();
    let uri = create_body["uri"].as_str().unwrap();
    let rkey = uri.split('/').next_back().unwrap();
    let get_res = http_client
        .get(format!("{}/xrpc/com.atproto.repo.getRecord", url))
        .bearer_auth(&session.access_token)
        .query(&[
            ("repo", session.did.as_str()),
            ("collection", collection),
            ("rkey", rkey),
        ])
        .send()
        .await
        .expect("getRecord failed");
    assert_eq!(
        get_res.status(),
        StatusCode::OK,
        "Should read record with OAuth token"
    );
    let get_body: Value = get_res.json().await.unwrap();
    assert_eq!(get_body["value"]["text"], post_text);
}

#[tokio::test]
async fn test_oauth_token_can_upload_blob() {
    let url = base_url().await;
    let http_client = client();
    let (session, _mock) =
        create_user_and_oauth_session("oauth-blob", "https://example.com/callback").await;
    let blob_data = b"This is test blob data uploaded via OAuth";
    let upload_res = http_client
        .post(format!("{}/xrpc/com.atproto.repo.uploadBlob", url))
        .bearer_auth(&session.access_token)
        .header("Content-Type", "text/plain")
        .body(blob_data.to_vec())
        .send()
        .await
        .expect("uploadBlob failed");
    assert_eq!(
        upload_res.status(),
        StatusCode::OK,
        "Should upload blob with OAuth token"
    );
    let upload_body: Value = upload_res.json().await.unwrap();
    assert!(upload_body["blob"]["ref"]["$link"].is_string());
    assert_eq!(upload_body["blob"]["mimeType"], "text/plain");
}

#[tokio::test]
async fn test_oauth_token_can_describe_repo() {
    let url = base_url().await;
    let http_client = client();
    let (session, _mock) =
        create_user_and_oauth_session("oauth-describe", "https://example.com/callback").await;
    let describe_res = http_client
        .get(format!("{}/xrpc/com.atproto.repo.describeRepo", url))
        .bearer_auth(&session.access_token)
        .query(&[("repo", session.did.as_str())])
        .send()
        .await
        .expect("describeRepo failed");
    assert_eq!(
        describe_res.status(),
        StatusCode::OK,
        "Should describe repo with OAuth token"
    );
    let describe_body: Value = describe_res.json().await.unwrap();
    assert_eq!(describe_body["did"], session.did);
    assert!(describe_body["handle"].is_string());
}

#[tokio::test]
async fn test_oauth_full_post_lifecycle_create_edit_delete() {
    let url = base_url().await;
    let http_client = client();
    let (session, _mock) =
        create_user_and_oauth_session("oauthlife", "https://example.com/callback").await;
    let collection = "app.bsky.feed.post";
    let original_text = "Original post content";
    let create_res = http_client
        .post(format!("{}/xrpc/com.atproto.repo.createRecord", url))
        .bearer_auth(&session.access_token)
        .json(&json!({
            "repo": session.did,
            "collection": collection,
            "record": {
                "$type": collection,
                "text": original_text,
                "createdAt": Utc::now().to_rfc3339()
            }
        }))
        .send()
        .await
        .unwrap();
    assert_eq!(create_res.status(), StatusCode::OK);
    let create_body: Value = create_res.json().await.unwrap();
    let uri = create_body["uri"].as_str().unwrap();
    let rkey = uri.split('/').next_back().unwrap();
    let updated_text = "Updated post content via OAuth putRecord";
    let put_res = http_client
        .post(format!("{}/xrpc/com.atproto.repo.putRecord", url))
        .bearer_auth(&session.access_token)
        .json(&json!({
            "repo": session.did,
            "collection": collection,
            "rkey": rkey,
            "record": {
                "$type": collection,
                "text": updated_text,
                "createdAt": Utc::now().to_rfc3339()
            }
        }))
        .send()
        .await
        .unwrap();
    assert_eq!(
        put_res.status(),
        StatusCode::OK,
        "Should update record with OAuth token"
    );
    let get_res = http_client
        .get(format!("{}/xrpc/com.atproto.repo.getRecord", url))
        .bearer_auth(&session.access_token)
        .query(&[
            ("repo", session.did.as_str()),
            ("collection", collection),
            ("rkey", rkey),
        ])
        .send()
        .await
        .unwrap();
    let get_body: Value = get_res.json().await.unwrap();
    assert_eq!(
        get_body["value"]["text"], updated_text,
        "Record should have updated text"
    );
    let delete_res = http_client
        .post(format!("{}/xrpc/com.atproto.repo.deleteRecord", url))
        .bearer_auth(&session.access_token)
        .json(&json!({
            "repo": session.did,
            "collection": collection,
            "rkey": rkey
        }))
        .send()
        .await
        .unwrap();
    assert_eq!(
        delete_res.status(),
        StatusCode::OK,
        "Should delete record with OAuth token"
    );
    let get_deleted_res = http_client
        .get(format!("{}/xrpc/com.atproto.repo.getRecord", url))
        .bearer_auth(&session.access_token)
        .query(&[
            ("repo", session.did.as_str()),
            ("collection", collection),
            ("rkey", rkey),
        ])
        .send()
        .await
        .unwrap();
    assert!(
        get_deleted_res.status() == StatusCode::BAD_REQUEST
            || get_deleted_res.status() == StatusCode::NOT_FOUND,
        "Deleted record should not be found, got {}",
        get_deleted_res.status()
    );
}

#[tokio::test]
async fn test_oauth_batch_operations_apply_writes() {
    let url = base_url().await;
    let http_client = client();
    let (session, _mock) =
        create_user_and_oauth_session("oauth-batch", "https://example.com/callback").await;
    let collection = "app.bsky.feed.post";
    let now = Utc::now().to_rfc3339();
    let apply_res = http_client
        .post(format!("{}/xrpc/com.atproto.repo.applyWrites", url))
        .bearer_auth(&session.access_token)
        .json(&json!({
            "repo": session.did,
            "writes": [
                {
                    "$type": "com.atproto.repo.applyWrites#create",
                    "collection": collection,
                    "rkey": "batch1",
                    "value": {
                        "$type": collection,
                        "text": "Batch post 1",
                        "createdAt": now
                    }
                },
                {
                    "$type": "com.atproto.repo.applyWrites#create",
                    "collection": collection,
                    "rkey": "batch2",
                    "value": {
                        "$type": collection,
                        "text": "Batch post 2",
                        "createdAt": now
                    }
                },
                {
                    "$type": "com.atproto.repo.applyWrites#create",
                    "collection": collection,
                    "rkey": "batch3",
                    "value": {
                        "$type": collection,
                        "text": "Batch post 3",
                        "createdAt": now
                    }
                }
            ]
        }))
        .send()
        .await
        .unwrap();
    assert_eq!(
        apply_res.status(),
        StatusCode::OK,
        "Should apply batch writes with OAuth token"
    );
    let list_res = http_client
        .get(format!("{}/xrpc/com.atproto.repo.listRecords", url))
        .bearer_auth(&session.access_token)
        .query(&[("repo", session.did.as_str()), ("collection", collection)])
        .send()
        .await
        .unwrap();
    assert_eq!(list_res.status(), StatusCode::OK);
    let list_body: Value = list_res.json().await.unwrap();
    let records = list_body["records"].as_array().unwrap();
    assert!(
        records.len() >= 3,
        "Should have at least 3 records from batch"
    );
}

#[tokio::test]
async fn test_oauth_token_refresh_maintains_access() {
    let url = base_url().await;
    let http_client = client();
    let (session, _mock) =
        create_user_and_oauth_session("oauth-refr", "https://example.com/callback").await;
    let collection = "app.bsky.feed.post";
    let create_res = http_client
        .post(format!("{}/xrpc/com.atproto.repo.createRecord", url))
        .bearer_auth(&session.access_token)
        .json(&json!({
            "repo": session.did,
            "collection": collection,
            "record": {
                "$type": collection,
                "text": "Post before refresh",
                "createdAt": Utc::now().to_rfc3339()
            }
        }))
        .send()
        .await
        .unwrap();
    assert_eq!(
        create_res.status(),
        StatusCode::OK,
        "Original token should work"
    );
    let refresh_res = http_client
        .post(format!("{}/oauth/token", url))
        .form(&[
            ("grant_type", "refresh_token"),
            ("refresh_token", &session.refresh_token),
            ("client_id", &session.client_id),
        ])
        .send()
        .await
        .unwrap();
    assert_eq!(refresh_res.status(), StatusCode::OK);
    let refresh_body: Value = refresh_res.json().await.unwrap();
    let new_access_token = refresh_body["access_token"].as_str().unwrap();
    assert_ne!(
        new_access_token, session.access_token,
        "New token should be different"
    );
    let create_res2 = http_client
        .post(format!("{}/xrpc/com.atproto.repo.createRecord", url))
        .bearer_auth(new_access_token)
        .json(&json!({
            "repo": session.did,
            "collection": collection,
            "record": {
                "$type": collection,
                "text": "Post after refresh with new token",
                "createdAt": Utc::now().to_rfc3339()
            }
        }))
        .send()
        .await
        .unwrap();
    assert_eq!(
        create_res2.status(),
        StatusCode::OK,
        "New token should work for creating records"
    );
    let list_res = http_client
        .get(format!("{}/xrpc/com.atproto.repo.listRecords", url))
        .bearer_auth(new_access_token)
        .query(&[("repo", session.did.as_str()), ("collection", collection)])
        .send()
        .await
        .unwrap();
    assert_eq!(
        list_res.status(),
        StatusCode::OK,
        "New token should work for listing records"
    );
    let list_body: Value = list_res.json().await.unwrap();
    let records = list_body["records"].as_array().unwrap();
    assert_eq!(records.len(), 2, "Should have both posts");
}

#[tokio::test]
async fn test_oauth_revoked_token_cannot_access_resources() {
    let url = base_url().await;
    let http_client = client();
    let (session, _mock) =
        create_user_and_oauth_session("oauth-revo", "https://example.com/callback").await;
    let collection = "app.bsky.feed.post";
    let create_res = http_client
        .post(format!("{}/xrpc/com.atproto.repo.createRecord", url))
        .bearer_auth(&session.access_token)
        .json(&json!({
            "repo": session.did,
            "collection": collection,
            "record": {
                "$type": collection,
                "text": "Post before revocation",
                "createdAt": Utc::now().to_rfc3339()
            }
        }))
        .send()
        .await
        .unwrap();
    assert_eq!(
        create_res.status(),
        StatusCode::OK,
        "Token should work before revocation"
    );
    let revoke_res = http_client
        .post(format!("{}/oauth/revoke", url))
        .form(&[("token", session.refresh_token.as_str())])
        .send()
        .await
        .unwrap();
    assert_eq!(
        revoke_res.status(),
        StatusCode::OK,
        "Revocation should succeed"
    );
    let refresh_res = http_client
        .post(format!("{}/oauth/token", url))
        .form(&[
            ("grant_type", "refresh_token"),
            ("refresh_token", &session.refresh_token),
            ("client_id", &session.client_id),
        ])
        .send()
        .await
        .unwrap();
    assert_eq!(
        refresh_res.status(),
        StatusCode::BAD_REQUEST,
        "Revoked refresh token should not work"
    );
}

#[tokio::test]
async fn test_oauth_multiple_clients_same_user() {
    let url = base_url().await;
    let http_client = client();
    let suffix = &uuid::Uuid::new_v4().simple().to_string()[..8];
    let handle = format!("mc{}", suffix);
    let email = format!("mc{}@example.com", suffix);
    let password = "MultiClient123!";
    let create_res = http_client
        .post(format!("{}/xrpc/com.atproto.server.createAccount", url))
        .json(&json!({
            "handle": handle,
            "email": email,
            "password": password
        }))
        .send()
        .await
        .unwrap();
    assert_eq!(create_res.status(), StatusCode::OK);
    let account: Value = create_res.json().await.unwrap();
    let user_did = account["did"].as_str().unwrap();
    let _ = verify_new_account(&http_client, user_did).await;
    let mock_client1 = setup_mock_client_metadata("https://client1.example.com/callback").await;
    let client1_id = mock_client1.uri();
    let mock_client2 = setup_mock_client_metadata("https://client2.example.com/callback").await;
    let client2_id = mock_client2.uri();
    let (verifier1, challenge1) = generate_pkce();
    let par_res1 = http_client
        .post(format!("{}/oauth/par", url))
        .form(&[
            ("response_type", "code"),
            ("client_id", &client1_id),
            ("redirect_uri", "https://client1.example.com/callback"),
            ("code_challenge", &challenge1),
            ("code_challenge_method", "S256"),
        ])
        .send()
        .await
        .unwrap();
    let par_body1: Value = par_res1.json().await.unwrap();
    let request_uri1 = par_body1["request_uri"].as_str().unwrap();
    let auth_res1 = http_client
        .post(format!("{}/oauth/authorize", url))
        .header("Content-Type", "application/json")
        .header("Accept", "application/json")
        .json(&json!({
            "request_uri": request_uri1,
            "username": &handle,
            "password": password,
            "remember_device": false
        }))
        .send()
        .await
        .unwrap();
    assert_eq!(auth_res1.status(), StatusCode::OK);
    let auth_body1: Value = auth_res1.json().await.unwrap();
    let mut location1 = auth_body1["redirect_uri"].as_str().unwrap().to_string();
    if location1.contains("/oauth/consent") {
        let consent_res = http_client
            .post(format!("{}/oauth/authorize/consent", url))
            .header("Content-Type", "application/json")
            .json(&json!({"request_uri": request_uri1, "approved_scopes": ["atproto", "transition:generic"], "remember": false}))
            .send().await.unwrap();
        let consent_body: Value = consent_res.json().await.unwrap();
        location1 = consent_body["redirect_uri"].as_str().unwrap().to_string();
    }
    let code1 = location1
        .split("code=")
        .nth(1)
        .unwrap()
        .split('&')
        .next()
        .unwrap();
    let token_res1 = http_client
        .post(format!("{}/oauth/token", url))
        .form(&[
            ("grant_type", "authorization_code"),
            ("code", code1),
            ("redirect_uri", "https://client1.example.com/callback"),
            ("code_verifier", &verifier1),
            ("client_id", &client1_id),
        ])
        .send()
        .await
        .unwrap();
    let token_body1: Value = token_res1.json().await.unwrap();
    let token1 = token_body1["access_token"].as_str().unwrap();
    let (verifier2, challenge2) = generate_pkce();
    let par_res2 = http_client
        .post(format!("{}/oauth/par", url))
        .form(&[
            ("response_type", "code"),
            ("client_id", &client2_id),
            ("redirect_uri", "https://client2.example.com/callback"),
            ("code_challenge", &challenge2),
            ("code_challenge_method", "S256"),
        ])
        .send()
        .await
        .unwrap();
    let par_body2: Value = par_res2.json().await.unwrap();
    let request_uri2 = par_body2["request_uri"].as_str().unwrap();
    let auth_res2 = http_client
        .post(format!("{}/oauth/authorize", url))
        .header("Content-Type", "application/json")
        .header("Accept", "application/json")
        .json(&json!({
            "request_uri": request_uri2,
            "username": &handle,
            "password": password,
            "remember_device": false
        }))
        .send()
        .await
        .unwrap();
    assert_eq!(auth_res2.status(), StatusCode::OK);
    let auth_body2: Value = auth_res2.json().await.unwrap();
    let mut location2 = auth_body2["redirect_uri"].as_str().unwrap().to_string();
    if location2.contains("/oauth/consent") {
        let consent_res = http_client
            .post(format!("{}/oauth/authorize/consent", url))
            .header("Content-Type", "application/json")
            .json(&json!({"request_uri": request_uri2, "approved_scopes": ["atproto", "transition:generic"], "remember": false}))
            .send().await.unwrap();
        let consent_body: Value = consent_res.json().await.unwrap();
        location2 = consent_body["redirect_uri"].as_str().unwrap().to_string();
    }
    let code2 = location2
        .split("code=")
        .nth(1)
        .unwrap()
        .split('&')
        .next()
        .unwrap();
    let token_res2 = http_client
        .post(format!("{}/oauth/token", url))
        .form(&[
            ("grant_type", "authorization_code"),
            ("code", code2),
            ("redirect_uri", "https://client2.example.com/callback"),
            ("code_verifier", &verifier2),
            ("client_id", &client2_id),
        ])
        .send()
        .await
        .unwrap();
    let token_body2: Value = token_res2.json().await.unwrap();
    let token2 = token_body2["access_token"].as_str().unwrap();
    assert_ne!(
        token1, token2,
        "Different clients should get different tokens"
    );
    let collection = "app.bsky.feed.post";
    let create_res1 = http_client
        .post(format!("{}/xrpc/com.atproto.repo.createRecord", url))
        .bearer_auth(token1)
        .json(&json!({
            "repo": user_did,
            "collection": collection,
            "record": {
                "$type": collection,
                "text": "Post from client 1",
                "createdAt": Utc::now().to_rfc3339()
            }
        }))
        .send()
        .await
        .unwrap();
    assert_eq!(
        create_res1.status(),
        StatusCode::OK,
        "Client 1 token should work"
    );
    let create_res2 = http_client
        .post(format!("{}/xrpc/com.atproto.repo.createRecord", url))
        .bearer_auth(token2)
        .json(&json!({
            "repo": user_did,
            "collection": collection,
            "record": {
                "$type": collection,
                "text": "Post from client 2",
                "createdAt": Utc::now().to_rfc3339()
            }
        }))
        .send()
        .await
        .unwrap();
    assert_eq!(
        create_res2.status(),
        StatusCode::OK,
        "Client 2 token should work"
    );
    let list_res = http_client
        .get(format!("{}/xrpc/com.atproto.repo.listRecords", url))
        .bearer_auth(token1)
        .query(&[("repo", user_did), ("collection", collection)])
        .send()
        .await
        .unwrap();
    let list_body: Value = list_res.json().await.unwrap();
    let records = list_body["records"].as_array().unwrap();
    assert_eq!(
        records.len(),
        2,
        "Both posts should be visible to either client"
    );
}

#[tokio::test]
async fn test_oauth_social_interactions_follow_like_repost() {
    let url = base_url().await;
    let http_client = client();
    let (alice, _mock_alice) =
        create_user_and_oauth_session("alice-social", "https://alice-app.example.com/callback")
            .await;
    let (bob, _mock_bob) =
        create_user_and_oauth_session("bob-social", "https://bob-app.example.com/callback").await;
    let post_collection = "app.bsky.feed.post";
    let post_res = http_client
        .post(format!("{}/xrpc/com.atproto.repo.createRecord", url))
        .bearer_auth(&alice.access_token)
        .json(&json!({
            "repo": alice.did,
            "collection": post_collection,
            "record": {
                "$type": post_collection,
                "text": "Hello from Alice! Looking for friends.",
                "createdAt": Utc::now().to_rfc3339()
            }
        }))
        .send()
        .await
        .unwrap();
    assert_eq!(post_res.status(), StatusCode::OK);
    let post_body: Value = post_res.json().await.unwrap();
    let post_uri = post_body["uri"].as_str().unwrap();
    let post_cid = post_body["cid"].as_str().unwrap();
    let follow_collection = "app.bsky.graph.follow";
    let follow_res = http_client
        .post(format!("{}/xrpc/com.atproto.repo.createRecord", url))
        .bearer_auth(&bob.access_token)
        .json(&json!({
            "repo": bob.did,
            "collection": follow_collection,
            "record": {
                "$type": follow_collection,
                "subject": alice.did,
                "createdAt": Utc::now().to_rfc3339()
            }
        }))
        .send()
        .await
        .unwrap();
    assert_eq!(
        follow_res.status(),
        StatusCode::OK,
        "Bob should be able to follow Alice via OAuth"
    );
    let like_collection = "app.bsky.feed.like";
    let like_res = http_client
        .post(format!("{}/xrpc/com.atproto.repo.createRecord", url))
        .bearer_auth(&bob.access_token)
        .json(&json!({
            "repo": bob.did,
            "collection": like_collection,
            "record": {
                "$type": like_collection,
                "subject": {
                    "uri": post_uri,
                    "cid": post_cid
                },
                "createdAt": Utc::now().to_rfc3339()
            }
        }))
        .send()
        .await
        .unwrap();
    assert_eq!(
        like_res.status(),
        StatusCode::OK,
        "Bob should be able to like Alice's post via OAuth"
    );
    let repost_collection = "app.bsky.feed.repost";
    let repost_res = http_client
        .post(format!("{}/xrpc/com.atproto.repo.createRecord", url))
        .bearer_auth(&bob.access_token)
        .json(&json!({
            "repo": bob.did,
            "collection": repost_collection,
            "record": {
                "$type": repost_collection,
                "subject": {
                    "uri": post_uri,
                    "cid": post_cid
                },
                "createdAt": Utc::now().to_rfc3339()
            }
        }))
        .send()
        .await
        .unwrap();
    assert_eq!(
        repost_res.status(),
        StatusCode::OK,
        "Bob should be able to repost Alice's post via OAuth"
    );
    let bob_follows = http_client
        .get(format!("{}/xrpc/com.atproto.repo.listRecords", url))
        .bearer_auth(&bob.access_token)
        .query(&[
            ("repo", bob.did.as_str()),
            ("collection", follow_collection),
        ])
        .send()
        .await
        .unwrap();
    let follows_body: Value = bob_follows.json().await.unwrap();
    let follows = follows_body["records"].as_array().unwrap();
    assert_eq!(follows.len(), 1, "Bob should have 1 follow");
    assert_eq!(follows[0]["value"]["subject"], alice.did);
    let bob_likes = http_client
        .get(format!("{}/xrpc/com.atproto.repo.listRecords", url))
        .bearer_auth(&bob.access_token)
        .query(&[("repo", bob.did.as_str()), ("collection", like_collection)])
        .send()
        .await
        .unwrap();
    let likes_body: Value = bob_likes.json().await.unwrap();
    let likes = likes_body["records"].as_array().unwrap();
    assert_eq!(likes.len(), 1, "Bob should have 1 like");
}

#[tokio::test]
async fn test_oauth_cannot_modify_other_users_repo() {
    let url = base_url().await;
    let http_client = client();
    let (alice, _mock_alice) =
        create_user_and_oauth_session("alice-boundary", "https://alice.example.com/callback").await;
    let (bob, _mock_bob) =
        create_user_and_oauth_session("bob-boundary", "https://bob.example.com/callback").await;
    let collection = "app.bsky.feed.post";
    let malicious_res = http_client
        .post(format!("{}/xrpc/com.atproto.repo.createRecord", url))
        .bearer_auth(&bob.access_token)
        .json(&json!({
            "repo": alice.did,
            "collection": collection,
            "record": {
                "$type": collection,
                "text": "Bob trying to post as Alice!",
                "createdAt": Utc::now().to_rfc3339()
            }
        }))
        .send()
        .await
        .unwrap();
    assert_ne!(
        malicious_res.status(),
        StatusCode::OK,
        "Bob should NOT be able to create records in Alice's repo"
    );
    let alice_posts = http_client
        .get(format!("{}/xrpc/com.atproto.repo.listRecords", url))
        .bearer_auth(&alice.access_token)
        .query(&[("repo", alice.did.as_str()), ("collection", collection)])
        .send()
        .await
        .unwrap();
    let posts_body: Value = alice_posts.json().await.unwrap();
    let posts = posts_body["records"].as_array().unwrap();
    assert_eq!(posts.len(), 0, "Alice's repo should have no posts from Bob");
}

#[tokio::test]
async fn test_oauth_session_isolation_between_users() {
    let url = base_url().await;
    let http_client = client();
    let (alice, _mock_alice) =
        create_user_and_oauth_session("alice-isol", "https://alice.example.com/callback").await;
    let (bob, _mock_bob) =
        create_user_and_oauth_session("bob-isolation", "https://bob.example.com/callback").await;
    let collection = "app.bsky.feed.post";
    let alice_post = http_client
        .post(format!("{}/xrpc/com.atproto.repo.createRecord", url))
        .bearer_auth(&alice.access_token)
        .json(&json!({
            "repo": alice.did,
            "collection": collection,
            "record": {
                "$type": collection,
                "text": "Alice's private thoughts",
                "createdAt": Utc::now().to_rfc3339()
            }
        }))
        .send()
        .await
        .unwrap();
    assert_eq!(alice_post.status(), StatusCode::OK);
    let bob_post = http_client
        .post(format!("{}/xrpc/com.atproto.repo.createRecord", url))
        .bearer_auth(&bob.access_token)
        .json(&json!({
            "repo": bob.did,
            "collection": collection,
            "record": {
                "$type": collection,
                "text": "Bob's different thoughts",
                "createdAt": Utc::now().to_rfc3339()
            }
        }))
        .send()
        .await
        .unwrap();
    assert_eq!(bob_post.status(), StatusCode::OK);
    let alice_list = http_client
        .get(format!("{}/xrpc/com.atproto.repo.listRecords", url))
        .bearer_auth(&alice.access_token)
        .query(&[("repo", alice.did.as_str()), ("collection", collection)])
        .send()
        .await
        .unwrap();
    let alice_records: Value = alice_list.json().await.unwrap();
    let alice_posts = alice_records["records"].as_array().unwrap();
    assert_eq!(alice_posts.len(), 1);
    assert_eq!(alice_posts[0]["value"]["text"], "Alice's private thoughts");
    let bob_list = http_client
        .get(format!("{}/xrpc/com.atproto.repo.listRecords", url))
        .bearer_auth(&bob.access_token)
        .query(&[("repo", bob.did.as_str()), ("collection", collection)])
        .send()
        .await
        .unwrap();
    let bob_records: Value = bob_list.json().await.unwrap();
    let bob_posts = bob_records["records"].as_array().unwrap();
    assert_eq!(bob_posts.len(), 1);
    assert_eq!(bob_posts[0]["value"]["text"], "Bob's different thoughts");
}

#[tokio::test]
async fn test_oauth_token_works_with_sync_endpoints() {
    let url = base_url().await;
    let http_client = client();
    let (session, _mock) =
        create_user_and_oauth_session("oauth-sync", "https://example.com/callback").await;
    let collection = "app.bsky.feed.post";
    http_client
        .post(format!("{}/xrpc/com.atproto.repo.createRecord", url))
        .bearer_auth(&session.access_token)
        .json(&json!({
            "repo": session.did,
            "collection": collection,
            "record": {
                "$type": collection,
                "text": "Post to sync",
                "createdAt": Utc::now().to_rfc3339()
            }
        }))
        .send()
        .await
        .unwrap();
    let latest_commit = http_client
        .get(format!("{}/xrpc/com.atproto.sync.getLatestCommit", url))
        .query(&[("did", session.did.as_str())])
        .send()
        .await
        .unwrap();
    assert_eq!(latest_commit.status(), StatusCode::OK);
    let commit_body: Value = latest_commit.json().await.unwrap();
    assert!(commit_body["cid"].is_string());
    assert!(commit_body["rev"].is_string());
    let repo_status = http_client
        .get(format!("{}/xrpc/com.atproto.sync.getRepoStatus", url))
        .query(&[("did", session.did.as_str())])
        .send()
        .await
        .unwrap();
    assert_eq!(repo_status.status(), StatusCode::OK);
    let status_body: Value = repo_status.json().await.unwrap();
    assert_eq!(status_body["did"], session.did);
    assert!(status_body["active"].as_bool().unwrap());
}
