//! GCP Secret Manager resolver for `gcpsm://` references (WOR-1452).
//!
//! Two grammars: the full resource name
//! `gcpsm://projects/<project>/secrets/<secret>/versions/<version>` (mirrors
//! GCP's own convention) and the short form `gcpsm://<secret>[?key=<field>]`
//! which fills the project from `GOOGLE_CLOUD_PROJECT` and defaults the version
//! to `latest`. `key` selects a field from a JSON payload.
//!
//! It calls the `AccessSecretVersion` REST method over the blocking client with
//! an Application Default Credentials access token from
//! `GOOGLE_OAUTH_ACCESS_TOKEN` (what `gcloud auth print-access-token` and the
//! `google-github-actions/auth` Workload Identity Federation step both produce),
//! so no GCP SDK is pulled in. The response payload is base64, which this
//! decodes before any `key` extraction.

use base64::engine::general_purpose::STANDARD as BASE64;
use base64::Engine;

use mcptest_config::secrets::{ResolverError, SecretRef, SecretResolver};

use super::{env_optional, env_required, http_client, json_field, split_query};

/// Resolver for the `gcpsm://` scheme. Stateless: the token, endpoint, and
/// default project are read from the environment per resolution.
pub(super) struct GcpSecretManagerResolver;

impl GcpSecretManagerResolver {
    pub(super) fn new() -> Self {
        Self
    }
}

impl SecretResolver for GcpSecretManagerResolver {
    fn scheme(&self) -> &str {
        "gcpsm"
    }

    fn resolve(&self, reference: &SecretRef) -> Result<String, ResolverError> {
        let token = env_required("GOOGLE_OAUTH_ACCESS_TOKEN")?;
        let endpoint = env_optional("GOOGLE_CLOUD_SECRETMANAGER_ENDPOINT")
            .unwrap_or_else(|| "https://secretmanager.googleapis.com".to_string());
        let endpoint = endpoint.trim_end_matches('/');

        let parsed = split_query(&reference.raw, "gcpsm");
        let resource = resource_name(&parsed.body, parsed.version.as_deref())?;
        let url = format!("{endpoint}/v1/{resource}:access");

        let client = http_client()?;
        let response = client
            .get(&url)
            .bearer_auth(&token)
            .send()
            .map_err(|e| ResolverError(format!("GCP Secret Manager request failed: {e}")))?;
        let status = response.status();
        let text = response
            .text()
            .map_err(|e| ResolverError(format!("GCP Secret Manager response read failed: {e}")))?;
        if !status.is_success() {
            return Err(ResolverError(format!(
                "GCP Secret Manager returned HTTP {} for `{}`",
                status.as_u16(),
                reference.raw
            )));
        }
        extract_secret(&text, parsed.key.as_deref())
    }
}

/// Build the `projects/.../secrets/.../versions/...` resource name from either
/// grammar. The full form is used as written (defaulting a missing version to
/// `latest`); the short form fills the project from `GOOGLE_CLOUD_PROJECT`.
fn resource_name(body: &str, version: Option<&str>) -> Result<String, ResolverError> {
    if body.starts_with("projects/") {
        if body.contains("/versions/") {
            return Ok(body.to_string());
        }
        let version = version.unwrap_or("latest");
        return Ok(format!("{}/versions/{version}", body.trim_end_matches('/')));
    }
    let project = env_optional("GOOGLE_CLOUD_PROJECT").ok_or_else(|| {
        ResolverError(
            "short gcpsm:// form needs GOOGLE_CLOUD_PROJECT, or use the full projects/.../secrets/.../versions/... form"
                .to_string(),
        )
    })?;
    let version = version.unwrap_or("latest");
    Ok(format!(
        "projects/{project}/secrets/{body}/versions/{version}"
    ))
}

/// Decode the base64 payload and optionally select a JSON field from it.
fn extract_secret(text: &str, key: Option<&str>) -> Result<String, ResolverError> {
    let json: serde_json::Value = serde_json::from_str(text)
        .map_err(|_| ResolverError("GCP Secret Manager response is not JSON".to_string()))?;
    let encoded = json
        .pointer("/payload/data")
        .and_then(serde_json::Value::as_str)
        .ok_or_else(|| {
            ResolverError("GCP Secret Manager response has no payload.data".to_string())
        })?;
    let bytes = BASE64
        .decode(encoded)
        .map_err(|_| ResolverError("GCP Secret Manager payload is not valid base64".to_string()))?;
    let plaintext = String::from_utf8(bytes)
        .map_err(|_| ResolverError("GCP Secret Manager payload is not valid UTF-8".to_string()))?;
    match key {
        Some(key) => json_field(&plaintext, key),
        None => Ok(plaintext),
    }
}

