// Copyright 2025 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

#[cfg(test)]
mod tests {
    use google_cloud_auth::credentials::EntityTag;
    use google_cloud_auth::credentials::external_account::ProgrammaticBuilder;
    use google_cloud_auth::credentials::impersonated::Builder as ImpersonatedBuilder;
    use google_cloud_auth::credentials::mds::Builder as MdsBuilder;
    use google_cloud_auth::credentials::service_account::Builder as ServiceAccountBuilder;
    use google_cloud_auth::credentials::subject_token::{
        Builder as SubjectTokenBuilder, SubjectToken, SubjectTokenProvider,
    };
    use google_cloud_auth::credentials::user_account::Builder as UserAccountCredentialBuilder;
    use google_cloud_auth::credentials::{AccessToken, AccessTokenCredentials};
    use google_cloud_auth::credentials::{
        AccessTokenCredentialsProvider, Builder as AccessTokenCredentialBuilder, CacheableResource,
        Credentials, CredentialsProvider, api_key_credentials::Builder as ApiKeyCredentialsBuilder,
    };
    use google_cloud_auth::errors::CredentialsError;
    use google_cloud_auth::errors::SubjectTokenProviderError;
    use http::header::{AUTHORIZATION, HeaderName, HeaderValue};
    use http::{Extensions, HeaderMap};
    use httptest::{Expectation, Server, matchers::*, responders::*};
    use scoped_env::ScopedEnv;
    use serde_json::json;
    use serial_test::{parallel, serial};
    use std::error::Error;
    use std::fmt;
    use test_case::test_case;

    type Result<T> = anyhow::Result<T>;
    type TestResult = anyhow::Result<()>;

