/////////////////////////////////////////////////////////////////////////////
// Name:        tests/net/webrequest.cpp
// Purpose:     wxWebRequest test
// Author:      Tobias Taschner
// Created:     2018-10-24
// Copyright:   (c) 2018 wxWidgets development team
// Licence:     wxWindows licence
/////////////////////////////////////////////////////////////////////////////

// ----------------------------------------------------------------------------
// headers
// ----------------------------------------------------------------------------

#include "testprec.h"

#ifndef WX_PRECOMP
    #include "wx/wx.h"
#endif // WX_PRECOMP

#if wxUSE_WEBREQUEST

#include "wx/webrequest.h"
#include "wx/filename.h"
#include "wx/uri.h"
#include "wx/wfstream.h"

#include <memory>
#include <unordered_map>

// This test uses httpbin service and by default uses the mirror at the
// location below, which seems to be more reliable than the main site at
// https://httpbin.org. Any other mirror, including a local one, which can be
// set by running kennethreitz/httpbin Docker container, can be used by setting
// WX_TEST_WEBREQUEST_URL environment variable to its URL.
//
// This variable can also be set to a special value "0" to disable running the
// test entirely.
static const char* WX_TEST_WEBREQUEST_URL_DEFAULT = "https://nghttp2.org/httpbin";

// Other environment variables used by this test:
//
// - WX_TEST_WEBREQUEST_USE_BADSSL: setting this to 1 is equivalent to setting
//   all the variables below to their example values using badssl.com.
//
// - WX_TEST_WEBREQUEST_URL_SELF_SIGNED: set to https://self-signed.badssl.com/
//   or any other server using self-signed certificate to test disabling SSL
//   certificate trust chain verification.
//
// - WX_TEST_WEBREQUEST_URL_EXPIRED: set to https://expired.badssl.com/ or any
//   other server using expired certificate to test disabling SSL certificate
//   date validity verification.
//
// - WX_TEST_WEBREQUEST_URL_BADHOST: set to https://wrong.host.badssl.com/ or
//   any other server using certificate with wrong host name to test disabling
//   SSL host name verification.

// Common base for sync and async tests fixtures.
class BaseRequestFixture
{
protected:
    // All tests should call this function first and skip the test entirely if
    // it returns false, as this indicates that web requests tests are disabled.
    bool InitBaseURL()
    {
        if ( wxGetEnv("WX_TEST_WEBREQUEST_URL", &baseURL) )
        {
            if ( baseURL == "0" )
                return false;

            static bool s_shown = false;
            if ( !s_shown )
            {
                s_shown = true;
                WARN("Using non-default root URL " << baseURL);
            }
        }
        else
        {
            baseURL = WX_TEST_WEBREQUEST_URL_DEFAULT;
        }

        REQUIRE( GetSession().SetBaseURL(baseURL) );

        return true;
    }

    enum class BadSSLKind
    {
        SelfSigned,
        Expired,
        BadHost
    };

    // Return true if the test is enabled by setting the corresponding
    // environment variable, false if it should be skipped.
    bool GetBadSSLURL(BadSSLKind kind, wxString* url) const
    {
        const auto useBadSSL = [](const char* badSSLURL, wxString* url)
        {
            wxString s;
            if ( !wxGetEnv("WX_TEST_WEBREQUEST_USE_BADSSL", &s) || s != "1" )
                return false;

            *url = badSSLURL;

            return true;
        };

        switch ( kind )
        {
            case BadSSLKind::SelfSigned:
                if ( wxGetEnv("WX_TEST_WEBREQUEST_URL_SELF_SIGNED", url) )
                    return true;

                if ( useBadSSL("https://self-signed.badssl.com/", url) )
                    return true;
                break;

            case BadSSLKind::Expired:
                if ( wxGetEnv("WX_TEST_WEBREQUEST_URL_EXPIRED", url) )
                    return true;

                if ( useBadSSL("https://expired.badssl.com/", url) )
                    return true;
                break;

            case BadSSLKind::BadHost:
                if ( wxGetEnv("WX_TEST_WEBREQUEST_URL_BADHOST", url) )
                    return true;

                if ( useBadSSL("https://wrong.host.badssl.com/", url) )
                    return true;
                break;
        }

        return false;
    }

    void
    CreateWithAuth(const wxString& relURL,
                   const wxString& user,
                   const wxString& password)
    {
        wxString url = baseURL;
        if ( !url.EndsWith('/') && !relURL.StartsWith('/') )
            url += '/';
        url += relURL;

        wxURI uri(url);
        uri.SetUserAndPassword(user, password);

        Create(uri.BuildURI());
    }

    virtual void Create(const wxString& url) = 0;

    virtual wxWebSessionBase& GetSession() = 0;

    virtual wxWebRequestBase& GetRequest() = 0;

