// SPDX-FileCopyrightText: 2026 Ololduck <ololduck@vit.am> and contributors
//
// SPDX-License-Identifier: GPL-3.0-or-later

//! Phase 3 tests: inbound Flag activities create moderation reports
//!
//! Tests for:
//! - Array object form (Mastodon style)
//! - String object form (single URL)
//! - Forwarded reports to instance actor inbox (forwarded=1)
//! - Duplicate delivery deduplication (same activity id)
//! - Unsigned requests rejected (401)
//! - Actor mismatch rejected (401)

mod support;

use std::io::Read;

use miette::IntoDiagnostic;
use sea_orm::{ColumnTrait, Database, EntityTrait, PaginatorTrait, QueryFilter};
use support::{Server, free_port};

const PASSWORD: &str = "correct horse battery staple";

/// Registers a user through the form, the way the browser would.
fn register(client: &mut support::Client, name: &str, email: &str) {
    let response = client.post_form(
        "/register",
        &[("name", name), ("email", email), ("password", PASSWORD)],
    );
    assert_eq!(
        response.status, 303,
        "registering {} should redirect on success",
        name
    );
}

/// Logs in with the given credentials.
fn login(client: &mut support::Client, name: &str, password: &str) {
    let response = client.post_form("/login", &[("name", name), ("password", password)]);
    assert_eq!(
        response.status, 303,
        "logging in as {} should redirect on success",
        name
    );
}

// ============================================================================
// Flag activity tests
// ============================================================================

#[tokio::test]
async fn flag_with_array_object_creates_report() -> miette::Result<()> {
    use frogue::entities::reports;
    use support::ap::RemoteActor;

    let port = free_port();
    let mut server = Server::spawn_federated(&format!("127.0.0.1:{port}"));
    server.wait_for_connection(port);

    let mut client = support::Client::new(port);

    // Register admin and a target user
    register(&mut client, "admin", "admin@example.com");
    register(&mut client, "alice", "alice@example.com");

    let db = Database::connect(server.database_url())
        .await
        .into_diagnostic()?;

    // Create a remote actor to send the Flag
    let remote_port = free_port();
    let remote_actor = RemoteActor::serve(remote_port);

    // Pre-populate the remote actor in the database
    let remote_actor_model = frogue::entities::ap_remote_actors::ActiveModel {
        actor_url: sea_orm::Set(remote_actor.actor_url.clone()),
        inbox_url: sea_orm::Set(remote_actor.inbox_url.clone()),
        shared_inbox_url: sea_orm::Set(None),
        public_key_pem: sea_orm::Set(remote_actor.public_key_pem.clone()),
        public_key_id: sea_orm::Set(remote_actor.key_id.clone()),
        name: sea_orm::Set(Some("Remote Reporter".to_string())),
        preferred_username: sea_orm::Set(Some("reporter".to_string())),
        icon_url: sea_orm::Set(None),
        fetched_at: sea_orm::Set(chrono::Utc::now().into()),
        ..Default::default()
    };
    use sea_orm::ActiveModelTrait;
    remote_actor_model.insert(&db).await.into_diagnostic()?;

    // Get alice's actor URL for the target
    let _alice_actor = frogue::activitypub::actor::get_or_create(&db, 2)
        .await
        .into_diagnostic()?;
    let alice_actor_url = format!(
        "{}/~alice",
        server.public_url().expect("server should have public URL")
    );

    // Create a Flag activity with array of objects (Mastodon style)
    let flag_activity = serde_json::json!({
        "@context": "https://www.w3.org/ns/activitystreams",
        "id": format!("{}/activities/flag-1", remote_actor.actor_url),
        "type": "Flag",
        "actor": remote_actor.actor_url,
        "object": [
            alice_actor_url,
            format!("{}/~alice/notes/note-1", server.public_url().unwrap())
        ],
        "content": "This user is spamming"
    });

    // Send the Flag to alice's inbox
    let inbox_url = format!("{}/inbox", alice_actor_url);
    remote_actor.send_activity(&inbox_url, flag_activity);

    // Wait a bit for async processing
    tokio::time::sleep(std::time::Duration::from_millis(500)).await;

    // Verify a report was created
    let report_count = reports::Entity::find()
        .filter(reports::Column::ReporterRemoteActorUrl.eq(&remote_actor.actor_url))
        .count(&db)
        .await
        .into_diagnostic()?;

    assert_eq!(report_count, 1, "should have created one report");

    // Verify the report details
    let report = reports::Entity::find()
        .filter(reports::Column::ReporterRemoteActorUrl.eq(&remote_actor.actor_url))
        .one(&db)
        .await
        .into_diagnostic()?
        .expect("report should exist");

    assert_eq!(report.state, "open", "report should be open");
    assert_eq!(report.inbound, 1, "report should be marked as inbound");
    assert_eq!(report.forwarded, 0, "report should not be forwarded");
    assert_eq!(
        report.target_local_user_id,
        Some(2),
        "report should target alice (user_id=2)"
    );
    assert!(report.comment.is_some(), "report should have a comment");
    assert!(
        report.comment.unwrap().contains("spamming"),
        "report comment should contain the content"
    );

    // Verify object URLs were stored
    let object_urls: Vec<String> =
        serde_json::from_str(&report.object_urls).expect("object_urls should be valid JSON array");
    assert_eq!(object_urls.len(), 2, "should have 2 object URLs");
    assert_eq!(
        object_urls[0], alice_actor_url,
        "first object should be the actor"
    );

    Ok(())
}