    fn find_source_error<'a, T: Error + 'static>(
        error: &'a (dyn Error + 'static),
    ) -> Option<&'a T> {
        let mut source = error.source();
        while let Some(err) = source {
            if let Some(target_err) = err.downcast_ref::<T>() {
                return Some(target_err);
            }
            source = err.source();
        }
        None
    }

    pub(crate) fn write_cred_json(contents: &str) -> tempfile::TempPath {
        let file = tempfile::NamedTempFile::new().unwrap();
        let path = file.into_temp_path();
        std::fs::write(&path, contents).expect("Unable to write to temporary file.");
        path
    }

    #[tokio::test]
    #[serial]
    async fn create_access_token_credentials_fallback_to_mds() {
        let _e1 = ScopedEnv::remove("GOOGLE_APPLICATION_CREDENTIALS");
        let _e2 = ScopedEnv::remove("HOME"); // For posix
        let _e3 = ScopedEnv::remove("APPDATA"); // For windows

        let mds = AccessTokenCredentialBuilder::default().build().unwrap();
        let fmt = format!("{mds:?}");
        assert!(fmt.contains("MDSCredentials"));
    }

    #[tokio::test]
    #[serial]
    async fn create_access_token_credentials_errors_if_adc_env_is_not_a_file() {
        let _e = ScopedEnv::set("GOOGLE_APPLICATION_CREDENTIALS", "file-does-not-exist.json");
        let err = AccessTokenCredentialBuilder::default().build().unwrap_err();
        assert!(err.is_loading(), "{err:?}");
        let msg = err.to_string();
        assert!(msg.contains("file-does-not-exist.json"));
        assert!(msg.contains("GOOGLE_APPLICATION_CREDENTIALS"));
    }

    #[tokio::test]
    #[serial]
    async fn create_access_token_credentials_malformed_adc_is_error() {
        for contents in ["{}", r#"{"type": 42}"#] {
            let path = write_cred_json(contents);
            let _e = ScopedEnv::set("GOOGLE_APPLICATION_CREDENTIALS", path.to_str().unwrap());

            let err = AccessTokenCredentialBuilder::default().build().unwrap_err();
            assert!(err.is_parsing(), "{err:?}");
            assert!(err.to_string().contains("`type` field"), "{err}");
        }
    }

    #[tokio::test]
    #[serial]
    async fn create_access_token_credentials_adc_unimplemented_credential_type() {
        let contents = r#"{
            "type": "some_unknown_credential_type"
        }"#;

        let path = write_cred_json(contents);
        let _e = ScopedEnv::set("GOOGLE_APPLICATION_CREDENTIALS", path.to_str().unwrap());

        let err = AccessTokenCredentialBuilder::default().build().unwrap_err();
        assert!(err.is_unknown_type(), "{err:?}");
        assert!(
            err.to_string().contains("some_unknown_credential_type"),
            "{err}"
        );
    }

    #[tokio::test]
    #[serial]
    async fn create_access_token_credentials_adc_user_credentials() {
        let contents = r#"{
            "client_id": "test-client-id",
            "client_secret": "test-client-secret",
            "refresh_token": "test-refresh-token",
            "type": "authorized_user"
        }"#;

        let path = write_cred_json(contents);
        let _e = ScopedEnv::set("GOOGLE_APPLICATION_CREDENTIALS", path.to_str().unwrap());

        let uc = AccessTokenCredentialBuilder::default().build().unwrap();
        let fmt = format!("{uc:?}");
        assert!(fmt.contains("UserCredentials"));
    }

    #[tokio::test]
    #[serial]
    async fn create_access_token_credentials_json_user_credentials() {
        let contents = r#"{
            "client_id": "test-client-id",
            "client_secret": "test-client-secret",
            "refresh_token": "test-refresh-token",
            "type": "authorized_user"
        }"#;

        let quota_project = "test-quota-project";

        let path = write_cred_json(contents);
        let _e = ScopedEnv::set("GOOGLE_APPLICATION_CREDENTIALS", path.to_str().unwrap());

        let uc = AccessTokenCredentialBuilder::default()
            .with_quota_project_id(quota_project)
            .build()
            .unwrap();

        let fmt = format!("{uc:?}");
        assert!(fmt.contains("UserCredentials"));
        assert!(fmt.contains(quota_project));
    }

    #[tokio::test]
    #[serial]
    async fn create_access_token_credentials_adc_impersonated_service_account() {
        let contents = json!({
            "type": "impersonated_service_account",
            "service_account_impersonation_url": "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/test-principal:generateAccessToken",
            "source_credentials": {
                "type": "authorized_user",
                "client_id": "test-client-id",
                "client_secret": "test-client-secret",
                "refresh_token": "test-refresh-token"
            }
        }).to_string();

        let path = write_cred_json(&contents);
        let _e = ScopedEnv::set("GOOGLE_APPLICATION_CREDENTIALS", path.to_str().unwrap());

        let ic = AccessTokenCredentialBuilder::default().build().unwrap();
        let fmt = format!("{ic:?}");
        assert!(fmt.contains("ImpersonatedServiceAccount"));
    }

    #[tokio::test]
    #[serial]
    async fn create_access_token_credentials_json_impersonated_service_account() {
        let contents = json!({
            "type": "impersonated_service_account",
            "service_account_impersonation_url": "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/test-principal:generateAccessToken",
            "source_credentials": {
                "type": "authorized_user",
                "client_id": "test-client-id",
                "client_secret": "test-client-secret",
                "refresh_token": "test-refresh-token"
            }
        });

        let quota_project = "test-quota-project";

        let path = write_cred_json(&contents.to_string());
        let _e = ScopedEnv::set("GOOGLE_APPLICATION_CREDENTIALS", path.to_str().unwrap());

        let ic = AccessTokenCredentialBuilder::default()
            .with_quota_project_id(quota_project)
            .build()
            .unwrap();

        let fmt = format!("{ic:?}");
        assert!(fmt.contains("ImpersonatedServiceAccount"));
        assert!(fmt.contains(quota_project));
    }

    #[tokio::test]
    #[serial]
    async fn create_access_token_credentials_adc_service_account_credentials() {
        let contents = r#"{
            "type": "service_account",
            "project_id": "test-project-id",
            "private_key_id": "test-private-key-id",
            "private_key": "-----BEGIN PRIVATE KEY-----\nBLAHBLAHBLAH\n-----END PRIVATE KEY-----\n",
            "client_email": "test-client-email",
            "universe_domain": "test-universe-domain"
        }"#;

        let path = write_cred_json(contents);
        let _e = ScopedEnv::set("GOOGLE_APPLICATION_CREDENTIALS", path.to_str().unwrap());

        let sac = AccessTokenCredentialBuilder::default().build().unwrap();
        let fmt = format!("{sac:?}");
        assert!(fmt.contains("ServiceAccountCredentials"));
    }

    #[tokio::test]
    #[serial]
    async fn create_access_token_credentials_json_service_account_credentials() {
        let contents = r#"{
            "type": "service_account",
            "project_id": "test-project-id",
            "private_key_id": "test-private-key-id",
            "private_key": "-----BEGIN PRIVATE KEY-----\nBLAHBLAHBLAH\n-----END PRIVATE KEY-----\n",
            "client_email": "test-client-email",
            "universe_domain": "test-universe-domain"
        }"#;

        let quota_project = "test-quota-project";

        let path = write_cred_json(contents);
        let _e = ScopedEnv::set("GOOGLE_APPLICATION_CREDENTIALS", path.to_str().unwrap());

        let sac = AccessTokenCredentialBuilder::default()
            .with_quota_project_id(quota_project)
            .build()
            .unwrap();
        let fmt = format!("{sac:?}");
        assert!(fmt.contains("ServiceAccountCredentials"));
        assert!(fmt.contains(quota_project));
    }

    #[tokio::test]
    #[parallel]
    async fn create_api_key_credentials_success() {
        let creds = ApiKeyCredentialsBuilder::new("test-api-key").build();
        let fmt = format!("{creds:?}");
        assert!(fmt.contains("ApiKeyCredentials"), "{fmt:?}");
        assert!(!fmt.contains("test-api-key"), "{fmt:?}");
    }

    #[tokio::test]
    #[serial]
    async fn create_external_account_access_token() -> TestResult {
        let source_token_response_body = json!({
            "access_token":"an_example_token",
        })
        .to_string();

        let token_response_body = json!({
            "access_token":"an_exchanged_token",
            "issued_token_type":"urn:ietf:params:oauth:token-type:access_token",
            "token_type":"Bearer",
            "expires_in":3600,
            "scope":"https://www.googleapis.com/auth/cloud-platform"
        })
        .to_string();

        let server = Server::run();
        server.expect(
            Expectation::matching(all_of![
                request::method_path("GET", "/source_token"),
                request::headers(contains(("metadata", "True",))),
            ])
            .respond_with(status_code(200).body(source_token_response_body)),
        );

        server.expect(
            Expectation::matching(all_of![
                request::method_path("POST", "/token"),
                request::body(url_decoded(contains(("subject_token", "an_example_token")))),
                request::body(url_decoded(contains((
                    "subject_token_type",
                    "urn:ietf:params:oauth:token-type:jwt"
                )))),
                request::body(url_decoded(contains(("audience", "some-audience")))),
                request::headers(contains((
                    "content-type",
                    "application/x-www-form-urlencoded"
                ))),
            ])
            .respond_with(status_code(200).body(token_response_body)),
        );

        let contents = json!({
          "type": "external_account",
          "audience": "some-audience",
          "subject_token_type": "urn:ietf:params:oauth:token-type:jwt",
          "token_url": server.url("/token").to_string(),
          "credential_source": {
            "url": server.url("/source_token").to_string(),
            "headers": {
              "Metadata": "True"
            },
            "format": {
              "type": "json",
              "subject_token_field_name": "access_token"
            }
          }
        })
        .to_string();

        let path = write_cred_json(&contents);
        let _e = ScopedEnv::set("GOOGLE_APPLICATION_CREDENTIALS", path.to_str().unwrap());

        let creds = AccessTokenCredentialBuilder::default().build().unwrap();

        // Use the debug output to verify the right kind of credentials are created.
        let fmt = format!("{creds:?}");
        assert!(fmt.contains("ExternalAccountCredentials"));

        let cached_headers = creds.headers(Extensions::new()).await?;
        match cached_headers {
            CacheableResource::New { data, .. } => {
                let token = data
                    .get(AUTHORIZATION)
                    .and_then(|token_value| token_value.to_str().ok())
                    .map(|s| s.to_string())
                    .unwrap();
                assert!(token.contains("Bearer an_exchanged_token"));
            }
            CacheableResource::NotModified => {
                unreachable!("Expecting a header to be present");
            }
        };

        Ok(())
    }

    #[tokio::test]
    #[serial]
    async fn create_external_account_access_token_fail() -> TestResult {
        let source_token_response_body = json!({
            "error":"invalid_token",
        })
        .to_string();

        let server = Server::run();
        server.expect(
            Expectation::matching(all_of![
                request::method_path("GET", "/source_token"),
                request::headers(contains(("metadata", "True",))),
            ])
            .respond_with(status_code(400).body(source_token_response_body)),
        );

        let contents = json!({
          "type": "external_account",
          "audience": "some-audience",
          "subject_token_type": "urn:ietf:params:oauth:token-type:jwt",
          "token_url": server.url("/token").to_string(),
          "credential_source": {
            "url": server.url("/source_token").to_string(),
            "headers": {
              "Metadata": "True"
            },
            "format": {
              "type": "json",
              "subject_token_field_name": "access_token"
            }
          }
        })
        .to_string();

        let path = write_cred_json(&contents);
        let _e = ScopedEnv::set("GOOGLE_APPLICATION_CREDENTIALS", path.to_str().unwrap());

        let creds = AccessTokenCredentialBuilder::default().build().unwrap();

        let error = creds.headers(Extensions::new()).await.unwrap_err();
        let original_error = find_source_error::<CredentialsError>(&error)
            .expect("source should be a CredentialsError");
        assert!(!original_error.is_transient());

        Ok(())
    }

    mockall::mock! {
        #[derive(Debug)]
        Credentials {}

        impl CredentialsProvider for Credentials {
            async fn headers(&self, extensions: Extensions) -> std::result::Result<CacheableResource<HeaderMap>, CredentialsError>;
            async fn universe_domain(&self) -> Option<String>;
        }

        impl AccessTokenCredentialsProvider for Credentials {
            async fn access_token(&self) -> std::result::Result<AccessToken, CredentialsError>;
        }
    }

    #[tokio::test]
    #[parallel]
    async fn mocking_with_default_values() -> Result<()> {
        let mut mock = MockCredentials::new();
        mock.expect_headers().return_once(|_extensions| {
            Ok(CacheableResource::New {
                entity_tag: EntityTag::default(),
                data: HeaderMap::new(),
            })
        });
        mock.expect_universe_domain().return_once(|| None);

        let creds = Credentials::from(mock);
        let cached_headers = creds.headers(Extensions::new()).await?;
        match cached_headers {
            CacheableResource::New { entity_tag, data } => {
                assert_eq!(entity_tag, EntityTag::default());
                assert!(data.is_empty(), "{data:?}");
            }
            CacheableResource::NotModified => {
                unreachable!("Expecting a header to be present");
            }
        };
        assert_eq!(creds.universe_domain().await, None);

        Ok(())
    }

    #[tokio::test]
    #[parallel]
    async fn mocking_access_token_credentials() -> Result<()> {
        let mut mock = MockCredentials::new();
        mock.expect_access_token().return_once(|| {
            Ok(AccessToken {
                token: "test-token".to_string(),
            })
        });

        let creds = AccessTokenCredentials::from(mock);
        let access_token = creds.access_token().await?;
        assert_eq!(access_token.token, "test-token");

        Ok(())
    }

    #[tokio::test]
    #[parallel]
    async fn mocking_with_custom_header() -> Result<()> {
        let mut mock = MockCredentials::new();
        let headers = HeaderMap::from_iter([(
            HeaderName::from_static("test-header"),
            HeaderValue::from_static("test-value"),
        )]);
        let cached_headers = CacheableResource::New {
            entity_tag: EntityTag::new(),
            data: headers,
        };
        let cached_headers_clone = cached_headers.clone();
        mock.expect_headers()
            .return_once(|_extensions| Ok(cached_headers));
        mock.expect_universe_domain().return_once(|| None);

        let creds = Credentials::from(mock);
        assert_eq!(
            creds.headers(Extensions::new()).await?,
            cached_headers_clone
        );
        assert_eq!(creds.universe_domain().await, None);

        Ok(())
    }

    #[tokio::test]
    #[parallel]
    async fn mocking_with_custom_universe_domain() -> Result<()> {
        let mut mock = MockCredentials::new();

        let universe_domain = "test-universe-domain";
        let universe_domain_clone = universe_domain.to_string();
        mock.expect_headers().return_once(|_extensions| {
            Ok(CacheableResource::New {
                entity_tag: EntityTag::default(),
                data: HeaderMap::new(),
            })
        });
        mock.expect_universe_domain()
            .return_once(|| Some(universe_domain_clone));

        let creds = Credentials::from(mock);
        match creds.headers(Extensions::new()).await? {
            CacheableResource::New { entity_tag, data } => {
                assert_eq!(entity_tag, EntityTag::default());
                assert!(data.is_empty(), "{data:?}");
            }
            CacheableResource::NotModified => {
                unreachable!("Expecting a header to be present");
            }
        };
        assert_eq!(creds.universe_domain().await.unwrap(), universe_domain);

        Ok(())
    }

    #[tokio::test]
    #[parallel]
    async fn get_mds_credentials_from_builder() -> Result<()> {
        let test_quota_project = "test-quota-project";
        let mdcs = MdsBuilder::default()
            .with_quota_project_id(test_quota_project)
            .build()?;
        let fmt = format!("{mdcs:?}");
        assert!(fmt.contains("MDSCredentials"));
        assert!(fmt.contains(test_quota_project));
        Ok(())
    }

    #[tokio::test]
    #[parallel]
    async fn get_service_account_credentials_from_builder() -> Result<()> {
        let test_quota_project = "test-quota-project";
        let service_account_info_json = json!({
            "client_email": "test-client-email",
            "private_key_id": "test-private-key-id",
            "private_key": "",
            "project_id": "test-project-id",
            "universe_domain": "test-universe-domain",
        });
        let service_account = ServiceAccountBuilder::new(service_account_info_json)
            .with_quota_project_id(test_quota_project)
            .build()?;
        let fmt = format!("{service_account:?}");
        assert!(fmt.contains("ServiceAccountCredentials"));
        assert!(fmt.contains(test_quota_project));
        Ok(())
    }

    #[tokio::test]
    #[parallel]
    async fn get_mds_access_token_credentials_from_builder() -> Result<()> {
        let server = Server::run();
        let response = json!({
            "access_token": "test-access-token",
            "expires_in": 3600,
            "token_type": "Bearer"
        });
        server.expect(
            Expectation::matching(request::path(
                "/computeMetadata/v1/instance/service-accounts/default/token",
            ))
            .respond_with(json_encoded(response)),
        );
        server.expect(
            Expectation::matching(request::path(
                "/computeMetadata/v1/instance/service-accounts/default/email",
            ))
            .times(0..)
            .respond_with(status_code(200).body("some-client-email")),
        ); // The access boundary background task might query the MDS for its email.
        server.expect(
            Expectation::matching(request::path(
                "/computeMetadata/v1/universe/universe-domain",
            ))
            .times(0..)
            .respond_with(status_code(404)),
        ); // The access boundary background task might query the MDS for its universe domain.

        let test_quota_project = "test-quota-project";
        let mdcs = MdsBuilder::default()
            .with_endpoint(server.url("").to_string().trim_end_matches("/"))
            .with_quota_project_id(test_quota_project)
            .build_access_token_credentials()?;

        let fmt = format!("{mdcs:?}");
        assert!(fmt.contains("MDSCredentials"));
        assert!(fmt.contains(test_quota_project));

        let access_token = mdcs.access_token().await?;
        assert_eq!(access_token.token, "test-access-token");
        Ok(())
    }

    #[tokio::test]
    #[parallel]
    async fn get_service_account_access_token_credentials_from_builder() -> Result<()> {
        let test_quota_project = "test-quota-project";
        let service_account_info_json = json!({
            "client_email": "test-client-email",
            "private_key_id": "test-private-key-id",
            "private_key": "-----BEGIN PRIVATE KEY-----\nBLAHBLAHBLAH\n-----END PRIVATE KEY-----\n",
            "project_id": "test-project-id",
            "universe_domain": "test-universe-domain",
        });
        let service_account = ServiceAccountBuilder::new(service_account_info_json)
            .with_quota_project_id(test_quota_project)
            .build_access_token_credentials()?;
        let fmt = format!("{service_account:?}");
        assert!(fmt.contains("ServiceAccountCredentials"));
        assert!(fmt.contains(test_quota_project));
        Ok(())
    }

    #[tokio::test]
    #[parallel]
    async fn get_user_account_access_token_credentials_from_builder() -> Result<()> {
        let test_quota_project = "test-quota-project";
        let authorized_user = serde_json::json!({
            "client_id": "test-client-id",
            "client_secret": "test-client-secret",
            "refresh_token": "test-refresh-token",
            "type": "authorized_user",
        });
        let user_account = UserAccountCredentialBuilder::new(authorized_user)
            .with_quota_project_id(test_quota_project)
            .build_access_token_credentials()?;
        let fmt = format!("{user_account:?}");
        assert!(fmt.contains("UserCredentials"), "{fmt:?}");
        assert!(fmt.contains(test_quota_project));
        Ok(())
    }

    #[tokio::test]
    #[parallel]
    async fn get_impersonated_access_token_credentials_from_builder() -> Result<()> {
        let contents = json!({
            "type": "impersonated_service_account",
            "service_account_impersonation_url": "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/test-principal:generateAccessToken",
            "source_credentials": {
                "type": "authorized_user",
                "client_id": "test-client-id",
                "client_secret": "test-client-secret",
                "refresh_token": "test-refresh-token"
            }
        });

        let quota_project = "test-quota-project";

        let ic = ImpersonatedBuilder::new(contents)
            .with_quota_project_id(quota_project)
            .build_access_token_credentials()
            .unwrap();

        let fmt = format!("{ic:?}");
        assert!(fmt.contains("ImpersonatedServiceAccount"));
        assert!(fmt.contains(quota_project));
        Ok(())
    }

    #[tokio::test]
    #[parallel]
    async fn get_user_account_credentials_from_builder() -> Result<()> {
        let test_quota_project = "test-quota-project";
        let authorized_user = serde_json::json!({
            "client_id": "test-client-id",
            "client_secret": "test-client-secret",
            "refresh_token": "test-refresh-token",
            "type": "authorized_user",
        });
        let user_account = UserAccountCredentialBuilder::new(authorized_user)
            .with_quota_project_id(test_quota_project)
            .build()?;
        let fmt = format!("{user_account:?}");
        assert!(fmt.contains("UserCredentials"), "{fmt:?}");
        assert!(fmt.contains(test_quota_project));
        Ok(())
    }

    #[derive(Debug)]
    struct TestProviderError;

    impl fmt::Display for TestProviderError {
        fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
            write!(f, "TestProviderError")
        }
    }

    impl Error for TestProviderError {}

    impl SubjectTokenProviderError for TestProviderError {
        fn is_transient(&self) -> bool {
            false
        }
    }

    #[derive(Debug)]
    struct TestSubjectTokenProvider;

    impl SubjectTokenProvider for TestSubjectTokenProvider {
        type Error = TestProviderError;

        async fn subject_token(&self) -> std::result::Result<SubjectToken, Self::Error> {
            Ok(SubjectTokenBuilder::new("test-subject-token".to_string()).build())
        }
    }

    #[test_case(Some(vec!["scope1".to_string(), "scope2".to_string()]), vec!["scope1", "scope2"]; "with custom scopes")]
    #[test_case(None, vec!["https://www.googleapis.com/auth/cloud-platform"]; "with default scopes")]
    #[tokio::test]
    async fn create_programmatic_external_account_access_token(
        scopes: Option<Vec<String>>,
        expected_scopes: Vec<&str>,
    ) -> TestResult {
        let token_response_body = json!({
            "access_token":"an_exchanged_token",
            "issued_token_type":"urn:ietf:params:oauth:token-type:access_token",
            "token_type":"Bearer",
            "expires_in":3600,
            "scope": expected_scopes.join(" "),
        })
        .to_string();

        let server = Server::run();
        server.expect(
            Expectation::matching(all_of![
                request::method_path("POST", "/token"),
                request::body(url_decoded(all_of![
                    contains(("subject_token", "test-subject-token")),
                    contains(("scope", expected_scopes.join(" "))),
                ]))
            ])
            .respond_with(status_code(200).body(token_response_body)),
        );

        let provider = TestSubjectTokenProvider;
        let mut builder = ProgrammaticBuilder::new(std::sync::Arc::new(provider))
            .with_audience("some-audience")
            .with_subject_token_type("urn:ietf:params:oauth:token-type:jwt")
            .with_token_url(server.url("/token").to_string());

        if let Some(scopes) = scopes {
            builder = builder.with_scopes(scopes);
        }

        let creds = builder.build().unwrap();

        let cached_headers = creds.headers(Extensions::new()).await?;
        match cached_headers {
            CacheableResource::New { data, .. } => {
                let token = data
                    .get(AUTHORIZATION)
                    .and_then(|token_value| token_value.to_str().ok())
                    .map(|s| s.to_string())
                    .unwrap();
                assert_eq!(token, "Bearer an_exchanged_token");
            }
            CacheableResource::NotModified => {
                unreachable!("Expecting a header to be present");
            }
        };

        Ok(())
    }

    #[derive(Debug)]
    struct FailingSubjectTokenProvider;

    impl SubjectTokenProvider for FailingSubjectTokenProvider {
        type Error = TestProviderError;

        async fn subject_token(&self) -> std::result::Result<SubjectToken, Self::Error> {
            Err(TestProviderError)
        }
    }

    #[tokio::test]
    #[parallel]
    async fn create_programmatic_token_provider_user_error() -> TestResult {
        let provider = FailingSubjectTokenProvider;
        let creds = ProgrammaticBuilder::new(std::sync::Arc::new(provider))
            .with_audience("some-audience")
            .with_subject_token_type("urn:ietf:params:oauth:token-type:jwt")
            .with_token_url("http://dummy.com/token")
            .build()
            .unwrap();

        let error = creds.headers(Extensions::new()).await.unwrap_err();

        assert!(!error.is_transient(), "Error should not be transient");
        let original_error = find_source_error::<TestProviderError>(&error)
            .expect("source should be a TestProviderError");
        assert!(original_error.to_string().contains("TestProviderError"));
        Ok(())
    }

    #[cfg(all(test, feature = "idtoken"))]
    #[tokio::test]
    #[serial]
    async fn create_id_token_credentials_fallback_to_mds() -> TestResult {
        let _e1 = ScopedEnv::remove("GOOGLE_APPLICATION_CREDENTIALS");
        let _e2 = ScopedEnv::remove("HOME"); // For posix
        let _e3 = ScopedEnv::remove("APPDATA"); // For windows

        let id_token_creds =
            google_cloud_auth::credentials::idtoken::Builder::new("test-audience").build()?;
        let fmt = format!("{id_token_creds:?}");
        assert!(fmt.contains("MDSCredentials"));

        Ok(())
    }

    #[cfg(all(test, feature = "idtoken"))]
    #[tokio::test]
    #[serial]
    async fn create_id_token_credentials_adc_service_account() -> TestResult {
        let contents = r#"{
            "type": "service_account",
            "project_id": "test-project-id",
            "private_key_id": "test-private-key-id",
            "private_key": "-----BEGIN PRIVATE KEY-----\nBLAHBLAHBLAH\n-----END PRIVATE KEY-----\n",
            "client_email": "test-client-email",
            "universe_domain": "googleapis.com"
        }"#;
        let path = write_cred_json(contents);
        let _e = ScopedEnv::set("GOOGLE_APPLICATION_CREDENTIALS", path.to_str().unwrap());

        let id_token_creds =
            google_cloud_auth::credentials::idtoken::Builder::new("test-audience").build()?;
        let fmt = format!("{id_token_creds:?}");
        assert!(fmt.contains("ServiceAccountCredentials"));

        Ok(())
    }

    #[cfg(all(test, feature = "idtoken"))]
    #[tokio::test]
    #[serial]
    async fn create_id_token_credentials_adc_service_account_non_gdu_fails() -> TestResult {
        let contents = r#"{
            "type": "service_account",
            "project_id": "test-project-id",
            "private_key_id": "test-private-key-id",
            "private_key": "-----BEGIN PRIVATE KEY-----\nBLAHBLAHBLAH\n-----END PRIVATE KEY-----\n",
            "client_email": "test-client-email",
            "universe_domain": "non-gdu.com"
        }"#;
        let path = write_cred_json(contents);
        let _e = ScopedEnv::set("GOOGLE_APPLICATION_CREDENTIALS", path.to_str().unwrap());

        let err = google_cloud_auth::credentials::idtoken::Builder::new("test-audience")
            .build()
            .unwrap_err();

        assert!(err.is_not_supported(), "{err:?}");

        Ok(())
    }

    #[cfg(all(test, feature = "idtoken"))]
    #[tokio::test]
    #[serial]
    async fn create_id_token_credentials_adc_impersonated_service_account() -> TestResult {
        let contents = json!({
            "type": "impersonated_service_account",
            "service_account_impersonation_url": "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/test-principal:generateAccessToken",
            "source_credentials": {
                "type": "authorized_user",
                "client_id": "test-client-id",
                "client_secret": "test-client-secret",
                "refresh_token": "test-refresh-token"
            }
        })
        .to_string();

        let path = write_cred_json(&contents);
        let _e = ScopedEnv::set("GOOGLE_APPLICATION_CREDENTIALS", path.to_str().unwrap());

        let id_token_creds =
            google_cloud_auth::credentials::idtoken::Builder::new("test-audience").build()?;
        let fmt = format!("{id_token_creds:?}");
        assert!(fmt.contains("ImpersonatedServiceAccount"));

        Ok(())
    }

    #[cfg(all(test, feature = "idtoken"))]
    #[tokio::test]
    #[serial]
    async fn create_id_token_credentials_adc_user_account_not_supported() -> TestResult {
        let contents = r#"{
                "client_id": "test-client-id",
                "client_secret": "test-client-secret",
                "refresh_token": "test-refresh-token",
                "type": "authorized_user"
            }"#;

        let path = write_cred_json(contents);
        let _e = ScopedEnv::set("GOOGLE_APPLICATION_CREDENTIALS", path.to_str().unwrap());

        let err = google_cloud_auth::credentials::idtoken::Builder::new("test-audience")
            .build()
            .unwrap_err();

        assert!(err.is_not_supported());
        assert!(
            err.to_string()
                .contains("authorized_user, use idtoken::user_account::Builder directly.")
        );

        Ok(())
    }

    #[tokio::test]
    #[serial]
    async fn create_signer_credentials_fallback_to_mds() -> TestResult {
        let _e1 = ScopedEnv::remove("GOOGLE_APPLICATION_CREDENTIALS");
        let _e2 = ScopedEnv::remove("HOME"); // For posix
        let _e3 = ScopedEnv::remove("APPDATA"); // For windows

        let signer = google_cloud_auth::credentials::Builder::default().build_signer()?;
        let fmt = format!("{signer:?}");
        assert!(fmt.contains("MDSSigner"));

        Ok(())
    }

    #[tokio::test]
    #[serial]
    async fn create_signer_credentials_adc_service_account() -> TestResult {
        let contents = r#"{
            "type": "service_account",
            "project_id": "test-project-id",
            "private_key_id": "test-private-key-id",
            "private_key": "-----BEGIN PRIVATE KEY-----\nBLAHBLAHBLAH\n-----END PRIVATE KEY-----\n",
            "client_email": "test-client-email",
            "universe_domain": "test-universe-domain"
        }"#;
        let path = write_cred_json(contents);
        let _e = ScopedEnv::set("GOOGLE_APPLICATION_CREDENTIALS", path.to_str().unwrap());

        let signer = google_cloud_auth::credentials::Builder::default().build_signer()?;
        let fmt = format!("{signer:?}");
        assert!(fmt.contains("ServiceAccountSigner"));

        Ok(())
    }

    #[tokio::test]
    #[serial]
    async fn create_signer_credentials_adc_impersonated_service_account() -> TestResult {
        let contents = json!({
            "type": "impersonated_service_account",
            "service_account_impersonation_url": "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/test-principal:generateAccessToken",
            "source_credentials": {
                "type": "authorized_user",
                "client_id": "test-client-id",
                "client_secret": "test-client-secret",
                "refresh_token": "test-refresh-token"
            }
        })
        .to_string();

        let path = write_cred_json(&contents);
        let _e = ScopedEnv::set("GOOGLE_APPLICATION_CREDENTIALS", path.to_str().unwrap());

        let signer = google_cloud_auth::credentials::Builder::default().build_signer()?;
        let fmt = format!("{signer:?}");
        assert!(fmt.contains("IamSigner"));

        Ok(())
    }

    #[tokio::test]
    #[serial]
    async fn create_signer_credentials_adc_user_account_not_supported() -> TestResult {
        let contents = r#"{
                "client_id": "test-client-id",
                "client_secret": "test-client-secret",
                "refresh_token": "test-refresh-token",
                "type": "authorized_user"
            }"#;

        let path = write_cred_json(contents);
        let _e = ScopedEnv::set("GOOGLE_APPLICATION_CREDENTIALS", path.to_str().unwrap());

        let err = google_cloud_auth::credentials::Builder::default()
            .build_signer()
            .unwrap_err();

        assert!(err.is_not_supported());
        assert!(
            err.to_string()
                .contains("authorized_user signer is not supported"),
            "{}",
            err
        );

        Ok(())
    }
}
