//! Helpers for non-admin (`instance_role: Member`) user lifecycle plus
//! direct API calls for granting / updating app memberships.
//!
//! These talk to the manager HTTP surface directly instead of going
//! through the CLI, so role-gated tests can stage state without
//! requiring `pic` to grow new commands.

use serde_json::{json, Value};

use super::cleanup::UserGuard;
use super::Fixture;

pub const MEMBER_PASSWORD: &str = "pic-cli-test-pw-12345678";

pub struct MemberUser {
    pub id: String,
    pub username: String,
    pub token: String,
    pub _guard: UserGuard,
}

/// Mint a fresh `instance_role: Member` user, log them in for a bearer
/// token, and register a `UserGuard` for teardown.
pub fn member_user(fx: &Fixture, username: &str) -> MemberUser {
    let client = reqwest::blocking::Client::new();

    let create = client
        .post(format!("{}/api/v1/admin/admins", fx.url))
        .bearer_auth(&fx.admin_token)
        .json(&json!({
            "username": username,
            "password": MEMBER_PASSWORD,
            // InstanceRole / AppRole serialize via `rename_all =
            // "snake_case"` — wire forms are always lowercase.
            "instance_role": "member",
        }))
        .send()
        .expect("create member user");
    assert!(
        create.status().is_success(),
        "create member user failed: {} {}",
        create.status(),
        create.text().unwrap_or_default(),
    );
    let body: Value = create.json().expect("admin create json");
    let id = body["id"]
        .as_str()
        .expect("admin create returns id")
        .to_string();

    // Register cleanup before we attempt anything else that could fail.
    let guard = UserGuard::new(&fx.url, &fx.admin_token, &id);

    let token = super::server::login_for_bearer_token(&fx.url, username, MEMBER_PASSWORD);

    MemberUser {
        id,
        username: username.to_string(),
        token,
        _guard: guard,
    }
}

/// `POST /api/v1/admin/apps/{slug}/members` — grant `role` to `user_id`.
pub fn grant_membership(fx: &Fixture, app_slug: &str, user_id: &str, role: &str) {
    let client = reqwest::blocking::Client::new();
    let resp = client
        .post(format!("{}/api/v1/admin/apps/{}/members", fx.url, app_slug))
        .bearer_auth(&fx.admin_token)
        .json(&json!({ "user_id": user_id, "role": role }))
        .send()
        .expect("grant membership");
    assert!(
        resp.status().is_success(),
        "grant membership failed: {} {}",
        resp.status(),
        resp.text().unwrap_or_default(),
    );
}

/// `POST /api/v1/admin/groups/{slug}/members` — grant `role` to `user_id` on a
/// GROUP (hierarchy-aware). §7 takeover tests use this to give a member an
/// `editor` group role — enough to reconcile a group node, but NOT to
/// `--takeover` (which needs `GroupAdmin`).
pub fn grant_group_membership(fx: &Fixture, group_slug: &str, user_id: &str, role: &str) {
    let client = reqwest::blocking::Client::new();
    let resp = client
        .post(format!(
            "{}/api/v1/admin/groups/{}/members",
            fx.url, group_slug
        ))
        .bearer_auth(&fx.admin_token)
        .json(&json!({ "user_id": user_id, "role": role }))
        .send()
        .expect("grant group membership");
    assert!(
        resp.status().is_success(),
        "grant group membership failed: {} {}",
        resp.status(),
        resp.text().unwrap_or_default(),
    );
}

/// `PATCH /api/v1/admin/apps/{slug}/members/{user_id}` — promote/demote.
pub fn update_membership(fx: &Fixture, app_slug: &str, user_id: &str, role: &str) {
    let client = reqwest::blocking::Client::new();
    let resp = client
        .patch(format!(
            "{}/api/v1/admin/apps/{}/members/{}",
            fx.url, app_slug, user_id
        ))
        .bearer_auth(&fx.admin_token)
        .json(&json!({ "role": role }))
        .send()
        .expect("update membership");
    assert!(
        resp.status().is_success(),
        "update membership failed: {} {}",
        resp.status(),
        resp.text().unwrap_or_default(),
    );
}