#[tokio::test]
async fn flag_with_string_object_creates_report() -> miette::Result<()> {
    use frogue::entities::reports;
    use sea_orm::ActiveModelTrait;
    use support::ap::RemoteActor;

    let port = free_port();
    let mut server = Server::spawn_federated(&format!("127.0.0.1:{port}"));
    server.wait_for_connection(port);

    let mut client = support::Client::new(port);

    // Register admin and a target user
    register(&mut client, "admin", "admin@example.com");
    register(&mut client, "bob", "bob@example.com");

    let db = Database::connect(server.database_url())
        .await
        .into_diagnostic()?;

    // Create a remote actor to send the Flag
    let remote_port = free_port();
    let remote_actor = RemoteActor::serve(remote_port);

    // Pre-populate the remote actor in the database
    let remote_actor_model = frogue::entities::ap_remote_actors::ActiveModel {
        actor_url: sea_orm::Set(remote_actor.actor_url.clone()),
        inbox_url: sea_orm::Set(remote_actor.inbox_url.clone()),
        shared_inbox_url: sea_orm::Set(None),
        public_key_pem: sea_orm::Set(remote_actor.public_key_pem.clone()),
        public_key_id: sea_orm::Set(remote_actor.key_id.clone()),
        name: sea_orm::Set(Some("Remote Reporter 2".to_string())),
        preferred_username: sea_orm::Set(Some("reporter2".to_string())),
        icon_url: sea_orm::Set(None),
        fetched_at: sea_orm::Set(chrono::Utc::now().into()),
        ..Default::default()
    };
    remote_actor_model.insert(&db).await.into_diagnostic()?;

    // Get bob's actor URL
    let bob_actor_url = format!(
        "{}/~bob",
        server.public_url().expect("server should have public URL")
    );

    // Create a Flag activity with a single string object
    let flag_activity = serde_json::json!({
        "@context": "https://www.w3.org/ns/activitystreams",
        "id": format!("{}/activities/flag-2", remote_actor.actor_url),
        "type": "Flag",
        "actor": remote_actor.actor_url,
        "object": bob_actor_url,
        "content": "Harassment"
    });

    // Send the Flag to bob's inbox
    let inbox_url = format!("{}/inbox", bob_actor_url);
    remote_actor.send_activity(&inbox_url, flag_activity);

    // Wait for processing
    tokio::time::sleep(std::time::Duration::from_millis(500)).await;

    // Verify a report was created
    let report = reports::Entity::find()
        .filter(reports::Column::ReporterRemoteActorUrl.eq(&remote_actor.actor_url))
        .one(&db)
        .await
        .into_diagnostic()?
        .expect("report should exist");

    assert_eq!(report.state, "open", "report should be open");
    assert_eq!(report.inbound, 1, "report should be marked as inbound");

    // Verify object URLs - single string should be converted to array
    let object_urls: Vec<String> =
        serde_json::from_str(&report.object_urls).expect("object_urls should be valid JSON array");
    assert_eq!(object_urls.len(), 1, "should have 1 object URL");
    assert_eq!(
        object_urls[0], bob_actor_url,
        "object should be the actor URL"
    );

    Ok(())
}

