/**
 * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
 * SPDX-License-Identifier: Apache-2.0.
 */

#include <aws/testing/aws_test_harness.h>

#include <aws/auth/credentials.h>
#include <aws/common/clock.h>
#include <aws/common/condition_variable.h>
#include <aws/common/environment.h>
#include <aws/common/mutex.h>
#include <aws/common/string.h>
#include <aws/common/thread.h>
#include <aws/http/http.h>

#include <aws/io/channel_bootstrap.h>
#include <aws/io/event_loop.h>
#include <credentials_provider_utils.h>

#include "shared_credentials_test_definitions.h"

#ifdef _MSC_VER
#    pragma warning(disable : 4996)
#endif

AWS_STATIC_STRING_FROM_LITERAL(s_access_key_id_test_value, "My Access Key");
AWS_STATIC_STRING_FROM_LITERAL(s_secret_access_key_test_value, "SekritKey");
AWS_STATIC_STRING_FROM_LITERAL(s_session_token_test_value, "Some Session Token");

static int s_credentials_create_destroy_test(struct aws_allocator *allocator, void *ctx) {
    (void)ctx;

    struct aws_credentials *credentials = aws_credentials_new_from_string(
        allocator, s_access_key_id_test_value, s_secret_access_key_test_value, s_session_token_test_value, UINT64_MAX);

    ASSERT_CURSOR_VALUE_STRING_EQUALS(aws_credentials_get_access_key_id(credentials), s_access_key_id_test_value);
    ASSERT_CURSOR_VALUE_STRING_EQUALS(
        aws_credentials_get_secret_access_key(credentials), s_secret_access_key_test_value);
    ASSERT_CURSOR_VALUE_STRING_EQUALS(aws_credentials_get_session_token(credentials), s_session_token_test_value);

    aws_credentials_release(credentials);

    return 0;
}

AWS_TEST_CASE(credentials_create_destroy_test, s_credentials_create_destroy_test);

struct aws_credentials_shutdown_checker {
    struct aws_mutex lock;
    struct aws_condition_variable signal;
    bool is_shutdown_complete;
};

static struct aws_credentials_shutdown_checker s_shutdown_checker;

static void s_aws_credentials_shutdown_checker_init(void) {
    AWS_ZERO_STRUCT(s_shutdown_checker);
    aws_mutex_init(&s_shutdown_checker.lock);
    aws_condition_variable_init(&s_shutdown_checker.signal);
}

static void s_aws_credentials_shutdown_checker_clean_up(void) {
    aws_mutex_clean_up(&s_shutdown_checker.lock);
    aws_condition_variable_clean_up(&s_shutdown_checker.signal);
}

static void s_on_shutdown_complete(void *user_data) {
    (void)user_data;

    aws_mutex_lock(&s_shutdown_checker.lock);
    s_shutdown_checker.is_shutdown_complete = true;
    aws_mutex_unlock(&s_shutdown_checker.lock);

    aws_condition_variable_notify_one(&s_shutdown_checker.signal);
}

static bool s_has_tester_received_shutdown_callback(void *user_data) {
    (void)user_data;

    return s_shutdown_checker.is_shutdown_complete;
}

static void s_aws_wait_for_provider_shutdown_callback(void) {
    aws_mutex_lock(&s_shutdown_checker.lock);
    aws_condition_variable_wait_pred(
        &s_shutdown_checker.signal, &s_shutdown_checker.lock, s_has_tester_received_shutdown_callback, NULL);
    aws_mutex_unlock(&s_shutdown_checker.lock);
}

/*
 * Helper function that takes a provider, expected results from a credentials query,
 * and uses the provider testing utils to query the results
 */
static int s_do_basic_provider_test(
    struct aws_credentials_provider *provider,
    int expected_calls,
    const struct aws_string *expected_access_key_id,
    const struct aws_string *expected_secret_access_key,
    const struct aws_string *expected_session_token) {

    struct aws_get_credentials_test_callback_result callback_results;
    aws_get_credentials_test_callback_result_init(&callback_results, expected_calls);

    int get_async_result =
        aws_credentials_provider_get_credentials(provider, aws_test_get_credentials_async_callback, &callback_results);
    ASSERT_TRUE(get_async_result == AWS_OP_SUCCESS);

    aws_wait_on_credentials_callback(&callback_results);

    ASSERT_TRUE(callback_results.count == expected_calls);

    if (callback_results.credentials != NULL) {
        ASSERT_CURSOR_VALUE_STRING_EQUALS(
            aws_credentials_get_access_key_id(callback_results.credentials), expected_access_key_id);

        ASSERT_CURSOR_VALUE_STRING_EQUALS(
            aws_credentials_get_secret_access_key(callback_results.credentials), expected_secret_access_key);

        if (expected_session_token != NULL) {
            ASSERT_CURSOR_VALUE_STRING_EQUALS(
                aws_credentials_get_session_token(callback_results.credentials), expected_session_token);
        } else {
            ASSERT_TRUE(aws_credentials_get_session_token(callback_results.credentials).len == 0);
        }
    } else {
        ASSERT_TRUE(expected_access_key_id == NULL);
        ASSERT_TRUE(expected_secret_access_key == NULL);
        ASSERT_TRUE(expected_session_token == NULL);
    }

    aws_get_credentials_test_callback_result_clean_up(&callback_results);

    return AWS_OP_SUCCESS;
}

