//! Registration modes: open vs invite-only.
//!
//! These tests verify that the registration mode (controlled by
//! FROGUE_REGISTRATION_MODE) works correctly, and that invite-only mode
//! enforces invite requirements.

mod support;

use frogue::entities::{invites, users};
use miette::IntoDiagnostic;
use sea_orm::{
    ActiveModelTrait, ColumnTrait, Database, DatabaseConnection, EntityTrait, PaginatorTrait,
    QueryFilter, Set,
};
use support::{Client, Server, free_port};

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)
}

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

async fn connect(server: &Server) -> DatabaseConnection {
    Database::connect(server.database_url())
        .await
        .expect("the server's database should be reachable")
}

fn register(client: &mut Client, name: &str, email: &str, password: &str) -> support::Response {
    client.post_form(
        "/register",
        &[("name", name), ("email", email), ("password", password)],
    )
}

fn register_with_invite(
    client: &mut Client,
    name: &str,
    email: &str,
    password: &str,
    invite: &str,
) -> support::Response {
    client.post_form(
        "/register",
        &[
            ("name", name),
            ("email", email),
            ("password", password),
            ("invite", invite),
        ],
    )
}

#[tokio::test]
async fn default_mode_is_open() -> miette::Result<()> {
    let (server, port) = spawn();
    let mut client = Client::new(port);

    let response = register(&mut client, "alice", "alice@example.org", PASSWORD);

    assert_eq!(
        response.status, 303,
        "registration should succeed in default (open) mode"
    );

    Ok(())
}

#[tokio::test]
async fn explicit_open_mode_behaves_like_default() -> miette::Result<()> {
    let (server, port) = spawn_with_env(&[("FROGUE_REGISTRATION_MODE", "open")]);
    let mut client = Client::new(port);

    let response = register(&mut client, "alice", "alice@example.org", PASSWORD);

    assert_eq!(
        response.status, 303,
        "registration should succeed in explicit open mode"
    );

    Ok(())
}

#[tokio::test]
async fn invite_only_blocks_registration_without_invite() -> miette::Result<()> {
    let (server, port) = spawn_with_env(&[("FROGUE_REGISTRATION_MODE", "invite_only")]);
    let db = connect(&server).await;

    // Bootstrap: create a user directly in the database
    let user = users::ActiveModel {
        uuid: Set(uuid::Uuid::new_v4()),
        name: Set("alice".to_string()),
        email: Set("alice@example.org".to_string()),
        password_hash: Set(
            frogue::auth::hash_password(PASSWORD).expect("password should be hashable")
        ),
        ..Default::default()
    };
    user.insert(&db).await.into_diagnostic()?;

    // Now try to register without an invite
    let mut client = Client::new(port);
    let response = register(&mut client, "bob", "bob@example.org", PASSWORD);

    assert_eq!(
        response.status, 403,
        "registration without invite should be refused"
    );
    assert!(
        response.body.contains("invite-only"),
        "error message should mention invite-only"
    );
    assert!(
        !response.body.contains("<form"),
        "refused page should not show a registration form"
    );

    // Verify no user was created
    let bob = users::Entity::find()
        .filter(users::Column::Name.eq("bob"))
        .one(&db)
        .await
        .into_diagnostic()?;
    assert!(bob.is_none(), "bob should not be in the database");

    Ok(())
}

#[tokio::test]
async fn first_user_can_register_even_in_invite_only_mode() -> miette::Result<()> {
    let (server, port) = spawn_with_env(&[("FROGUE_REGISTRATION_MODE", "invite_only")]);
    let _db = connect(&server).await;

    // First registration (bootstrap) should work
    let mut client = Client::new(port);
    let response = register(&mut client, "alice", "alice@example.org", PASSWORD);

    assert_eq!(
        response.status, 303,
        "first registration should succeed even in invite-only mode (bootstrap)"
    );

    // Second registration without invite should fail
    let mut client2 = Client::new(port);
    let response2 = register(&mut client2, "bob", "bob@example.org", PASSWORD);

    assert_eq!(
        response2.status, 403,
        "second registration without invite should be refused"
    );

    Ok(())
}

