//! Integration tests for the patch proposals web UI.
//!
//! These tests verify the list, detail, and edit pages for patch proposals,
//! including visibility rules and permissions.

mod support;

use sea_orm::{ActiveModelTrait, ColumnTrait, Database, EntityTrait, QueryFilter, Set};
use support::{Client, Server, free_port};
use uuid::Uuid;

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_repository(client: &mut Client, owner: &str, slug: &str, visibility: &str) {
    let response = client.post_form(
        &format!("/~{owner}/repositories"),
        &[
            ("slug", slug),
            ("description", "a repository from the patch tests"),
            ("visibility", visibility),
        ],
    );
    assert_eq!(
        response.status, 303,
        "creating a repository should redirect\n{}",
        response.body
    );
}

/// Creates a patch proposal directly in the database for testing.
async fn create_patch(
    server: &Server,
    repository_id: i64,
    creator_id: i64,
    number: i64,
    summary: &str,
    status: &str,
) -> i64 {
    use chrono::Utc;
    use frogue::entities::patch_proposals::{ActiveModel, MergePolicy, Status};

    let db = Database::connect(server.database_url())
        .await
        .expect("connecting to test database");

    let status = match status {
        "draft" => Status::Draft,
        "open" => Status::Open,
        "closed" => Status::Closed,
        "merged" => Status::Merged,
        _ => panic!("unknown status"),
    };

    let now = Utc::now().fixed_offset();
    let pp = ActiveModel {
        uuid: Set(Uuid::new_v4()),
        repository_id: Set(repository_id),
        creator_id: Set(Some(creator_id)),
        number: Set(number),
        summary: Set(summary.to_string()),
        description: Set(Some("Test description".to_string())),
        source_ref: Set(format!("patches/{number}")),
        target_branch: Set("main".to_string()),
        status: Set(status),
        merge_policy: Set(MergePolicy::PreferFastForward),
        created_at: Set(now),
        updated_at: Set(now),
        ..Default::default()
    };

    let pp = pp.insert(&db).await.expect("inserting patch");
    pp.id
}

#[actix_web::test]
async fn the_patches_list_page_is_shown() -> miette::Result<()> {
    let (_server, port) = spawn();

    let mut alice = Client::new(port);
    register(&mut alice, "alice");
    create_repository(&mut alice, "alice", "test-repo", "public");

    // Visit the patches list page
    let response = alice.get("/~alice/test-repo/patches");
    assert_eq!(response.status, 200, "patches list should be shown");
    assert!(
        response.body.contains("Patches"),
        "page should have patches heading"
    );

    Ok(())
}

#[actix_web::test]
async fn drafts_are_only_visible_to_creator_and_owner() -> miette::Result<()> {
    let (server, port) = spawn();

    let mut alice = Client::new(port);
    register(&mut alice, "alice");
    create_repository(&mut alice, "alice", "test-repo", "public");

    // Get repository and user IDs
    let db = Database::connect(server.database_url())
        .await
        .expect("connecting to test database");

    let repository = 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 _alice_user = frogue::entities::users::Entity::find()
        .filter(frogue::entities::users::Column::Name.eq("alice"))
        .one(&db)
        .await
        .expect("finding alice")
        .expect("alice should exist");

    // Register bob
    let mut bob = Client::new(port);
    register(&mut bob, "bob");

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

    // Bob creates a draft patch
    create_patch(
        &server,
        repository.id,
        bob_user.id,
        1,
        "Bob's draft",
        "draft",
    )
    .await;

    // Bob (creator) can see the draft
    let response = bob.get(&format!("/~alice/test-repo/patches/1"));
    assert_eq!(response.status, 200, "creator should see draft");
    assert!(response.body.contains("draft"), "should show draft status");

    // Alice (owner) can see the draft
    let response = alice.get(&format!("/~alice/test-repo/patches/1"));
    assert_eq!(response.status, 200, "owner should see draft");

    // Register carol (stranger) who cannot see the draft
    let mut carol = Client::new(port);
    register(&mut carol, "carol");
    let response = carol.get(&format!("/~alice/test-repo/patches/1"));
    assert_eq!(
        response.status, 404,
        "stranger should not see draft, got: {}",
        response.body
    );

    Ok(())
}