static int s_static_credentials_provider_basic_test(struct aws_allocator *allocator, void *ctx) {
    (void)ctx;

    struct aws_credentials_provider_static_options options = {
        .access_key_id = aws_byte_cursor_from_string(s_access_key_id_test_value),
        .secret_access_key = aws_byte_cursor_from_string(s_secret_access_key_test_value),
        .session_token = aws_byte_cursor_from_string(s_session_token_test_value),
        .shutdown_options =
            {
                .shutdown_callback = s_on_shutdown_complete,
                .shutdown_user_data = NULL,
            },
    };

    s_aws_credentials_shutdown_checker_init();

    struct aws_credentials_provider *provider = aws_credentials_provider_new_static(allocator, &options);

    ASSERT_TRUE(
        s_do_basic_provider_test(
            provider, 1, s_access_key_id_test_value, s_secret_access_key_test_value, s_session_token_test_value) ==
        AWS_OP_SUCCESS);

    aws_credentials_provider_release(provider);

    s_aws_wait_for_provider_shutdown_callback();

    s_aws_credentials_shutdown_checker_clean_up();

    return 0;
}

AWS_TEST_CASE(static_credentials_provider_basic_test, s_static_credentials_provider_basic_test);

static int s_environment_credentials_provider_basic_test(struct aws_allocator *allocator, void *ctx) {
    (void)ctx;

    s_aws_credentials_shutdown_checker_init();

    aws_set_environment_value(s_access_key_id_env_var, s_access_key_id_test_value);
    aws_set_environment_value(s_secret_access_key_env_var, s_secret_access_key_test_value);
    aws_set_environment_value(s_session_token_env_var, s_session_token_test_value);

    struct aws_credentials_provider_environment_options options = {
        .shutdown_options =
            {
                .shutdown_callback = s_on_shutdown_complete,
                .shutdown_user_data = NULL,
            },
    };

    struct aws_credentials_provider *provider = aws_credentials_provider_new_environment(allocator, &options);

    ASSERT_TRUE(
        s_do_basic_provider_test(
            provider, 1, s_access_key_id_test_value, s_secret_access_key_test_value, s_session_token_test_value) ==
        AWS_OP_SUCCESS);

    aws_credentials_provider_release(provider);

    s_aws_wait_for_provider_shutdown_callback();

    s_aws_credentials_shutdown_checker_clean_up();

    return 0;
}

AWS_TEST_CASE(environment_credentials_provider_basic_test, s_environment_credentials_provider_basic_test);

static int s_do_environment_credentials_provider_failure(struct aws_allocator *allocator) {
    s_aws_credentials_shutdown_checker_init();

    aws_unset_environment_value(s_access_key_id_env_var);
    aws_unset_environment_value(s_secret_access_key_env_var);
    aws_unset_environment_value(s_session_token_env_var);

    struct aws_credentials_provider_environment_options options = {
        .shutdown_options =
            {
                .shutdown_callback = s_on_shutdown_complete,
                .shutdown_user_data = NULL,
            },
    };

    struct aws_credentials_provider *provider = aws_credentials_provider_new_environment(allocator, &options);

    ASSERT_TRUE(s_do_basic_provider_test(provider, 1, NULL, NULL, NULL) == AWS_OP_SUCCESS);

    aws_credentials_provider_release(provider);

    s_aws_wait_for_provider_shutdown_callback();

    s_aws_credentials_shutdown_checker_clean_up();

    return 0;
}

/*
 * Set of related tests that all check and make sure that if you don't specify enough
 * of the credentials data in the environment, you get nothing when you query an
 * environment provider.
 */
static int s_environment_credentials_provider_negative_test(struct aws_allocator *allocator, void *ctx) {
    (void)ctx;

    /* nothing in the environment */
    ASSERT_TRUE(s_do_environment_credentials_provider_failure(allocator) == 0);

    /* access key only shouldn't work */
    aws_set_environment_value(s_access_key_id_env_var, s_access_key_id_test_value);
    ASSERT_TRUE(s_do_environment_credentials_provider_failure(allocator) == 0);

    /* secret key only shouldn't work either */
    aws_unset_environment_value(s_access_key_id_env_var);
    aws_set_environment_value(s_secret_access_key_env_var, s_secret_access_key_test_value);
    ASSERT_TRUE(s_do_environment_credentials_provider_failure(allocator) == 0);

    return 0;
}

AWS_TEST_CASE(environment_credentials_provider_negative_test, s_environment_credentials_provider_negative_test);

#define TEST_CACHE_REFRESH_TIME_MS 10000

AWS_STATIC_STRING_FROM_LITERAL(s_access_key_id_1, "AccessKey1");
AWS_STATIC_STRING_FROM_LITERAL(s_secret_access_key_1, "SecretKey1");
AWS_STATIC_STRING_FROM_LITERAL(s_session_token_1, "SessionToken1");
AWS_STATIC_STRING_FROM_LITERAL(s_access_key_id_2, "AccessKey2");
AWS_STATIC_STRING_FROM_LITERAL(s_secret_access_key_2, "SecretKey2");
AWS_STATIC_STRING_FROM_LITERAL(s_session_token_2, "SessionToken2");

int s_wait_for_get_credentials(struct aws_get_credentials_test_callback_result *callback_results) {
    aws_wait_on_credentials_callback(callback_results);

    return 0;
}

