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

#include <aws/testing/AwsCppSdkGTestSuite.h>
#include <aws/core/http/HttpRequest.h>
#include <aws/core/http/HttpResponse.h>
#include <aws/core/http/HttpClientFactory.h>
#include <aws/core/http/HttpClient.h>
#include <aws/core/http/standard/StandardHttpRequest.h>
#include <aws/core/client/ClientConfiguration.h>
#include <aws/core/utils/logging/LogMacros.h>
#include <future>
#include <chrono>

#if defined(ENABLE_CURL_CLIENT) && ! defined(__ANDROID__)
#include <curl/curl.h>
#endif

using namespace Aws::Http;
using namespace Aws::Utils;
using namespace Aws::Client;

#ifndef NO_HTTP_CLIENT
static const char randomUri[] = "http://some.unknown1234xxx.test.aws";
static const char randomDomain[] = "some.unknown1234xxx.test.aws";

static void makeRandomHttpRequest(std::shared_ptr<HttpClient> httpClient, bool expectProxyError)
{
    auto request = CreateHttpRequest(Aws::String(randomUri),HttpMethod::HTTP_GET, Aws::Utils::Stream::DefaultResponseStreamFactoryMethod);
    auto response = httpClient->MakeRequest(request);
    ASSERT_NE(nullptr, response);
    //Modified the tests so that we catch an edge case where ISP's would try to get a response to the weird url
    //by doing a search instead of failing, we've had 2 issues where they get forbidden instead: #1305 & #1051
    if(expectProxyError)
    {
        ASSERT_TRUE(response->HasClientError());
        ASSERT_EQ(CoreErrors::NETWORK_CONNECTION, response->GetClientErrorType());
        ASSERT_EQ(Aws::Http::HttpResponseCode::REQUEST_NOT_MADE, response->GetResponseCode());
    }
    else
    {
        if (response->HasClientError()) {
            ASSERT_EQ(CoreErrors::NETWORK_CONNECTION, response->GetClientErrorType());
            ASSERT_EQ(Aws::Http::HttpResponseCode::REQUEST_NOT_MADE, response->GetResponseCode());
        }
        else
        {
            ASSERT_EQ(HttpResponseCode::FORBIDDEN, response->GetResponseCode());
        }
    }
}

static ClientConfiguration makeClientConfigurationWithProxy()
{
    ClientConfiguration configuration = Aws::Client::ClientConfiguration();
    configuration.proxyHost = "192.168.1.1";
    configuration.proxyPort = HTTPS_DEFAULT_PORT;
    configuration.proxyScheme = Aws::Http::Scheme::HTTPS;
    configuration.proxyUserName = "Anonymous";
    configuration.proxyPassword = "Test";
    return configuration;
}

class HttpClientTest : public Aws::Testing::AwsCppSdkGTestSuite
{
};

TEST_F(HttpClientTest, TestRandomURLWithNoProxy)
{
    auto httpClient = CreateHttpClient(Aws::Client::ClientConfiguration());
    makeRandomHttpRequest(httpClient, false);
}

TEST_F(HttpClientTest, TestRandomURLWithProxy)
{
    ClientConfiguration configuration = makeClientConfigurationWithProxy();
    auto httpClient = CreateHttpClient(configuration);
    makeRandomHttpRequest(httpClient, true); // we expect it to try to use proxy that is invalid
}

TEST_F(HttpClientTest, TestRandomURLWithProxyAndDeclaredAsNonProxyHost)
{
    ClientConfiguration configuration = makeClientConfigurationWithProxy();
    configuration.nonProxyHosts = Aws::Utils::Array<Aws::String>(2);
    configuration.nonProxyHosts[0] = "test.aws";
    configuration.nonProxyHosts[1] = "test.non.filtered.aws";
    auto httpClient = CreateHttpClient(configuration);
    makeRandomHttpRequest(httpClient, false);
}

TEST_F(HttpClientTest, TestRandomURLWithProxyAndDeclaredParentDomainAsNonProxyHost)
{
    ClientConfiguration configuration = makeClientConfigurationWithProxy();
    configuration.nonProxyHosts = Aws::Utils::Array<Aws::String>(2);
    configuration.nonProxyHosts[0] = randomDomain;
    configuration.nonProxyHosts[1] = "test.non.filtered.aws";
    auto httpClient = CreateHttpClient(configuration);
    makeRandomHttpRequest(httpClient, false);
}

TEST_F(HttpClientTest, TestRandomURLWithProxyAndOtherDeclaredAsNonProxyHost)
{
    ClientConfiguration configuration = makeClientConfigurationWithProxy();
    configuration.nonProxyHosts = Aws::Utils::Array<Aws::String>(1);
    configuration.nonProxyHosts[0] = "http://test.non.filtered.aws";
    auto httpClient = CreateHttpClient(configuration);
    makeRandomHttpRequest(httpClient, true);
}