#[tokio::test]
async fn flag_to_instance_actor_inbox_creates_forwarded_report() -> miette::Result<()> {
    use frogue::entities::reports;
    use sea_orm::{ActiveModelTrait, Set};
    use support::ap::RemoteActor;

    let port = free_port();
    let mut server = Server::spawn_federated(&format!("127.0.0.1:{port}"));
    server.wait_for_connection(port);

    let mut client = support::Client::new(port);

    // Register admin
    register(&mut client, "admin", "admin@example.com");

    let db = Database::connect(server.database_url())
        .await
        .into_diagnostic()?;

    // Create the instance actor
    let (private_key_pem, public_key_pem) =
        frogue::activitypub::keys::generate().expect("key generation failed");
    let instance_actor = frogue::entities::ap_actors::ActiveModel {
        user_id: Set(None),
        repo_id: Set(None),
        private_key_pem: Set(private_key_pem),
        public_key_pem: Set(public_key_pem),
        ..Default::default()
    };
    instance_actor.insert(&db).await.into_diagnostic()?;

    // Create a remote actor to send the Flag
    let remote_port = free_port();
    let remote_actor = RemoteActor::serve(remote_port);

    // Pre-populate the remote actor in the database
    let remote_actor_model = frogue::entities::ap_remote_actors::ActiveModel {
        actor_url: Set(remote_actor.actor_url.clone()),
        inbox_url: Set(remote_actor.inbox_url.clone()),
        shared_inbox_url: Set(None),
        public_key_pem: Set(remote_actor.public_key_pem.clone()),
        public_key_id: Set(remote_actor.key_id.clone()),
        name: Set(Some("Remote Reporter 3".to_string())),
        preferred_username: Set(Some("reporter3".to_string())),
        icon_url: Set(None),
        fetched_at: Set(chrono::Utc::now().into()),
        ..Default::default()
    };
    remote_actor_model.insert(&db).await.into_diagnostic()?;

    // Create a Flag activity to the instance actor inbox
    let flag_activity = serde_json::json!({
        "@context": "https://www.w3.org/ns/activitystreams",
        "id": format!("{}/activities/flag-3", remote_actor.actor_url),
        "type": "Flag",
        "actor": remote_actor.actor_url,
        "object": [format!("{}/~admin", server.public_url().unwrap())],
        "content": "Admin is abusive"
    });

    // Send to instance actor inbox
    let instance_inbox_url = format!("{}/actor/inbox", server.public_url().unwrap());
    remote_actor.send_activity(&instance_inbox_url, flag_activity);

    // Wait for processing
    tokio::time::sleep(std::time::Duration::from_millis(500)).await;

    // Verify a forwarded report was created
    let report = reports::Entity::find()
        .filter(reports::Column::ReporterRemoteActorUrl.eq(&remote_actor.actor_url))
        .one(&db)
        .await
        .into_diagnostic()?
        .expect("report should exist");

    assert_eq!(report.state, "open", "report should be open");
    assert_eq!(report.inbound, 1, "report should be marked as inbound");
    assert_eq!(report.forwarded, 1, "report should be marked as forwarded");
    assert_eq!(
        report.target_local_user_id,
        Some(1),
        "report should target admin (user_id=1)"
    );

    Ok(())
}