    // Check that the response is a JSON object containing a specific key with the
    // expected value.
    void CheckExpectedJSON(const wxString& response, const wxString& key,
                           const wxString& value)
    {
        // We ought to really parse the returned JSON object, but to keep things as
        // simple as possible for now we just treat it as a string.
        INFO("Response: " << response);

        wxString expectedKey = wxString::Format("\"%s\":", key);
        size_t pos = response.find(expectedKey);
        REQUIRE( pos != wxString::npos );

        pos += expectedKey.size();

        // There may, or not, be a space after it.
        // And the value may be returned in an array.
        while ( wxIsspace(response[pos]) ||
                response[pos] == '"' ||
                response[pos] == '[' )
        {
            ++pos;
        }

        wxString actualValue = response.substr(pos, value.size());
        REQUIRE( actualValue == value );
    }

    // Special helper for "manual" tests taking the URL from the environment.
    void InitManualRequest()
    {
        // Allow getting 8-bit strings from the environment correctly.
        setlocale(LC_ALL, "");

        wxString url;
        if ( !wxGetEnv("WX_TEST_WEBREQUEST_URL", &url) )
        {
            FAIL("Specify WX_TEST_WEBREQUEST_URL");
        }

        wxString proxyURL;
        if ( wxGetEnv("WX_TEST_WEBREQUEST_PROXY", &proxyURL) )
        {
            wxWebProxy proxy = wxWebProxy::Default();

            // Interpret some values specially.
            if ( proxyURL == "0" )
                proxy = wxWebProxy::Disable();
            else if ( proxyURL != "1" )
                proxy = wxWebProxy::FromURL(proxyURL);

            REQUIRE( GetSession().SetProxy(proxy) );
        }

        Create(url);

        wxString insecure;
        if ( wxGetEnv("WX_TEST_WEBREQUEST_INSECURE", &insecure) )
        {
            int flags = 0;
            REQUIRE( insecure.ToInt(&flags) );

            GetRequest().MakeInsecure(flags);
        }
    }

private:
    wxString baseURL;
};

class RequestFixture : public wxTimer, public BaseRequestFixture
{
public:
    RequestFixture()
    {
        expectedFileSize = 0;
        dataSize = 0;
        stateFromEvent = wxWebRequest::State_Idle;
        statusFromEvent = 0;
    }

    void Create(const wxString& url) override
    {
        request = wxWebSession::GetDefault().CreateRequest(this, url);
        REQUIRE( request.IsOk() );

        Bind(wxEVT_WEBREQUEST_STATE, &RequestFixture::OnRequestState, this);
        Bind(wxEVT_WEBREQUEST_DATA, &RequestFixture::OnData, this);
    }

    wxWebSessionBase& GetSession() override
    {
        return wxWebSession::GetDefault();
    }

    wxWebRequestBase& GetRequest() override
    {
        return request;
    }

    void OnRequestState(wxWebRequestEvent& evt)
    {
        stateFromEvent = evt.GetState();
        const wxWebResponse& response = evt.GetResponse();
        if ( response.IsOk() )
        {
            // Note that the response object itself may be deleted if request
            // using it is, so we need to copy its data to use it later.
            statusFromEvent = response.GetStatus();
            responseStringFromEvent = response.AsString();
        }

        switch ( stateFromEvent )
        {
        case wxWebRequest::State_Idle:
            FAIL("should never get events with State_Idle");
            break;

        case wxWebRequest::State_Active:
            CHECK( request.GetNativeHandle() );
            break;

        case wxWebRequest::State_Completed:
            if ( request.IsOk() && request.GetStorage() == wxWebRequest::Storage_File )
            {
                wxFileName fn(evt.GetDataFile());
                CHECK( fn.GetSize() == expectedFileSize );
            }
            wxFALLTHROUGH;

        case wxWebRequest::State_Unauthorized:
        case wxWebRequest::State_Failed:
        case wxWebRequest::State_Cancelled:
            errorDescription = evt.GetErrorDescription();
            loop.Exit();
            break;
        }
    }

    void Notify() override
    {
        WARN("Exiting loop on timeout");
        loop.Exit();
    }

    void OnData(wxWebRequestEvent& evt)
    {
        // Count all bytes received via data event for Storage_None
        dataSize += evt.GetDataSize();
    }

    void RunLoopWithTimeout()
    {
        StartOnce(30000); // Ensure that we exit the loop after 30s.
        loop.Run();
        Stop();
    }

    void Run(wxWebRequest::State requiredState = wxWebRequest::State_Completed,
        int requiredStatus = 200)
    {
        REQUIRE( request.GetState() == wxWebRequest::State_Idle );
        request.Start();
        RunLoopWithTimeout();

        if ( stateFromEvent != requiredState )
        {
            errorDescription.Trim();
            if ( !errorDescription.empty() )
                WARN("Error: " << errorDescription);
        }

        REQUIRE( stateFromEvent == requiredState );

        CHECK( request.GetState() == stateFromEvent );

        if (requiredStatus)
        {
            CHECK( statusFromEvent == requiredStatus );
            CHECK( request.GetResponse().GetStatus() == requiredStatus );
        }
    }