#[tokio::test]
async fn registration_with_unknown_or_malformed_invite_is_refused() -> miette::Result<()> {
    let (server, port) = spawn_with_env(&[("FROGUE_REGISTRATION_MODE", "invite_only")]);
    let db = connect(&server).await;

    // Bootstrap
    let user = users::ActiveModel {
        uuid: Set(uuid::Uuid::new_v4()),
        name: Set("alice".to_string()),
        email: Set("alice@example.org".to_string()),
        password_hash: Set(
            frogue::auth::hash_password(PASSWORD).expect("password should be hashable")
        ),
        ..Default::default()
    };
    user.insert(&db).await.into_diagnostic()?;

    // Malformed UUID
    let mut client1 = Client::new(port);
    let response1 = register_with_invite(
        &mut client1,
        "bob",
        "bob@example.org",
        PASSWORD,
        "not-a-uuid",
    );
    assert_eq!(response1.status, 403, "malformed invite should be refused");

    // Valid UUID format but not in database
    let mut client2 = Client::new(port);
    let response2 = register_with_invite(
        &mut client2,
        "bob",
        "bob@example.org",
        PASSWORD,
        "550e8400-e29b-41d4-a716-446655440000",
    );
    assert_eq!(response2.status, 403, "unknown invite should be refused");

    // Verify no user was created
    let bob = users::Entity::find()
        .filter(users::Column::Name.eq("bob"))
        .one(&db)
        .await
        .into_diagnostic()?;
    assert!(bob.is_none(), "bob should not be in the database");

    Ok(())
}

#[tokio::test]
async fn inviter_can_generate_and_redeem_an_invite() -> miette::Result<()> {
    let (server, port) = spawn_with_env(&[("FROGUE_REGISTRATION_MODE", "invite_only")]);
    let db = connect(&server).await;

    // Bootstrap alice
    let mut client_alice = Client::new(port);
    let response = register(&mut client_alice, "alice", "alice@example.org", PASSWORD);
    assert_eq!(response.status, 303, "bootstrap should succeed");

    // Alice generates an invite
    let response = client_alice.post_form("/~alice/invites", &[]);
    assert_eq!(response.status, 303, "generating an invite should redirect");

    // Fetch the invites page to get the UUID
    let response = client_alice.get("/~alice/invites");
    assert_eq!(response.status, 200);

    // Extract the invite UUID from the page
    let body = &response.body;
    let invite_start = body
        .find("/register/?invite=")
        .expect("invite link should be present");
    let uuid_start = invite_start + "/register/?invite=".len();
    let uuid_end = body[uuid_start..]
        .find('"')
        .map(|i| uuid_start + i)
        .unwrap_or(body.len());
    let invite_uuid = &body[uuid_start..uuid_end];

    // Bob uses the invite to register
    let mut client_bob = Client::new(port);
    let response = client_bob.get(&format!("/register/?invite={}", invite_uuid));
    assert_eq!(
        response.status, 200,
        "GET /register with valid invite should show the form"
    );
    assert!(
        response.body.contains("<form"),
        "valid invite should show the registration form"
    );

    // Bob posts the registration with the invite
    let response = register_with_invite(
        &mut client_bob,
        "bob",
        "bob@example.org",
        PASSWORD,
        invite_uuid,
    );
    assert_eq!(
        response.status, 303,
        "registration with valid invite should succeed"
    );

    // Verify the invite is marked as used
    let invite = invites::Entity::find()
        .filter(invites::Column::Uuid.eq(invite_uuid))
        .one(&db)
        .await
        .into_diagnostic()?
        .expect("invite should exist");
    assert!(invite.used_by.is_some(), "invite should be marked as used");

    Ok(())
}

#[tokio::test]
async fn a_used_invite_cannot_be_reused() -> miette::Result<()> {
    let (server, port) = spawn_with_env(&[("FROGUE_REGISTRATION_MODE", "invite_only")]);
    let db = connect(&server).await;

    // Bootstrap alice
    let mut client_alice = Client::new(port);
    register(&mut client_alice, "alice", "alice@example.org", PASSWORD);

    // Alice generates an invite
    client_alice.post_form("/~alice/invites", &[]);

    // Extract the invite UUID
    let response = client_alice.get("/~alice/invites");
    let body = &response.body;
    let invite_start = body.find("/register/?invite=").unwrap();
    let uuid_start = invite_start + "/register/?invite=".len();
    let uuid_end = body[uuid_start..]
        .find('"')
        .map(|i| uuid_start + i)
        .unwrap_or(body.len());
    let invite_uuid = &body[uuid_start..uuid_end];

    // Bob uses the invite
    let mut client_bob = Client::new(port);
    let response = register_with_invite(
        &mut client_bob,
        "bob",
        "bob@example.org",
        PASSWORD,
        invite_uuid,
    );
    assert_eq!(response.status, 303);

    // Carol tries to use the same invite
    let mut client_carol = Client::new(port);
    let response = register_with_invite(
        &mut client_carol,
        "carol",
        "carol@example.org",
        PASSWORD,
        invite_uuid,
    );
    assert_eq!(response.status, 403, "reusing an invite should be refused");

    // Verify carol is not in the database
    let carol = users::Entity::find()
        .filter(users::Column::Name.eq("carol"))
        .one(&db)
        .await
        .into_diagnostic()?;
    assert!(carol.is_none(), "carol should not be in the database");

    Ok(())
}

