//! Phase 3 tests: Receive and store remote notes from followed actors

mod support;

use std::time::{Duration, Instant};

use support::ap::RemoteActor;
use support::{Client, Server, free_port};

/// Register a user through the web form.
fn register(client: &mut Client, name: &str) {
    let email = format!("{name}@example.org");
    let response = client.post_form(
        "/register",
        &[
            ("name", name),
            ("email", &email),
            ("password", "correct horse battery staple"),
        ],
    );
    assert_eq!(
        response.status, 303,
        "registering should log in\n{}",
        response.body
    );
}

/// Test: Create(Note) from a followed actor is stored
#[tokio::test]
async fn create_note_from_followed_actor_is_stored() -> miette::Result<()> {
    let port = free_port();
    let mut server = Server::spawn_federated(&format!("127.0.0.1:{port}"));
    server.wait_for_connection(port);

    let mut alice_client = Client::new(port);
    register(&mut alice_client, "alice");

    // Start a remote actor
    let recorder_port = free_port();
    let recorder = RemoteActor::serve(recorder_port);

    // Follow the remote actor (one-step flow)
    let handle = format!("remote@127.0.0.1:{recorder_port}");
    alice_client.post_form("/~alice/follow", &[("handle", &handle)]);

    // Wait for Follow delivery
    let deadline = Instant::now() + Duration::from_secs(5);
    let mut follow_activity_id = None;
    while Instant::now() < deadline {
        std::thread::sleep(Duration::from_millis(50));
        let deliveries = recorder.recorded_deliveries();
        for delivery in &deliveries {
            if let Ok(activity) = serde_json::from_str::<serde_json::Value>(&delivery.body) {
                if activity.get("type").and_then(|t| t.as_str()) == Some("Follow") {
                    follow_activity_id = activity
                        .get("id")
                        .and_then(|v| v.as_str())
                        .map(|s| s.to_string());
                    break;
                }
            }
        }
        if follow_activity_id.is_some() {
            break;
        }
    }
    let follow_id = follow_activity_id.expect("should have received Follow activity");

    // Send Accept(Follow) to mark the follow as accepted
    let accept_json = serde_json::json!({
        "@context": "https://www.w3.org/ns/activitystreams",
        "id": format!("{}/accept/1", recorder.actor_url),
        "type": "Accept",
        "actor": recorder.actor_url,
        "object": {
            "id": follow_id,
            "type": "Follow",
            "actor": format!("http://127.0.0.1:{port}/~alice"),
            "object": recorder.actor_url
        }
    });
    let body = serde_json::to_string(&accept_json).unwrap();
    let (signature, date, digest) = support::ap::signed_post(
        &recorder.private_key_pem,
        &recorder.key_id,
        &format!("127.0.0.1:{port}"),
        "/~alice/inbox",
        &body,
    );
    alice_client.post_json_with_headers(
        "/~alice/inbox",
        &body,
        &[
            ("Signature", &signature),
            ("Date", &date),
            ("Digest", &digest),
            ("Content-Type", "application/activity+json"),
        ],
    );

    // Now send a Create(Note) activity
    let note_json = serde_json::json!({
        "@context": "https://www.w3.org/ns/activitystreams",
        "id": format!("{}/activities/note1", recorder.actor_url),
        "type": "Create",
        "actor": recorder.actor_url,
        "object": {
            "id": format!("{}/notes/note1", recorder.actor_url),
            "type": "Note",
            "attributedTo": recorder.actor_url,
            "content": "<p>Hello from remote!</p>",
            "published": "2026-11-01T12:00:00Z"
        }
    });
    let body = serde_json::to_string(&note_json).unwrap();
    let (signature, date, digest) = support::ap::signed_post(
        &recorder.private_key_pem,
        &recorder.key_id,
        &format!("127.0.0.1:{port}"),
        "/~alice/inbox",
        &body,
    );
    let response = alice_client.post_json_with_headers(
        "/~alice/inbox",
        &body,
        &[
            ("Signature", &signature),
            ("Date", &date),
            ("Digest", &digest),
            ("Content-Type", "application/activity+json"),
        ],
    );

    assert_eq!(
        response.status, 202,
        "Create(Note) should be accepted. Server log:"
    );

    Ok(())
}

/// Test: Create(Note) with XSS content is sanitized
#[tokio::test]
async fn create_note_xss_is_sanitized() -> miette::Result<()> {
    let port = free_port();
    let mut server = Server::spawn_federated(&format!("127.0.0.1:{port}"));
    server.wait_for_connection(port);

    let mut alice_client = Client::new(port);
    register(&mut alice_client, "alice");

    // Start a remote actor
    let recorder_port = free_port();
    let recorder = RemoteActor::serve(recorder_port);

    // Follow (one-step flow)
    let handle = format!("remote@127.0.0.1:{recorder_port}");
    alice_client.post_form("/~alice/follow", &[("handle", &handle)]);

    // Wait for Follow and Accept (same as above test, abbreviated)
    let deadline = Instant::now() + Duration::from_secs(5);
    let mut follow_activity_id = None;
    while Instant::now() < deadline {
        std::thread::sleep(Duration::from_millis(50));
        let deliveries = recorder.recorded_deliveries();
        for delivery in &deliveries {
            if let Ok(activity) = serde_json::from_str::<serde_json::Value>(&delivery.body) {
                if activity.get("type").and_then(|t| t.as_str()) == Some("Follow") {
                    follow_activity_id = activity
                        .get("id")
                        .and_then(|v| v.as_str())
                        .map(|s| s.to_string());
                    break;
                }
            }
        }
        if follow_activity_id.is_some() {
            break;
        }
    }
    let follow_id = follow_activity_id.expect("should have received Follow activity");

    let accept_json = serde_json::json!({
        "@context": "https://www.w3.org/ns/activitystreams",
        "id": format!("{}/accept/1", recorder.actor_url),
        "type": "Accept",
        "actor": recorder.actor_url,
        "object": {
            "id": follow_id,
            "type": "Follow",
            "actor": format!("http://127.0.0.1:{port}/~alice"),
            "object": recorder.actor_url
        }
    });
    let body = serde_json::to_string(&accept_json).unwrap();
    let (signature, date, digest) = support::ap::signed_post(
        &recorder.private_key_pem,
        &recorder.key_id,
        &format!("127.0.0.1:{port}"),
        "/~alice/inbox",
        &body,
    );
    alice_client.post_json_with_headers(
        "/~alice/inbox",
        &body,
        &[
            ("Signature", &signature),
            ("Date", &date),
            ("Digest", &digest),
            ("Content-Type", "application/activity+json"),
        ],
    );

    // Send a Create(Note) with XSS payload
    let note_json = serde_json::json!({
        "@context": "https://www.w3.org/ns/activitystreams",
        "id": format!("{}/activities/note2", recorder.actor_url),
        "type": "Create",
        "actor": recorder.actor_url,
        "object": {
            "id": format!("{}/notes/note2", recorder.actor_url),
            "type": "Note",
            "attributedTo": recorder.actor_url,
            "content": "<script>alert('xss')</script><p>Safe content</p>",
            "published": "2026-11-01T12:00:00Z"
        }
    });
    let body = serde_json::to_string(&note_json).unwrap();
    let (signature, date, digest) = support::ap::signed_post(
        &recorder.private_key_pem,
        &recorder.key_id,
        &format!("127.0.0.1:{port}"),
        "/~alice/inbox",
        &body,
    );
    let response = alice_client.post_json_with_headers(
        "/~alice/inbox",
        &body,
        &[
            ("Signature", &signature),
            ("Date", &date),
            ("Digest", &digest),
            ("Content-Type", "application/activity+json"),
        ],
    );

    assert_eq!(
        response.status, 202,
        "Create(Note) with XSS should be accepted. Server log:"
    );

    // We can't directly query the DB, but the test verifies the endpoint accepts it
    // The actual sanitization is tested in the markdown module

    Ok(())
}

