//! Integration tests for comments on tickets and patch proposals.
//!
//! Tests the full stack: HTTP endpoints, database, views.
//! Comments can be created, edited, and deleted on both tickets and patches.
//! Permissions: author or repo owner can edit/delete, anyone logged in can
//! comment.

mod support;

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

/// The password the tests register and log in with.
const PASSWORD: &str = "correct horse battery staple";

fn spawn() -> (Server, u16) {
    let port = free_port();
    let mut server = Server::spawn(&format!("127.0.0.1:{port}"));
    server.wait_for_connection(port);
    (server, port)
}

/// Registers `name` through the form, which logs them in.
fn register(client: &mut Client, name: &str) {
    let email = format!("{name}@example.org");
    let response = client.post_form(
        "/register",
        &[("name", name), ("email", &email), ("password", PASSWORD)],
    );
    assert_eq!(
        response.status, 303,
        "registering should log in\n{}",
        response.body
    );
}

/// Creates a repository through the form.
fn create_repo(client: &mut Client, owner: &str, slug: &str) {
    let response = client.post_form(
        &format!("/~{owner}/repositories"),
        &[
            ("slug", slug),
            ("description", "a repository from the comments tests"),
            ("visibility", "public"),
        ],
    );
    assert_eq!(
        response.status, 303,
        "creating repository should redirect\n{}",
        response.body
    );
}

/// Opens a ticket and returns its number.
fn open_ticket(client: &mut Client, owner: &str, slug: &str, summary: &str) -> i64 {
    let response = client.post_form(
        &format!("/~{owner}/{slug}/tickets"),
        &[
            ("summary", summary),
            ("description", "Test description"),
            ("state", "open"),
        ],
    );
    assert_eq!(
        response.status, 303,
        "opening ticket should redirect\n{}",
        response.body
    );

    // Extract ticket number from redirect location
    let location = response.location.as_ref().expect("should have location");
    let parts: Vec<&str> = location.split('/').collect();
    parts
        .last()
        .expect("should have ticket number")
        .parse()
        .expect("should be a number")
}

/// Extracts the UUID of the last comment from the ticket page HTML.
fn extract_last_comment_uuid(body: &str) -> Option<String> {
    // Look for the delete form which contains the UUID in the action
    // Pattern: action="/~owner/repo/tickets/1/comments/{uuid}/delete"
    let mut uuid_start = body.rfind("/comments/")?;
    uuid_start += "/comments/".len();
    let uuid_end = body[uuid_start..].find("/delete")?;
    Some(body[uuid_start..uuid_start + uuid_end].to_string())
}

#[tokio::test]
async fn can_create_comment_on_ticket() -> miette::Result<()> {
    let (_server, port) = spawn();
    let mut alice = Client::new(port);
    register(&mut alice, "alice");
    create_repo(&mut alice, "alice", "test-repo");
    let ticket_num = open_ticket(&mut alice, "alice", "test-repo", "Test ticket");

    // Create a comment
    let response = alice.post_multipart(
        &format!("/~alice/test-repo/tickets/{ticket_num}/comments"),
        &[("content", "This is a test comment")],
        &[],
    );
    assert_eq!(
        response.status, 303,
        "comment creation should redirect\n{}",
        response.body
    );

    // Verify comment appears on ticket page
    let response = alice.get(&format!("/~alice/test-repo/tickets/{ticket_num}"));
    assert_eq!(
        response.status, 200,
        "ticket page should load\n{}",
        response.body
    );
    assert!(
        response.body.contains("This is a test comment"),
        "comment should appear on ticket page, got:\n{}",
        response.body
    );

    Ok(())
}