int s_invoke_get_credentials(
    struct aws_credentials_provider *provider,
    struct aws_get_credentials_test_callback_result *callback_results,
    int call_count) {
    aws_get_credentials_test_callback_result_init(callback_results, call_count);

    for (int i = 0; i < call_count; ++i) {
        int get_async_result = aws_credentials_provider_get_credentials(
            provider, aws_test_get_credentials_async_callback, callback_results);
        ASSERT_TRUE(get_async_result == AWS_OP_SUCCESS);
    }

    return 0;
}

#define ASYNC_TEST_DELAY_NS 1000000

int s_wait_for_get_credentials_with_mock_async_provider(
    struct aws_get_credentials_test_callback_result *callback_results,
    struct aws_credentials_provider *mock_async_provider) {

    /* Mock provider already has credentials, but won't invoke any
     * get-credentials callbacks until this function is called.
     * The callbacks will fire on another thread. */
    aws_credentials_provider_mock_async_fire_callbacks(mock_async_provider);

    /* Wait for all queued get-credentials callbacks to fire */
    aws_wait_on_credentials_callback(callback_results);

    return 0;
}

static int s_verify_callback_status(
    struct aws_get_credentials_test_callback_result *results,
    int expected_call_count,
    const struct aws_string *expected_access_key_id,
    const struct aws_string *expected_secret_access_key,
    const struct aws_string *expected_session_token) {

    aws_mutex_lock(&results->sync);
    ASSERT_TRUE(results->count == expected_call_count);

    if (expected_access_key_id == NULL) {
        ASSERT_NULL(results->credentials);
    } else {
        ASSERT_CURSOR_VALUE_STRING_EQUALS(
            aws_credentials_get_access_key_id(results->credentials), expected_access_key_id);
    }

    if (expected_secret_access_key == NULL) {
        ASSERT_NULL(results->credentials);
    } else {
        ASSERT_CURSOR_VALUE_STRING_EQUALS(
            aws_credentials_get_secret_access_key(results->credentials), expected_secret_access_key);
    }

    if (expected_session_token != NULL) {
        ASSERT_CURSOR_VALUE_STRING_EQUALS(
            aws_credentials_get_session_token(results->credentials), expected_session_token);
    }

    aws_mutex_unlock(&results->sync);

    return 0;
}

static int s_cached_credentials_provider_elapsed_test(struct aws_allocator *allocator, void *ctx) {
    (void)ctx;

    mock_aws_set_system_time(0);
    mock_aws_set_high_res_time(1);

    s_aws_credentials_shutdown_checker_init();

    struct aws_credentials *first_creds = aws_credentials_new_from_string(
        allocator, s_access_key_id_1, s_secret_access_key_1, s_session_token_1, UINT64_MAX);
    struct aws_credentials *second_creds = aws_credentials_new_from_string(
        allocator, s_access_key_id_2, s_secret_access_key_2, s_session_token_2, UINT64_MAX);

    struct get_credentials_mock_result mock_results[] = {
        {.error_code = 0, .credentials = first_creds},
        {.error_code = 0, .credentials = second_creds},
    };

    struct aws_credentials_provider_shutdown_options shutdown_options = {
        .shutdown_callback = NULL,
        .shutdown_user_data = NULL,
    };

    struct aws_credentials_provider *mock_provider =
        aws_credentials_provider_new_mock(allocator, mock_results, 2, &shutdown_options);

    struct aws_credentials_provider_cached_options options;
    AWS_ZERO_STRUCT(options);
    options.source = mock_provider;
    options.refresh_time_in_milliseconds = TEST_CACHE_REFRESH_TIME_MS;
    options.high_res_clock_fn = mock_aws_get_high_res_time;
    options.system_clock_fn = mock_aws_get_system_time;
    options.shutdown_options.shutdown_callback = s_on_shutdown_complete;
    options.shutdown_options.shutdown_user_data = NULL;

    struct aws_credentials_provider *cached_provider = aws_credentials_provider_new_cached(allocator, &options);
    aws_credentials_provider_release(mock_provider);

    struct aws_get_credentials_test_callback_result callback_results;
    ASSERT_TRUE(s_invoke_get_credentials(cached_provider, &callback_results, 1) == 0);
    ASSERT_TRUE(s_wait_for_get_credentials(&callback_results) == 0);
    ASSERT_TRUE(
        s_verify_callback_status(&callback_results, 1, s_access_key_id_1, s_secret_access_key_1, s_session_token_1) ==
        0);

    /*
     * Invoke a couple more times to verify the mock isn't getting called
     */
    aws_get_credentials_test_callback_result_clean_up(&callback_results);
    ASSERT_TRUE(s_invoke_get_credentials(cached_provider, &callback_results, 1) == 0);
    ASSERT_TRUE(s_wait_for_get_credentials(&callback_results) == 0);
    ASSERT_TRUE(
        s_verify_callback_status(&callback_results, 1, s_access_key_id_1, s_secret_access_key_1, s_session_token_1) ==
        0);

    aws_get_credentials_test_callback_result_clean_up(&callback_results);
    ASSERT_TRUE(s_invoke_get_credentials(cached_provider, &callback_results, 1) == 0);
    ASSERT_TRUE(s_wait_for_get_credentials(&callback_results) == 0);
    ASSERT_TRUE(
        s_verify_callback_status(&callback_results, 1, s_access_key_id_1, s_secret_access_key_1, s_session_token_1) ==
        0);

    /*
     * Advance time, but not enough to cause a cache expiration, verify everything's the same
     */
    uint64_t refresh_in_ns =
        aws_timestamp_convert(TEST_CACHE_REFRESH_TIME_MS, AWS_TIMESTAMP_MILLIS, AWS_TIMESTAMP_NANOS, NULL);
    uint64_t now = 0;
    mock_aws_get_high_res_time(&now);
    mock_aws_set_high_res_time(now + refresh_in_ns - 1);

    aws_get_credentials_test_callback_result_clean_up(&callback_results);
    ASSERT_TRUE(s_invoke_get_credentials(cached_provider, &callback_results, 1) == 0);
    ASSERT_TRUE(s_wait_for_get_credentials(&callback_results) == 0);
    ASSERT_TRUE(
        s_verify_callback_status(&callback_results, 1, s_access_key_id_1, s_secret_access_key_1, s_session_token_1) ==
        0);

    /*
     * Advance time enough to cause cache expiration, verify we get the second set of mocked credentials
     */
    mock_aws_set_high_res_time(now + refresh_in_ns);

    aws_get_credentials_test_callback_result_clean_up(&callback_results);
    ASSERT_TRUE(s_invoke_get_credentials(cached_provider, &callback_results, 1) == 0);
    ASSERT_TRUE(s_wait_for_get_credentials(&callback_results) == 0);
    ASSERT_TRUE(
        s_verify_callback_status(&callback_results, 1, s_access_key_id_2, s_secret_access_key_2, s_session_token_2) ==
        0);

    aws_get_credentials_test_callback_result_clean_up(&callback_results);
    aws_credentials_provider_release(cached_provider);

    s_aws_wait_for_provider_shutdown_callback();

    s_aws_credentials_shutdown_checker_clean_up();

    aws_credentials_release(second_creds);
    aws_credentials_release(first_creds);

    return 0;
}