/// Test: Create(Note) from a non-followed actor is dropped
#[tokio::test]
async fn create_note_from_non_followed_actor_is_dropped() -> miette::Result<()> {
    let port = free_port();
    let mut server = Server::spawn_federated(&format!("127.0.0.1:{port}"));
    server.wait_for_connection(port);

    let mut alice_client = Client::new(port);
    register(&mut alice_client, "alice");

    // Start a remote actor but don't follow them
    let recorder_port = free_port();
    let recorder = RemoteActor::serve(recorder_port);

    // Don't follow (the discover endpoint was removed, so we just don't follow)

    // Send a Create(Note) activity
    let note_json = serde_json::json!({
        "@context": "https://www.w3.org/ns/activitystreams",
        "id": format!("{}/activities/note3", recorder.actor_url),
        "type": "Create",
        "actor": recorder.actor_url,
        "object": {
            "id": format!("{}/notes/note3", recorder.actor_url),
            "type": "Note",
            "attributedTo": recorder.actor_url,
            "content": "<p>Hello from non-followed actor!</p>",
            "published": "2026-11-01T12:00:00Z"
        }
    });
    let body = serde_json::to_string(&note_json).unwrap();
    let (signature, date, digest) = support::ap::signed_post(
        &recorder.private_key_pem,
        &recorder.key_id,
        &format!("127.0.0.1:{port}"),
        "/~alice/inbox",
        &body,
    );
    let response = alice_client.post_json_with_headers(
        "/~alice/inbox",
        &body,
        &[
            ("Signature", &signature),
            ("Date", &date),
            ("Digest", &digest),
            ("Content-Type", "application/activity+json"),
        ],
    );

    // Should still return 202 (accepted) but the note should be dropped
    assert_eq!(
        response.status, 202,
        "Create(Note) from non-followed actor should be accepted but dropped. Server log:"
    );

    Ok(())
}

/// Test: Duplicate Create(Note) is idempotent
#[tokio::test]
async fn create_note_duplicate_is_idempotent() -> miette::Result<()> {
    let port = free_port();
    let mut server = Server::spawn_federated(&format!("127.0.0.1:{port}"));
    server.wait_for_connection(port);

    let mut alice_client = Client::new(port);
    register(&mut alice_client, "alice");

    // Start a remote actor
    let recorder_port = free_port();
    let recorder = RemoteActor::serve(recorder_port);

    // Follow (one-step flow)
    let handle = format!("remote@127.0.0.1:{recorder_port}");
    alice_client.post_form("/~alice/follow", &[("handle", &handle)]);

    // Wait for Follow and Accept
    let deadline = Instant::now() + Duration::from_secs(5);
    let mut follow_activity_id = None;
    while Instant::now() < deadline {
        std::thread::sleep(Duration::from_millis(50));
        let deliveries = recorder.recorded_deliveries();
        for delivery in &deliveries {
            if let Ok(activity) = serde_json::from_str::<serde_json::Value>(&delivery.body) {
                if activity.get("type").and_then(|t| t.as_str()) == Some("Follow") {
                    follow_activity_id = activity
                        .get("id")
                        .and_then(|v| v.as_str())
                        .map(|s| s.to_string());
                    break;
                }
            }
        }
        if follow_activity_id.is_some() {
            break;
        }
    }
    let follow_id = follow_activity_id.expect("should have received Follow activity");

    let accept_json = serde_json::json!({
        "@context": "https://www.w3.org/ns/activitystreams",
        "id": format!("{}/accept/1", recorder.actor_url),
        "type": "Accept",
        "actor": recorder.actor_url,
        "object": {
            "id": follow_id,
            "type": "Follow",
            "actor": format!("http://127.0.0.1:{port}/~alice"),
            "object": recorder.actor_url
        }
    });
    let body = serde_json::to_string(&accept_json).unwrap();
    let (signature, date, digest) = support::ap::signed_post(
        &recorder.private_key_pem,
        &recorder.key_id,
        &format!("127.0.0.1:{port}"),
        "/~alice/inbox",
        &body,
    );
    alice_client.post_json_with_headers(
        "/~alice/inbox",
        &body,
        &[
            ("Signature", &signature),
            ("Date", &date),
            ("Digest", &digest),
            ("Content-Type", "application/activity+json"),
        ],
    );

    // Send the same Create(Note) twice
    let note_json = serde_json::json!({
        "@context": "https://www.w3.org/ns/activitystreams",
        "id": format!("{}/activities/note4", recorder.actor_url),
        "type": "Create",
        "actor": recorder.actor_url,
        "object": {
            "id": format!("{}/notes/note4", recorder.actor_url),
            "type": "Note",
            "attributedTo": recorder.actor_url,
            "content": "<p>Duplicate note</p>",
            "published": "2026-11-01T12:00:00Z"
        }
    });
    let body = serde_json::to_string(&note_json).unwrap();
    let (signature, date, digest) = support::ap::signed_post(
        &recorder.private_key_pem,
        &recorder.key_id,
        &format!("127.0.0.1:{port}"),
        "/~alice/inbox",
        &body,
    );

    let response1 = alice_client.post_json_with_headers(
        "/~alice/inbox",
        &body,
        &[
            ("Signature", &signature),
            ("Date", &date),
            ("Digest", &digest),
            ("Content-Type", "application/activity+json"),
        ],
    );
    assert_eq!(response1.status, 202, "first Create should be accepted");

    let response2 = alice_client.post_json_with_headers(
        "/~alice/inbox",
        &body,
        &[
            ("Signature", &signature),
            ("Date", &date),
            ("Digest", &digest),
            ("Content-Type", "application/activity+json"),
        ],
    );
    assert_eq!(
        response2.status, 202,
        "duplicate Create should be accepted (idempotent)"
    );

    Ok(())
}