    // Precondition: we must have an auth challenge.
    void UseCredentials(const wxString& user, const wxString& password)
    {
        request.GetAuthChallenge().SetCredentials(
            wxWebCredentials(user, wxSecretValue(password)));
    }

    wxEventLoop loop;
    wxWebRequest request;
    wxWebRequest::State stateFromEvent;
    int statusFromEvent;
    wxString responseStringFromEvent;
    wxInt64 expectedFileSize;
    wxInt64 dataSize;
    wxString errorDescription;
};

// Download more than 64KiB bytes to test that downloading more than the
// default buffer size works correctly.
constexpr int DOWNLOAD_BYTES = 99999;

// Substring used to check that we got the expected response after
// authenticating successfully. It is so weird because httpbin and go-httpbin
// use different strings for this: one uses "authenticated" while the other
// ones uses "authorized", so we use a substring common to both of them.
constexpr char AUTHORIZED_SUBSTRING[] = R"(ed": true)";

TEST_CASE_METHOD(RequestFixture,
                 "WebRequest::Get::Bytes", "[net][webrequest][get]")
{
    if ( !InitBaseURL() )
        return;

    Create(wxString::Format("bytes/%d", DOWNLOAD_BYTES));
    Run();
    CHECK( request.GetResponse().GetContentLength() == DOWNLOAD_BYTES );
    CHECK( request.GetBytesExpectedToReceive() == DOWNLOAD_BYTES );
    CHECK( request.GetBytesReceived() == DOWNLOAD_BYTES );
}

TEST_CASE_METHOD(RequestFixture,
                 "WebRequest::Get::Simple", "[net][webrequest][get]")
{
    if ( !InitBaseURL() )
        return;

    // Note that the session may be initialized on demand, so don't check the
    // native handle before actually using it.
    wxWebSession& session = wxWebSession::GetDefault();
    REQUIRE( session.IsOpened() );

    // Request is not initialized yet.
    CHECK( !request.IsOk() );
    CHECK( !request.GetNativeHandle() );

    Create("status/200");
    CHECK( request.IsOk() );
    CHECK( session.GetNativeHandle() );

    // Note that the request must be started to have a valid native handle.
    request.Start();
    CHECK( request.GetNativeHandle() );
    RunLoopWithTimeout();
    CHECK( request.GetState() == wxWebRequest::State_Completed );
    CHECK( request.GetResponse().GetStatus() == 200 );
}

TEST_CASE_METHOD(RequestFixture,
                 "WebRequest::Get::String", "[net][webrequest][get]")
{
    if ( !InitBaseURL() )
        return;

    Create("base64/VGhlIHF1aWNrIGJyb3duIGZveCBqdW1wcyBvdmVyIHRoZSBsYXp5IGRvZw==");
    Run();
    CHECK( request.GetResponse().AsString() == "The quick brown fox jumps over the lazy dog" );
}

TEST_CASE_METHOD(RequestFixture,
                 "WebRequest::Get::Header", "[net][webrequest][get]")
{
    if ( !InitBaseURL() )
        return;

    Create("response-headers?freeform=wxWidgets%20works!");
    Run();
    CHECK( request.GetResponse().GetHeader("freeform") == "wxWidgets works!" );
}

TEST_CASE_METHOD(RequestFixture,
    "WebRequest::Get::AllHeaderValues", "[net][webrequest][get]")
{
    if ( !InitBaseURL() )
        return;

    Create("response-headers?freeform=wxWidgets&freeform=works!");
    Run();
    std::vector<wxString> headers = request.GetResponse().GetAllHeaderValues("freeform");

#if wxUSE_WEBREQUEST_URLSESSION
    // The httpbin service concatenates the given parameters.
    REQUIRE( headers.size() == 1 );
    CHECK( headers[0] == "wxWidgets, works!" );
#else
    REQUIRE( headers.size() == 2 );
    CHECK( (headers[0] == "wxWidgets" && headers[1] == "works!") );
#endif
}

TEST_CASE_METHOD(RequestFixture,
                 "WebRequest::Get::Param", "[net][webrequest][get]")
{
    if ( !InitBaseURL() )
        return;

    wxString key = "pi";
    wxString value = "3.14159265358979323";

    Create(wxString::Format("get?%s=%s", key, value));
    Run();

    CheckExpectedJSON( request.GetResponse().AsString(), key, value );
}

TEST_CASE_METHOD(RequestFixture,
                 "WebRequest::Get::File", "[net][webrequest][get]")
{
    if ( !InitBaseURL() )
        return;

    expectedFileSize = 99 * 1024;
    Create(wxString::Format("bytes/%lld", expectedFileSize));
    request.SetStorage(wxWebRequest::Storage_File);
    Run();
    CHECK( request.GetBytesReceived() == expectedFileSize );
}

TEST_CASE_METHOD(RequestFixture,
                 "WebRequest::Get::None", "[net][webrequest][get]")
{
    if ( !InitBaseURL() )
        return;

    int processingSize = 99 * 1024;
    Create(wxString::Format("bytes/%d", processingSize));
    request.SetStorage(wxWebRequest::Storage_None);
    Run();
    CHECK( request.GetBytesReceived() == processingSize );
    CHECK( dataSize == processingSize );
}

