//! Phase-2 groups, end to end via `pic`: tree CRUD, delete=RESTRICT,
//! reparent cycle rejection, and the headline invariant — a `group_admin`
//! on an ancestor group can act on an app it is NOT a direct member of
//! (inherited membership), and loses that access the instant the group
//! grant is revoked.

use predicates::prelude::*;

use crate::common;
use crate::common::cleanup::{AppGuard, GroupGuard};
use crate::common::member;

#[ignore = "needs DATABASE_URL pointing at a running Postgres"]
#[test]
fn group_tree_create_show_and_delete_restrict() {
    let Some(fx) = common::fixture_or_skip() else {
        return;
    };
    let env = common::admin_env(fx);
    let acme = common::unique_slug("g-acme");
    let team = common::unique_slug("g-team");
    let app = common::unique_slug("g-app");

    // Root-level group, then a subgroup under it.
    let _g_acme = GroupGuard::new(&env.url, &env.token, &acme);
    common::pic_as(&env)
        .args(["groups", "create", &acme])
        .assert()
        .success();
    let _g_team = GroupGuard::new(&env.url, &env.token, &team);
    common::pic_as(&env)
        .args(["groups", "create", &team, "--parent", &acme])
        .assert()
        .success();

    // An app under the subgroup.
    let _app = AppGuard::new(&env.url, &env.token, &app);
    common::pic_as(&env)
        .args(["apps", "create", &app, "--group", &team])
        .assert()
        .success();

    // `groups show team` lists the app.
    let out = String::from_utf8(
        common::pic_as(&env)
            .args(["groups", "show", &team])
            .output()
            .unwrap()
            .stdout,
    )
    .unwrap();
    assert!(
        out.contains(&app),
        "group detail should list its app:\n{out}"
    );

    // delete=RESTRICT: acme has a subgroup → refused.
    common::pic_as(&env)
        .args(["groups", "rm", &acme])
        .assert()
        .failure()
        .stderr(predicate::str::contains("409").or(predicate::str::contains("subgroup")));
}

#[ignore = "needs DATABASE_URL pointing at a running Postgres"]
#[test]
fn reparent_into_own_descendant_is_rejected() {
    let Some(fx) = common::fixture_or_skip() else {
        return;
    };
    let env = common::admin_env(fx);
    let parent = common::unique_slug("g-cyc-p");
    let child = common::unique_slug("g-cyc-c");

    let _g_parent = GroupGuard::new(&env.url, &env.token, &parent);
    common::pic_as(&env)
        .args(["groups", "create", &parent])
        .assert()
        .success();
    let _g_child = GroupGuard::new(&env.url, &env.token, &child);
    common::pic_as(&env)
        .args(["groups", "create", &child, "--parent", &parent])
        .assert()
        .success();

    // Moving the parent under its own child would form a cycle → refused.
    common::pic_as(&env)
        .args(["groups", "reparent", &parent, "--to", &child])
        .assert()
        .failure()
        .stderr(predicate::str::contains("409").or(predicate::str::contains("descendant")));
}

#[ignore = "needs DATABASE_URL pointing at a running Postgres"]
#[test]
fn inherited_group_admin_can_deploy_then_revoke() {
    let Some(fx) = common::fixture_or_skip() else {
        return;
    };
    let env = common::admin_env(fx);
    let acme = common::unique_slug("g-inh");
    let app = common::unique_slug("g-inh-app");

    let _g_acme = GroupGuard::new(&env.url, &env.token, &acme);
    common::pic_as(&env)
        .args(["groups", "create", &acme])
        .assert()
        .success();
    let _app = AppGuard::new(&env.url, &env.token, &app);
    common::pic_as(&env)
        .args(["apps", "create", &app, "--group", &acme])
        .assert()
        .success();

    // A fresh Member with NO app membership.
    let m = member::member_user(fx, &common::unique_username("inh"));
    let member_env = common::custom_env(&fx.url, &m.token);
    common::seed_credentials(&member_env, &m.username);
    let fixture = common::fixture_path("hello.rhai");

    // Baseline: without any grant, deploy is forbidden.
    common::pic_as(&member_env)
        .args([
            "scripts",
            "deploy",
            fixture.to_str().unwrap(),
            "--app",
            &app,
        ])
        .assert()
        .failure()
        .stderr(predicate::str::contains("HTTP 403"));

    // Grant group_admin on the ANCESTOR group (no app_members row).
    common::pic_as(&env)
        .args([
            "groups",
            "members",
            "add",
            &acme,
            &m.id,
            "--role",
            "app_admin",
        ])
        .assert()
        .success();

    // Inherited: the member can now deploy to the app it never joined.
    // (Deploy prints a KvBlock — assert on the script name + create action,
    // not a prose string.)
    common::pic_as(&member_env)
        .args([
            "scripts",
            "deploy",
            fixture.to_str().unwrap(),
            "--app",
            &app,
        ])
        .assert()
        .success()
        .stdout(predicate::str::contains("hello").and(predicate::str::contains("created")));

    // Revoke the group grant → access drops immediately (no cache lag).
    common::pic_as(&env)
        .args(["groups", "members", "rm", &acme, &m.id])
        .assert()
        .success();
    common::pic_as(&member_env)
        .args([
            "scripts",
            "deploy",
            fixture.to_str().unwrap(),
            "--app",
            &app,
        ])
        .assert()
        .failure()
        .stderr(predicate::str::contains("HTTP 403"));
}