#[tokio::test]
async fn duplicate_flag_activity_id_creates_single_report() -> miette::Result<()> {
    use frogue::entities::reports;
    use sea_orm::ActiveModelTrait;
    use support::ap::RemoteActor;

    let port = free_port();
    let mut server = Server::spawn_federated(&format!("127.0.0.1:{port}"));
    server.wait_for_connection(port);

    let mut client = support::Client::new(port);

    // Register admin and a target user
    register(&mut client, "admin", "admin@example.com");
    register(&mut client, "charlie", "charlie@example.com");

    let db = Database::connect(server.database_url())
        .await
        .into_diagnostic()?;

    // Create a remote actor
    let remote_port = free_port();
    let remote_actor = RemoteActor::serve(remote_port);

    let remote_actor_model = frogue::entities::ap_remote_actors::ActiveModel {
        actor_url: sea_orm::Set(remote_actor.actor_url.clone()),
        inbox_url: sea_orm::Set(remote_actor.inbox_url.clone()),
        shared_inbox_url: sea_orm::Set(None),
        public_key_pem: sea_orm::Set(remote_actor.public_key_pem.clone()),
        public_key_id: sea_orm::Set(remote_actor.key_id.clone()),
        name: sea_orm::Set(Some("Remote Reporter 4".to_string())),
        preferred_username: sea_orm::Set(Some("reporter4".to_string())),
        icon_url: sea_orm::Set(None),
        fetched_at: sea_orm::Set(chrono::Utc::now().into()),
        ..Default::default()
    };
    remote_actor_model.insert(&db).await.into_diagnostic()?;

    let charlie_actor_url = format!(
        "{}/~charlie",
        server.public_url().expect("server should have public URL")
    );

    // Create a Flag activity with a specific ID
    let activity_id = format!("{}/activities/flag-dup", remote_actor.actor_url);
    let flag_activity = serde_json::json!({
        "@context": "https://www.w3.org/ns/activitystreams",
        "id": activity_id,
        "type": "Flag",
        "actor": remote_actor.actor_url,
        "object": charlie_actor_url,
        "content": "Duplicate test"
    });

    // Send the same activity twice
    let inbox_url = format!("{}/inbox", charlie_actor_url);
    remote_actor.send_activity(&inbox_url, flag_activity.clone());
    tokio::time::sleep(std::time::Duration::from_millis(200)).await;

    remote_actor.send_activity(&inbox_url, flag_activity);
    tokio::time::sleep(std::time::Duration::from_millis(500)).await;

    // Verify only one report was created (deduplication by activity_url)
    let report_count = reports::Entity::find()
        .filter(reports::Column::ReporterRemoteActorUrl.eq(&remote_actor.actor_url))
        .count(&db)
        .await
        .into_diagnostic()?;

    assert_eq!(
        report_count, 1,
        "should have created only one report despite duplicate delivery"
    );

    // Verify the activity_url is stored
    let report = reports::Entity::find()
        .filter(reports::Column::ReporterRemoteActorUrl.eq(&remote_actor.actor_url))
        .one(&db)
        .await
        .into_diagnostic()?
        .expect("report should exist");

    assert_eq!(
        report.activity_url,
        Some(activity_id),
        "activity_url should be stored for deduplication"
    );

    Ok(())
}