#[cfg(test)]
mod tests {
    use super::super::testutil::{rt, EnvScope};
    use super::*;
    use mcptest_config::secrets::detect_reference;
    use wiremock::matchers::{header, method, path};
    use wiremock::{Mock, MockServer, ResponseTemplate};

    const GCP_VARS: &[&str] = &[
        "GOOGLE_OAUTH_ACCESS_TOKEN",
        "GOOGLE_CLOUD_PROJECT",
        "GOOGLE_CLOUD_SECRETMANAGER_ENDPOINT",
    ];

    fn reference(raw: &str) -> SecretRef {
        detect_reference(raw).expect("a gcpsm:// value is a reference")
    }

    #[test]
    fn full_resource_form_decodes_the_payload() {
        let scope = EnvScope::new(GCP_VARS);
        let secret = "sk-gcp-secret";
        let encoded = BASE64.encode(secret.as_bytes());
        let runtime = rt();
        let (uri, _server) = runtime.block_on(async {
            let server = MockServer::start().await;
            Mock::given(method("GET"))
                .and(path(
                    "/v1/projects/my-proj/secrets/openai/versions/latest:access",
                ))
                .and(header("authorization", "Bearer ya29.token"))
                .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
                    "payload": { "data": encoded }
                })))
                .mount(&server)
                .await;
            let uri = server.uri();
            (uri, server)
        });
        scope.set("GOOGLE_OAUTH_ACCESS_TOKEN", "ya29.token");
        scope.set("GOOGLE_CLOUD_SECRETMANAGER_ENDPOINT", &uri);

        let value = GcpSecretManagerResolver::new()
            .resolve(&reference(
                "gcpsm://projects/my-proj/secrets/openai/versions/latest",
            ))
            .expect("full resource form resolves");
        assert_eq!(value, secret);
    }

    #[test]
    fn short_form_fills_project_and_latest_then_extracts_a_key() {
        let scope = EnvScope::new(GCP_VARS);
        let encoded = BASE64.encode(br#"{"api_key":"sk-short"}"#);
        let runtime = rt();
        let (uri, _server) = runtime.block_on(async {
            let server = MockServer::start().await;
            Mock::given(method("GET"))
                .and(path(
                    "/v1/projects/env-proj/secrets/openai/versions/latest:access",
                ))
                .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
                    "payload": { "data": encoded }
                })))
                .mount(&server)
                .await;
            let uri = server.uri();
            (uri, server)
        });
        scope.set("GOOGLE_OAUTH_ACCESS_TOKEN", "ya29.token");
        scope.set("GOOGLE_CLOUD_PROJECT", "env-proj");
        scope.set("GOOGLE_CLOUD_SECRETMANAGER_ENDPOINT", &uri);

        let value = GcpSecretManagerResolver::new()
            .resolve(&reference("gcpsm://openai?key=api_key"))
            .expect("short form with key resolves");
        assert_eq!(value, "sk-short");
    }

    #[test]
    fn fails_closed_without_a_token() {
        let _scope = EnvScope::new(GCP_VARS);
        let err = GcpSecretManagerResolver::new()
            .resolve(&reference("gcpsm://projects/p/secrets/s/versions/latest"))
            .expect_err("no access token must fail closed");
        assert!(
            err.0.contains("GOOGLE_OAUTH_ACCESS_TOKEN"),
            "got: {}",
            err.0
        );
    }

    #[test]
    fn resource_name_keeps_a_full_name_and_defaults_a_missing_version() {
        assert_eq!(
            resource_name("projects/p/secrets/s/versions/7", None).unwrap(),
            "projects/p/secrets/s/versions/7"
        );
        assert_eq!(
            resource_name("projects/p/secrets/s", None).unwrap(),
            "projects/p/secrets/s/versions/latest"
        );
    }
}