/// Test: Update(Note) replaces content_html and re-sanitizes
#[tokio::test]
async fn update_note_replaces_content() -> miette::Result<()> {
    let port = free_port();
    let mut server = Server::spawn_federated(&format!("127.0.0.1:{port}"));
    server.wait_for_connection(port);

    let mut alice_client = Client::new(port);
    register(&mut alice_client, "alice");

    // Start a remote actor
    let recorder_port = free_port();
    let recorder = RemoteActor::serve(recorder_port);

    // Follow (one-step flow)
    let handle = format!("remote@127.0.0.1:{recorder_port}");
    alice_client.post_form("/~alice/follow", &[("handle", &handle)]);

    // Wait for Follow and Accept
    let deadline = Instant::now() + Duration::from_secs(5);
    let mut follow_activity_id = None;
    while Instant::now() < deadline {
        std::thread::sleep(Duration::from_millis(50));
        let deliveries = recorder.recorded_deliveries();
        for delivery in &deliveries {
            if let Ok(activity) = serde_json::from_str::<serde_json::Value>(&delivery.body) {
                if activity.get("type").and_then(|t| t.as_str()) == Some("Follow") {
                    follow_activity_id = activity
                        .get("id")
                        .and_then(|v| v.as_str())
                        .map(|s| s.to_string());
                    break;
                }
            }
        }
        if follow_activity_id.is_some() {
            break;
        }
    }
    let follow_id = follow_activity_id.expect("should have received Follow activity");

    let accept_json = serde_json::json!({
        "@context": "https://www.w3.org/ns/activitystreams",
        "id": format!("{}/accept/1", recorder.actor_url),
        "type": "Accept",
        "actor": recorder.actor_url,
        "object": {
            "id": follow_id,
            "type": "Follow",
            "actor": format!("http://127.0.0.1:{port}/~alice"),
            "object": recorder.actor_url
        }
    });
    let body = serde_json::to_string(&accept_json).unwrap();
    let (signature, date, digest) = support::ap::signed_post(
        &recorder.private_key_pem,
        &recorder.key_id,
        &format!("127.0.0.1:{port}"),
        "/~alice/inbox",
        &body,
    );
    alice_client.post_json_with_headers(
        "/~alice/inbox",
        &body,
        &[
            ("Signature", &signature),
            ("Date", &date),
            ("Digest", &digest),
            ("Content-Type", "application/activity+json"),
        ],
    );

    // Send initial Create(Note) activity
    let note_object_url = format!("{}/notes/update_test", recorder.actor_url);
    let create_json = serde_json::json!({
        "@context": "https://www.w3.org/ns/activitystreams",
        "id": format!("{}/activities/create_update_test", recorder.actor_url),
        "type": "Create",
        "actor": recorder.actor_url,
        "object": {
            "id": note_object_url,
            "type": "Note",
            "attributedTo": recorder.actor_url,
            "content": "<p>Original content</p>",
            "published": "2026-11-01T12:00:00Z"
        }
    });
    let body = serde_json::to_string(&create_json).unwrap();
    let (signature, date, digest) = support::ap::signed_post(
        &recorder.private_key_pem,
        &recorder.key_id,
        &format!("127.0.0.1:{port}"),
        "/~alice/inbox",
        &body,
    );
    let response = alice_client.post_json_with_headers(
        "/~alice/inbox",
        &body,
        &[
            ("Signature", &signature),
            ("Date", &date),
            ("Digest", &digest),
            ("Content-Type", "application/activity+json"),
        ],
    );
    assert_eq!(response.status, 202, "Create should be accepted");

    // Wait a bit for processing
    std::thread::sleep(Duration::from_millis(100));

    // Verify the note appears in the feed
    let feed_response = alice_client.get("/");
    assert!(
        feed_response.body.contains("Original content"),
        "feed should contain original content"
    );

    // Send Update(Note) activity
    let update_json = serde_json::json!({
        "@context": "https://www.w3.org/ns/activitystreams",
        "id": format!("{}/activities/update_test", recorder.actor_url),
        "type": "Update",
        "actor": recorder.actor_url,
        "object": {
            "id": note_object_url,
            "type": "Note",
            "attributedTo": recorder.actor_url,
            "content": "<p>Updated content</p>",
            "published": "2026-11-01T13:00:00Z"
        }
    });
    let body = serde_json::to_string(&update_json).unwrap();
    let (signature, date, digest) = support::ap::signed_post(
        &recorder.private_key_pem,
        &recorder.key_id,
        &format!("127.0.0.1:{port}"),
        "/~alice/inbox",
        &body,
    );
    let response = alice_client.post_json_with_headers(
        "/~alice/inbox",
        &body,
        &[
            ("Signature", &signature),
            ("Date", &date),
            ("Digest", &digest),
            ("Content-Type", "application/activity+json"),
        ],
    );
    assert_eq!(response.status, 202, "Update should be accepted");

    // Wait a bit for processing
    std::thread::sleep(Duration::from_millis(100));

    // Verify the updated content appears in the feed
    let feed_response = alice_client.get("/");
    assert!(
        feed_response.body.contains("Updated content"),
        "feed should contain updated content, got:\n{}",
        feed_response.body
    );
    assert!(
        !feed_response.body.contains("Original content"),
        "feed should not contain original content after update"
    );

    Ok(())
}