#[tokio::test]
async fn comment_content_is_trimmed() -> miette::Result<()> {
    let (_server, port) = spawn();
    let mut alice = Client::new(port);
    register(&mut alice, "alice");
    create_repo(&mut alice, "alice", "test-repo");
    let ticket_num = open_ticket(&mut alice, "alice", "test-repo", "Test ticket");

    // Create a comment with extra whitespace
    let response = alice.post_multipart(
        &format!("/~alice/test-repo/tickets/{ticket_num}/comments"),
        &[("content", "   trimmed content   ")],
        &[],
    );
    assert_eq!(response.status, 303);

    // Verify comment was trimmed
    let response = alice.get(&format!("/~alice/test-repo/tickets/{ticket_num}"));
    assert_eq!(response.status, 200);
    assert!(
        response.body.contains("trimmed content"),
        "comment should be trimmed, got:\n{}",
        response.body
    );
    assert!(
        !response.body.contains("   trimmed content   "),
        "whitespace should be removed, got:\n{}",
        response.body
    );

    Ok(())
}

#[tokio::test]
async fn unauthenticated_user_cannot_comment() -> miette::Result<()> {
    let (_server, port) = spawn();
    let mut alice = Client::new(port);
    register(&mut alice, "alice");
    create_repo(&mut alice, "alice", "test-repo");
    let ticket_num = open_ticket(&mut alice, "alice", "test-repo", "Test ticket");

    // Try to comment without being logged in
    let mut anon = Client::new(port);
    let response = anon.post_multipart(
        &format!("/~alice/test-repo/tickets/{ticket_num}/comments"),
        &[("content", "This should be rejected")],
        &[],
    );

    // Should redirect to login or reject
    assert!(
        response.status == 401
            || (response.status == 303 && response.location.as_deref() == Some("/login")),
        "unauthenticated user should be rejected or redirected to login, got status {} location {:?}\n{}",
        response.status,
        response.location,
        response.body
    );

    Ok(())
}

#[tokio::test]
async fn can_edit_own_comment() -> miette::Result<()> {
    let (_server, port) = spawn();
    let mut alice = Client::new(port);
    register(&mut alice, "alice");
    create_repo(&mut alice, "alice", "test-repo");
    let ticket_num = open_ticket(&mut alice, "alice", "test-repo", "Test ticket");

    // Create a comment
    let response = alice.post_multipart(
        &format!("/~alice/test-repo/tickets/{ticket_num}/comments"),
        &[("content", "Original comment")],
        &[],
    );
    assert_eq!(response.status, 303);

    // Get the comment UUID from the ticket page
    let response = alice.get(&format!("/~alice/test-repo/tickets/{ticket_num}"));
    assert_eq!(response.status, 200);
    let uuid = extract_last_comment_uuid(&response.body).expect("should find comment UUID");

    // Edit the comment
    let response = alice.post_form(
        &format!("/~alice/test-repo/tickets/{ticket_num}/comments/{uuid}"),
        &[("content", "Edited comment")],
    );
    assert_eq!(
        response.status, 303,
        "editing comment should redirect\n{}",
        response.body
    );

    // Verify comment was updated
    let response = alice.get(&format!("/~alice/test-repo/tickets/{ticket_num}"));
    assert_eq!(response.status, 200);
    assert!(
        response.body.contains("Edited comment"),
        "comment should be edited, got:\n{}",
        response.body
    );

    Ok(())
}

#[tokio::test]
async fn cannot_edit_someone_elses_comment() -> miette::Result<()> {
    let (_server, port) = spawn();
    let mut alice = Client::new(port);
    register(&mut alice, "alice");
    create_repo(&mut alice, "alice", "test-repo");
    let ticket_num = open_ticket(&mut alice, "alice", "test-repo", "Test ticket");

    // Alice creates a comment
    let response = alice.post_multipart(
        &format!("/~alice/test-repo/tickets/{ticket_num}/comments"),
        &[("content", "Alice's comment")],
        &[],
    );
    assert_eq!(response.status, 303);

    // Get the comment UUID
    let response = alice.get(&format!("/~alice/test-repo/tickets/{ticket_num}"));
    let uuid = extract_last_comment_uuid(&response.body).expect("should find UUID");

    // Bob tries to edit it
    let mut bob = Client::new(port);
    register(&mut bob, "bob");
    let response = bob.post_form(
        &format!("/~alice/test-repo/tickets/{ticket_num}/comments/{uuid}"),
        &[("content", "Bob's edit")],
    );
    assert_eq!(
        response.status, 403,
        "bob should not be able to edit alice's comment\n{}",
        response.body
    );

    Ok(())
}