#[tokio::test]
async fn unsigned_flag_gets_401() -> miette::Result<()> {
    use frogue::entities::reports;

    let port = free_port();
    let mut server = Server::spawn_federated(&format!("127.0.0.1:{port}"));
    server.wait_for_connection(port);

    let mut client = support::Client::new(port);

    // Register admin and a target user
    register(&mut client, "admin", "admin@example.com");
    register(&mut client, "dave", "dave@example.com");

    let db = Database::connect(server.database_url())
        .await
        .into_diagnostic()?;

    let dave_actor_url = format!(
        "{}/~dave",
        server.public_url().expect("server should have public URL")
    );

    // Create an unsigned Flag activity
    let flag_activity = serde_json::json!({
        "@context": "https://www.w3.org/ns/activitystreams",
        "id": "https://evil.example/activities/flag-unsigned",
        "type": "Flag",
        "actor": "https://evil.example/actor",
        "object": dave_actor_url,
        "content": "Unsigned report"
    });

    // Send unsigned request directly
    let response = client.post_json(
        &format!("{}/inbox", dave_actor_url),
        &flag_activity.to_string(),
        &[],
    );

    // Should be rejected with 401
    assert_eq!(response.status, 401, "unsigned request should be rejected");

    // Verify no report was created
    let report_count = reports::Entity::find().count(&db).await.into_diagnostic()?;

    assert_eq!(
        report_count, 0,
        "no report should be created from unsigned request"
    );

    Ok(())
}

#[tokio::test]
async fn actor_mismatch_flag_gets_401() -> miette::Result<()> {
    use frogue::entities::reports;
    use sea_orm::ActiveModelTrait;
    use support::ap::RemoteActor;

    let port = free_port();
    let mut server = Server::spawn_federated(&format!("127.0.0.1:{port}"));
    server.wait_for_connection(port);

    let mut client = support::Client::new(port);

    // Register admin and a target user
    register(&mut client, "admin", "admin@example.com");
    register(&mut client, "eve", "eve@example.com");

    let db = Database::connect(server.database_url())
        .await
        .into_diagnostic()?;

    // Create a remote actor
    let remote_port = free_port();
    let remote_actor = RemoteActor::serve(remote_port);

    let remote_actor_model = frogue::entities::ap_remote_actors::ActiveModel {
        actor_url: sea_orm::Set(remote_actor.actor_url.clone()),
        inbox_url: sea_orm::Set(remote_actor.inbox_url.clone()),
        shared_inbox_url: sea_orm::Set(None),
        public_key_pem: sea_orm::Set(remote_actor.public_key_pem.clone()),
        public_key_id: sea_orm::Set(remote_actor.key_id.clone()),
        name: sea_orm::Set(Some("Remote Reporter 5".to_string())),
        preferred_username: sea_orm::Set(Some("reporter5".to_string())),
        icon_url: sea_orm::Set(None),
        fetched_at: sea_orm::Set(chrono::Utc::now().into()),
        ..Default::default()
    };
    remote_actor_model.insert(&db).await.into_diagnostic()?;

    let eve_actor_url = format!(
        "{}/~eve",
        server.public_url().expect("server should have public URL")
    );

    // Create a Flag activity where the actor field doesn't match the signer
    let flag_activity = serde_json::json!({
        "@context": "https://www.w3.org/ns/activitystreams",
        "id": format!("{}/activities/flag-mismatch", remote_actor.actor_url),
        "type": "Flag",
        "actor": "https://different.example/actor", // Different from signer!
        "object": eve_actor_url,
        "content": "Actor mismatch test"
    });

    // Send the activity (signed by remote_actor but claims to be from different
    // actor) We need to send this manually with the wrong actor field
    // The send_activity method will sign with remote_actor's key
    // but the activity claims to be from a different actor
    use std::io::Write;
    use std::net::TcpStream;

    let body = serde_json::to_string(&flag_activity).unwrap();
    let target_host = format!("127.0.0.1:{}", port);
    let target_path = "/~eve/inbox".to_string();

    let (signature, date, digest) = support::ap::signed_post(
        &remote_actor.private_key_pem,
        &remote_actor.key_id,
        &target_host,
        &target_path,
        &body,
    );

    let request = format!(
        "POST {} HTTP/1.1\r\n\
         Host: {}\r\n\
         Date: {}\r\n\
         Digest: {}\r\n\
         Signature: {}\r\n\
         Content-Type: application/activity+json\r\n\
         Content-Length: {}\r\n\
         \r\n\
         {}",
        target_path,
        target_host,
        date,
        digest,
        signature,
        body.len(),
        body
    );

    let mut stream = TcpStream::connect(&target_host).expect("failed to connect");
    stream
        .write_all(request.as_bytes())
        .expect("failed to send");

    // Read response
    let mut buffer = [0; 1024];
    let n = stream.read(&mut buffer).expect("failed to read");
    let response = String::from_utf8_lossy(&buffer[..n]);

    // Should be rejected with 401
    assert!(
        response.contains("401") || response.contains("Unauthorized"),
        "actor mismatch should be rejected with 401, got: {}",
        response
    );

    // Verify no report was created
    let report_count = reports::Entity::find().count(&db).await.into_diagnostic()?;

    assert_eq!(
        report_count, 0,
        "no report should be created from actor mismatch"
    );

    Ok(())
}