AWS_TEST_CASE(cached_credentials_provider_elapsed_test, s_cached_credentials_provider_elapsed_test);

static int s_cached_credentials_provider_queued_async_test(struct aws_allocator *allocator, void *ctx) {
    (void)ctx;

    s_aws_credentials_shutdown_checker_init();

    mock_aws_set_system_time(0);
    mock_aws_set_high_res_time(1);

    struct aws_credentials *first_creds = aws_credentials_new_from_string(
        allocator, s_access_key_id_1, s_secret_access_key_1, s_session_token_1, UINT64_MAX);
    struct aws_credentials *second_creds = aws_credentials_new_from_string(
        allocator, s_access_key_id_2, s_secret_access_key_2, s_session_token_2, UINT64_MAX);

    struct aws_event_loop_group *event_loop_group = aws_event_loop_group_new_default(allocator, 1, NULL);

    struct get_credentials_mock_result mock_results[] = {
        {.error_code = 0, .credentials = first_creds},
        {.error_code = 0, .credentials = second_creds},
    };

    struct aws_credentials_provider_shutdown_options shutdown_options;
    AWS_ZERO_STRUCT(shutdown_options);

    struct aws_credentials_provider *mock_provider =
        aws_credentials_provider_new_mock_async(allocator, mock_results, 2, event_loop_group, &shutdown_options);

    struct aws_credentials_provider_cached_options options;
    AWS_ZERO_STRUCT(options);
    options.source = mock_provider;
    options.refresh_time_in_milliseconds = TEST_CACHE_REFRESH_TIME_MS;
    options.high_res_clock_fn = mock_aws_get_high_res_time;
    options.system_clock_fn = mock_aws_get_system_time;
    options.shutdown_options.shutdown_callback = s_on_shutdown_complete;
    options.shutdown_options.shutdown_user_data = NULL;

    struct aws_credentials_provider *cached_provider = aws_credentials_provider_new_cached(allocator, &options);
    aws_credentials_provider_release(mock_provider);

    struct aws_get_credentials_test_callback_result callback_results;

    ASSERT_TRUE(s_invoke_get_credentials(cached_provider, &callback_results, 2) == 0);
    ASSERT_TRUE(s_wait_for_get_credentials_with_mock_async_provider(&callback_results, mock_provider) == 0);
    ASSERT_TRUE(
        s_verify_callback_status(&callback_results, 2, s_access_key_id_1, s_secret_access_key_1, s_session_token_1) ==
        0);

    /*
     * Invoke a couple more times to verify the mock isn't getting called
     */
    aws_get_credentials_test_callback_result_clean_up(&callback_results);
    ASSERT_TRUE(s_invoke_get_credentials(cached_provider, &callback_results, 2) == 0);
    ASSERT_TRUE(s_wait_for_get_credentials_with_mock_async_provider(&callback_results, mock_provider) == 0);
    ASSERT_TRUE(
        s_verify_callback_status(&callback_results, 2, s_access_key_id_1, s_secret_access_key_1, s_session_token_1) ==
        0);

    aws_get_credentials_test_callback_result_clean_up(&callback_results);
    ASSERT_TRUE(s_invoke_get_credentials(cached_provider, &callback_results, 2) == 0);
    ASSERT_TRUE(s_wait_for_get_credentials_with_mock_async_provider(&callback_results, mock_provider) == 0);
    ASSERT_TRUE(
        s_verify_callback_status(&callback_results, 2, s_access_key_id_1, s_secret_access_key_1, s_session_token_1) ==
        0);

    /*
     * Advance time, but not enough to cause a cache expiration, verify everything's the same
     */
    uint64_t refresh_in_ns =
        aws_timestamp_convert(TEST_CACHE_REFRESH_TIME_MS, AWS_TIMESTAMP_MILLIS, AWS_TIMESTAMP_NANOS, NULL);
    uint64_t now = 0;
    mock_aws_get_high_res_time(&now);
    mock_aws_set_high_res_time(now + refresh_in_ns - 1);

    aws_get_credentials_test_callback_result_clean_up(&callback_results);
    ASSERT_TRUE(s_invoke_get_credentials(cached_provider, &callback_results, 2) == 0);
    ASSERT_TRUE(s_wait_for_get_credentials_with_mock_async_provider(&callback_results, mock_provider) == 0);
    ASSERT_TRUE(
        s_verify_callback_status(&callback_results, 2, s_access_key_id_1, s_secret_access_key_1, s_session_token_1) ==
        0);

    /*
     * Advance time enough to cause cache expiration, verify we get the second set of mocked credentials
     */
    mock_aws_set_high_res_time(now + refresh_in_ns);

    aws_get_credentials_test_callback_result_clean_up(&callback_results);
    ASSERT_TRUE(s_invoke_get_credentials(cached_provider, &callback_results, 2) == 0);
    ASSERT_TRUE(s_wait_for_get_credentials_with_mock_async_provider(&callback_results, mock_provider) == 0);
    ASSERT_TRUE(
        s_verify_callback_status(&callback_results, 2, s_access_key_id_2, s_secret_access_key_2, s_session_token_2) ==
        0);

    aws_credentials_provider_release(cached_provider);

    s_aws_wait_for_provider_shutdown_callback();

    s_aws_credentials_shutdown_checker_clean_up();

    aws_get_credentials_test_callback_result_clean_up(&callback_results);

    aws_credentials_release(second_creds);
    aws_credentials_release(first_creds);

    aws_event_loop_group_release(event_loop_group);
    aws_global_thread_creator_shutdown_wait_for(10);

    return 0;
}