#[tokio::test]
async fn repo_owner_can_delete_any_comment() -> miette::Result<()> {
    let (_server, port) = spawn();
    let mut alice = Client::new(port);
    register(&mut alice, "alice");
    create_repo(&mut alice, "alice", "test-repo");
    let ticket_num = open_ticket(&mut alice, "alice", "test-repo", "Test ticket");

    // Bob creates a comment
    let mut bob = Client::new(port);
    register(&mut bob, "bob");
    let response = bob.post_multipart(
        &format!("/~alice/test-repo/tickets/{ticket_num}/comments"),
        &[("content", "Bob's comment")],
        &[],
    );
    assert_eq!(response.status, 303);

    // Get the comment UUID
    let response = bob.get(&format!("/~alice/test-repo/tickets/{ticket_num}"));
    let uuid = extract_last_comment_uuid(&response.body).expect("should find UUID");

    // Alice (repo owner) deletes it
    let response = alice.post_form(
        &format!("/~alice/test-repo/tickets/{ticket_num}/comments/{uuid}/delete"),
        &[],
    );
    assert_eq!(
        response.status, 303,
        "alice should be able to delete bob's comment\n{}",
        response.body
    );

    // Verify comment was deleted
    let response = alice.get(&format!("/~alice/test-repo/tickets/{ticket_num}"));
    assert_eq!(response.status, 200);
    assert!(
        !response.body.contains("Bob's comment"),
        "comment should be deleted, got:\n{}",
        response.body
    );

    Ok(())
}

#[tokio::test]
async fn comment_author_can_delete_own_comment() -> miette::Result<()> {
    let (_server, port) = spawn();
    let mut alice = Client::new(port);
    register(&mut alice, "alice");
    create_repo(&mut alice, "alice", "test-repo");
    let ticket_num = open_ticket(&mut alice, "alice", "test-repo", "Test ticket");

    // Alice creates a comment
    let response = alice.post_multipart(
        &format!("/~alice/test-repo/tickets/{ticket_num}/comments"),
        &[("content", "Alice's comment")],
        &[],
    );
    assert_eq!(response.status, 303);

    // Get the comment UUID
    let response = alice.get(&format!("/~alice/test-repo/tickets/{ticket_num}"));
    let uuid = extract_last_comment_uuid(&response.body).expect("should find UUID");

    // Alice deletes her own comment
    let response = alice.post_form(
        &format!("/~alice/test-repo/tickets/{ticket_num}/comments/{uuid}/delete"),
        &[],
    );
    assert_eq!(response.status, 303);

    // Verify comment was deleted
    let response = alice.get(&format!("/~alice/test-repo/tickets/{ticket_num}"));
    assert_eq!(response.status, 200);
    assert!(
        !response.body.contains("Alice's comment"),
        "comment should be deleted, got:\n{}",
        response.body
    );

    Ok(())
}

#[tokio::test]
async fn stranger_cannot_delete_comment() -> miette::Result<()> {
    let (_server, port) = spawn();
    let mut alice = Client::new(port);
    register(&mut alice, "alice");
    create_repo(&mut alice, "alice", "test-repo");
    let ticket_num = open_ticket(&mut alice, "alice", "test-repo", "Test ticket");

    // Bob creates a comment
    let mut bob = Client::new(port);
    register(&mut bob, "bob");
    let response = bob.post_multipart(
        &format!("/~alice/test-repo/tickets/{ticket_num}/comments"),
        &[("content", "Bob's comment")],
        &[],
    );
    assert_eq!(response.status, 303);

    // Get the comment UUID
    let response = bob.get(&format!("/~alice/test-repo/tickets/{ticket_num}"));
    let uuid = extract_last_comment_uuid(&response.body).expect("should find UUID");

    // Charlie (stranger) tries to delete it
    let mut charlie = Client::new(port);
    register(&mut charlie, "charlie");
    let response = charlie.post_form(
        &format!("/~alice/test-repo/tickets/{ticket_num}/comments/{uuid}/delete"),
        &[],
    );
    assert_eq!(
        response.status, 403,
        "charlie should not be able to delete bob's comment\n{}",
        response.body
    );

    Ok(())
}