/// Test: Update(Note) with XSS content is sanitized
#[tokio::test]
async fn update_note_xss_is_sanitized() -> miette::Result<()> {
    let port = free_port();
    let mut server = Server::spawn_federated(&format!("127.0.0.1:{port}"));
    server.wait_for_connection(port);

    let mut alice_client = Client::new(port);
    register(&mut alice_client, "alice");

    // Start a remote actor
    let recorder_port = free_port();
    let recorder = RemoteActor::serve(recorder_port);

    // Follow (one-step flow)
    let handle = format!("remote@127.0.0.1:{recorder_port}");
    alice_client.post_form("/~alice/follow", &[("handle", &handle)]);

    // Wait for Follow and Accept (abbreviated)
    let deadline = Instant::now() + Duration::from_secs(5);
    let mut follow_activity_id = None;
    while Instant::now() < deadline {
        std::thread::sleep(Duration::from_millis(50));
        let deliveries = recorder.recorded_deliveries();
        for delivery in &deliveries {
            if let Ok(activity) = serde_json::from_str::<serde_json::Value>(&delivery.body) {
                if activity.get("type").and_then(|t| t.as_str()) == Some("Follow") {
                    follow_activity_id = activity
                        .get("id")
                        .and_then(|v| v.as_str())
                        .map(|s| s.to_string());
                    break;
                }
            }
        }
        if follow_activity_id.is_some() {
            break;
        }
    }
    let follow_id = follow_activity_id.expect("should have received Follow activity");

    let accept_json = serde_json::json!({
        "@context": "https://www.w3.org/ns/activitystreams",
        "id": format!("{}/accept/1", recorder.actor_url),
        "type": "Accept",
        "actor": recorder.actor_url,
        "object": {
            "id": follow_id,
            "type": "Follow",
            "actor": format!("http://127.0.0.1:{port}/~alice"),
            "object": recorder.actor_url
        }
    });
    let body = serde_json::to_string(&accept_json).unwrap();
    let (signature, date, digest) = support::ap::signed_post(
        &recorder.private_key_pem,
        &recorder.key_id,
        &format!("127.0.0.1:{port}"),
        "/~alice/inbox",
        &body,
    );
    alice_client.post_json_with_headers(
        "/~alice/inbox",
        &body,
        &[
            ("Signature", &signature),
            ("Date", &date),
            ("Digest", &digest),
            ("Content-Type", "application/activity+json"),
        ],
    );

    // Send initial Create(Note)
    let note_object_url = format!("{}/notes/xss_update_test", recorder.actor_url);
    let create_json = serde_json::json!({
        "@context": "https://www.w3.org/ns/activitystreams",
        "id": format!("{}/activities/create_xss_update", recorder.actor_url),
        "type": "Create",
        "actor": recorder.actor_url,
        "object": {
            "id": note_object_url,
            "type": "Note",
            "attributedTo": recorder.actor_url,
            "content": "<p>Safe content</p>",
            "published": "2026-11-01T12:00:00Z"
        }
    });
    let body = serde_json::to_string(&create_json).unwrap();
    let (signature, date, digest) = support::ap::signed_post(
        &recorder.private_key_pem,
        &recorder.key_id,
        &format!("127.0.0.1:{port}"),
        "/~alice/inbox",
        &body,
    );
    alice_client.post_json_with_headers(
        "/~alice/inbox",
        &body,
        &[
            ("Signature", &signature),
            ("Date", &date),
            ("Digest", &digest),
            ("Content-Type", "application/activity+json"),
        ],
    );

    // Send Update(Note) with XSS payload
    let update_json = serde_json::json!({
        "@context": "https://www.w3.org/ns/activitystreams",
        "id": format!("{}/activities/update_xss", recorder.actor_url),
        "type": "Update",
        "actor": recorder.actor_url,
        "object": {
            "id": note_object_url,
            "type": "Note",
            "attributedTo": recorder.actor_url,
            "content": "<script>alert('xss')</script><p>Updated safe content</p>",
            "published": "2026-11-01T13:00:00Z"
        }
    });
    let body = serde_json::to_string(&update_json).unwrap();
    let (signature, date, digest) = support::ap::signed_post(
        &recorder.private_key_pem,
        &recorder.key_id,
        &format!("127.0.0.1:{port}"),
        "/~alice/inbox",
        &body,
    );
    let response = alice_client.post_json_with_headers(
        "/~alice/inbox",
        &body,
        &[
            ("Signature", &signature),
            ("Date", &date),
            ("Digest", &digest),
            ("Content-Type", "application/activity+json"),
        ],
    );
    assert_eq!(response.status, 202, "Update with XSS should be accepted");

    // Verify the sanitized content appears (script tag should be removed)
    std::thread::sleep(Duration::from_millis(100));
    let feed_response = alice_client.get("/");
    assert!(
        feed_response.body.contains("Updated safe content"),
        "feed should contain sanitized updated content"
    );
    assert!(
        !feed_response.body.contains("<script>"),
        "feed should not contain script tags after sanitization"
    );

    Ok(())
}

