//! RBAC mirror of the dashboard's role-shadowing specs. A Member user
//! is minted via the admin API, granted (or denied) membership on an
//! app, then `pic` is driven against the member's bearer token to
//! confirm the server's capability gates surface as expected exit
//! codes / error messages.

use predicates::prelude::*;

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

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

    let slug_visible = common::unique_slug("roles-visible");
    let slug_hidden = common::unique_slug("roles-hidden");
    common::pic_as(&admin_env)
        .args(["apps", "create", &slug_visible])
        .assert()
        .success();
    let _g1 = AppGuard::new(&admin_env.url, &admin_env.token, &slug_visible);
    common::pic_as(&admin_env)
        .args(["apps", "create", &slug_hidden])
        .assert()
        .success();
    let _g2 = AppGuard::new(&admin_env.url, &admin_env.token, &slug_hidden);

    let m = member::member_user(fx, &common::unique_username("rls"));
    member::grant_membership(fx, &slug_visible, &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", "ls"])
        .output()
        .expect("apps ls");
    assert!(out.status.success(), "apps ls failed: {out:?}");
    let stdout = String::from_utf8(out.stdout).unwrap();
    assert!(
        stdout.contains(&slug_visible),
        "member should see {slug_visible}, got: {stdout}"
    );
    assert!(
        !stdout.contains(&slug_hidden),
        "member should NOT see {slug_hidden}, got: {stdout}"
    );
}

#[ignore = "needs DATABASE_URL pointing at a running Postgres"]
#[test]
fn viewer_cannot_deploy_but_editor_can() {
    let Some(fx) = common::fixture_or_skip() else {
        return;
    };
    let admin_env = common::admin_env(fx);
    let slug = common::unique_slug("roles-write");
    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("vw"));
    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 fixture = common::fixture_path("hello.rhai");
    common::pic_as(&member_env)
        .args([
            "scripts",
            "deploy",
            fixture.to_str().unwrap(),
            "--app",
            &slug,
        ])
        .assert()
        .failure()
        // `Forbidden` → 403. A regressed predicate of `"HTTP 4"` would
        // have masked an auth break (401) as an authz issue.
        .stderr(predicate::str::contains("HTTP 403"));

    // Promote to Editor and retry — the same command should now succeed.
    member::update_membership(fx, &slug, &m.id, "editor");
    common::pic_as(&member_env)
        .args([
            "scripts",
            "deploy",
            fixture.to_str().unwrap(),
            "--app",
            &slug,
        ])
        .assert()
        .success()
        .stdout(predicate::str::contains("hello").and(predicate::str::contains("created")));
}

#[ignore = "needs DATABASE_URL pointing at a running Postgres"]
#[test]
fn member_can_invoke_any_script_with_id() {
    let Some(fx) = common::fixture_or_skip() else {
        return;
    };
    // `/api/v1/execute/{id}` is the unguarded data-plane ingress — even
    // a member with no app membership can hit it as long as they hold
    // a valid token (the orchestrator doesn't gate it).
    let admin_env = common::admin_env(fx);
    let (id, _guard) = common::deploy_fixture(&admin_env, "roles-inv", "hello.rhai");

    let m = member::member_user(fx, &common::unique_username("inv"));
    let member_env = common::custom_env(&fx.url, &m.token);
    common::seed_credentials(&member_env, &m.username);

    common::pic_as(&member_env)
        .args(["scripts", "invoke", &id])
        .assert()
        .success();
}

#[ignore = "needs DATABASE_URL pointing at a running Postgres"]
#[test]
fn non_member_cannot_read_logs() {
    let Some(fx) = common::fixture_or_skip() else {
        return;
    };
    let admin_env = common::admin_env(fx);
    let (id, _guard) = common::deploy_fixture(&admin_env, "roles-log", "hello.rhai");

    let m = member::member_user(fx, &common::unique_username("rl"));
    let member_env = common::custom_env(&fx.url, &m.token);
    common::seed_credentials(&member_env, &m.username);

    common::pic_as(&member_env)
        .args(["logs", &id])
        .assert()
        .failure()
        // Non-member → 403 from the authz layer, not 404 — the script
        // exists; the caller just can't see it.
        .stderr(predicate::str::contains("HTTP 403"));
}