#[tokio::test]
async fn comment_markdown_is_sanitized() -> miette::Result<()> {
    let (_server, port) = spawn();
    let mut alice = Client::new(port);
    register(&mut alice, "alice");
    create_repo(&mut alice, "alice", "test-repo");
    let ticket_num = open_ticket(&mut alice, "alice", "test-repo", "Test ticket");

    // Create a comment with a script tag
    let response = alice.post_multipart(
        &format!("/~alice/test-repo/tickets/{ticket_num}/comments"),
        &[("content", "Hello <script>alert(1)</script> world")],
        &[],
    );
    assert_eq!(response.status, 303);

    // Verify the script tag is removed or escaped, not executed
    let response = alice.get(&format!("/~alice/test-repo/tickets/{ticket_num}"));
    assert_eq!(response.status, 200);
    assert!(
        !response.body.contains("<script>alert(1)</script>"),
        "script tag should not be rendered as HTML, got:\n{}",
        response.body
    );

    Ok(())
}

#[tokio::test]
async fn edited_comment_shows_edited_marker() -> miette::Result<()> {
    let (_server, port) = spawn();
    let mut alice = Client::new(port);
    register(&mut alice, "alice");
    create_repo(&mut alice, "alice", "test-repo");
    let ticket_num = open_ticket(&mut alice, "alice", "test-repo", "Test ticket");

    // Create a comment
    let response = alice.post_multipart(
        &format!("/~alice/test-repo/tickets/{ticket_num}/comments"),
        &[("content", "Original comment")],
        &[],
    );
    assert_eq!(response.status, 303);

    // Get the comment UUID
    let response = alice.get(&format!("/~alice/test-repo/tickets/{ticket_num}"));
    let uuid = extract_last_comment_uuid(&response.body).expect("should find UUID");

    // Edit the comment
    let response = alice.post_form(
        &format!("/~alice/test-repo/tickets/{ticket_num}/comments/{uuid}"),
        &[("content", "Edited comment")],
    );
    assert_eq!(response.status, 303);

    // Verify the edited marker appears
    let response = alice.get(&format!("/~alice/test-repo/tickets/{ticket_num}"));
    assert_eq!(response.status, 200);
    assert!(
        response.body.contains("(last updated at "),
        "edited comment should show marker, got:\n{}",
        response.body
    );

    Ok(())
}

#[tokio::test]
async fn empty_comment_is_refused() -> miette::Result<()> {
    let (_server, port) = spawn();
    let mut alice = Client::new(port);
    register(&mut alice, "alice");
    create_repo(&mut alice, "alice", "test-repo");
    let ticket_num = open_ticket(&mut alice, "alice", "test-repo", "Test ticket");

    // Try to create an empty comment
    let response = alice.post_multipart(
        &format!("/~alice/test-repo/tickets/{ticket_num}/comments"),
        &[("content", "")],
        &[],
    );
    // Refused comments re-render the detail page with the error shown (200
    // status)
    assert_eq!(
        response.status, 200,
        "empty comment should be refused with page re-rendered\n{}",
        response.body
    );
    assert!(
        response.body.contains("a comment needs content"),
        "error message should be shown, got:\n{}",
        response.body
    );

    // Try whitespace only
    let response = alice.post_multipart(
        &format!("/~alice/test-repo/tickets/{ticket_num}/comments"),
        &[("content", "   ")],
        &[],
    );
    assert_eq!(
        response.status, 200,
        "whitespace-only comment should be refused with page re-rendered\n{}",
        response.body
    );
    assert!(
        response.body.contains("a comment needs content"),
        "error message should be shown, got:\n{}",
        response.body
    );

    Ok(())
}

