//! `pic apps create` / `pic apps ls` edge cases. The integration smoke
//! test covers the happy path; this module covers conflict, validation,
//! and the persistence of the optional `--name` / `--description` flags
//! (which `apps ls` doesn't surface).

use predicates::prelude::*;
use serde_json::Value;

use crate::common;
use crate::common::cleanup::AppGuard;
use crate::common::member;

#[ignore = "needs DATABASE_URL pointing at a running Postgres"]
#[test]
fn create_with_name_and_description_persists() {
    let Some(fx) = common::fixture_or_skip() else {
        return;
    };
    let env = common::admin_env(fx);
    let slug = common::unique_slug("apps-named");

    common::pic_as(&env)
        .args([
            "apps",
            "create",
            &slug,
            "--name",
            "Pretty Name",
            "--description",
            "test description",
        ])
        .assert()
        .success();
    let _guard = AppGuard::new(&env.url, &env.token, &slug);

    // `apps ls` only shows slug+name+role+created_at, so verify the
    // persisted shape via the admin GET endpoint.
    let client = reqwest::blocking::Client::new();
    let resp = client
        .get(format!("{}/api/v1/admin/apps/{}", env.url, slug))
        .bearer_auth(&env.token)
        .send()
        .expect("GET app");
    assert!(resp.status().is_success(), "GET app failed: {resp:?}");
    let body: Value = resp.json().expect("app json");
    assert_eq!(body["slug"].as_str(), Some(slug.as_str()));
    assert_eq!(body["name"].as_str(), Some("Pretty Name"));
    assert_eq!(body["description"].as_str(), Some("test description"));
}

#[ignore = "needs DATABASE_URL pointing at a running Postgres"]
#[test]
fn create_duplicate_slug_conflicts() {
    let Some(fx) = common::fixture_or_skip() else {
        return;
    };
    let env = common::admin_env(fx);
    let slug = common::unique_slug("apps-dup");

    common::pic_as(&env)
        .args(["apps", "create", &slug])
        .assert()
        .success();
    let _guard = AppGuard::new(&env.url, &env.token, &slug);

    common::pic_as(&env)
        .args(["apps", "create", &slug])
        .assert()
        .failure()
        .stderr(predicate::str::contains("409").or(predicate::str::contains("conflict")));
}

#[ignore = "needs DATABASE_URL pointing at a running Postgres"]
#[test]
fn create_invalid_slug_rejected() {
    let Some(fx) = common::fixture_or_skip() else {
        return;
    };
    let env = common::admin_env(fx);

    // Server slug regex is `^[a-z0-9][a-z0-9-]{0,62}$` — uppercase
    // breaks the rule on the very first char. The server returns 422
    // (`InvalidSlug` → `UNPROCESSABLE_ENTITY`), not 400 — the previous
    // `"HTTP 4"` predicate would have silently matched any other 4xx
    // (a regressed 401 from broken auth, for example).
    common::pic_as(&env)
        .args(["apps", "create", "NotALowerSlug"])
        .assert()
        .failure()
        .stderr(predicate::str::contains("HTTP 422"));
}

#[ignore = "needs DATABASE_URL pointing at a running Postgres"]
#[test]
fn ls_includes_created_app_with_expected_columns() {
    let Some(fx) = common::fixture_or_skip() else {
        return;
    };
    let env = common::admin_env(fx);
    let slug = common::unique_slug("apps-ls");

    common::pic_as(&env)
        .args(["apps", "create", &slug])
        .assert()
        .success();
    let _guard = AppGuard::new(&env.url, &env.token, &slug);

    let out = common::pic_as(&env)
        .args(["apps", "ls"])
        .output()
        .expect("apps ls");
    assert!(out.status.success(), "apps ls failed: {out:?}");
    let stdout = String::from_utf8(out.stdout).expect("utf8 stdout");
    let mut lines = stdout.lines();
    let header = lines.next().expect("header row");
    assert_eq!(
        common::cells(header),
        vec!["slug", "name", "my_role", "created_at"]
    );

    // The slug must appear in some data row and its row's my_role column
    // is dashed (the ls endpoint doesn't compute it per-app).
    let row = lines
        .map(common::cells)
        .find(|c| c.first().copied() == Some(slug.as_str()))
        .unwrap_or_else(|| panic!("slug {slug} not in apps ls output: {stdout}"));
    assert_eq!(row.len(), 4, "row should have 4 cells: {row:?}");
    assert_eq!(row[2], "-", "my_role column should be dashed: {row:?}");
}