#[actix_web::test]
async fn open_patches_are_visible_to_everyone() -> miette::Result<()> {
    let (server, port) = spawn();

    let mut alice = Client::new(port);
    register(&mut alice, "alice");
    create_repository(&mut alice, "alice", "test-repo", "public");

    // Get repository and user IDs
    let db = Database::connect(server.database_url())
        .await
        .expect("connecting to test database");

    let repository = 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 alice_user = frogue::entities::users::Entity::find()
        .filter(frogue::entities::users::Column::Name.eq("alice"))
        .one(&db)
        .await
        .expect("finding alice")
        .expect("alice should exist");

    // Alice creates an open patch
    create_patch(
        &server,
        repository.id,
        alice_user.id,
        1,
        "Open patch",
        "open",
    )
    .await;

    // Register bob (stranger) who can see the open patch
    let mut bob = Client::new(port);
    register(&mut bob, "bob");
    let response = bob.get(&format!("/~alice/test-repo/patches/1"));
    assert_eq!(response.status, 200, "stranger should see open patch");
    assert!(response.body.contains("open"), "should show open status");

    Ok(())
}

#[actix_web::test]
async fn only_creator_or_owner_can_edit_patch() -> miette::Result<()> {
    let (server, port) = spawn();

    let mut alice = Client::new(port);
    register(&mut alice, "alice");
    create_repository(&mut alice, "alice", "test-repo", "public");

    // Get repository and user IDs
    let db = Database::connect(server.database_url())
        .await
        .expect("connecting to test database");

    let repository = 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 _alice_user = frogue::entities::users::Entity::find()
        .filter(frogue::entities::users::Column::Name.eq("alice"))
        .one(&db)
        .await
        .expect("finding alice")
        .expect("alice should exist");

    // Register bob
    let mut bob = Client::new(port);
    register(&mut bob, "bob");

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

    // Bob creates an open patch
    create_patch(
        &server,
        repository.id,
        bob_user.id,
        1,
        "Bob's patch",
        "open",
    )
    .await;

    // Register carol (stranger) who cannot edit the patch
    let mut carol = Client::new(port);
    register(&mut carol, "carol");
    let response = carol.get(&format!("/~alice/test-repo/patches/1/edit"));
    assert_eq!(
        response.status, 403,
        "stranger should not edit patch, got: {}",
        response.body
    );

    // Bob (creator) can edit
    let response = bob.get(&format!("/~alice/test-repo/patches/1/edit"));
    assert_eq!(response.status, 200, "creator should edit patch");

    // Alice (owner) can edit
    let response = alice.get(&format!("/~alice/test-repo/patches/1/edit"));
    assert_eq!(response.status, 200, "owner should edit patch");

    Ok(())
}