TEST_CASE_METHOD(RequestFixture,
                 "WebRequest::Error::HTTP", "[net][webrequest][error]")
{
    if ( !InitBaseURL() )
        return;

    Create("status/404");
    Run(wxWebRequest::State_Failed, 404);
}

TEST_CASE_METHOD(RequestFixture,
                 "WebRequest::Error::Body", "[net][webrequest][error]")
{
    if ( !InitBaseURL() )
        return;

    Create("status/418");
    Run(wxWebRequest::State_Failed, 0);

    CHECK( request.GetResponse().GetStatus() == 418 );

    const wxString& response = request.GetResponse().AsString();
    INFO( "Response: " << response);
    CHECK( response.Contains("teapot") );
}

TEST_CASE_METHOD(RequestFixture,
                 "WebRequest::Error::Connect", "[net][webrequest][error]")
{
    if ( !InitBaseURL() )
        return;

    Create("http://127.0.0.1:51234");
    Run(wxWebRequest::State_Failed, 0);
}

TEST_CASE_METHOD(RequestFixture,
    "WebRequest::SSL::Error", "[net][webrequest][error]")
{
    wxString url;

    if ( GetBadSSLURL(BadSSLKind::SelfSigned, &url) )
    {
        INFO("Testing self-signed certificate at " << url);

        Create(url);
        Run(wxWebRequest::State_Failed, 0);

        Create(url);
        request.DisablePeerVerify();
        Run(wxWebRequest::State_Completed, 200);
    }

    if ( GetBadSSLURL(BadSSLKind::Expired, &url) )
    {
        INFO("Testing expired certificate at " << url);

        Create(url);
        Run(wxWebRequest::State_Failed, 0);

        Create(url);
        request.MakeInsecure(wxWebRequest::Ignore_Certificate);
        Run(wxWebRequest::State_Completed, 200);
    }

    if ( GetBadSSLURL(BadSSLKind::BadHost, &url) )
    {
        INFO("Testing certificate with bad host at " << url);

        Create(url);
        Run(wxWebRequest::State_Failed, 0);

        // Currently disabling certificate verification also disables host name
        // verification in NSURLSession backend, so skip this test with it.
        if ( wxWebSession::GetDefault().GetLibraryVersionInfo().GetName()
                != "URLSession" )
        {
            Create(url);
            request.MakeInsecure(wxWebRequest::Ignore_Certificate);
            Run(wxWebRequest::State_Failed, 0);
        }

        Create(url);
        request.MakeInsecure(wxWebRequest::Ignore_Host);
        Run(wxWebRequest::State_Completed, 200);
    }
}

TEST_CASE_METHOD(RequestFixture,
                 "WebRequest::Post", "[net][webrequest]")
{
    if ( !InitBaseURL() )
        return;

    Create("post");
    request.SetData("app=WebRequestSample&version=1", "application/x-www-form-urlencoded");
    Run();
}

TEST_CASE_METHOD(RequestFixture,
                 "WebRequest::Put", "[net][webrequest]")
{
    if ( !InitBaseURL() )
        return;

    Create("put");
    std::unique_ptr<wxInputStream> is(new wxFileInputStream("horse.png"));
    REQUIRE( is->IsOk() );

    request.SetData(is.release(), "image/png");
    request.SetMethod("PUT");
    Run();
}

TEST_CASE_METHOD(RequestFixture,
                 "WebRequest::Delete", "[net][webrequest]")
{
    if ( !InitBaseURL() )
        return;

    Create("delete");
    request.SetData(R"({"bloordyblop": 17})", "application/json");
    request.SetMethod("DELETE");
    Run();

    const wxString& response = request.GetResponse().AsString();
    CHECK_THAT( response.utf8_string(),
                Catch::Contains(R"("bloordyblop": 17)") );
}

TEST_CASE_METHOD(RequestFixture,
                 "WebRequest::Auth::Basic", "[net][webrequest][auth]")
{
    if ( !InitBaseURL() )
        return;

    Create("basic-auth/wxtest/wxwidgets");
    Run(wxWebRequest::State_Unauthorized, 401);
    REQUIRE( request.GetAuthChallenge().IsOk() );

    SECTION("Good password")
    {
        UseCredentials("wxtest", "wxwidgets");
        RunLoopWithTimeout();
        CHECK( request.GetState() == wxWebRequest::State_Completed );

        const auto& response = request.GetResponse();
        CHECK( response.GetStatus() == 200 );
        CHECK_THAT( response.AsString().utf8_string(),
                    Catch::Contains(AUTHORIZED_SUBSTRING) );
    }

    SECTION("Bad password")
    {
        UseCredentials("wxtest", "foobar");
        RunLoopWithTimeout();
        CHECK( request.GetResponse().GetStatus() == 401 );
        CHECK( request.GetState() == wxWebRequest::State_Unauthorized );
    }
}