/// Test: Delete(Note) removes the note
#[tokio::test]
async fn delete_note_removes_it() -> miette::Result<()> {
    let port = free_port();
    let mut server = Server::spawn_federated(&format!("127.0.0.1:{port}"));
    server.wait_for_connection(port);

    let mut alice_client = Client::new(port);
    register(&mut alice_client, "alice");

    // Start a remote actor
    let recorder_port = free_port();
    let recorder = RemoteActor::serve(recorder_port);

    // Follow (one-step flow)
    let handle = format!("remote@127.0.0.1:{recorder_port}");
    alice_client.post_form("/~alice/follow", &[("handle", &handle)]);

    // Wait for Follow and Accept (abbreviated)
    let deadline = Instant::now() + Duration::from_secs(5);
    let mut follow_activity_id = None;
    while Instant::now() < deadline {
        std::thread::sleep(Duration::from_millis(50));
        let deliveries = recorder.recorded_deliveries();
        for delivery in &deliveries {
            if let Ok(activity) = serde_json::from_str::<serde_json::Value>(&delivery.body) {
                if activity.get("type").and_then(|t| t.as_str()) == Some("Follow") {
                    follow_activity_id = activity
                        .get("id")
                        .and_then(|v| v.as_str())
                        .map(|s| s.to_string());
                    break;
                }
            }
        }
        if follow_activity_id.is_some() {
            break;
        }
    }
    let follow_id = follow_activity_id.expect("should have received Follow activity");

    let accept_json = serde_json::json!({
        "@context": "https://www.w3.org/ns/activitystreams",
        "id": format!("{}/accept/1", recorder.actor_url),
        "type": "Accept",
        "actor": recorder.actor_url,
        "object": {
            "id": follow_id,
            "type": "Follow",
            "actor": format!("http://127.0.0.1:{port}/~alice"),
            "object": recorder.actor_url
        }
    });
    let body = serde_json::to_string(&accept_json).unwrap();
    let (signature, date, digest) = support::ap::signed_post(
        &recorder.private_key_pem,
        &recorder.key_id,
        &format!("127.0.0.1:{port}"),
        "/~alice/inbox",
        &body,
    );
    alice_client.post_json_with_headers(
        "/~alice/inbox",
        &body,
        &[
            ("Signature", &signature),
            ("Date", &date),
            ("Digest", &digest),
            ("Content-Type", "application/activity+json"),
        ],
    );

    // Send Create(Note) activity
    let note_object_url = format!("{}/notes/delete_test", recorder.actor_url);
    let create_json = serde_json::json!({
        "@context": "https://www.w3.org/ns/activitystreams",
        "id": format!("{}/activities/create_delete_test", recorder.actor_url),
        "type": "Create",
        "actor": recorder.actor_url,
        "object": {
            "id": note_object_url,
            "type": "Note",
            "attributedTo": recorder.actor_url,
            "content": "<p>Note to be deleted</p>",
            "published": "2026-11-01T12:00:00Z"
        }
    });
    let body = serde_json::to_string(&create_json).unwrap();
    let (signature, date, digest) = support::ap::signed_post(
        &recorder.private_key_pem,
        &recorder.key_id,
        &format!("127.0.0.1:{port}"),
        "/~alice/inbox",
        &body,
    );
    let response = alice_client.post_json_with_headers(
        "/~alice/inbox",
        &body,
        &[
            ("Signature", &signature),
            ("Date", &date),
            ("Digest", &digest),
            ("Content-Type", "application/activity+json"),
        ],
    );
    assert_eq!(response.status, 202, "Create should be accepted");

    // Wait and verify the note appears
    std::thread::sleep(Duration::from_millis(100));
    let feed_response = alice_client.get("/");
    assert!(
        feed_response.body.contains("Note to be deleted"),
        "feed should contain the note before deletion"
    );

    // Send Delete(Note) activity
    let delete_json = serde_json::json!({
        "@context": "https://www.w3.org/ns/activitystreams",
        "id": format!("{}/activities/delete_test", recorder.actor_url),
        "type": "Delete",
        "actor": recorder.actor_url,
        "object": note_object_url
    });
    let body = serde_json::to_string(&delete_json).unwrap();
    let (signature, date, digest) = support::ap::signed_post(
        &recorder.private_key_pem,
        &recorder.key_id,
        &format!("127.0.0.1:{port}"),
        "/~alice/inbox",
        &body,
    );
    let response = alice_client.post_json_with_headers(
        "/~alice/inbox",
        &body,
        &[
            ("Signature", &signature),
            ("Date", &date),
            ("Digest", &digest),
            ("Content-Type", "application/activity+json"),
        ],
    );
    assert_eq!(response.status, 202, "Delete should be accepted");

    // Wait and verify the note is gone
    std::thread::sleep(Duration::from_millis(100));
    let feed_response = alice_client.get("/");
    assert!(
        !feed_response.body.contains("Note to be deleted"),
        "feed should not contain the note after deletion"
    );

    Ok(())
}

/// Test: Delete of unknown object returns 202
#[tokio::test]
async fn delete_unknown_object_returns_202() -> miette::Result<()> {
    let port = free_port();
    let mut server = Server::spawn_federated(&format!("127.0.0.1:{port}"));
    server.wait_for_connection(port);

    let mut alice_client = Client::new(port);
    register(&mut alice_client, "alice");

    // Start a remote actor
    let recorder_port = free_port();
    let recorder = RemoteActor::serve(recorder_port);

    // Follow (one-step flow)
    let handle = format!("remote@127.0.0.1:{recorder_port}");
    alice_client.post_form("/~alice/follow", &[("handle", &handle)]);

    // Wait for Follow and Accept (abbreviated)
    let deadline = Instant::now() + Duration::from_secs(5);
    let mut follow_activity_id = None;
    while Instant::now() < deadline {
        std::thread::sleep(Duration::from_millis(50));
        let deliveries = recorder.recorded_deliveries();
        for delivery in &deliveries {
            if let Ok(activity) = serde_json::from_str::<serde_json::Value>(&delivery.body) {
                if activity.get("type").and_then(|t| t.as_str()) == Some("Follow") {
                    follow_activity_id = activity
                        .get("id")
                        .and_then(|v| v.as_str())
                        .map(|s| s.to_string());
                    break;
                }
            }
        }
        if follow_activity_id.is_some() {
            break;
        }
    }
    let follow_id = follow_activity_id.expect("should have received Follow activity");

    let accept_json = serde_json::json!({
        "@context": "https://www.w3.org/ns/activitystreams",
        "id": format!("{}/accept/1", recorder.actor_url),
        "type": "Accept",
        "actor": recorder.actor_url,
        "object": {
            "id": follow_id,
            "type": "Follow",
            "actor": format!("http://127.0.0.1:{port}/~alice"),
            "object": recorder.actor_url
        }
    });
    let body = serde_json::to_string(&accept_json).unwrap();
    let (signature, date, digest) = support::ap::signed_post(
        &recorder.private_key_pem,
        &recorder.key_id,
        &format!("127.0.0.1:{port}"),
        "/~alice/inbox",
        &body,
    );
    alice_client.post_json_with_headers(
        "/~alice/inbox",
        &body,
        &[
            ("Signature", &signature),
            ("Date", &date),
            ("Digest", &digest),
            ("Content-Type", "application/activity+json"),
        ],
    );

    // Send Delete for unknown note
    let unknown_note_url = format!("{}/notes/unknown_note", recorder.actor_url);
    let delete_json = serde_json::json!({
        "@context": "https://www.w3.org/ns/activitystreams",
        "id": format!("{}/activities/delete_unknown", recorder.actor_url),
        "type": "Delete",
        "actor": recorder.actor_url,
        "object": unknown_note_url
    });
    let body = serde_json::to_string(&delete_json).unwrap();
    let (signature, date, digest) = support::ap::signed_post(
        &recorder.private_key_pem,
        &recorder.key_id,
        &format!("127.0.0.1:{port}"),
        "/~alice/inbox",
        &body,
    );
    let response = alice_client.post_json_with_headers(
        "/~alice/inbox",
        &body,
        &[
            ("Signature", &signature),
            ("Date", &date),
            ("Digest", &digest),
            ("Content-Type", "application/activity+json"),
        ],
    );
    assert_eq!(
        response.status, 202,
        "Delete of unknown object should return 202 (no error)"
    );

    Ok(())
}