#[tokio::test]
async fn flag_report_visible_in_admin_queue() -> miette::Result<()> {
    use frogue::entities::reports;
    use sea_orm::ActiveModelTrait;
    use support::ap::RemoteActor;

    let port = free_port();
    let mut server = Server::spawn_federated(&format!("127.0.0.1:{port}"));
    server.wait_for_connection(port);

    let mut client = support::Client::new(port);

    // Register admin and a target user
    register(&mut client, "admin", "admin@example.com");
    register(&mut client, "frank", "frank@example.com");
    login(&mut client, "admin", PASSWORD);

    let db = Database::connect(server.database_url())
        .await
        .into_diagnostic()?;

    // Create a remote actor
    let remote_port = free_port();
    let remote_actor = RemoteActor::serve(remote_port);

    let remote_actor_model = frogue::entities::ap_remote_actors::ActiveModel {
        actor_url: sea_orm::Set(remote_actor.actor_url.clone()),
        inbox_url: sea_orm::Set(remote_actor.inbox_url.clone()),
        shared_inbox_url: sea_orm::Set(None),
        public_key_pem: sea_orm::Set(remote_actor.public_key_pem.clone()),
        public_key_id: sea_orm::Set(remote_actor.key_id.clone()),
        name: sea_orm::Set(Some("Remote Reporter 6".to_string())),
        preferred_username: sea_orm::Set(Some("reporter6".to_string())),
        icon_url: sea_orm::Set(None),
        fetched_at: sea_orm::Set(chrono::Utc::now().into()),
        ..Default::default()
    };
    remote_actor_model.insert(&db).await.into_diagnostic()?;

    let frank_actor_url = format!(
        "{}/~frank",
        server.public_url().expect("server should have public URL")
    );

    // Send a Flag activity
    let flag_activity = serde_json::json!({
        "@context": "https://www.w3.org/ns/activitystreams",
        "id": format!("{}/activities/flag-admin-test", remote_actor.actor_url),
        "type": "Flag",
        "actor": remote_actor.actor_url,
        "object": frank_actor_url,
        "content": "Visible in admin queue"
    });

    let inbox_url = format!("{}/inbox", frank_actor_url);
    remote_actor.send_activity(&inbox_url, flag_activity);

    // Wait for processing
    tokio::time::sleep(std::time::Duration::from_millis(500)).await;

    // Check the admin reports page
    let response = client.get("/admin/reports");
    assert_eq!(response.status, 200, "admin should be able to view reports");

    // The report should be visible in the admin queue
    assert!(
        response.body.contains("Visible in admin queue") || response.body.contains("frank"),
        "report should be visible in admin queue, got: {}",
        response.body
    );

    // Verify the report count in the database
    let report_count = reports::Entity::find()
        .filter(reports::Column::State.eq("open"))
        .count(&db)
        .await
        .into_diagnostic()?;

    assert!(report_count >= 1, "should have at least one open report");

    Ok(())
}