TEST_CASE_METHOD(RequestFixture,
                 "WebRequest::Auth::Basic/Reserved", "[net][webrequest][auth]")
{
    if ( !InitBaseURL() )
        return;

    // Use some reserved (in the RFC 3986 sense) characters in the user name and
    // the password (as well as a sub-delimiter character '=' in the password).
    Create("basic-auth/u%40d/1%3d2%3f");
    Run(wxWebRequest::State_Unauthorized, 401);
    REQUIRE( request.GetAuthChallenge().IsOk() );

    UseCredentials("u@d", "1=2?");
    RunLoopWithTimeout();
    CHECK( request.GetState() == wxWebRequest::State_Completed );

    const auto& response = request.GetResponse();
    CHECK( response.GetStatus() == 200 );
    CHECK_THAT( response.AsString().utf8_string(),
                Catch::Contains(AUTHORIZED_SUBSTRING) );
}

TEST_CASE_METHOD(RequestFixture,
                 "WebRequest::Auth::Digest", "[net][webrequest][auth]")
{
    if ( !InitBaseURL() )
        return;

    Create("digest-auth/auth/wxtest/wxwidgets");
    Run(wxWebRequest::State_Unauthorized, 401);
    REQUIRE( request.GetAuthChallenge().IsOk() );

    SECTION("Good password")
    {
        UseCredentials("wxtest", "wxwidgets");
        RunLoopWithTimeout();
        CHECK( request.GetState() == wxWebRequest::State_Completed );

        const auto& response = request.GetResponse();
        CHECK( response.GetStatus() == 200 );
        CHECK_THAT( response.AsString().utf8_string(),
                    Catch::Contains(AUTHORIZED_SUBSTRING) );
    }

    SECTION("Bad password")
    {
        UseCredentials("foo", "bar");
        RunLoopWithTimeout();
        CHECK( request.GetResponse().GetStatus() == 401 );
        CHECK( request.GetState() == wxWebRequest::State_Unauthorized );
    }
}

TEST_CASE_METHOD(RequestFixture,
                 "WebRequest::Auth::BasicInURL", "[net][webrequest][auth]")
{
    if ( !InitBaseURL() )
        return;

    CreateWithAuth("basic-auth/wxtest/wxwidgets", "wxtest", "wxwidgets");
    Run();

    CHECK( request.GetState() == wxWebRequest::State_Completed );

    const auto& response = request.GetResponse();
    CHECK( response.GetStatus() == 200 );
    CHECK_THAT( response.AsString().utf8_string(),
                Catch::Contains(AUTHORIZED_SUBSTRING) );
}

TEST_CASE_METHOD(RequestFixture,
                 "WebRequest::Auth::DigestInURL", "[net][webrequest][auth]")
{
    if ( !InitBaseURL() )
        return;

    CreateWithAuth("digest-auth/auth/wxtest/wxwidgets", "wxtest", "wxwidgets");
    Run();

    CHECK( request.GetState() == wxWebRequest::State_Completed );

    const auto& response = request.GetResponse();
    CHECK( response.GetStatus() == 200 );
    CHECK_THAT( response.AsString().utf8_string(),
                Catch::Contains(AUTHORIZED_SUBSTRING) );
}

TEST_CASE_METHOD(RequestFixture,
                 "WebRequest::Cancel", "[net][webrequest]")
{
    if ( !InitBaseURL() )
        return;

    Create("delay/10");
    request.Start();
    request.Cancel();
    RunLoopWithTimeout();

#ifdef __WINDOWS__
    // This is another weird test failure that happens only on AppVeyor:
    // sometimes (perhaps because the test machine is too slow?) the request
    // fails instead of (before?) being cancelled.
    if ( IsAutomaticTest() )
    {
        if ( request.GetState() == wxWebRequest::State_Failed )
        {
            WARN("Request unexpectedly failed after cancelling.");
            return;
        }
    }
#endif // __WINDOWS__

    REQUIRE( request.GetState() == wxWebRequest::State_Cancelled );
}

TEST_CASE_METHOD(RequestFixture,
                 "WebRequest::Destroy", "[net][webrequest]")
{
    if ( !InitBaseURL() )
        return;

    Create("base64/U3RpbGwgYWxpdmUh");
    request.Start();

    // Destroy the original request: this shouldn't prevent it from running to
    // the completion!
    request = wxWebRequest();

    RunLoopWithTimeout();

    CHECK( stateFromEvent == wxWebRequest::State_Completed );
    CHECK( statusFromEvent == 200 );
    CHECK( responseStringFromEvent == "Still alive!" );
}

TEST_CASE_METHOD(RequestFixture, "WebRequest::LifeTime", "[net][webrequest]")
{
    if ( !InitBaseURL() )
        return;

    Create("status/200");
    Run();

    // Close the session before the request is destroyed: this shouldn't result
    // in a crash.
    wxWebSession::GetDefault().Close();

    CHECK( request.GetResponse().GetStatus() == 200 );
}

