//! The repository settings page: who may reach it, what may be changed, and
//! what stays put.
//!
//! Every assertion goes through the forms the forge serves and the HTML a
//! browser would be handed.

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, and checks it landed where it should.
fn create(client: &mut Client, owner: &str, slug: &str, visibility: &str) {
    let response = client.post_form(
        &format!("/~{owner}/repositories"),
        &[
            ("slug", slug),
            ("description", "a repository from the settings tests"),
            ("visibility", visibility),
        ],
    );

    assert_eq!(
        response.status, 303,
        "creating `{owner}/{slug}` should redirect\n{}",
        response.body
    );
}

#[tokio::test]
async fn the_settings_page_needs_an_owner() -> miette::Result<()> {
    let (server, port) = spawn();
    let mut alice = Client::new(port);
    register(&mut alice, "alice");
    create(&mut alice, "alice", "test-repo", "public");

    // Anonymous users are told to sign in rather than being shown the form.
    let mut anonymous = Client::new(port);
    assert_eq!(
        anonymous.get("/~alice/test-repo/settings").status,
        401,
        "a stranger should not reach the settings page"
    );
    assert_eq!(
        anonymous
            .post_form("/~alice/test-repo/settings", &[("visibility", "private")])
            .status,
        401,
        "a stranger should not be able to change visibility"
    );

    // Another signed-in user is refused with 403, the same as for ticket
    // writes: ownership is the one rule here.
    let mut bob = Client::new(port);
    register(&mut bob, "bob");
    assert_eq!(
        bob.get("/~alice/test-repo/settings").status,
        403,
        "a non-owner should not reach the settings page"
    );
    assert_eq!(
        bob.post_form("/~alice/test-repo/settings", &[("visibility", "private")])
            .status,
        403,
        "a non-owner should not be able to change visibility"
    );

    // A private repository the stranger cannot see is answered as though it
    // did not exist, just like every other repository-scoped page does.
    create(&mut alice, "alice", "secret", "private");
    assert_eq!(
        bob.get("/~alice/secret/settings").status,
        404,
        "another user should not reach a private repository's settings"
    );

    // The owner, of course, gets the page.
    let response = alice.get("/~alice/test-repo/settings");
    assert_eq!(
        response.status, 200,
        "the owner should reach the settings page"
    );

    // The settings page has the repository tab bar, with Settings marked as current.
    let body = &response.body;
    assert!(
        body.contains(r#"<a href="/~alice/test-repo">Summary</a>"#),
        "the settings page should show the Summary tab, got:\n{body}"
    );
    assert!(
        body.contains(r#"<a href="/~alice/test-repo/tickets">Tickets</a>"#),
        "the settings page should show the Tickets tab, got:\n{body}"
    );
    assert!(
        body.contains(r#"<a href="/~alice/test-repo/refs">Refs</a>"#),
        "the settings page should show the Refs tab, got:\n{body}"
    );
    assert!(
        body.contains(r#"<a href="/~alice/test-repo/tree/main">Browse</a>"#),
        "the settings page should show the Browse tab, got:\n{body}"
    );
    assert!(
        body.contains(r#"<a href="/~alice/test-repo/settings" aria-current="page">Settings</a>"#),
        "the settings page should show the Settings tab as current, got:\n{body}"
    );

    Ok(())
}

#[tokio::test]
async fn the_owner_may_flip_visibility() -> miette::Result<()> {
    let (server, port) = spawn();
    let mut alice = Client::new(port);
    register(&mut alice, "alice");
    create(&mut alice, "alice", "test-repo", "public");

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

    // A public repository is visible to bob: the page loads.
    assert_eq!(
        bob.get("/~alice/test-repo").status,
        200,
        "a public repository should be reachable"
    );

    // Alice flips it to private.
    let saved = alice.post_form("/~alice/test-repo/settings", &[("visibility", "private")]);
    assert_eq!(
        saved.status, 303,
        "saving visibility should redirect\n{}",
        saved.body
    );
    assert_eq!(
        saved.location.as_deref(),
        Some("/~alice/test-repo/settings"),
        "saving should lead back to the settings page"
    );

    // Bob is now answered as though the repository were not there.
    assert_eq!(
        bob.get("/~alice/test-repo").status,
        404,
        "a private repository should be invisible to a stranger"
    );
    assert_eq!(
        bob.get("/~alice/test-repo/tickets").status,
        404,
        "a private repository's tickets should be invisible too"
    );

    // Flip it back.
    let reopened = alice.post_form("/~alice/test-repo/settings", &[("visibility", "public")]);
    assert_eq!(
        reopened.status, 303,
        "reopening should redirect\n{}",
        reopened.body
    );
    assert_eq!(
        bob.get("/~alice/test-repo").status,
        200,
        "a public repository should be visible again"
    );

    Ok(())
}

#[tokio::test]
async fn the_settings_page_shows_the_current_visibility() -> miette::Result<()> {
    let (server, port) = spawn();
    let mut alice = Client::new(port);
    register(&mut alice, "alice");
    create(&mut alice, "alice", "test-repo", "public");

    let page = alice.get("/~alice/test-repo/settings");
    assert_eq!(page.status, 200, "the settings page");
    let body = &page.body;

    // The current visibility is the one that comes back checked.
    assert!(
        body.contains(r#"value="public" checked"#),
        "a public repository should show `public` checked, got:\n{body}"
    );
    assert!(
        !body.contains(r#"value="private" checked"#),
        "a public repository should not show `private` checked, got:\n{body}"
    );

    // Flip to private and look again.
    alice.post_form("/~alice/test-repo/settings", &[("visibility", "private")]);
    let private = alice.get("/~alice/test-repo/settings");
    assert!(
        private.body.contains(r#"value="private" checked"#),
        "a private repository should show `private` checked, got:\n{}",
        private.body
    );

    Ok(())
}

#[tokio::test]
async fn the_settings_page_lists_the_labels_and_lets_the_owner_add_one() -> miette::Result<()> {
    let (server, port) = spawn();
    let mut alice = Client::new(port);
    register(&mut alice, "alice");
    create(&mut alice, "alice", "test-repo", "public");

    let page = alice.get("/~alice/test-repo/settings");
    assert_eq!(page.status, 200, "the settings page");
    let body = &page.body;

    // The defaults a repository starts with are all listed.
    for label in ["bug", "improvement"] {
        assert!(
            body.contains(&format!(">{label}<")),
            "`{label}` should be listed, got:\n{body}"
        );
    }

    // A new label is made and then shown on the page.
    let created = alice.post_form("/~alice/test-repo/settings/labels", &[("name", "urgent")]);
    assert_eq!(
        created.status, 303,
        "creating a label should redirect\n{}",
        created.body
    );

    let refreshed = alice.get("/~alice/test-repo/settings");
    assert!(
        refreshed.body.contains(">urgent<"),
        "a new label should appear on the settings page, got:\n{}",
        refreshed.body
    );

    // And on the tickets list as a filter.
    let tickets = alice.get("/~alice/test-repo/tickets");
    assert!(
        tickets.body.contains("?state=open&amp;labels=urgent\""),
        "a new label should be offered as a filter, got:\n{}",
        tickets.body
    );

    Ok(())
}

#[tokio::test]
async fn a_duplicate_label_name_is_refused_with_an_inline_error() -> miette::Result<()> {
    let (_server, port) = spawn();
    let mut alice = Client::new(port);
    register(&mut alice, "alice");
    create(&mut alice, "alice", "test-repo", "public");

    let again = alice.post_form("/~alice/test-repo/settings/labels", &[("name", "bug")]);
    assert_eq!(
        again.status, 200,
        "a duplicate label should be refused, not a redirect\n{}",
        again.body
    );
    assert!(
        again.body.contains("`bug`"),
        "the refusal should name the label, got:\n{}",
        again.body
    );

    // A label the repository has, made again: same story.
    alice.post_form("/~alice/test-repo/settings/labels", &[("name", "urgent")]);
    let twice = alice.post_form("/~alice/test-repo/settings/labels", &[("name", "urgent")]);
    assert_eq!(
        twice.status, 200,
        "a label of the same name again should be refused\n{}",
        twice.body
    );
    assert!(
        twice.body.contains("`urgent`"),
        "the refusal should name the label, got:\n{}",
        twice.body
    );

    Ok(())
}

#[tokio::test]
async fn a_label_can_be_deleted_and_tickets_lose_it() -> miette::Result<()> {
    let (server, port) = spawn();
    let mut alice = Client::new(port);
    register(&mut alice, "alice");
    create(&mut alice, "alice", "test-repo", "public");

    // Create a label to delete
    alice.post_form("/~alice/test-repo/settings/labels", &[("name", "urgent")]);

    // Create a ticket with the label (state is now separate from labels)
    let mut fields = vec![
        ("summary", "a ticket"),
        ("description", ""),
        ("state", "open"),
    ];
    fields.push(("labels", "urgent"));
    let ticket = alice.post_form("/~alice/test-repo/tickets", &fields);
    assert_eq!(ticket.status, 303, "opening a ticket\n{}", ticket.body);

    // Grab the label id off the settings page: the delete form posts to it.
    let page = alice.get("/~alice/test-repo/settings");
    let delete_marker = "/~alice/test-repo/settings/labels/";

    // Find the "urgent" label specifically by searching for its name
    let urgent_name_pos = page
        .body
        .find(">urgent<")
        .expect("the urgent label should be listed");
    // Look forward from the label name to find the delete form URL
    let after_urgent = &page.body[urgent_name_pos..];
    let delete_marker_pos = after_urgent
        .find(delete_marker)
        .expect("delete form should be after label name");
    let urgent_start = urgent_name_pos + delete_marker_pos + delete_marker.len();
    let urgent_end = page.body[urgent_start..]
        .find("/delete")
        .expect("the id should end at /delete")
        + urgent_start;
    let label_id = &page.body[urgent_start..urgent_end];
    let delete_url = format!("/~alice/test-repo/settings/labels/{label_id}/delete");

    let deleted = alice.post_form(&delete_url, &[]);
    assert_eq!(
        deleted.status, 303,
        "deleting a label should redirect\n{}",
        deleted.body
    );

    // Gone from the settings page.
    let refreshed = alice.get("/~alice/test-repo/settings");
    assert!(
        !refreshed.body.contains(">urgent<"),
        "a deleted label should leave the settings page, got:\n{}",
        refreshed.body
    );

    // Gone from the tickets-list filters.
    let tickets = alice.get("/~alice/test-repo/tickets");
    assert!(
        !tickets.body.contains("?labels=urgent\""),
        "a deleted label should no longer be offered as a filter, got:\n{}",
        tickets.body
    );

    // The ticket is still there, but without the label.
    let detail = alice.get("/~alice/test-repo/tickets/1");
    assert_eq!(detail.status, 200, "the ticket");
    assert!(
        !detail.body.contains("ticket-label\">urgent"),
        "the ticket should no longer show the label, got:\n{}",
        detail.body
    );

    Ok(())
}

#[tokio::test]
async fn a_deleted_default_label_stays_deleted() -> miette::Result<()> {
    // The lazy seeding used to bring defaults back on the next visit to the
    // tickets list. This test makes sure that does not happen.
    let (_server, port) = spawn();
    let mut alice = Client::new(port);
    register(&mut alice, "alice");
    create(&mut alice, "alice", "test-repo", "public");

    // Find the "bug" label and its delete form
    let page = alice.get("/~alice/test-repo/settings");
    let bug_label_pos = page.body.find(">bug<").expect("bug should be listed");
    let delete_marker = "/~alice/test-repo/settings/labels/";

    // Find the delete form after the label
    let after_bug = &page.body[bug_label_pos..];
    let delete_form_pos = after_bug
        .find(delete_marker)
        .expect("delete form should be after label");
    let bug_start = bug_label_pos + delete_form_pos + delete_marker.len();
    let bug_end = page.body[bug_start..]
        .find("/delete")
        .expect("the id should end at /delete")
        + bug_start;
    let bug_id = &page.body[bug_start..bug_end];

    let delete_url = format!("/~alice/test-repo/settings/labels/{bug_id}/delete");
    let deleted = alice.post_form(&delete_url, &[]);
    assert_eq!(
        deleted.status, 303,
        "deleting `bug` should redirect\n{}",
        deleted.body
    );

    // Visit the tickets list: the label is not there.
    let list = alice.get("/~alice/test-repo/tickets");
    assert!(
        !list.body.contains("?labels=bug\""),
        "a deleted default should not come back as a filter, got:\n{}",
        list.body
    );

    // And the settings page agrees: still gone.
    let settings = alice.get("/~alice/test-repo/settings");
    assert!(
        !settings.body.contains(">bug<"),
        "a deleted default should not come back on the settings page, got:\n{}",
        settings.body
    );

    Ok(())
}

#[tokio::test]
async fn a_new_repository_gets_the_default_labels() -> miette::Result<()> {
    let (server, port) = spawn();
    let mut alice = Client::new(port);
    register(&mut alice, "alice");
    create(&mut alice, "alice", "test-repo", "public");

    // The settings page lists them all without any prior visit to the tickets
    // list, because seeding happens at creation time now.
    let page = alice.get("/~alice/test-repo/settings");
    assert_eq!(page.status, 200, "the settings page");
    Ok(())
}