// TODO: Pending Fix on Windows.
#if ENABLE_CURL_CLIENT
TEST_F(HttpClientTest, TestRandomURLMultiThreaded)
{
    const int threadCount = 50;
    const int timeoutSecs = 5;
    auto httpClient = CreateHttpClient(Aws::Client::ClientConfiguration());
    std::vector<std::future<void>> futures;
    for (int thread = 0; thread < threadCount; ++thread)
    {
        futures.push_back(std::async(std::launch::async, &makeRandomHttpRequest, httpClient, false));
    }

    auto start = std::chrono::system_clock::now();
    bool hasPendingTasks = true;
    while (hasPendingTasks)
    {
        hasPendingTasks = false;
        for (auto& future : futures)
        {
            auto status = future.wait_for(std::chrono::milliseconds(1));
            if (status != std::future_status::ready)
            {
                hasPendingTasks = true;
                break;
            }
        }
        auto end  = std::chrono::system_clock::now();
        auto elapsed = std::chrono::duration_cast<std::chrono::seconds>(end - start);
        if (elapsed.count() > timeoutSecs)
        {
            break;
        }
    }
    ASSERT_FALSE(hasPendingTasks);
}
#endif // ENABLE_CURL_CLIENT

// Test Http Client timeout
// Run "scripts/dummy_web_server.py -l localhost -p 8778" to setup a dummy web server first.
#if ENABLE_HTTP_CLIENT_TESTING
static const char ALLOCATION_TAG[] = "HttpClientTest";

#if ENABLE_CURL_CLIENT
#include <aws/core/http/curl/CurlHttpClient.h>
#include <signal.h>
class LongRunningCurlHttpClient : public Aws::Http::CurlHttpClient
{
public:
    LongRunningCurlHttpClient(const Aws::Client::ClientConfiguration& clientConfig) : Aws::Http::CurlHttpClient(clientConfig) {}

protected:
    void OverrideOptionsOnConnectionHandle(CURL* connectionHandle) const override
    {
        // Override low speed limit and low speed time
        curl_easy_setopt(connectionHandle, CURLOPT_LOW_SPEED_LIMIT, 1);
        curl_easy_setopt(connectionHandle, CURLOPT_LOW_SPEED_TIME, 10);
    }
};
#elif ENABLE_WINDOWS_CLIENT
#include <windows.h>
#if ENABLE_WINDOWS_IXML_HTTP_REQUEST_2_CLIENT
#include <aws/core/http/windows/IXmlHttpRequest2HttpClient.h>
#include <aws/core/platform/refs/IXmlHttpRequest2Ref.h>
class LongRunningIXmlHttpRequest2HttpClient : public Aws::Http::IXmlHttpRequest2HttpClient
{
public:
    LongRunningIXmlHttpRequest2HttpClient(const Aws::Client::ClientConfiguration& clientConfig) : Aws::Http::IXmlHttpRequest2HttpClient(clientConfig) {}

protected:
    // Override total timeout.
    void OverrideOptionsOnRequestHandle(const Aws::Http::HttpRequestComHandle& handle) const override
    {
        handle->SetProperty(XHR_PROP_TIMEOUT, 10000);
    }
};
#if BYPASS_DEFAULT_PROXY
#include <aws/core/http/windows/WinHttpSyncHttpClient.h>
#include <winhttp.h>
class LongRunningWinHttpSyncHttpClient : public Aws::Http::WinHttpSyncHttpClient
{
public:
    LongRunningWinHttpSyncHttpClient(const Aws::Client::ClientConfiguration& clientConfig) : Aws::Http::WinHttpSyncHttpClient(clientConfig) {}

protected:
    // Override receive timeout.
    void OverrideOptionsOnRequestHandle(void* handle) const override
    {
        DWORD requestMs = 10000;
        if (!WinHttpSetOption(handle, WINHTTP_OPTION_RECEIVE_TIMEOUT, &requestMs, sizeof(requestMs)))
        {
            AWS_LOGSTREAM_ERROR(ALLOCATION_TAG, "Error setting timeouts " << GetLastError());
        }
    }
};
#endif
#else
#include <aws/core/http/windows/WinHttpSyncHttpClient.h>
#include <winhttp.h>
class LongRunningWinHttpSyncHttpClient : public Aws::Http::WinHttpSyncHttpClient
{
public:
    LongRunningWinHttpSyncHttpClient(const Aws::Client::ClientConfiguration& clientConfig) : Aws::Http::WinHttpSyncHttpClient(clientConfig) {}

protected:
    // Override receive timeout.
    void OverrideOptionsOnRequestHandle(void* handle) const override
    {
        DWORD requestMs = 10000;
        if (!WinHttpSetOption(handle, WINHTTP_OPTION_RECEIVE_TIMEOUT, &requestMs, sizeof(requestMs)))
        {
            AWS_LOGSTREAM_ERROR(ALLOCATION_TAG, "Error setting timeouts " << GetLastError());
        }
    }
};
#endif
#endif