class SyncRequestFixture : public BaseRequestFixture
{
public:
    void Create(const wxString& url) override
    {
        request = wxWebSessionSync::GetDefault().CreateRequest(url);
        REQUIRE( request.IsOk() );
    }

    wxWebSessionBase& GetSession() override
    {
        return wxWebSessionSync::GetDefault();
    }

    wxWebRequestBase& GetRequest() override
    {
        return request;
    }

    bool Execute(const wxString& url)
    {
        Create(url);
        return Execute();
    }

    bool Execute()
    {
        const auto result = request.Execute();

        response = request.GetResponse();
        state = result.state;
        error = result.error;

        switch ( state )
        {
            case wxWebRequest::State_Idle:
            case wxWebRequest::State_Active:
            case wxWebRequest::State_Cancelled:
                wxFAIL_MSG("Unexpected state");
                wxFALLTHROUGH;

            case wxWebRequest::State_Failed:
            case wxWebRequest::State_Unauthorized:
                break;

            case wxWebRequest::State_Completed:
                return true;
        }

        return false;
    }

    wxWebRequestSync request;
    wxWebResponse response;
    wxWebRequest::State state = wxWebRequest::State_Idle;
    wxString error;
};

TEST_CASE_METHOD(SyncRequestFixture,
                 "WebRequest::Sync::Get::Bytes", "[net][webrequest][sync][get]")
{
    if ( !InitBaseURL() )
        return;

    REQUIRE( Execute(wxString::Format("bytes/%d", DOWNLOAD_BYTES)) );

    CHECK( response.GetStatus() == 200 );
    CHECK( response.GetContentLength() == DOWNLOAD_BYTES );
}

TEST_CASE_METHOD(SyncRequestFixture,
                 "WebRequest::Sync::Get::Simple", "[net][webrequest][sync][get]")
{
    if ( !InitBaseURL() )
        return;

    // Note that the session may be initialized on demand, so don't check the
    // native handle before actually using it.
    wxWebSessionSync& session = wxWebSessionSync::GetDefault();
    REQUIRE( session.IsOpened() );

    // Request is not initialized yet.
    CHECK( !request.IsOk() );
    CHECK( !request.GetNativeHandle() );

    REQUIRE( Execute("status/200") );
    CHECK( request.IsOk() );
    CHECK( request.GetNativeHandle() );
    CHECK( session.GetNativeHandle() );

    CHECK( response.GetStatus() == 200 );
}

TEST_CASE_METHOD(SyncRequestFixture,
                 "WebRequest::Sync::Get::String", "[net][webrequest][sync][get]")
{
    if ( !InitBaseURL() )
        return;

    REQUIRE( Execute("base64/VGhlIHF1aWNrIGJyb3duIGZveCBqdW1wcyBvdmVyIHRoZSBsYXp5IGRvZw==") );

    CHECK( response.AsString() == "The quick brown fox jumps over the lazy dog" );
}

TEST_CASE_METHOD(SyncRequestFixture,
                 "WebRequest::Sync::Get::Header", "[net][webrequest][sync][get]")
{
    if ( !InitBaseURL() )
        return;

    REQUIRE( Execute("response-headers?freeform=wxWidgets%20works!") );

    CHECK( response.GetHeader("freeform") == "wxWidgets works!" );
}

TEST_CASE_METHOD(SyncRequestFixture,
                 "WebRequest::Sync::Get::Param", "[net][webrequest][sync][get]")
{
    if ( !InitBaseURL() )
        return;

    wxString key = "pi";
    wxString value = "3.14159265358979323";

    REQUIRE( Execute(wxString::Format("get?%s=%s", key, value)) );

    CheckExpectedJSON( response.AsString(), key, value );
}

TEST_CASE_METHOD(SyncRequestFixture,
                 "WebRequest::Sync::Get::File", "[net][webrequest][sync][get]")
{
    if ( !InitBaseURL() )
        return;

    wxInt64 expectedFileSize = 99 * 1024;
    Create(wxString::Format("bytes/%lld", expectedFileSize));
    request.SetStorage(wxWebRequest::Storage_File);

    REQUIRE( Execute() );

    CHECK( request.GetBytesReceived() == expectedFileSize );
}

TEST_CASE_METHOD(SyncRequestFixture,
                 "WebRequest::Sync::Get::None", "[net][webrequest][sync][get]")
{
    if ( !InitBaseURL() )
        return;

    int processingSize = 99 * 1024;
    Create(wxString::Format("bytes/%d", processingSize));
    request.SetStorage(wxWebRequest::Storage_None);

    REQUIRE( Execute() );

    CHECK( request.GetBytesReceived() == processingSize );
}

TEST_CASE_METHOD(SyncRequestFixture,
                 "WebRequest::Sync::Error::HTTP", "[net][webrequest][sync][error]")
{
    if ( !InitBaseURL() )
        return;

    CHECK_FALSE( Execute("status/404") );

    CHECK( response.GetStatus() == 404 );
}