AWS_TEST_CASE(cached_credentials_provider_queued_async_test, s_cached_credentials_provider_queued_async_test);

static int s_profile_credentials_provider_new_destroy_defaults_test(struct aws_allocator *allocator, void *ctx) {
    (void)ctx;
    (void)allocator;

    s_aws_credentials_shutdown_checker_init();

    struct aws_credentials_provider_profile_options options;
    AWS_ZERO_STRUCT(options);
    options.shutdown_options.shutdown_callback = s_on_shutdown_complete;
    options.shutdown_options.shutdown_user_data = NULL;

    struct aws_credentials_provider *provider = aws_credentials_provider_new_profile(allocator, &options);

    aws_credentials_provider_release(provider);

    if (provider) {
        s_aws_wait_for_provider_shutdown_callback();
    }

    s_aws_credentials_shutdown_checker_clean_up();

    return 0;
}

AWS_TEST_CASE(
    profile_credentials_provider_new_destroy_defaults_test,
    s_profile_credentials_provider_new_destroy_defaults_test);

AWS_STATIC_STRING_FROM_LITERAL(s_config_file_path, "~derp/.aws/config");
AWS_STATIC_STRING_FROM_LITERAL(s_credentials_file_path, "/Ithink/globalpaths/arebroken/.aws/credentials");
AWS_STATIC_STRING_FROM_LITERAL(s_profile_name, "notdefault");

static int s_profile_credentials_provider_new_destroy_overrides_test(struct aws_allocator *allocator, void *ctx) {
    (void)ctx;
    (void)allocator;

    s_aws_credentials_shutdown_checker_init();

    struct aws_credentials_provider_profile_options options;
    AWS_ZERO_STRUCT(options);
    options.config_file_name_override = aws_byte_cursor_from_string(s_config_file_path);
    options.credentials_file_name_override = aws_byte_cursor_from_string(s_credentials_file_path);
    options.profile_name_override = aws_byte_cursor_from_string(s_profile_name);
    options.shutdown_options.shutdown_callback = s_on_shutdown_complete;
    options.shutdown_options.shutdown_user_data = NULL;

    struct aws_credentials_provider *provider = aws_credentials_provider_new_profile(allocator, &options);

    aws_credentials_provider_release(provider);

    s_aws_wait_for_provider_shutdown_callback();

    s_aws_credentials_shutdown_checker_clean_up();

    return 0;
}

AWS_TEST_CASE(
    profile_credentials_provider_new_destroy_overrides_test,
    s_profile_credentials_provider_new_destroy_overrides_test);

typedef int(s_verify_credentials_callback_fn)(struct aws_get_credentials_test_callback_result *callback_results);

static int s_do_credentials_provider_profile_test(
    struct aws_allocator *allocator,
    const struct aws_string *config_file_path,
    const struct aws_string *config_contents,
    const struct aws_string *creds_file_path,
    const struct aws_string *credentials_contents,
    struct aws_credentials_provider_profile_options *options,
    s_verify_credentials_callback_fn verifier,
    bool reset_environment) {

    s_aws_credentials_shutdown_checker_init();

    int result = AWS_OP_ERR;

    if (reset_environment) {
        /* Zero out all of the environment variables, just in case the user has it set (other tests may re-set it) */
        aws_unset_environment_value(s_default_profile_env_variable_name);
        aws_unset_environment_value(s_default_config_path_env_variable_name);
        aws_unset_environment_value(s_default_credentials_path_env_variable_name);
    }

    if (aws_create_profile_file(config_file_path, config_contents) ||
        aws_create_profile_file(creds_file_path, credentials_contents)) {
        return AWS_OP_ERR;
    }

    struct aws_credentials_provider *provider = aws_credentials_provider_new_profile(allocator, options);
    if (provider == NULL) {
        return AWS_OP_ERR;
    }

    struct aws_get_credentials_test_callback_result callback_results;
    aws_get_credentials_test_callback_result_init(&callback_results, 1);

    int get_async_result =
        aws_credentials_provider_get_credentials(provider, aws_test_get_credentials_async_callback, &callback_results);

    if (get_async_result == AWS_OP_SUCCESS) {
        aws_wait_on_credentials_callback(&callback_results);

        result = verifier(&callback_results);
    }

    aws_get_credentials_test_callback_result_clean_up(&callback_results);

    aws_credentials_provider_release(provider);

    s_aws_wait_for_provider_shutdown_callback();

    s_aws_credentials_shutdown_checker_clean_up();

    return result;
}