#[ignore = "needs DATABASE_URL pointing at a running Postgres"]
#[test]
fn delete_removes_app_from_ls() {
    let Some(fx) = common::fixture_or_skip() else {
        return;
    };
    let env = common::admin_env(fx);
    let slug = common::unique_slug("apps-del");

    common::pic_as(&env)
        .args(["apps", "create", &slug])
        .assert()
        .success();

    common::pic_as(&env)
        .args(["apps", "delete", &slug])
        .assert()
        .success()
        .stdout(predicate::str::contains(format!("Deleted app {slug}")));

    let out = common::pic_as(&env)
        .args(["apps", "ls"])
        .output()
        .expect("apps ls");
    assert!(out.status.success());
    let stdout = String::from_utf8(out.stdout).unwrap();
    assert!(
        !stdout.lines().any(|l| l.starts_with(&slug)),
        "deleted slug should not appear in ls: {stdout}"
    );
}

#[ignore = "needs DATABASE_URL pointing at a running Postgres"]
#[test]
fn delete_with_scripts_errors_without_force() {
    let Some(fx) = common::fixture_or_skip() else {
        return;
    };
    let env = common::admin_env(fx);
    let slug = common::unique_slug("apps-del-busy");
    common::pic_as(&env)
        .args(["apps", "create", &slug])
        .assert()
        .success();
    // AppGuard is the safety net: if the no-force delete fails (as
    // expected) the app stays around; AppGuard force-deletes on drop.
    let _guard = AppGuard::new(&env.url, &env.token, &slug);

    let fixture = common::fixture_path("hello.rhai");
    common::pic_as(&env)
        .args([
            "scripts",
            "deploy",
            fixture.to_str().unwrap(),
            "--app",
            &slug,
        ])
        .assert()
        .success();

    common::pic_as(&env)
        .args(["apps", "delete", &slug])
        .assert()
        .failure()
        // Server `HasScripts` → 409 with a "scripts present" message.
        .stderr(predicate::str::contains("HTTP 409"));
}

#[ignore = "needs DATABASE_URL pointing at a running Postgres"]
#[test]
fn delete_with_scripts_succeeds_with_force() {
    let Some(fx) = common::fixture_or_skip() else {
        return;
    };
    let env = common::admin_env(fx);
    let slug = common::unique_slug("apps-del-force");
    common::pic_as(&env)
        .args(["apps", "create", &slug])
        .assert()
        .success();

    let fixture = common::fixture_path("hello.rhai");
    common::pic_as(&env)
        .args([
            "scripts",
            "deploy",
            fixture.to_str().unwrap(),
            "--app",
            &slug,
        ])
        .assert()
        .success();

    common::pic_as(&env)
        .args(["apps", "delete", &slug, "--force"])
        .assert()
        .success()
        .stdout(predicate::str::contains(format!("Deleted app {slug}")));
}

#[ignore = "needs DATABASE_URL pointing at a running Postgres"]
#[test]
fn show_prints_my_role_for_member() {
    let Some(fx) = common::fixture_or_skip() else {
        return;
    };
    let admin_env = common::admin_env(fx);
    let slug = common::unique_slug("apps-show");
    common::pic_as(&admin_env)
        .args(["apps", "create", &slug])
        .assert()
        .success();
    let _g = AppGuard::new(&admin_env.url, &admin_env.token, &slug);

    let m = member::member_user(fx, &common::unique_username("show"));
    member::grant_membership(fx, &slug, &m.id, "viewer");

    let member_env = common::custom_env(&fx.url, &m.token);
    common::seed_credentials(&member_env, &m.username);

    let out = common::pic_as(&member_env)
        .args(["apps", "show", &slug])
        .output()
        .expect("apps show");
    assert!(out.status.success(), "apps show failed: {out:?}");
    let stdout = String::from_utf8(out.stdout).unwrap();
    // KvBlock output: `my_role` row carries the wire form (`viewer`).
    assert!(
        stdout
            .lines()
            .any(|l| l.starts_with("my_role") && l.trim_end().ends_with("viewer")),
        "show should surface my_role=viewer, got: {stdout}"
    );
    assert!(
        stdout.lines().any(|l| l.starts_with("slug")),
        "show should include slug row: {stdout}"
    );
}