TEST_CASE_METHOD(SyncRequestFixture,
                 "WebRequest::Sync::Error::Body", "[net][webrequest][sync][error]")
{
    if ( !InitBaseURL() )
        return;

    CHECK_FALSE( Execute("status/418") );

    CHECK( response.GetStatus() == 418 );
    CHECK_THAT( response.AsString().utf8_string(), Catch::Contains("teapot") );
}

TEST_CASE_METHOD(SyncRequestFixture,
                 "WebRequest::Sync::Error::Connect", "[net][webrequest][sync][error]")
{
    if ( !InitBaseURL() )
        return;

    Create("http://127.0.0.1:51234");
    CHECK( !Execute() );
}

TEST_CASE_METHOD(SyncRequestFixture,
                 "WebRequest::Sync::SSL::Error", "[net][webrequest][sync][error]")
{
    if ( wxWebSession::GetDefault().GetLibraryVersionInfo().GetName()
            == "URLSession" )
    {
        WARN("Disabling SSL verification is not supported with URLSession backend");
        return;
    }

    wxString url;

    if ( GetBadSSLURL(BadSSLKind::SelfSigned, &url) )
    {
        INFO("Testing self-signed certificate at " << url);

        Create(url);
        CHECK( !Execute() );

        Create(url);
        request.DisablePeerVerify();

        REQUIRE( Execute() );
        CHECK( response.GetStatus() == 200 );
    }

    if ( GetBadSSLURL(BadSSLKind::Expired, &url) )
    {
        INFO("Testing expired certificate at " << url);

        Create(url);
        CHECK( !Execute() );

        Create(url);
        request.MakeInsecure(wxWebRequest::Ignore_Certificate);
        REQUIRE( Execute() );
        CHECK( response.GetStatus() == 200 );
    }

    if ( GetBadSSLURL(BadSSLKind::BadHost, &url) )
    {
        INFO("Testing certificate with bad host at " << url);

        Create(url);
        CHECK( !Execute() );

        Create(url);
        request.MakeInsecure(wxWebRequest::Ignore_Certificate);
        CHECK( !Execute() );

        Create(url);
        request.MakeInsecure(wxWebRequest::Ignore_Host);
        REQUIRE( Execute() );
        CHECK( response.GetStatus() == 200 );
    }
}

TEST_CASE_METHOD(SyncRequestFixture,
                 "WebRequest::Sync::Post", "[net][webrequest][sync]")
{
    if ( !InitBaseURL() )
        return;

    Create("post");
    request.SetData("app=WebRequestSample&version=1", "application/x-www-form-urlencoded");
    REQUIRE( Execute() );
    CHECK( response.GetStatus() == 200 );
}

TEST_CASE_METHOD(SyncRequestFixture,
                 "WebRequest::Sync::Put", "[net][webrequest][sync]")
{
    if ( !InitBaseURL() )
        return;

    Create("put");
    std::unique_ptr<wxInputStream> is(new wxFileInputStream("horse.png"));
    REQUIRE( is->IsOk() );

    request.SetData(is.release(), "image/png");
    request.SetMethod("PUT");
    REQUIRE( Execute() );
    CHECK( response.GetStatus() == 200 );
}

TEST_CASE_METHOD(SyncRequestFixture,
                 "WebRequest::Sync::Delete", "[net][webrequest][sync]")
{
    if ( !InitBaseURL() )
        return;

    Create("delete");
    request.SetData(R"({"bloordyblop": 17})", "application/json");
    request.SetMethod("DELETE");
    REQUIRE( Execute() );

    CHECK( response.GetStatus() == 200 );
    CHECK_THAT( response.AsString().utf8_string(),
                Catch::Contains(R"("bloordyblop": 17)") );
}

TEST_CASE_METHOD(SyncRequestFixture,
                 "WebRequest::Sync::Auth::Basic", "[net][webrequest][sync][auth]")
{
    if ( !InitBaseURL() )
        return;

    SECTION("No password")
    {
        Create("basic-auth/wxtest/wxwidgets");
        CHECK_FALSE( Execute() );
        CHECK( state == wxWebRequest::State_Unauthorized );
        CHECK( response.GetStatus() == 401 );
    }

    SECTION("Good password")
    {
        CreateWithAuth("basic-auth/wxtest/wxwidgets", "wxtest", "wxwidgets");
        CHECK( Execute() );
        CHECK( response.GetStatus() == 200 );
        CHECK( state == wxWebRequest::State_Completed );

        CHECK_THAT( response.AsString().utf8_string(),
                    Catch::Contains(AUTHORIZED_SUBSTRING) );
    }

    SECTION("Bad password")
    {
        CreateWithAuth("basic-auth/wxtest/wxwidgets", "wxtest", "foobar");
        CHECK_FALSE( Execute() );
        CHECK( response.GetStatus() == 401 );
        CHECK( state == wxWebRequest::State_Unauthorized );
    }

    SECTION("Reserved characters")
    {
        if ( wxWebSession::GetDefault().GetLibraryVersionInfo().GetName()
                == "URLSession" )
        {
            // NSURLSession doesn't decode percent-encoded characters in the
            // password (as indirectly confirmed by the documentation of NSURL
            // password property, which says that "Any percent-encoded
            // characters are not unescaped.", resulting in sending wrong
            // password to the server if we use any reserved characters in it.
            CreateWithAuth("basic-auth/u%40d/1=2", "u@d", "1=2");
        }
        else
        {
            // With the other backends, using reserved characters in the
            // password does work.
            CreateWithAuth("basic-auth/u%40d/1%3d2%3f", "u@d", "1=2?");
        }

        REQUIRE( Execute() );
        CHECK( response.GetStatus() == 200 );
        CHECK( state == wxWebRequest::State_Completed );

        CHECK_THAT( response.AsString().utf8_string(),
                    Catch::Contains(AUTHORIZED_SUBSTRING) );
    }
}