#[actix_web::test]
async fn status_transitions_work() -> miette::Result<()> {
    let (server, port) = spawn();

    let mut alice = Client::new(port);
    register(&mut alice, "alice");
    create_repository(&mut alice, "alice", "test-repo", "public");

    // Get repository and user IDs
    let db = Database::connect(server.database_url())
        .await
        .expect("connecting to test database");

    let repository = 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 alice_user = frogue::entities::users::Entity::find()
        .filter(frogue::entities::users::Column::Name.eq("alice"))
        .one(&db)
        .await
        .expect("finding alice")
        .expect("alice should exist");

    // Alice creates a draft patch
    create_patch(
        &server,
        repository.id,
        alice_user.id,
        1,
        "Draft patch",
        "draft",
    )
    .await;

    // Alice opens the patch via edit page
    let response = alice.post_form(
        &format!("/~alice/test-repo/patches/1"),
        &[
            ("summary", "Draft patch"),
            ("description", "Test description"),
            ("target_branch", "main"),
            ("status", "open"),
        ],
    );
    assert_eq!(response.status, 303, "edit should redirect");

    // Verify the patch is now open
    let response = alice.get(&format!("/~alice/test-repo/patches/1"));
    assert_eq!(response.status, 200, "patch should be shown");
    assert!(response.body.contains("open"), "should show open status");

    // Alice closes the patch
    let response = alice.post_form(
        &format!("/~alice/test-repo/patches/1"),
        &[
            ("summary", "Draft patch"),
            ("description", "Test description"),
            ("target_branch", "main"),
            ("status", "closed"),
        ],
    );
    assert_eq!(response.status, 303, "edit should redirect");

    // Verify the patch is now closed
    let response = alice.get(&format!("/~alice/test-repo/patches/1"));
    assert_eq!(response.status, 200, "patch should be shown");
    assert!(
        response.body.contains("closed"),
        "should show closed status"
    );

    Ok(())
}

#[actix_web::test]
async fn only_owner_can_merge_patch() -> miette::Result<()> {
    let (server, port) = spawn();

    let mut alice = Client::new(port);
    register(&mut alice, "alice");
    create_repository(&mut alice, "alice", "test-repo", "public");

    // Get repository and user IDs
    let db = Database::connect(server.database_url())
        .await
        .expect("connecting to test database");

    let repository = 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 _alice_user = frogue::entities::users::Entity::find()
        .filter(frogue::entities::users::Column::Name.eq("alice"))
        .one(&db)
        .await
        .expect("finding alice")
        .expect("alice should exist");

    // Register bob
    let mut bob = Client::new(port);
    register(&mut bob, "bob");

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

    // Bob creates an open patch
    create_patch(
        &server,
        repository.id,
        bob_user.id,
        1,
        "Bob's patch",
        "open",
    )
    .await;

    // Bob (creator, but not owner) cannot merge
    let response = bob.post_form(&format!("/~alice/test-repo/patches/1/merge"), &[]);
    assert_eq!(
        response.status, 403,
        "creator should not merge patch, got: {}",
        response.body
    );

    // Register carol (stranger) who cannot merge
    let mut carol = Client::new(port);
    register(&mut carol, "carol");
    let response = carol.post_form(&format!("/~alice/test-repo/patches/1/merge"), &[]);
    assert_eq!(
        response.status, 403,
        "stranger should not merge patch, got: {}",
        response.body
    );

    // Alice (owner) can merge (will fail because no actual git refs, but should be 403 for others)
    // For now, just test that the endpoint exists and rejects non-owners
    Ok(())
}

#[actix_web::test]
async fn owner_can_merge_a_patch() -> miette::Result<()> {
    let (server, port) = spawn();

    let mut alice = Client::new(port);
    register(&mut alice, "alice");
    create_repository(&mut alice, "alice", "test-repo", "public");

    // Get repository and user IDs
    let db = Database::connect(server.database_url())
        .await
        .expect("connecting to test database");

    let repository = 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 alice_user = frogue::entities::users::Entity::find()
        .filter(frogue::entities::users::Column::Name.eq("alice"))
        .one(&db)
        .await
        .expect("finding alice")
        .expect("alice should exist");

    // Alice creates an open patch
    create_patch(
        &server,
        repository.id,
        alice_user.id,
        1,
        "Alice's patch",
        "open",
    )
    .await;

    // Alice (owner) can attempt to merge (will fail because no actual git refs exist)
    // but should get a redirect or error, not 403
    let response = alice.post_form(&format!("/~alice/test-repo/patches/1/merge"), &[]);
    // The merge will fail because there are no actual git refs, but it should not be 403
    assert_ne!(
        response.status, 403,
        "owner should be allowed to merge, got: {}",
        response.body
    );

    Ok(())
}