AWS_STATIC_STRING_FROM_LITERAL(
    s_config_contents,
    "[profile default]\naws_access_key_id=fake_access_key\naws_secret_access_key=fake_secret_key\n");
AWS_STATIC_STRING_FROM_LITERAL(
    s_credentials_contents,
    "[foo]\naws_access_key_id=foo_access\naws_secret_access_key=foo_secret\naws_session_token=foo_session\n");

AWS_STATIC_STRING_FROM_LITERAL(s_fake_access, "fake_access_key");
AWS_STATIC_STRING_FROM_LITERAL(s_fake_secret, "fake_secret_key");

int s_verify_default_credentials_callback(struct aws_get_credentials_test_callback_result *callback_results) {
    ASSERT_TRUE(callback_results->count == 1);
    ASSERT_TRUE(callback_results->credentials != NULL);
    ASSERT_CURSOR_VALUE_STRING_EQUALS(aws_credentials_get_access_key_id(callback_results->credentials), s_fake_access);
    ASSERT_CURSOR_VALUE_STRING_EQUALS(
        aws_credentials_get_secret_access_key(callback_results->credentials), s_fake_secret);
    ASSERT_TRUE(aws_credentials_get_session_token(callback_results->credentials).len == 0);

    return AWS_OP_SUCCESS;
}

static int s_profile_credentials_provider_default_test(struct aws_allocator *allocator, void *ctx) {
    (void)ctx;

    struct aws_string *config_file_str = aws_create_process_unique_file_name(allocator);
    struct aws_string *creds_file_str = aws_create_process_unique_file_name(allocator);

    struct aws_credentials_provider_profile_options options = {
        .config_file_name_override = aws_byte_cursor_from_string(config_file_str),
        .credentials_file_name_override = aws_byte_cursor_from_string(creds_file_str),
        .shutdown_options =
            {
                .shutdown_callback = s_on_shutdown_complete,
                .shutdown_user_data = NULL,
            },
    };

    ASSERT_SUCCESS(s_do_credentials_provider_profile_test(
        allocator,
        config_file_str,
        s_config_contents,
        creds_file_str,
        s_credentials_contents,
        &options,
        s_verify_default_credentials_callback,
        true));

    aws_string_destroy(config_file_str);
    aws_string_destroy(creds_file_str);

    return AWS_OP_SUCCESS;
}

AWS_TEST_CASE(profile_credentials_provider_default_test, s_profile_credentials_provider_default_test);

AWS_STATIC_STRING_FROM_LITERAL(s_foo_profile, "foo");

AWS_STATIC_STRING_FROM_LITERAL(s_foo_access, "foo_access");
AWS_STATIC_STRING_FROM_LITERAL(s_foo_secret, "foo_secret");
AWS_STATIC_STRING_FROM_LITERAL(s_foo_session, "foo_session");

int s_verify_nondefault_credentials_callback(struct aws_get_credentials_test_callback_result *callback_results) {
    ASSERT_TRUE(callback_results->count == 1);
    ASSERT_TRUE(callback_results->credentials != NULL);
    ASSERT_CURSOR_VALUE_STRING_EQUALS(aws_credentials_get_access_key_id(callback_results->credentials), s_foo_access);
    ASSERT_CURSOR_VALUE_STRING_EQUALS(
        aws_credentials_get_secret_access_key(callback_results->credentials), s_foo_secret);
    ASSERT_CURSOR_VALUE_STRING_EQUALS(aws_credentials_get_session_token(callback_results->credentials), s_foo_session);

    return AWS_OP_SUCCESS;
}

static int s_profile_credentials_provider_nondefault_test(struct aws_allocator *allocator, void *ctx) {
    (void)ctx;

    struct aws_string *config_file_str = aws_create_process_unique_file_name(allocator);
    struct aws_string *creds_file_str = aws_create_process_unique_file_name(allocator);

    struct aws_credentials_provider_profile_options options = {
        .config_file_name_override = aws_byte_cursor_from_string(config_file_str),
        .credentials_file_name_override = aws_byte_cursor_from_string(creds_file_str),
        .profile_name_override = aws_byte_cursor_from_string(s_foo_profile),
        .shutdown_options =
            {
                .shutdown_callback = s_on_shutdown_complete,
                .shutdown_user_data = NULL,
            },
    };

    ASSERT_SUCCESS(s_do_credentials_provider_profile_test(
        allocator,
        config_file_str,
        s_config_contents,
        creds_file_str,
        s_credentials_contents,
        &options,
        s_verify_nondefault_credentials_callback,
        true));

    aws_string_destroy(config_file_str);
    aws_string_destroy(creds_file_str);
    return AWS_OP_SUCCESS;
}

AWS_TEST_CASE(profile_credentials_provider_nondefault_test, s_profile_credentials_provider_nondefault_test);