#[tokio::test]
async fn expired_invites_are_refused() -> miette::Result<()> {
    let (server, port) = spawn_with_env(&[
        ("FROGUE_REGISTRATION_MODE", "invite_only"),
        ("FROGUE_INVITES_VALIDITY_SECONDS", "1"),
    ]);

    // Bootstrap alice
    let mut client_alice = Client::new(port);
    register(&mut client_alice, "alice", "alice@example.org", PASSWORD);

    // Alice generates an invite
    client_alice.post_form("/~alice/invites", &[]);

    // Extract the invite UUID
    let response = client_alice.get("/~alice/invites");
    let body = &response.body;
    let invite_start = body.find("/register/?invite=").unwrap();
    let uuid_start = invite_start + "/register/?invite=".len();
    let uuid_end = body[uuid_start..]
        .find('"')
        .map(|i| uuid_start + i)
        .unwrap_or(body.len());
    let invite_uuid = &body[uuid_start..uuid_end];

    // Wait for the invite to expire
    std::thread::sleep(std::time::Duration::from_secs(2));

    // Bob tries to use the expired invite
    let mut client_bob = Client::new(port);
    let response = client_bob.get(&format!("/register/?invite={}", invite_uuid));
    assert_eq!(
        response.status, 403,
        "expired invite should be refused on GET"
    );

    let response = register_with_invite(
        &mut client_bob,
        "bob",
        "bob@example.org",
        PASSWORD,
        invite_uuid,
    );
    assert_eq!(
        response.status, 403,
        "expired invite should be refused on POST"
    );

    Ok(())
}

#[tokio::test]
async fn daily_invite_quota_is_enforced() -> miette::Result<()> {
    let (server, port) = spawn_with_env(&[
        ("FROGUE_REGISTRATION_MODE", "invite_only"),
        ("FROGUE_INVITES_ALLOWED_PER_DAY", "2"),
    ]);
    let db = connect(&server).await;

    // Bootstrap alice
    let mut client_alice = Client::new(port);
    register(&mut client_alice, "alice", "alice@example.org", PASSWORD);

    // Generate 2 invites (should succeed)
    let response1 = client_alice.post_form("/~alice/invites", &[]);
    assert_eq!(response1.status, 303);
    let response2 = client_alice.post_form("/~alice/invites", &[]);
    assert_eq!(response2.status, 303);

    // Third invite should be refused
    let response3 = client_alice.post_form("/~alice/invites", &[]);
    assert_eq!(
        response3.status,
        200, // Page re-rendered with error
        "third invite should be refused"
    );
    assert!(
        response3.body.contains("maximum number of invites"),
        "error message should mention quota"
    );

    // Verify only 2 invites in database
    let invite_count = invites::Entity::find()
        .filter(invites::Column::CreatedBy.eq(1)) // alice's id
        .count(&db)
        .await
        .into_diagnostic()?;
    assert_eq!(invite_count, 2, "only 2 invites should be in the database");

    Ok(())
}

#[tokio::test]
async fn invites_page_is_owner_only() -> miette::Result<()> {
    let (server, port) = spawn_with_env(&[("FROGUE_REGISTRATION_MODE", "invite_only")]);

    // Bootstrap alice
    let mut client_alice = Client::new(port);
    register(&mut client_alice, "alice", "alice@example.org", PASSWORD);

    // Anonymous access should be refused
    let mut anon = Client::new(port);
    let response = anon.get("/~alice/invites");
    assert_eq!(response.status, 401, "anonymous access should be refused");

    // Alice creates an invite for Bob
    client_alice.post_form("/~alice/invites", &[]);
    let response = client_alice.get("/~alice/invites");
    let body = &response.body;
    let invite_start = body.find("/register/?invite=").unwrap();
    let uuid_start = invite_start + "/register/?invite=".len();
    let uuid_end = body[uuid_start..]
        .find('"')
        .map(|i| uuid_start + i)
        .unwrap_or(body.len());
    let invite_uuid = &body[uuid_start..uuid_end];

    // Bob uses the invite to register
    let mut client_bob = Client::new(port);
    let response = register_with_invite(
        &mut client_bob,
        "bob",
        "bob@example.org",
        PASSWORD,
        invite_uuid,
    );
    assert_eq!(response.status, 303, "bob should register with invite");

    // Bob tries to access alice's invites
    let response = client_bob.get("/~alice/invites");
    assert_eq!(
        response.status, 403,
        "other user's invites should be refused"
    );

    // Alice can access her own invites
    let response = client_alice.get("/~alice/invites");
    assert_eq!(
        response.status, 200,
        "owner should access their own invites"
    );

    Ok(())
}

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

    // Register with a bogus invite value (should be ignored in open mode)
    let mut client = Client::new(port);
    let response = register_with_invite(
        &mut client,
        "alice",
        "alice@example.org",
        PASSWORD,
        "not-a-real-uuid",
    );
    assert_eq!(
        response.status, 303,
        "open mode should ignore invite and register successfully"
    );

    Ok(())
}