#[tokio::test]
async fn can_create_edit_delete_comment_on_patch() -> miette::Result<()> {
    use sea_orm::{ActiveModelTrait, ColumnTrait, Database, EntityTrait, QueryFilter, Set};
    use uuid::Uuid;

    let (server, port) = spawn();
    let mut alice = Client::new(port);
    register(&mut alice, "alice");
    create_repo(&mut alice, "alice", "test-repo");

    // Create a patch proposal directly in the database
    let db = Database::connect(server.database_url())
        .await
        .expect("connecting to test database");

    // Get the repository ID
    let repo = frogue::entities::repositories::Entity::find()
        .filter(frogue::entities::repositories::Column::Slug.eq("test-repo"))
        .one(&db)
        .await
        .expect("finding repository")
        .expect("repository should exist");

    // Get alice's user ID
    let user = frogue::entities::users::Entity::find()
        .filter(frogue::entities::users::Column::Name.eq("alice"))
        .one(&db)
        .await
        .expect("finding user")
        .expect("user should exist");

    // Create a patch
    let now = chrono::Utc::now().fixed_offset();
    let pp = frogue::entities::patch_proposals::ActiveModel {
        uuid: Set(Uuid::new_v4()),
        repository_id: Set(repo.id),
        creator_id: Set(Some(user.id)),
        number: Set(1),
        summary: Set("Test patch".to_string()),
        description: Set(Some("Test description".to_string())),
        source_ref: Set("patches/1".to_string()),
        target_branch: Set("main".to_string()),
        status: Set(frogue::entities::patch_proposals::Status::Open),
        merge_policy: Set(frogue::entities::patch_proposals::MergePolicy::PreferFastForward),
        created_at: Set(now),
        updated_at: Set(now),
        ..Default::default()
    };
    let _pp = pp.insert(&db).await.expect("inserting patch");

    // Create a comment on the patch
    let response = alice.post_multipart(
        "/~alice/test-repo/patches/1/comments",
        &[("content", "Comment on patch")],
        &[],
    );
    assert_eq!(response.status, 303);

    // Verify comment appears on patch page
    let response = alice.get("/~alice/test-repo/patches/1");
    assert_eq!(response.status, 200);
    assert!(
        response.body.contains("Comment on patch"),
        "comment should appear on patch page, got:\n{}",
        response.body
    );

    // Get the comment UUID
    let uuid = extract_last_comment_uuid(&response.body).expect("should find UUID");

    // Edit the comment
    let response = alice.post_form(
        &format!("/~alice/test-repo/patches/1/comments/{uuid}"),
        &[("content", "Edited patch comment")],
    );
    assert_eq!(response.status, 303);

    // Verify it was edited
    let response = alice.get("/~alice/test-repo/patches/1");
    assert!(
        response.body.contains("Edited patch comment"),
        "comment should be edited, got:\n{}",
        response.body
    );

    // Delete the comment
    let response = alice.post_form(
        &format!("/~alice/test-repo/patches/1/comments/{uuid}/delete"),
        &[],
    );
    assert_eq!(response.status, 303);

    // Verify it was deleted
    let response = alice.get("/~alice/test-repo/patches/1");
    assert!(
        !response.body.contains("Edited patch comment"),
        "comment should be deleted, got:\n{}",
        response.body
    );

    Ok(())
}