static int s_profile_credentials_provider_environment_test(struct aws_allocator *allocator, void *ctx) {
    (void)ctx;

    /*
     * Force a profile switch via environment variable
     */
    aws_set_environment_value(s_default_profile_env_variable_name, s_foo_profile);

    struct aws_string *config_file_str = aws_create_process_unique_file_name(allocator);
    struct aws_string *creds_file_str = aws_create_process_unique_file_name(allocator);

    /*
     * Redirect config and credentials files by environment
     */
    aws_set_environment_value(s_default_config_path_env_variable_name, config_file_str);
    aws_set_environment_value(s_default_credentials_path_env_variable_name, creds_file_str);

    struct aws_credentials_provider_profile_options options = {
        .shutdown_options =
            {
                .shutdown_callback = s_on_shutdown_complete,
                .shutdown_user_data = NULL,
            },
    };

    ASSERT_SUCCESS(s_do_credentials_provider_profile_test(
        allocator,
        config_file_str,
        s_config_contents,
        creds_file_str,
        s_credentials_contents,
        &options,
        s_verify_nondefault_credentials_callback,
        false));

    aws_string_destroy(config_file_str);
    aws_string_destroy(creds_file_str);
    return AWS_OP_SUCCESS;
}

AWS_TEST_CASE(profile_credentials_provider_environment_test, s_profile_credentials_provider_environment_test);

AWS_STATIC_STRING_FROM_LITERAL(s_access_key_id_value1, "Access1");
AWS_STATIC_STRING_FROM_LITERAL(s_secret_access_key_value1, "Secret1");
AWS_STATIC_STRING_FROM_LITERAL(s_session_token_value1, "Session1");

AWS_STATIC_STRING_FROM_LITERAL(s_access_key_id_value2, "Access2");
AWS_STATIC_STRING_FROM_LITERAL(s_secret_access_key_value2, "Secret2");
AWS_STATIC_STRING_FROM_LITERAL(s_session_token_value2, "Session2");

static int s_do_provider_chain_test(
    struct aws_allocator *allocator,
    struct aws_credentials_provider *provider1,
    struct aws_credentials_provider *provider2,
    s_verify_credentials_callback_fn verifier) {

    s_aws_credentials_shutdown_checker_init();

    struct aws_credentials_provider *providers[2] = {provider1, provider2};

    struct aws_credentials_provider_chain_options options;
    AWS_ZERO_STRUCT(options);
    options.providers = providers;
    options.provider_count = 2;
    options.shutdown_options.shutdown_callback = s_on_shutdown_complete;
    options.shutdown_options.shutdown_user_data = NULL;

    struct aws_credentials_provider *provider_chain = aws_credentials_provider_new_chain(allocator, &options);
    aws_credentials_provider_release(provider1);
    aws_credentials_provider_release(provider2);
    if (provider_chain == NULL) {
        return 0;
    }

    struct aws_get_credentials_test_callback_result callback_results;
    aws_get_credentials_test_callback_result_init(&callback_results, 1);

    int get_async_result = aws_credentials_provider_get_credentials(
        provider_chain, aws_test_get_credentials_async_callback, &callback_results);

    int verification_result = AWS_OP_ERR;
    if (get_async_result == AWS_OP_SUCCESS) {
        aws_wait_on_credentials_callback(&callback_results);

        verification_result = verifier(&callback_results);
    }

    aws_get_credentials_test_callback_result_clean_up(&callback_results);

    aws_credentials_provider_release(provider_chain);

    s_aws_wait_for_provider_shutdown_callback();

    s_aws_credentials_shutdown_checker_clean_up();

    return verification_result;
}

int s_verify_first_credentials_callback(struct aws_get_credentials_test_callback_result *callback_results) {
    ASSERT_TRUE(callback_results->count == 1);
    ASSERT_TRUE(callback_results->credentials != NULL);
    ASSERT_CURSOR_VALUE_STRING_EQUALS(
        aws_credentials_get_access_key_id(callback_results->credentials), s_access_key_id_value1);
    ASSERT_CURSOR_VALUE_STRING_EQUALS(
        aws_credentials_get_secret_access_key(callback_results->credentials), s_secret_access_key_value1);
    ASSERT_CURSOR_VALUE_STRING_EQUALS(
        aws_credentials_get_session_token(callback_results->credentials), s_session_token_value1);

    return AWS_OP_SUCCESS;
}

static int s_credentials_provider_first_in_chain_test(struct aws_allocator *allocator, void *ctx) {
    (void)ctx;

    struct aws_credentials_provider_static_options options1 = {
        .access_key_id = aws_byte_cursor_from_string(s_access_key_id_value1),
        .secret_access_key = aws_byte_cursor_from_string(s_secret_access_key_value1),
        .session_token = aws_byte_cursor_from_string(s_session_token_value1),
    };

    struct aws_credentials_provider_static_options options2 = {
        .access_key_id = aws_byte_cursor_from_string(s_access_key_id_value2),
        .secret_access_key = aws_byte_cursor_from_string(s_secret_access_key_value2),
        .session_token = aws_byte_cursor_from_string(s_session_token_value2),
    };

    return s_do_provider_chain_test(
        allocator,
        aws_credentials_provider_new_static(allocator, &options1),
        aws_credentials_provider_new_static(allocator, &options2),
        s_verify_first_credentials_callback);
}

AWS_TEST_CASE(credentials_provider_first_in_chain_test, s_credentials_provider_first_in_chain_test);