/// Test: Create with URL-only object fetches the note via signed GET
#[tokio::test]
async fn create_with_url_only_object_is_fetched() -> miette::Result<()> {
    let port = free_port();
    let mut server = Server::spawn_federated(&format!("127.0.0.1:{port}"));
    server.wait_for_connection(port);

    let mut alice_client = Client::new(port);
    register(&mut alice_client, "alice");

    // Start a remote actor that will also serve the note
    let recorder_port = free_port();
    let recorder = RemoteActor::serve_with_note(recorder_port);

    // Follow the remote actor (one-step flow)
    let handle = format!("remote@127.0.0.1:{recorder_port}");
    alice_client.post_form("/~alice/follow", &[("handle", &handle)]);

    // Wait for Follow and Accept
    let deadline = Instant::now() + Duration::from_secs(5);
    let mut follow_activity_id = None;
    while Instant::now() < deadline {
        std::thread::sleep(Duration::from_millis(50));
        let deliveries = recorder.recorded_deliveries();
        for delivery in &deliveries {
            if let Ok(activity) = serde_json::from_str::<serde_json::Value>(&delivery.body) {
                if activity.get("type").and_then(|t| t.as_str()) == Some("Follow") {
                    follow_activity_id = activity
                        .get("id")
                        .and_then(|v| v.as_str())
                        .map(|s| s.to_string());
                    break;
                }
            }
        }
        if follow_activity_id.is_some() {
            break;
        }
    }
    let follow_id = follow_activity_id.expect("should have received Follow activity");

    let accept_json = serde_json::json!({
        "@context": "https://www.w3.org/ns/activitystreams",
        "id": format!("{}/accept/1", recorder.actor_url),
        "type": "Accept",
        "actor": recorder.actor_url,
        "object": {
            "id": follow_id,
            "type": "Follow",
            "actor": format!("http://127.0.0.1:{port}/~alice"),
            "object": recorder.actor_url
        }
    });
    let body = serde_json::to_string(&accept_json).unwrap();
    let (signature, date, digest) = support::ap::signed_post(
        &recorder.private_key_pem,
        &recorder.key_id,
        &format!("127.0.0.1:{port}"),
        "/~alice/inbox",
        &body,
    );
    alice_client.post_json_with_headers(
        "/~alice/inbox",
        &body,
        &[
            ("Signature", &signature),
            ("Date", &date),
            ("Digest", &digest),
            ("Content-Type", "application/activity+json"),
        ],
    );

    // Set up the note to be served at a URL
    let note_object_url = format!("{}/notes/url_only_test", recorder.actor_url);
    let note_json = serde_json::json!({
        "@context": "https://www.w3.org/ns/activitystreams",
        "id": note_object_url,
        "type": "Note",
        "attributedTo": recorder.actor_url,
        "content": "<p>Note fetched from URL</p>",
        "published": "2026-11-01T14:00:00Z"
    });
    // Extract the path from the URL for the mock server
    let note_path = url::Url::parse(&note_object_url)
        .unwrap()
        .path()
        .to_string();
    recorder.serve_note_at_url(&note_path, note_json);

    // Send Create activity with URL-only object
    let create_json = serde_json::json!({
        "@context": "https://www.w3.org/ns/activitystreams",
        "id": format!("{}/activities/create_url_test", recorder.actor_url),
        "type": "Create",
        "actor": recorder.actor_url,
        "object": note_object_url
    });
    let body = serde_json::to_string(&create_json).unwrap();
    let (signature, date, digest) = support::ap::signed_post(
        &recorder.private_key_pem,
        &recorder.key_id,
        &format!("127.0.0.1:{port}"),
        "/~alice/inbox",
        &body,
    );
    let response = alice_client.post_json_with_headers(
        "/~alice/inbox",
        &body,
        &[
            ("Signature", &signature),
            ("Date", &date),
            ("Digest", &digest),
            ("Content-Type", "application/activity+json"),
        ],
    );
    assert_eq!(
        response.status, 202,
        "Create with URL-only object should be accepted"
    );

    // Wait a bit for the fetch to complete
    std::thread::sleep(Duration::from_millis(200));

    // Verify the note appears in the feed
    let feed_response = alice_client.get("/");
    assert!(
        feed_response.body.contains("Note fetched from URL"),
        "feed should contain the fetched note content"
    );

    // Verify the GET request had a Signature header
    let get_requests = recorder.recorded_get_requests();
    assert!(
        get_requests
            .iter()
            .any(|req| { req.path.contains("/notes/url_only_test") && req.has_signature }),
        "should have made a signed GET request for the note URL"
    );

    Ok(())
}