TEST_CASE_METHOD(SyncRequestFixture,
                 "WebRequest::Sync::Auth::Digest", "[net][webrequest][sync][auth]")
{
    if ( !InitBaseURL() )
        return;

    const auto& versionInfo = wxWebSession::GetDefault().GetLibraryVersionInfo();
    if ( versionInfo.GetName() == "libcurl" && !versionInfo.AtLeast(7, 60) )
    {
        // This test fails under Ubuntu 18.04 which uses libcurl 7.58 with GnuTLS.
        // It's not clear whether it does it because libcurl is too old or
        // because of using GnuTLS instead of OpenSSL used elsewhere, but for
        // now just skip it.
        WARN("Skipping Digest auth test because it's known to fail with old libcurl");
        return;
    }

    SECTION("No password")
    {
        Create("digest-auth/auth/wxtest/wxwidgets");
        CHECK_FALSE( Execute() );
        CHECK( state == wxWebRequest::State_Unauthorized );
        CHECK( response.GetStatus() == 401 );
    }

    SECTION("Good password")
    {
        CreateWithAuth("digest-auth/auth/wxtest/wxwidgets", "wxtest", "wxwidgets");
        CHECK( Execute() );
        CHECK( response.GetStatus() == 200 );
        CHECK( state == wxWebRequest::State_Completed );

        CHECK_THAT( response.AsString().utf8_string(),
                    Catch::Contains(AUTHORIZED_SUBSTRING) );
    }

    SECTION("Bad password")
    {
        CreateWithAuth("digest-auth/auth/wxtest/wxwidgets", "foo", "bar");
        CHECK_FALSE( Execute() );
        CHECK( response.GetStatus() == 401 );
        CHECK( state == wxWebRequest::State_Unauthorized );
    }
}


static void DumpResponse(const wxWebResponse& response)
{
    REQUIRE( response.IsOk() );
    WARN("URL: " << response.GetURL() << "\n" <<
         "Status: " << response.GetStatus()
                    << " (" << response.GetStatusText() << ")\n" <<
         "Body length: " << response.GetContentLength() << "\n" <<
         "Body: " << response.AsString() << "\n");

    // Also show the value of the given header if requested.
    wxString header;
    if ( wxGetEnv("WX_TEST_WEBREQUEST_HEADER", &header) )
    {
        WARN("Header " << header << ": " << response.GetHeader(header));
    }
}

// This test is not run by default and has to be explicitly selected to run.
TEST_CASE_METHOD(RequestFixture,
                 "WebRequest::Manual", "[.]")
{
    InitManualRequest();

    request.Start();
    RunLoopWithTimeout();

    INFO("Error: \"" << errorDescription << "\"");
    CHECK( request.GetState() == wxWebRequest::State_Completed );

    DumpResponse(request.GetResponse());
}

// A sync version of the pseudo-test above.
TEST_CASE_METHOD(SyncRequestFixture,
                 "WebRequest::Sync::Manual", "[.]")
{
    InitManualRequest();

    if ( !Execute() )
    {
        FAIL_CHECK("Error: \"" << error << "\"");
    }

    DumpResponse(request.GetResponse());
}

using wxWebRequestHeaderMap = std::unordered_map<wxString, wxString>;

namespace wxPrivate
{
WXDLLIMPEXP_NET wxString
SplitParameters(const wxString& s, wxWebRequestHeaderMap& parameters);
}

TEST_CASE("WebRequestUtils", "[net][webrequest]")
{
    wxString value;
    wxWebRequestHeaderMap params;

    wxString header = "multipart/mixed; boundary=\"MIME_boundary_01234567\"";

    value = wxPrivate::SplitParameters(header, params);
    CHECK( value == "multipart/mixed" );
    CHECK( params.size() == 1 );
    CHECK( params["boundary"] == "MIME_boundary_01234567" );
}

// This is not a real test, run it to see the version of the library used.
TEST_CASE("WebRequest::Version", "[.]")
{
    const auto& info = wxWebSession::GetDefault().GetLibraryVersionInfo();
    WARN("Using " << info.GetName() << " backend (" << info.ToString() << ")");
}

#endif // wxUSE_WEBREQUEST
