//! Opt-in live GitHub App test for additional-repository access.
//!
//! Verifies against the real GitHub API that one installation token scoped
//! to the primary repository plus one declared additional repository grants
//! Git read access to both. Runs only in live mode with these variables set
//! (it skips clearly otherwise):
//!
//! - `FABRO_TEST_GITHUB_APP_ID` — GitHub App id
//! - `GITHUB_APP_PRIVATE_KEY` — App private key (PEM, or base64-encoded PEM)
//! - `FABRO_TEST_GITHUB_ORIGIN` — HTTPS origin URL of the primary repository
//! - `FABRO_TEST_GITHUB_ADDITIONAL_REPO` — an `owner/repository` slug the
//!   installation can see, ideally private, sharing the origin's owner
//!
//! The repositories come from the environment so no private slug is baked
//! into durable test output, and the minted token is only ever passed to
//! `git` through the child process environment.

use std::collections::BTreeSet;
use std::process::Stdio;
use std::time::Duration;

use fabro_github::token_source::InstallationTokenSource;
use fabro_github::{GitHubAppCredentials, GitHubCredentials, GitHubRepositoryAccess};
use fabro_types::GitHubRepositorySlug;
use tokio::process::Command;
use tokio::time::sleep;

fn env_var(name: &str) -> String {
    #[expect(
        clippy::disallowed_methods,
        reason = "live e2e configuration comes from the process environment by design"
    )]
    std::env::var(name).unwrap_or_else(|_| panic!("{name} must be set for this live test"))
}

async fn ls_remote_with_token(slug: &GitHubRepositorySlug, token: &str) -> bool {
    let url = slug.https_url();
    let mut command = Command::new("git");
    fabro_github::apply_probe_git_env(&mut command, token);
    let output = command
        .args(["ls-remote", &url, "HEAD"])
        .stdout(Stdio::null())
        .stderr(Stdio::null())
        .output()
        .await
        .expect("git should run");
    output.status.success()
}

#[fabro_macros::e2e_test(
    live("FABRO_TEST_GITHUB_APP_ID"),
    live("GITHUB_APP_PRIVATE_KEY"),
    live("FABRO_TEST_GITHUB_ORIGIN"),
    live("FABRO_TEST_GITHUB_ADDITIONAL_REPO")
)]
async fn scoped_token_reaches_the_declared_additional_repository() {
    let app_id = env_var("FABRO_TEST_GITHUB_APP_ID");
    let app = GitHubAppCredentials::from_env(Some(&app_id))
        .expect("GITHUB_APP_PRIVATE_KEY should decode as PEM or base64 PEM")
        .expect("GITHUB_APP_PRIVATE_KEY must be set for this live test");
    let origin = env_var("FABRO_TEST_GITHUB_ORIGIN");
    let additional: GitHubRepositorySlug = env_var("FABRO_TEST_GITHUB_ADDITIONAL_REPO")
        .parse()
        .expect("FABRO_TEST_GITHUB_ADDITIONAL_REPO must be an owner/repository slug");
    let additional_set: BTreeSet<GitHubRepositorySlug> = [additional.clone()].into_iter().collect();

    let access = GitHubRepositoryAccess::new(
        Some(&origin),
        &additional_set,
        std::collections::HashMap::from([("contents".to_string(), "read".to_string())]),
    )
    .expect("access request should validate")
    .expect("origin should produce an access value");

    // The production choreography: every target resolves to one shared
    // installation, then one mint scoped to the whole effective set.
    let creds = GitHubCredentials::App(app.clone());
    let source =
        InstallationTokenSource::for_access(&creds, &access).expect("token source should build");
    let resolved = source.resolve().await.expect("scoped mint should succeed");
    let token = resolved.token.expose();

    // The one token reads both the primary and the additional repository.
    // A freshly minted token can hit GitHub's replication lag, so retry a
    // few times with the same token before failing.
    for slug in access.targets() {
        let mut reachable = false;
        for _ in 0..3 {
            if ls_remote_with_token(slug, token).await {
                reachable = true;
                break;
            }
            sleep(Duration::from_secs(2)).await;
        }
        assert!(
            reachable,
            "scoped token should read every declared repository"
        );
    }

    // Negative scope check: a token minted for the primary alone must not
    // read the additional repository (proves server-side scoping, not just
    // possession of a token).
    let primary_only = GitHubRepositoryAccess::new(
        Some(&origin),
        &BTreeSet::new(),
        std::collections::HashMap::from([("contents".to_string(), "read".to_string())]),
    )
    .expect("primary-only access should validate")
    .expect("origin should produce an access value");
    let narrow_source =
        InstallationTokenSource::for_access(&GitHubCredentials::App(app), &primary_only)
            .expect("primary-only token source should build");
    let narrow = narrow_source
        .resolve()
        .await
        .expect("primary-only mint should succeed");
    assert!(
        !ls_remote_with_token(&additional, narrow.token.expose()).await,
        "a primary-only token must not read the additional repository"
    );
}