#[tokio::test]
async fn stranger_cannot_comment_on_draft_patch() -> miette::Result<()> {
    use sea_orm::{ActiveModelTrait, ColumnTrait, Database, EntityTrait, QueryFilter, Set};
    use uuid::Uuid;

    let (server, port) = spawn();
    let mut alice = Client::new(port);
    register(&mut alice, "alice");
    create_repo(&mut alice, "alice", "test-repo");

    // Create a draft patch proposal
    let db = Database::connect(server.database_url())
        .await
        .expect("connecting to test database");

    let repo = frogue::entities::repositories::Entity::find()
        .filter(frogue::entities::repositories::Column::Slug.eq("test-repo"))
        .one(&db)
        .await
        .expect("finding repository")
        .expect("repository should exist");

    let user = frogue::entities::users::Entity::find()
        .filter(frogue::entities::users::Column::Name.eq("alice"))
        .one(&db)
        .await
        .expect("finding user")
        .expect("user should exist");

    let now = chrono::Utc::now().fixed_offset();
    let pp = frogue::entities::patch_proposals::ActiveModel {
        uuid: Set(Uuid::new_v4()),
        repository_id: Set(repo.id),
        creator_id: Set(Some(user.id)),
        number: Set(1),
        summary: Set("Draft patch".to_string()),
        description: Set(Some("Draft description".to_string())),
        source_ref: Set("patches/1".to_string()),
        target_branch: Set("main".to_string()),
        status: Set(frogue::entities::patch_proposals::Status::Draft),
        merge_policy: Set(frogue::entities::patch_proposals::MergePolicy::PreferFastForward),
        created_at: Set(now),
        updated_at: Set(now),
        ..Default::default()
    };
    pp.insert(&db).await.expect("inserting draft patch");

    // Bob (stranger) tries to comment on the draft
    let mut bob = Client::new(port);
    register(&mut bob, "bob");
    let response = bob.post_form(
        "/~alice/test-repo/patches/1/comments",
        &[("content", "Should not work")],
    );

    // Should get 404 (same as viewing the draft)
    assert_eq!(
        response.status, 404,
        "stranger should get 404 on draft patch comment, got:\n{}",
        response.body
    );

    Ok(())
}

#[tokio::test]
async fn a_comment_attachment_can_be_uploaded_and_downloaded() -> miette::Result<()> {
    let (_server, port) = spawn();
    let mut alice = Client::new(port);
    register(&mut alice, "alice");
    create_repo(&mut alice, "alice", "test-repo");
    let ticket_num = open_ticket(&mut alice, "alice", "test-repo", "Test ticket");

    // Create a comment with an attachment using multipart
    let response = alice.post_multipart(
        &format!("/~alice/test-repo/tickets/{ticket_num}/comments"),
        &[("content", "Comment with attachment")],
        &[(
            "attachment",
            "sauce.txt",
            "text/plain",
            b"attachment data".to_vec(),
        )],
    );
    assert_eq!(
        response.status, 303,
        "comment with attachment should redirect\n{}",
        response.body
    );

    // Verify comment appears on ticket page
    let response = alice.get(&format!("/~alice/test-repo/tickets/{ticket_num}"));
    assert_eq!(response.status, 200);
    assert!(
        response.body.contains("Comment with attachment"),
        "comment should appear on ticket page, got:\n{}",
        response.body
    );
    assert!(
        response.body.contains("sauce.txt"),
        "attachment filename should appear, got:\n{}",
        response.body
    );

    // Get the comment UUID
    let uuid = extract_last_comment_uuid(&response.body).expect("should find UUID");

    // Extract the attachment UUID from the page (look for attachment link)
    let attachment_uuid_start = response
        .body
        .find(&format!("/comments/{uuid}/attachments/"))
        .expect("should find attachment link");
    let after_uuid = attachment_uuid_start + format!("/comments/{uuid}/attachments/").len();
    let uuid_end = response.body[after_uuid..]
        .find('"')
        .expect("should find end of UUID");
    let attachment_uuid = &response.body[after_uuid..after_uuid + uuid_end];

    // Download the attachment
    let response = alice.get(&format!(
        "/~alice/test-repo/tickets/{ticket_num}/comments/{uuid}/attachments/{attachment_uuid}"
    ));
    assert_eq!(
        response.status, 200,
        "attachment should be downloadable\n{}",
        response.body
    );
    assert_eq!(response.body, "attachment data");

    Ok(())
}