AWS_STATIC_STRING_FROM_LITERAL(s_access2, "Access2");
AWS_STATIC_STRING_FROM_LITERAL(s_secret2, "Secret2");
AWS_STATIC_STRING_FROM_LITERAL(s_session2, "Session2");

int s_verify_second_credentials_callback(struct aws_get_credentials_test_callback_result *callback_results) {
    ASSERT_TRUE(callback_results->count == 1);
    ASSERT_TRUE(callback_results->credentials != NULL);
    ASSERT_CURSOR_VALUE_STRING_EQUALS(aws_credentials_get_access_key_id(callback_results->credentials), s_access2);
    ASSERT_CURSOR_VALUE_STRING_EQUALS(aws_credentials_get_secret_access_key(callback_results->credentials), s_secret2);
    ASSERT_CURSOR_VALUE_STRING_EQUALS(aws_credentials_get_session_token(callback_results->credentials), s_session2);

    return AWS_OP_SUCCESS;
}

static int s_credentials_provider_second_in_chain_test(struct aws_allocator *allocator, void *ctx) {
    (void)ctx;

    struct aws_credentials_provider_shutdown_options null_options;
    AWS_ZERO_STRUCT(null_options);

    struct aws_credentials_provider_static_options options = {
        .access_key_id = aws_byte_cursor_from_string(s_access_key_id_value2),
        .secret_access_key = aws_byte_cursor_from_string(s_secret_access_key_value2),
        .session_token = aws_byte_cursor_from_string(s_session_token_value2),
    };

    return s_do_provider_chain_test(
        allocator,
        aws_credentials_provider_new_null(allocator, &null_options),
        aws_credentials_provider_new_static(allocator, &options),
        s_verify_second_credentials_callback);
}

AWS_TEST_CASE(credentials_provider_second_in_chain_test, s_credentials_provider_second_in_chain_test);

int s_verify_null_credentials_callback(struct aws_get_credentials_test_callback_result *callback_results) {
    ASSERT_TRUE(callback_results->count == 1);
    ASSERT_TRUE(callback_results->credentials == NULL);

    return AWS_OP_SUCCESS;
}

static int s_credentials_provider_null_chain_test(struct aws_allocator *allocator, void *ctx) {
    (void)ctx;

    struct aws_credentials_provider_shutdown_options null_options;
    AWS_ZERO_STRUCT(null_options);

    return s_do_provider_chain_test(
        allocator,
        aws_credentials_provider_new_null(allocator, &null_options),
        aws_credentials_provider_new_null(allocator, &null_options),
        s_verify_null_credentials_callback);
}

AWS_TEST_CASE(credentials_provider_null_chain_test, s_credentials_provider_null_chain_test);

static int s_credentials_provider_default_basic_test(struct aws_allocator *allocator, void *ctx) {
    (void)ctx;

    aws_auth_library_init(allocator);

    s_aws_credentials_shutdown_checker_init();
    /*
     * Do a basic environment provider test, but use the default provider chain
     */

    aws_set_environment_value(s_access_key_id_env_var, s_access_key_id_test_value);
    aws_set_environment_value(s_secret_access_key_env_var, s_secret_access_key_test_value);
    aws_set_environment_value(s_session_token_env_var, s_session_token_test_value);

    struct aws_event_loop_group *el_group = aws_event_loop_group_new_default(allocator, 1, NULL);
    struct aws_host_resolver *resolver = aws_host_resolver_new_default(allocator, 4, el_group, NULL);

    struct aws_client_bootstrap_options bootstrap_options = {
        .host_resolver = resolver,
        .on_shutdown_complete = NULL,
        .host_resolution_config = NULL,
        .user_data = NULL,
        .event_loop_group = el_group,
    };

    struct aws_client_bootstrap *bootstrap = aws_client_bootstrap_new(allocator, &bootstrap_options);
    ASSERT_NOT_NULL(bootstrap);

    struct aws_credentials_provider_chain_default_options options = {
        .bootstrap = bootstrap,
        .shutdown_options =
            {
                .shutdown_callback = s_on_shutdown_complete,
                .shutdown_user_data = NULL,
            },
    };

    struct aws_credentials_provider *provider = aws_credentials_provider_new_chain_default(allocator, &options);

    ASSERT_TRUE(
        s_do_basic_provider_test(
            provider, 1, s_access_key_id_test_value, s_secret_access_key_test_value, s_session_token_test_value) ==
        AWS_OP_SUCCESS);

    /*
     * Verify that there's some caching before the environment by modifying the environment and requerying
     */
    aws_set_environment_value(s_access_key_id_env_var, s_access_key_id_1);
    aws_set_environment_value(s_secret_access_key_env_var, s_secret_access_key_1);
    aws_set_environment_value(s_session_token_env_var, s_session_token_1);

    ASSERT_TRUE(
        s_do_basic_provider_test(
            provider, 1, s_access_key_id_test_value, s_secret_access_key_test_value, s_session_token_test_value) ==
        AWS_OP_SUCCESS);

    aws_credentials_provider_release(provider);

    s_aws_wait_for_provider_shutdown_callback();
    s_aws_credentials_shutdown_checker_clean_up();
    aws_client_bootstrap_release(bootstrap);
    aws_host_resolver_release(resolver);
    aws_event_loop_group_release(el_group);
    ASSERT_SUCCESS(aws_global_thread_creator_shutdown_wait_for(10));

    aws_auth_library_clean_up();

    return 0;
}

AWS_TEST_CASE(credentials_provider_default_basic_test, s_credentials_provider_default_basic_test);