#[tokio::test]
async fn the_invite_tree_records_who_invited_whom() -> miette::Result<()> {
    let (server, port) = spawn_with_env(&[("FROGUE_REGISTRATION_MODE", "invite_only")]);
    let db = connect(&server).await;

    // Bootstrap alice
    let mut client_alice = Client::new(port);
    register(&mut client_alice, "alice", "alice@example.org", PASSWORD);

    // Extract alice's user record
    let alice = users::Entity::find()
        .filter(users::Column::Name.eq("alice"))
        .one(&db)
        .await
        .into_diagnostic()?
        .expect("alice should exist");
    assert_eq!(
        alice.invited_by, None,
        "alice (bootstrap) should have no inviter"
    );

    // Alice invites bob
    client_alice.post_form("/~alice/invites", &[]);
    let response = client_alice.get("/~alice/invites");
    let body = &response.body;
    let invite_start = body.find("/register/?invite=").unwrap();
    let uuid_start = invite_start + "/register/?invite=".len();
    let uuid_end = body[uuid_start..]
        .find('"')
        .map(|i| uuid_start + i)
        .unwrap_or(body.len());
    let invite_uuid_1 = body[uuid_start..uuid_end].to_string();

    let mut client_bob = Client::new(port);
    register_with_invite(
        &mut client_bob,
        "bob",
        "bob@example.org",
        PASSWORD,
        &invite_uuid_1,
    );

    // Verify bob was invited by alice
    let bob = users::Entity::find()
        .filter(users::Column::Name.eq("bob"))
        .one(&db)
        .await
        .into_diagnostic()?
        .expect("bob should exist");
    assert_eq!(
        bob.invited_by,
        Some(alice.id),
        "bob should be invited by alice"
    );

    // Verify the invite's used_by is bob
    let invite1 = invites::Entity::find()
        .filter(invites::Column::Uuid.eq(&invite_uuid_1))
        .one(&db)
        .await
        .into_diagnostic()?
        .expect("invite should exist");
    assert_eq!(
        invite1.used_by,
        Some(bob.id),
        "invite should be used by bob"
    );

    // Bob invites carol
    client_bob.post_form("/~bob/invites", &[]);
    let response = client_bob.get("/~bob/invites");
    let body = &response.body;
    let invite_start = body.find("/register/?invite=").unwrap();
    let uuid_start = invite_start + "/register/?invite=".len();
    let uuid_end = body[uuid_start..]
        .find('"')
        .map(|i| uuid_start + i)
        .unwrap_or(body.len());
    let invite_uuid_2 = body[uuid_start..uuid_end].to_string();

    let mut client_carol = Client::new(port);
    register_with_invite(
        &mut client_carol,
        "carol",
        "carol@example.org",
        PASSWORD,
        &invite_uuid_2,
    );

    // Verify carol was invited by bob
    let carol = users::Entity::find()
        .filter(users::Column::Name.eq("carol"))
        .one(&db)
        .await
        .into_diagnostic()?
        .expect("carol should exist");
    assert_eq!(
        carol.invited_by,
        Some(bob.id),
        "carol should be invited by bob"
    );

    // Verify the second invite's used_by is carol
    let invite2 = invites::Entity::find()
        .filter(invites::Column::Uuid.eq(&invite_uuid_2))
        .one(&db)
        .await
        .into_diagnostic()?
        .expect("second invite should exist");
    assert_eq!(
        invite2.used_by,
        Some(carol.id),
        "second invite should be used by carol"
    );

    Ok(())
}