/// Test: Create with URL-only object that is not a Note is dropped
#[tokio::test]
async fn create_with_url_only_non_note_is_dropped() -> miette::Result<()> {
    let port = free_port();
    let mut server = Server::spawn_federated(&format!("127.0.0.1:{port}"));
    server.wait_for_connection(port);

    let mut alice_client = Client::new(port);
    register(&mut alice_client, "alice");

    // Start a remote actor
    let recorder_port = free_port();
    let recorder = RemoteActor::serve_with_note(recorder_port);

    // Follow the remote actor
    let handle = format!("remote@127.0.0.1:{recorder_port}");
    alice_client.post_form("/~alice/follow", &[("handle", &handle)]);

    // Wait for Follow and Accept (abbreviated)
    let deadline = Instant::now() + Duration::from_secs(5);
    let mut follow_activity_id = None;
    while Instant::now() < deadline {
        std::thread::sleep(Duration::from_millis(50));
        let deliveries = recorder.recorded_deliveries();
        for delivery in &deliveries {
            if let Ok(activity) = serde_json::from_str::<serde_json::Value>(&delivery.body) {
                if activity.get("type").and_then(|t| t.as_str()) == Some("Follow") {
                    follow_activity_id = activity
                        .get("id")
                        .and_then(|v| v.as_str())
                        .map(|s| s.to_string());
                    break;
                }
            }
        }
        if follow_activity_id.is_some() {
            break;
        }
    }
    let follow_id = follow_activity_id.expect("should have received Follow activity");

    let accept_json = serde_json::json!({
        "@context": "https://www.w3.org/ns/activitystreams",
        "id": format!("{}/accept/1", recorder.actor_url),
        "type": "Accept",
        "actor": recorder.actor_url,
        "object": {
            "id": follow_id,
            "type": "Follow",
            "actor": format!("http://127.0.0.1:{port}/~alice"),
            "object": recorder.actor_url
        }
    });
    let body = serde_json::to_string(&accept_json).unwrap();
    let (signature, date, digest) = support::ap::signed_post(
        &recorder.private_key_pem,
        &recorder.key_id,
        &format!("127.0.0.1:{port}"),
        "/~alice/inbox",
        &body,
    );
    alice_client.post_json_with_headers(
        "/~alice/inbox",
        &body,
        &[
            ("Signature", &signature),
            ("Date", &date),
            ("Digest", &digest),
            ("Content-Type", "application/activity+json"),
        ],
    );

    // Set up a non-Note object to be served
    let article_url = format!("{}/articles/not_a_note", recorder.actor_url);
    let article_json = serde_json::json!({
        "@context": "https://www.w3.org/ns/activitystreams",
        "id": article_url,
        "type": "Article",
        "attributedTo": recorder.actor_url,
        "content": "This is an article, not a note"
    });
    // Extract the path from the URL for the mock server
    let article_path = url::Url::parse(&article_url).unwrap().path().to_string();
    recorder.serve_note_at_url(&article_path, article_json);

    // Send Create activity with URL pointing to non-Note
    let create_json = serde_json::json!({
        "@context": "https://www.w3.org/ns/activitystreams",
        "id": format!("{}/activities/create_article_test", recorder.actor_url),
        "type": "Create",
        "actor": recorder.actor_url,
        "object": article_url
    });
    let body = serde_json::to_string(&create_json).unwrap();
    let (signature, date, digest) = support::ap::signed_post(
        &recorder.private_key_pem,
        &recorder.key_id,
        &format!("127.0.0.1:{port}"),
        "/~alice/inbox",
        &body,
    );
    let response = alice_client.post_json_with_headers(
        "/~alice/inbox",
        &body,
        &[
            ("Signature", &signature),
            ("Date", &date),
            ("Digest", &digest),
            ("Content-Type", "application/activity+json"),
        ],
    );
    assert_eq!(
        response.status, 202,
        "Create should be accepted even for non-Note"
    );

    // Wait a bit for processing
    std::thread::sleep(Duration::from_millis(200));

    // Verify the non-Note does not appear in the feed
    let feed_response = alice_client.get("/");
    assert!(
        !feed_response
            .body
            .contains("This is an article, not a note"),
        "feed should not contain non-Note object"
    );

    Ok(())
}

/// Test: Announce with embedded Note from a followee stores the note
#[tokio::test]
async fn announce_with_embedded_note_from_followee_is_stored() -> miette::Result<()> {
    let port = free_port();
    let mut server = Server::spawn_federated(&format!("127.0.0.1:{port}"));
    server.wait_for_connection(port);

    let mut alice_client = Client::new(port);
    register(&mut alice_client, "alice");

    // Start a remote actor
    let recorder_port = free_port();
    let recorder = RemoteActor::serve(recorder_port);

    // Follow the remote actor (one-step flow)
    let handle = format!("remote@127.0.0.1:{recorder_port}");
    alice_client.post_form("/~alice/follow", &[("handle", &handle)]);

    // Wait for Follow and Accept
    let deadline = Instant::now() + Duration::from_secs(5);
    let mut follow_activity_id = None;
    while Instant::now() < deadline {
        std::thread::sleep(Duration::from_millis(50));
        let deliveries = recorder.recorded_deliveries();
        for delivery in &deliveries {
            if let Ok(activity) = serde_json::from_str::<serde_json::Value>(&delivery.body) {
                if activity.get("type").and_then(|t| t.as_str()) == Some("Follow") {
                    follow_activity_id = activity
                        .get("id")
                        .and_then(|v| v.as_str())
                        .map(|s| s.to_string());
                    break;
                }
            }
        }
        if follow_activity_id.is_some() {
            break;
        }
    }
    let follow_id = follow_activity_id.expect("should have received Follow activity");

    let accept_json = serde_json::json!({
        "@context": "https://www.w3.org/ns/activitystreams",
        "id": format!("{}/accept/1", recorder.actor_url),
        "type": "Accept",
        "actor": recorder.actor_url,
        "object": {
            "id": follow_id,
            "type": "Follow",
            "actor": format!("http://127.0.0.1:{port}/~alice"),
            "object": recorder.actor_url
        }
    });
    let body = serde_json::to_string(&accept_json).unwrap();
    let (signature, date, digest) = support::ap::signed_post(
        &recorder.private_key_pem,
        &recorder.key_id,
        &format!("127.0.0.1:{port}"),
        "/~alice/inbox",
        &body,
    );
    alice_client.post_json_with_headers(
        "/~alice/inbox",
        &body,
        &[
            ("Signature", &signature),
            ("Date", &date),
            ("Digest", &digest),
            ("Content-Type", "application/activity+json"),
        ],
    );

    // Send Announce with embedded Note
    let note_url = format!("{}/notes/boosted_note", recorder.actor_url);
    let announce_json = serde_json::json!({
        "@context": "https://www.w3.org/ns/activitystreams",
        "id": format!("{}/activities/announce1", recorder.actor_url),
        "type": "Announce",
        "actor": recorder.actor_url,
        "object": {
            "id": note_url,
            "type": "Note",
            "attributedTo": recorder.actor_url,
            "content": "<p>This is a boosted note!</p>",
            "published": "2026-11-01T14:00:00Z"
        }
    });
    let body = serde_json::to_string(&announce_json).unwrap();
    let (signature, date, digest) = support::ap::signed_post(
        &recorder.private_key_pem,
        &recorder.key_id,
        &format!("127.0.0.1:{port}"),
        "/~alice/inbox",
        &body,
    );
    let response = alice_client.post_json_with_headers(
        "/~alice/inbox",
        &body,
        &[
            ("Signature", &signature),
            ("Date", &date),
            ("Digest", &digest),
            ("Content-Type", "application/activity+json"),
        ],
    );
    assert_eq!(response.status, 202, "Announce should be accepted");

    // Wait for processing
    std::thread::sleep(Duration::from_millis(200));

    // Verify the note appears in the feed
    let feed_response = alice_client.get("/");
    assert!(
        feed_response.body.contains("This is a boosted note!"),
        "feed should contain the boosted note"
    );

    Ok(())
}