class MockCustomHttpClientFactory : public Aws::Http::HttpClientFactory
{
    std::shared_ptr<Aws::Http::HttpClient> CreateHttpClient(const Aws::Client::ClientConfiguration& clientConfiguration) const override
    {
#if ENABLE_CURL_CLIENT
        return Aws::MakeShared<LongRunningCurlHttpClient>(ALLOCATION_TAG, clientConfiguration);
#elif ENABLE_WINDOWS_CLIENT
#if ENABLE_WINDOWS_IXML_HTTP_REQUEST_2_CLIENT
#if BYPASS_DEFAULT_PROXY
        return Aws::MakeShared<LongRunningWinHttpSyncHttpClient>(ALLOCATION_TAG, clientConfiguration);
#else
        return Aws::MakeShared<LongRunningIXmlHttpRequest2HttpClient>(ALLOCATION_TAG, clientConfiguration);
#endif // BYPASS_DEFAULT_PROXY
#else
        return Aws::MakeShared<LongRunningWinHttpSyncHttpClient>(ALLOCATION_TAG, clientConfiguration);

#endif // ENABLE_WINDOWS_IXML_HTTP_REQUEST_2_CLIENT
#else
        AWS_LOGSTREAM_ERROR(ALLOCATION_TAG, "For testing purpose, this factory will not fallback to default http client intentionally.");
        return nullptr;
#endif
    }

    std::shared_ptr<Aws::Http::HttpRequest> CreateHttpRequest(const Aws::String &uri, Aws::Http::HttpMethod method,
                                                    const Aws::IOStreamFactory &streamFactory) const override
    {
        return CreateHttpRequest(Aws::Http::URI(uri), method, streamFactory);
    }

    std::shared_ptr<Aws::Http::HttpRequest> CreateHttpRequest(const Aws::Http::URI& uri, Aws::Http::HttpMethod method, const Aws::IOStreamFactory& streamFactory) const override
    {
        auto request = Aws::MakeShared<Aws::Http::Standard::StandardHttpRequest>(ALLOCATION_TAG, uri, method);
        request->SetResponseStreamFactory(streamFactory);

        return request;
    }

    void InitStaticState() override
    {
#if ENABLE_CURL_CLIENT
        LongRunningCurlHttpClient::InitGlobalState();
#elif ENABLE_WINDOWS_IXML_HTTP_REQUEST_2_CLIENT
        LongRunningIXmlHttpRequest2HttpClient::InitCOM();
#endif
    }

    void CleanupStaticState() override
    {
#if ENABLE_CURL_CLIENT
        LongRunningCurlHttpClient::CleanupGlobalState();
#endif
    }
};

TEST_F(HttpClientTest, TestHttpClientOverride)
{
    auto request = CreateHttpRequest(Aws::String("http://127.0.0.1:8778"),
                                     HttpMethod::HTTP_GET, Aws::Utils::Stream::DefaultResponseStreamFactoryMethod);
    request->SetHeaderValue("WaitSeconds", "5");
    Aws::Client::ClientConfiguration config;
    config.requestTimeoutMs = 1000; // http server wait 4 seconds to respond
    auto httpClient = CreateHttpClient(config);
    auto response = httpClient->MakeRequest(request);
    EXPECT_NE(nullptr, response);
    ASSERT_TRUE(response->HasClientError());
    ASSERT_EQ(CoreErrors::NETWORK_CONNECTION, response->GetClientErrorType());
    EXPECT_EQ(Aws::Http::HttpResponseCode::REQUEST_NOT_MADE, response->GetResponseCode());

    // With custom HTTP client factory, the request timeout is 10 seconds for each HTTP client.
    SetHttpClientFactory(Aws::MakeShared<MockCustomHttpClientFactory>(ALLOCATION_TAG));
    request = CreateHttpRequest(Aws::String("http://127.0.0.1:8778"),
                                HttpMethod::HTTP_GET, Aws::Utils::Stream::DefaultResponseStreamFactoryMethod);
    request->SetHeaderValue("WaitSeconds", "5");
    httpClient = CreateHttpClient(config);
    response = httpClient->MakeRequest(request);
    EXPECT_NE(nullptr, response);
    ASSERT_FALSE(response->HasClientError());
    EXPECT_EQ(Aws::Http::HttpResponseCode::OK, response->GetResponseCode());

    CleanupHttp();
    InitHttp();
}

