//! Phase-3 group secrets, end to end via `pic`:
//!
//!   1. **Inheritance + proximity** — a group-owned secret is injected into a
//!      descendant app's script via `secrets::get` (decrypted under the
//!      group AAD), and an app-owned secret of the same name shadows it
//!      (decrypted under the app AAD). Exercises the resolver + the dual
//!      owner-AAD open path against real Postgres.
//!   2. **Masked-read boundary** — a `group_admin` reads the secret VALUE,
//!      an app-only dev is denied the value (403) yet still sees the secret
//!      EXISTS (masked) in `config/effective`. That is the headline §5.3
//!      property: an app runs with config its own devs cannot read.

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_secret_is_injected_then_app_value_overrides() {
    let Some(fx) = common::fixture_or_skip() else {
        return;
    };
    let env = common::admin_env(fx);
    let acme = common::unique_slug("gs-acme");
    let app = common::unique_slug("gs-app");

    // Group `acme` with a `stripe-key` group secret (value via stdin).
    let _g_acme = GroupGuard::new(&env.url, &env.token, &acme);
    common::pic_as(&env)
        .args(["groups", "create", &acme])
        .assert()
        .success();
    common::pic_as(&env)
        .args(["secrets", "set", "--group", &acme, "stripe-key"])
        .write_stdin("sk_group")
        .assert()
        .success();

    // App under acme with a script that reads + returns the resolved secret.
    let _app = AppGuard::new(&env.url, &env.token, &app);
    common::pic_as(&env)
        .args(["apps", "create", &app, "--group", &acme])
        .assert()
        .success();
    let fixture = common::fixture_path("read-secret.rhai");
    common::pic_as(&env)
        .args([
            "scripts",
            "deploy",
            fixture.to_str().unwrap(),
            "--app",
            &app,
        ])
        .assert()
        .success();
    let ls = common::pic_as(&env)
        .args(["scripts", "ls", "--app", &app])
        .output()
        .expect("scripts ls");
    let id = common::parse_first_id(std::str::from_utf8(&ls.stdout).unwrap())
        .expect("scripts ls should produce one row");

    // Inherited: the app has no own `stripe-key`, so the group's value is
    // injected (decrypted under the GROUP AAD).
    assert_eq!(
        invoke_body(&env, &id),
        serde_json::json!("sk_group"),
        "inherited group secret"
    );

    // Proximity override: an app-owned `stripe-key` shadows the group value
    // (decrypted under the APP AAD — proving both AAD namespaces open).
    common::pic_as(&env)
        .args(["secrets", "set", "--app", &app, "stripe-key"])
        .write_stdin("sk_app")
        .assert()
        .success();
    assert_eq!(
        invoke_body(&env, &id),
        serde_json::json!("sk_app"),
        "app override"
    );
}

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

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

    // An app dev: a Member granted `editor` on the app, but NO group role.
    let dev = member::member_user(fx, &common::unique_username("appdev"));
    let dev_env = common::custom_env(&fx.url, &dev.token);
    common::seed_credentials(&dev_env, &dev.username);
    member::grant_membership(fx, &app, &dev.id, "editor");

    // Denied the VALUE: the value endpoint is gated GroupSecretsRead at the
    // owning group, which the app dev does not hold.
    common::pic_as(&dev_env)
        .args(["secrets", "read", "--group", &acme, "stripe-key"])
        .assert()
        .failure()
        .stderr(predicate::str::contains("HTTP 403"));

    // But the dev DOES see it EXISTS (masked) in the app's effective config —
    // status `set`, owner group, never the value. Asserted directly against
    // the endpoint to avoid the CLI's manifest requirement.
    let effective = reqwest::blocking::Client::new()
        .get(format!(
            "{}/api/v1/admin/apps/{}/config/effective",
            fx.url, app
        ))
        .bearer_auth(&dev.token)
        .send()
        .expect("config effective");
    assert!(
        effective.status().is_success(),
        "dev can read effective config"
    );
    let body: serde_json::Value = effective.json().expect("effective json");
    let masked = &body["secrets"]["stripe-key"];
    assert_eq!(masked["status"], "set", "secret shown as set");
    assert_eq!(masked["owner"]["kind"], "group", "owned by the group");
    assert!(
        masked.get("value").is_none(),
        "value must never appear in effective config: {masked}"
    );

    // A group_admin CAN read the value.
    let gadmin = member::member_user(fx, &common::unique_username("gadmin"));
    let gadmin_env = common::custom_env(&fx.url, &gadmin.token);
    common::seed_credentials(&gadmin_env, &gadmin.username);
    common::pic_as(&env)
        .args([
            "groups",
            "members",
            "add",
            &acme,
            &gadmin.id,
            "--role",
            "app_admin",
        ])
        .assert()
        .success();
    common::pic_as(&gadmin_env)
        .args(["secrets", "read", "--group", &acme, "stripe-key"])
        .assert()
        .success()
        .stdout(predicate::str::contains("sk_live_masked"));
}

/// Invoke a script via `pic scripts invoke <id>` (→ `/api/v1/execute/{id}`)
/// and parse its JSON body.
fn invoke_body(env: &common::TestEnv, id: &str) -> serde_json::Value {
    let out = common::pic_as(env)
        .args(["scripts", "invoke", id])
        .output()
        .expect("scripts invoke");
    assert!(
        out.status.success(),
        "invoke failed: {}",
        String::from_utf8_lossy(&out.stderr)
    );
    serde_json::from_slice(&out.stdout).expect("invoke body is JSON")
}