/// Test: Announce from a non-followee is dropped
#[tokio::test]
async fn announce_from_non_followee_is_dropped() -> miette::Result<()> {
    let port = free_port();
    let mut server = Server::spawn_federated(&format!("127.0.0.1:{port}"));
    server.wait_for_connection(port);

    let mut alice_client = Client::new(port);
    register(&mut alice_client, "alice");

    // Start a remote actor (but don't follow)
    let recorder_port = free_port();
    let recorder = RemoteActor::serve(recorder_port);

    // Send Announce without following
    let note_url = format!("{}/notes/unfollowed_note", recorder.actor_url);
    let announce_json = serde_json::json!({
        "@context": "https://www.w3.org/ns/activitystreams",
        "id": format!("{}/activities/announce_unfollowed", recorder.actor_url),
        "type": "Announce",
        "actor": recorder.actor_url,
        "object": {
            "id": note_url,
            "type": "Note",
            "attributedTo": recorder.actor_url,
            "content": "<p>This should not be stored</p>",
            "published": "2026-11-01T15:00:00Z"
        }
    });
    let body = serde_json::to_string(&announce_json).unwrap();
    let (signature, date, digest) = support::ap::signed_post(
        &recorder.private_key_pem,
        &recorder.key_id,
        &format!("127.0.0.1:{port}"),
        "/~alice/inbox",
        &body,
    );
    let response = alice_client.post_json_with_headers(
        "/~alice/inbox",
        &body,
        &[
            ("Signature", &signature),
            ("Date", &date),
            ("Digest", &digest),
            ("Content-Type", "application/activity+json"),
        ],
    );
    assert_eq!(
        response.status, 202,
        "Announce should be accepted (but dropped)"
    );

    // Wait for processing
    std::thread::sleep(Duration::from_millis(200));

    // Verify the note does NOT appear in the feed
    let feed_response = alice_client.get("/");
    assert!(
        !feed_response.body.contains("This should not be stored"),
        "feed should not contain note from non-followee"
    );

    Ok(())
}

/// Test: Announce of an already-stored note is idempotent
#[tokio::test]
async fn announce_of_already_stored_note_is_idempotent() -> miette::Result<()> {
    let port = free_port();
    let mut server = Server::spawn_federated(&format!("127.0.0.1:{port}"));
    server.wait_for_connection(port);

    let mut alice_client = Client::new(port);
    register(&mut alice_client, "alice");

    // Start a remote actor
    let recorder_port = free_port();
    let recorder = RemoteActor::serve(recorder_port);

    // Follow the remote actor
    let handle = format!("remote@127.0.0.1:{recorder_port}");
    alice_client.post_form("/~alice/follow", &[("handle", &handle)]);

    // Wait for Follow and Accept
    let deadline = Instant::now() + Duration::from_secs(5);
    let mut follow_activity_id = None;
    while Instant::now() < deadline {
        std::thread::sleep(Duration::from_millis(50));
        let deliveries = recorder.recorded_deliveries();
        for delivery in &deliveries {
            if let Ok(activity) = serde_json::from_str::<serde_json::Value>(&delivery.body) {
                if activity.get("type").and_then(|t| t.as_str()) == Some("Follow") {
                    follow_activity_id = activity
                        .get("id")
                        .and_then(|v| v.as_str())
                        .map(|s| s.to_string());
                    break;
                }
            }
        }
        if follow_activity_id.is_some() {
            break;
        }
    }
    let follow_id = follow_activity_id.expect("should have received Follow activity");

    let accept_json = serde_json::json!({
        "@context": "https://www.w3.org/ns/activitystreams",
        "id": format!("{}/accept/1", recorder.actor_url),
        "type": "Accept",
        "actor": recorder.actor_url,
        "object": {
            "id": follow_id,
            "type": "Follow",
            "actor": format!("http://127.0.0.1:{port}/~alice"),
            "object": recorder.actor_url
        }
    });
    let body = serde_json::to_string(&accept_json).unwrap();
    let (signature, date, digest) = support::ap::signed_post(
        &recorder.private_key_pem,
        &recorder.key_id,
        &format!("127.0.0.1:{port}"),
        "/~alice/inbox",
        &body,
    );
    alice_client.post_json_with_headers(
        "/~alice/inbox",
        &body,
        &[
            ("Signature", &signature),
            ("Date", &date),
            ("Digest", &digest),
            ("Content-Type", "application/activity+json"),
        ],
    );

    // First, send a Create to store the note
    let note_url = format!("{}/notes/idempotent_note", recorder.actor_url);
    let create_json = serde_json::json!({
        "@context": "https://www.w3.org/ns/activitystreams",
        "id": format!("{}/activities/create_idempotent", recorder.actor_url),
        "type": "Create",
        "actor": recorder.actor_url,
        "object": {
            "id": note_url,
            "type": "Note",
            "attributedTo": recorder.actor_url,
            "content": "<p>Original note content</p>",
            "published": "2026-11-01T16:00:00Z"
        }
    });
    let body = serde_json::to_string(&create_json).unwrap();
    let (signature, date, digest) = support::ap::signed_post(
        &recorder.private_key_pem,
        &recorder.key_id,
        &format!("127.0.0.1:{port}"),
        "/~alice/inbox",
        &body,
    );
    alice_client.post_json_with_headers(
        "/~alice/inbox",
        &body,
        &[
            ("Signature", &signature),
            ("Date", &date),
            ("Digest", &digest),
            ("Content-Type", "application/activity+json"),
        ],
    );

    std::thread::sleep(Duration::from_millis(100));

    // Now send an Announce for the same note (should be idempotent)
    let announce_json = serde_json::json!({
        "@context": "https://www.w3.org/ns/activitystreams",
        "id": format!("{}/activities/announce_idempotent", recorder.actor_url),
        "type": "Announce",
        "actor": recorder.actor_url,
        "object": note_url
    });
    let body = serde_json::to_string(&announce_json).unwrap();
    let (signature, date, digest) = support::ap::signed_post(
        &recorder.private_key_pem,
        &recorder.key_id,
        &format!("127.0.0.1:{port}"),
        "/~alice/inbox",
        &body,
    );
    let response = alice_client.post_json_with_headers(
        "/~alice/inbox",
        &body,
        &[
            ("Signature", &signature),
            ("Date", &date),
            ("Digest", &digest),
            ("Content-Type", "application/activity+json"),
        ],
    );
    assert_eq!(response.status, 202, "Announce should be accepted");

    std::thread::sleep(Duration::from_millis(100));

    // Verify the note still appears only once in the feed
    let feed_response = alice_client.get("/");
    let count = feed_response.body.matches("Original note content").count();
    assert_eq!(count, 1, "note should appear exactly once in feed");

    Ok(())
}