//Test CURL HTTP Client specific Settings.
#if ENABLE_CURL_CLIENT
#include <aws/core/platform/FileSystem.h>
#include <aws/core/utils/DateTime.h>
#include <unistd.h>
#include <signal.h>
#include <stdlib.h>
#include <thread>

TEST_F(CURLHttpClientTest, TestConnectionTimeout)
{
    auto request = CreateHttpRequest(Aws::String("https://8.8.8.8:53"),//unless 8.8.8.8 is localhost, it's unlikely to succeed.
                                     HttpMethod::HTTP_GET, Aws::Utils::Stream::DefaultResponseStreamFactoryMethod);
    Aws::Client::ClientConfiguration config;
    config.connectTimeoutMs = 1; //1ms should be short enough to timeout the request
    auto httpClient = CreateHttpClient(config);
    auto response = httpClient->MakeRequest(request);
    ASSERT_NE(nullptr, response);
    ASSERT_TRUE(response->HasClientError());
    ASSERT_EQ(CoreErrors::NETWORK_CONNECTION, response->GetClientErrorType());
    ASSERT_EQ(Aws::Http::HttpResponseCode::REQUEST_NOT_MADE, response->GetResponseCode());
    ASSERT_TRUE(response->GetClientErrorMessage().find("curlCode: 28") == 0);
}

TEST_F(CURLHttpClientTest, TestHttpRequestTimeout)
{
    auto request = CreateHttpRequest(Aws::String("http://127.0.0.1:8778"),
                                     HttpMethod::HTTP_GET, Aws::Utils::Stream::DefaultResponseStreamFactoryMethod);
    request->SetHeaderValue("WaitSeconds", "2");
    Aws::Client::ClientConfiguration config;
    config.httpRequestTimeoutMs = 1000; // http server wait 2 seconds to respond
    auto httpClient = CreateHttpClient(config);
    auto response = httpClient->MakeRequest(request);
    EXPECT_NE(nullptr, response);
    ASSERT_TRUE(response->HasClientError());
    ASSERT_EQ(CoreErrors::NETWORK_CONNECTION, response->GetClientErrorType());
    EXPECT_EQ(Aws::Http::HttpResponseCode::REQUEST_NOT_MADE, response->GetResponseCode());
    EXPECT_TRUE(response->GetClientErrorMessage().find("curlCode: 28") == 0);
}

TEST_F(CURLHttpClientTest, TestHttpRequestTimeoutBeforeFinishing)
{
    auto request = CreateHttpRequest(Aws::String("http://127.0.0.1:8778"),
                                     HttpMethod::HTTP_GET, Aws::Utils::Stream::DefaultResponseStreamFactoryMethod);
    request->SetHeaderValue("WaitSeconds", "2");
    Aws::Client::ClientConfiguration config;
    config.requestTimeoutMs = 3000; // http server wait 2 seconds to respond
    config.httpRequestTimeoutMs = 1000;
    auto httpClient = CreateHttpClient(config);
    auto response = httpClient->MakeRequest(request);
    EXPECT_NE(nullptr, response);
    ASSERT_TRUE(response->HasClientError());
    ASSERT_EQ(CoreErrors::NETWORK_CONNECTION, response->GetClientErrorType());
    EXPECT_EQ(Aws::Http::HttpResponseCode::REQUEST_NOT_MADE, response->GetResponseCode());
    EXPECT_TRUE(response->GetClientErrorMessage().find("curlCode: 28") == 0);
}

TEST_F(CURLHttpClientTest, TestHttpRequestWorksFine)
{
    auto request = CreateHttpRequest(Aws::String("http://127.0.0.1:8778"),
                                     HttpMethod::HTTP_GET, Aws::Utils::Stream::DefaultResponseStreamFactoryMethod);
    request->SetHeaderValue("WaitSeconds", "2");
    Aws::Client::ClientConfiguration config;
    config.requestTimeoutMs = 10000; // http server wait 2 seconds to respond
    //config.httpRequestTimeoutMs defaults to 0, never timeout
    auto httpClient = CreateHttpClient(config);
    auto response = httpClient->MakeRequest(request);
    EXPECT_NE(nullptr, response);
    ASSERT_FALSE(response->HasClientError());
    EXPECT_EQ(Aws::Http::HttpResponseCode::OK, response->GetResponseCode());
    EXPECT_EQ("", response->GetClientErrorMessage());
}
#endif // ENABLE_CURL_CLIENT
#endif // ENABLE_HTTP_CLIENT_TESTING
#endif // NO_HTTP_CLIENT