#[tokio::test]
async fn a_comment_attachment_can_be_deleted() -> miette::Result<()> {
    let (_server, port) = spawn();
    let mut alice = Client::new(port);
    register(&mut alice, "alice");
    create_repo(&mut alice, "alice", "test-repo");
    let ticket_num = open_ticket(&mut alice, "alice", "test-repo", "Test ticket");

    // Create a comment with an attachment
    let response = alice.post_multipart(
        &format!("/~alice/test-repo/tickets/{ticket_num}/comments"),
        &[("content", "Comment with attachment")],
        &[("attachment", "file.txt", "text/plain", b"data".to_vec())],
    );
    assert_eq!(response.status, 303);

    // Get the comment UUID
    let response = alice.get(&format!("/~alice/test-repo/tickets/{ticket_num}"));
    let comment_uuid = extract_last_comment_uuid(&response.body).expect("should find UUID");

    // Extract attachment UUID from the page (look for delete form)
    let attachment_uuid_start = response
        .body
        .find(&format!("/comments/{comment_uuid}/attachments/"))
        .expect("should find attachment link");
    let after_uuid = attachment_uuid_start + format!("/comments/{comment_uuid}/attachments/").len();
    let uuid_end = response.body[after_uuid..]
        .find('"')
        .expect("should find end of UUID");
    let attachment_uuid = &response.body[after_uuid..after_uuid + uuid_end];

    // Delete the attachment
    let response = alice.post_form(
        &format!(
            "/~alice/test-repo/tickets/{ticket_num}/comments/{comment_uuid}/attachments/{attachment_uuid}/delete"
        ),
        &[],
    );
    assert_eq!(response.status, 303);

    // Verify attachment is gone
    let response = alice.get(&format!(
        "/~alice/test-repo/tickets/{ticket_num}/comments/{comment_uuid}/attachments/{attachment_uuid}"
    ));
    assert_eq!(response.status, 404);

    Ok(())
}

#[tokio::test]
async fn stranger_cannot_delete_comment_attachment() -> miette::Result<()> {
    let (_server, port) = spawn();
    let mut alice = Client::new(port);
    register(&mut alice, "alice");
    create_repo(&mut alice, "alice", "test-repo");
    let ticket_num = open_ticket(&mut alice, "alice", "test-repo", "Test ticket");

    // Alice creates a comment with an attachment
    let response = alice.post_multipart(
        &format!("/~alice/test-repo/tickets/{ticket_num}/comments"),
        &[("content", "Alice's comment")],
        &[("attachment", "file.txt", "text/plain", b"data".to_vec())],
    );
    assert_eq!(response.status, 303);

    // Get the comment UUID
    let response = alice.get(&format!("/~alice/test-repo/tickets/{ticket_num}"));
    let comment_uuid = extract_last_comment_uuid(&response.body).expect("should find UUID");

    // Extract attachment UUID
    let attachment_uuid_start = response
        .body
        .find(&format!("/comments/{comment_uuid}/attachments/"))
        .expect("should find attachment");
    let after_uuid = attachment_uuid_start + format!("/comments/{comment_uuid}/attachments/").len();
    let uuid_end = response.body[after_uuid..]
        .find('"')
        .expect("should find end");
    let attachment_uuid = &response.body[after_uuid..after_uuid + uuid_end];

    // Bob tries to delete the attachment
    let mut bob = Client::new(port);
    register(&mut bob, "bob");
    let response = bob.post_form(
        &format!(
            "/~alice/test-repo/tickets/{ticket_num}/comments/{comment_uuid}/attachments/{attachment_uuid}/delete"
        ),
        &[],
    );
    assert_eq!(
        response.status, 403,
        "bob should not be able to delete alice's attachment\n{}",
        response.body
    );

    Ok(())
}
