/*
 *  Copyright (C) 2015-2018 Team Kodi
 *  This file is part of Kodi - https://kodi.tv
 *
 *  SPDX-License-Identifier: GPL-2.0-or-later
 *  See LICENSES/README.md for more information.
 */

#if defined(TARGET_WINDOWS)
#  include <windows.h>
#endif

#include "PasswordManager.h"
#include "ServiceBroker.h"
#include "URL.h"
#include "filesystem/CurlFile.h"
#include "filesystem/File.h"
#include "interfaces/json-rpc/JSONRPC.h"
#include "network/DNSNameCache.h"
#include "network/WebServer.h"
#include "network/httprequesthandler/HTTPJsonRpcHandler.h"
#include "network/httprequesthandler/HTTPVfsHandler.h"
#include "settings/MediaSourceSettings.h"
#include "test/TestUtils.h"
#include "utils/JSONVariantParser.h"
#include "utils/StringUtils.h"
#include "utils/URIUtils.h"
#include "utils/Variant.h"

#include <errno.h>
#include <stdlib.h>

#include <gtest/gtest.h>

using namespace XFILE;

#define WEBSERVER_HOST          "127.0.0.1"

#define TEST_URL_JSONRPC        "jsonrpc"

#define TEST_FILES_DATA         "test"
#define TEST_FILES_DATA_RANGES  "range1;range2;range3"
#define TEST_FILES_HTML         TEST_FILES_DATA ".html"
#define TEST_FILES_RANGES       TEST_FILES_DATA "-ranges.txt"

class TestWebServer : public testing::Test
{
protected:
  TestWebServer()
    : webserver(),
      sourcePath(XBMC_REF_FILE_PATH("xbmc/network/test/data/webserver/"))
  {
  }
  ~TestWebServer() override = default;

protected:
  void SetUp() override
  {
    CServiceBroker::RegisterDNSNameCache(std::make_shared<CDNSNameCache>());

    SetupMediaSources();

    ASSERT_TRUE(webserver.Start(0, GetServerUsername(), GetServerPassword()));
    baseUrl = StringUtils::Format("http://" WEBSERVER_HOST ":{}", webserver.GetPort());

    webserver.RegisterRequestHandler(&m_jsonRpcHandler);
    webserver.RegisterRequestHandler(&m_vfsHandler);
  }

  //! \brief Credentials the test web server requires. Empty means no authentication.
  virtual std::string GetServerUsername() const { return ""; }
  virtual std::string GetServerPassword() const { return ""; }

  void TearDown() override
  {
    if (webserver.IsStarted())
      webserver.Stop();

    webserver.UnregisterRequestHandler(&m_vfsHandler);
    webserver.UnregisterRequestHandler(&m_jsonRpcHandler);

    TearDownMediaSources();

    CServiceBroker::UnregisterDNSNameCache();
  }

  void SetupMediaSources()
  {
    CMediaSource source;
    source.strName = "WebServer Share";
    source.strPath = sourcePath;
    source.vecPaths.push_back(sourcePath);
    source.m_allowSharing = true;
    source.m_iDriveType = SourceType::LOCAL;
    source.GetLockInfo().SetMode(LockMode::EVERYONE);
    source.m_ignore = true;

    CMediaSourceSettings::GetInstance().AddShare("videos", source);
  }

  void TearDownMediaSources()
  {
    CMediaSourceSettings::GetInstance().Clear();
  }

  std::string GetUrl(const std::string& path)
  {
    if (path.empty())
      return baseUrl;

    return URIUtils::AddFileToFolder(baseUrl, path);
  }

  std::string GetUrlOfTestFile(const std::string& testFile)
  {
    if (testFile.empty())
      return "";

    std::string path = URIUtils::AddFileToFolder(sourcePath, testFile);
    path = CURL::Encode(path);
    path = URIUtils::AddFileToFolder("vfs", path);

    return GetUrl(path);
  }

  bool GetLastModifiedOfTestFile(const std::string& testFile, CDateTime& lastModified)
  {
    CFile file;
    if (!file.Open(URIUtils::AddFileToFolder(sourcePath, testFile), READ_NO_CACHE))
      return false;

    struct __stat64 statBuffer;
    if (file.Stat(&statBuffer) != 0)
      return false;

    struct tm *time;
#ifdef HAVE_LOCALTIME_R
    struct tm result = {};
    time = localtime_r((time_t*)&statBuffer.st_mtime, &result);
#else
    time = localtime((time_t *)&statBuffer.st_mtime);
#endif
    if (time == NULL)
      return false;

    lastModified = *time;
    return lastModified.IsValid();
  }

  void CheckHtmlTestFileResponse(const CCurlFile& curl)
  {
    // get the HTTP header details
    const CHttpHeader& httpHeader = curl.GetHttpHeader();

    // Content-Type must be "text/html"
    EXPECT_STREQ("text/html", httpHeader.GetMimeType().c_str());
    // Must be only one "Content-Length" header
    ASSERT_EQ(1U, httpHeader.GetValues(MHD_HTTP_HEADER_CONTENT_LENGTH).size());
    // Content-Length must be "4"
    EXPECT_STREQ("4", httpHeader.GetValue(MHD_HTTP_HEADER_CONTENT_LENGTH).c_str());
    // Accept-Ranges must be "bytes"
    EXPECT_STREQ("bytes", httpHeader.GetValue(MHD_HTTP_HEADER_ACCEPT_RANGES).c_str());

    // check Last-Modified
    CDateTime lastModified;
    ASSERT_TRUE(GetLastModifiedOfTestFile(TEST_FILES_HTML, lastModified));
    ASSERT_STREQ(lastModified.GetAsRFC1123DateTime().c_str(), httpHeader.GetValue(MHD_HTTP_HEADER_LAST_MODIFIED).c_str());

    // Cache-Control must contain "mag-age=0" and "no-cache"
    std::string cacheControl = httpHeader.GetValue(MHD_HTTP_HEADER_CACHE_CONTROL);
    EXPECT_TRUE(cacheControl.find("max-age=0") != std::string::npos);
    EXPECT_TRUE(cacheControl.find("no-cache") != std::string::npos);
  }

  void CheckRangesTestFileResponse(const CCurlFile& curl, int httpStatus = MHD_HTTP_OK, bool empty = false)
  {
    // get the HTTP header details
    const CHttpHeader& httpHeader = curl.GetHttpHeader();

    // Only zero or one "Content-Length" headers
    ASSERT_GE(1U, httpHeader.GetValues(MHD_HTTP_HEADER_CONTENT_LENGTH).size());

    // check the protocol line for the expected HTTP status
    std::string httpStatusString = StringUtils::Format(" {} ", httpStatus);
    const std::string& protocolLine = httpHeader.GetProtoLine();
    ASSERT_TRUE(protocolLine.find(httpStatusString) != std::string::npos);

    // Content-Type must be "text/html"
    EXPECT_STREQ("text/plain", httpHeader.GetMimeType().c_str());
    // check Content-Length
    if (!empty)
    {
      ASSERT_EQ(1U, httpHeader.GetValues(MHD_HTTP_HEADER_CONTENT_LENGTH).size());
      EXPECT_STREQ("20", httpHeader.GetValue(MHD_HTTP_HEADER_CONTENT_LENGTH).c_str());
    }
    // Accept-Ranges must be "bytes"
    EXPECT_STREQ("bytes", httpHeader.GetValue(MHD_HTTP_HEADER_ACCEPT_RANGES).c_str());

    // check Last-Modified
    CDateTime lastModified;
    ASSERT_TRUE(GetLastModifiedOfTestFile(TEST_FILES_RANGES, lastModified));
    ASSERT_STREQ(lastModified.GetAsRFC1123DateTime().c_str(), httpHeader.GetValue(MHD_HTTP_HEADER_LAST_MODIFIED).c_str());

    // Cache-Control must contain "mag-age=0" and "no-cache"
    std::string cacheControl = httpHeader.GetValue(MHD_HTTP_HEADER_CACHE_CONTROL);
    EXPECT_TRUE(cacheControl.find("max-age=31536000") != std::string::npos);
    EXPECT_TRUE(cacheControl.find("public") != std::string::npos);
  }

  void CheckRangesTestFileResponse(const CCurlFile& curl, const std::string& result, const CHttpRanges& ranges)
  {
    // get the HTTP header details
    const CHttpHeader& httpHeader = curl.GetHttpHeader();

    // Only zero or one "Content-Length" headers
    ASSERT_GE(1U, httpHeader.GetValues(MHD_HTTP_HEADER_CONTENT_LENGTH).size());

    // check the protocol line for the expected HTTP status
    std::string httpStatusString = StringUtils::Format(" {} ", MHD_HTTP_PARTIAL_CONTENT);
    const std::string& protocolLine = httpHeader.GetProtoLine();
    ASSERT_TRUE(protocolLine.find(httpStatusString) != std::string::npos);

    // Accept-Ranges must be "bytes"
    EXPECT_STREQ("bytes", httpHeader.GetValue(MHD_HTTP_HEADER_ACCEPT_RANGES).c_str());

    // check Last-Modified
    CDateTime lastModified;
    ASSERT_TRUE(GetLastModifiedOfTestFile(TEST_FILES_RANGES, lastModified));
    ASSERT_STREQ(lastModified.GetAsRFC1123DateTime().c_str(), httpHeader.GetValue(MHD_HTTP_HEADER_LAST_MODIFIED).c_str());

    // Cache-Control must contain "mag-age=0" and "no-cache"
    std::string cacheControl = httpHeader.GetValue(MHD_HTTP_HEADER_CACHE_CONTROL);
    EXPECT_TRUE(cacheControl.find("max-age=31536000") != std::string::npos);
    EXPECT_TRUE(cacheControl.find("public") != std::string::npos);

    // If there's no range Content-Length must be "20"
    if (ranges.IsEmpty())
    {
      ASSERT_EQ(1U, httpHeader.GetValues(MHD_HTTP_HEADER_CONTENT_LENGTH).size());
      EXPECT_STREQ("20", httpHeader.GetValue(MHD_HTTP_HEADER_CONTENT_LENGTH).c_str());
      EXPECT_STREQ(TEST_FILES_DATA_RANGES, result.c_str());
      return;
    }

    // check Content-Range
    uint64_t firstPosition, lastPosition;
    ASSERT_TRUE(ranges.GetFirstPosition(firstPosition));
    ASSERT_TRUE(ranges.GetLastPosition(lastPosition));
    EXPECT_STREQ(HttpRangeUtils::GenerateContentRangeHeaderValue(firstPosition, lastPosition, 20).c_str(), httpHeader.GetValue(MHD_HTTP_HEADER_CONTENT_RANGE).c_str());

    std::string expectedContent = TEST_FILES_DATA_RANGES;
    const std::string expectedContentType = "text/plain";
    if (ranges.Size() == 1)
    {
      // Content-Type must be "text/html"
      EXPECT_STREQ(expectedContentType.c_str(), httpHeader.GetMimeType().c_str());

      // check the content
      CHttpRange firstRange;
      ASSERT_TRUE(ranges.GetFirst(firstRange));
      expectedContent = expectedContent.substr(static_cast<size_t>(firstRange.GetFirstPosition()), static_cast<size_t>(firstRange.GetLength()));
      EXPECT_STREQ(expectedContent.c_str(), result.c_str());

      // and Content-Length
      ASSERT_EQ(1U, httpHeader.GetValues(MHD_HTTP_HEADER_CONTENT_LENGTH).size());
      EXPECT_STREQ(std::to_string(static_cast<unsigned int>(expectedContent.size())).c_str(),
                   httpHeader.GetValue(MHD_HTTP_HEADER_CONTENT_LENGTH).c_str());

      return;
    }

    // Content-Type contains the multipart boundary
    const std::string expectedMimeType = "multipart/byteranges";
    std::string mimeType = httpHeader.GetMimeType();
    ASSERT_STREQ(expectedMimeType.c_str(), mimeType.c_str());

    std::string contentType = httpHeader.GetValue(MHD_HTTP_HEADER_CONTENT_TYPE);
    std::string contentTypeStart = expectedMimeType + "; boundary=";
    // it must start with "multipart/byteranges; boundary=" followed by the boundary
    ASSERT_EQ(0U, contentType.find(contentTypeStart));
    ASSERT_GT(contentType.size(), contentTypeStart.size());
    // extract the boundary
    std::string multipartBoundary = contentType.substr(contentTypeStart.size());
    ASSERT_FALSE(multipartBoundary.empty());
    multipartBoundary = "--" + multipartBoundary;

    ASSERT_EQ(0U, result.find(multipartBoundary));
    std::vector<std::string> rangeParts = StringUtils::Split(result, multipartBoundary);
    // the first part is not really a part and is therefore empty (the place before the first boundary)
    ASSERT_TRUE(rangeParts.front().empty());
    rangeParts.erase(rangeParts.begin());
    // the last part is the end of the end multipart boundary
    ASSERT_STREQ("--", rangeParts.back().c_str());
    rangeParts.erase(rangeParts.begin() + rangeParts.size() - 1);
    ASSERT_EQ(ranges.Size(), rangeParts.size());

    for (size_t i = 0; i < rangeParts.size(); ++i)
    {
      std::string data = rangeParts.at(i);
      StringUtils::Trim(data, " \r\n");

      // find the separator between header and data
      size_t pos = data.find("\r\n\r\n");
      ASSERT_NE(std::string::npos, pos);

      std::string header = data.substr(0, pos + 4);
      data = data.substr(pos + 4);

      // get the expected range
      CHttpRange range;
      ASSERT_TRUE(ranges.Get(i, range));

      // parse the header of the range part
      CHttpHeader rangeHeader;
      rangeHeader.Parse(header);

      // check Content-Type
      EXPECT_STREQ(expectedContentType.c_str(), rangeHeader.GetMimeType().c_str());

      // parse and check Content-Range
      std::string contentRangeHeader = rangeHeader.GetValue(MHD_HTTP_HEADER_CONTENT_RANGE);
      std::vector<std::string> contentRangeHeaderParts = StringUtils::Split(contentRangeHeader, "/");
      ASSERT_EQ(2U, contentRangeHeaderParts.size());

      // check the length of the range
      EXPECT_TRUE(StringUtils::IsNaturalNumber(contentRangeHeaderParts.back()));
      uint64_t contentRangeLength = str2uint64(contentRangeHeaderParts.back());
      EXPECT_EQ(range.GetLength(), contentRangeLength);

      // remove the leading "bytes " string from the range definition
      std::string contentRangeDefinition = contentRangeHeaderParts.front();
      ASSERT_EQ(0U, contentRangeDefinition.find("bytes "));
      contentRangeDefinition = contentRangeDefinition.substr(6);

      // check the start and end positions of the range
      std::vector<std::string> contentRangeParts = StringUtils::Split(contentRangeDefinition, "-");
      ASSERT_EQ(2U, contentRangeParts.size());
      EXPECT_TRUE(StringUtils::IsNaturalNumber(contentRangeParts.front()));
      uint64_t contentRangeStart = str2uint64(contentRangeParts.front());
      EXPECT_EQ(range.GetFirstPosition(), contentRangeStart);
      EXPECT_TRUE(StringUtils::IsNaturalNumber(contentRangeParts.back()));
      uint64_t contentRangeEnd = str2uint64(contentRangeParts.back());
      EXPECT_EQ(range.GetLastPosition(), contentRangeEnd);

      // make sure the length of the content matches the one of the expected range
      EXPECT_EQ(range.GetLength(), data.size());
      EXPECT_STREQ(expectedContent.substr(static_cast<size_t>(range.GetFirstPosition()), static_cast<size_t>(range.GetLength())).c_str(), data.c_str());
    }
  }

  std::string GenerateRangeHeaderValue(unsigned int start, unsigned int end)
  {
    return StringUtils::Format("bytes={}-{}", start, end);
  }

  CWebServer webserver;
  CHTTPJsonRpcHandler m_jsonRpcHandler;
  CHTTPVfsHandler m_vfsHandler;
  std::string baseUrl;
  std::string sourcePath;
};

TEST_F(TestWebServer, IsStarted)
{
  ASSERT_TRUE(webserver.IsStarted());
}

TEST_F(TestWebServer, ReportsThePortAssignedByTheOperatingSystem)
{
  ASSERT_TRUE(webserver.IsStarted());
  EXPECT_NE(0, webserver.GetPort());
}

TEST_F(TestWebServer, TwoServersDoNotShareAPort)
{
  CWebServer other;
  ASSERT_TRUE(other.Start(0, "", ""));

  EXPECT_NE(0, other.GetPort());
  EXPECT_NE(webserver.GetPort(), other.GetPort());

  other.Stop();
  EXPECT_EQ(0, other.GetPort());
}

TEST_F(TestWebServer, CanGetJsonRpcApiDescriptionWithHttpGet)
{
  std::string result;
  CCurlFile curl;
  ASSERT_TRUE(curl.Get(GetUrl(TEST_URL_JSONRPC), result));
  ASSERT_FALSE(result.empty());

  // get the HTTP header details
  const CHttpHeader& httpHeader = curl.GetHttpHeader();

  // Content-Length header must be present
  ASSERT_EQ(1U, httpHeader.GetValues(MHD_HTTP_HEADER_CONTENT_LENGTH).size());
  // Content-Type must be "application/json"
  EXPECT_STREQ("application/json", httpHeader.GetMimeType().c_str());
  // Accept-Ranges must be "none"
  EXPECT_STREQ("none", httpHeader.GetValue(MHD_HTTP_HEADER_ACCEPT_RANGES).c_str());

  // Cache-Control must contain "mag-age=0" and "no-cache"
  std::string cacheControl = httpHeader.GetValue(MHD_HTTP_HEADER_CACHE_CONTROL);
  EXPECT_TRUE(cacheControl.find("max-age=0") != std::string::npos);
  EXPECT_TRUE(cacheControl.find("no-cache") != std::string::npos);
}

TEST_F(TestWebServer, CanReadDataOverJsonRpcWithHttpGet)
{
  // initialized JSON-RPC
  JSONRPC::CJSONRPC::Initialize();

  std::string result;
  CCurlFile curl;
  ASSERT_TRUE(curl.Get(GetUrl(TEST_URL_JSONRPC "?request=" + CURL::Encode("{ \"jsonrpc\": \"2.0\", \"method\": \"JSONRPC.Version\", \"id\": 1 }")), result));
  ASSERT_FALSE(result.empty());

  // parse the JSON-RPC response
  CVariant resultObj;
  ASSERT_TRUE(CJSONVariantParser::Parse(result, resultObj));
  // make sure it's an object
  ASSERT_TRUE(resultObj.isObject());

  // get the HTTP header details
  const CHttpHeader& httpHeader = curl.GetHttpHeader();

  // Content-Length header must be present
  ASSERT_EQ(1U, httpHeader.GetValues(MHD_HTTP_HEADER_CONTENT_LENGTH).size());
  // Content-Type must be "application/json"
  EXPECT_STREQ("application/json", httpHeader.GetMimeType().c_str());
  // Accept-Ranges must be "none"
  EXPECT_STREQ("none", httpHeader.GetValue(MHD_HTTP_HEADER_ACCEPT_RANGES).c_str());

  // Cache-Control must contain "mag-age=0" and "no-cache"
  std::string cacheControl = httpHeader.GetValue(MHD_HTTP_HEADER_CACHE_CONTROL);
  EXPECT_TRUE(cacheControl.find("max-age=0") != std::string::npos);
  EXPECT_TRUE(cacheControl.find("no-cache") != std::string::npos);

  // uninitialize JSON-RPC
  JSONRPC::CJSONRPC::Cleanup();
}

TEST_F(TestWebServer, CannotModifyOverJsonRpcWithHttpGet)
{
  // initialized JSON-RPC
  JSONRPC::CJSONRPC::Initialize();

  std::string result;
  CCurlFile curl;
  ASSERT_TRUE(curl.Get(GetUrl(TEST_URL_JSONRPC "?request=" + CURL::Encode("{ \"jsonrpc\": \"2.0\", \"method\": \"Input.Left\", \"id\": 1 }")), result));
  ASSERT_FALSE(result.empty());

  // parse the JSON-RPC response
  CVariant resultObj;
  ASSERT_TRUE(CJSONVariantParser::Parse(result, resultObj));
  // make sure it's an object
  ASSERT_TRUE(resultObj.isObject());
  // it must contain the "error" property with the "Bad client permission" error code
  ASSERT_TRUE(resultObj.isMember("error") && resultObj["error"].isObject());
  ASSERT_TRUE(resultObj["error"].isMember("code") && resultObj["error"]["code"].isInteger());
  ASSERT_EQ(JSONRPC::BadPermission, resultObj["error"]["code"].asInteger());

  // get the HTTP header details
  const CHttpHeader& httpHeader = curl.GetHttpHeader();

  // Content-Length header must be present
  ASSERT_EQ(1U, httpHeader.GetValues(MHD_HTTP_HEADER_CONTENT_LENGTH).size());
  // Content-Type must be "application/json"
  EXPECT_STREQ("application/json", httpHeader.GetMimeType().c_str());
  // Accept-Ranges must be "none"
  EXPECT_STREQ("none", httpHeader.GetValue(MHD_HTTP_HEADER_ACCEPT_RANGES).c_str());

  // Cache-Control must contain "mag-age=0" and "no-cache"
  std::string cacheControl = httpHeader.GetValue(MHD_HTTP_HEADER_CACHE_CONTROL);
  EXPECT_TRUE(cacheControl.find("max-age=0") != std::string::npos);
  EXPECT_TRUE(cacheControl.find("no-cache") != std::string::npos);

  // uninitialize JSON-RPC
  JSONRPC::CJSONRPC::Cleanup();
}

TEST_F(TestWebServer, CanReadDataOverJsonRpcWithHttpPost)
{
  // initialized JSON-RPC
  JSONRPC::CJSONRPC::Initialize();

  std::string result;
  CCurlFile curl;
  curl.SetMimeType("application/json");
  ASSERT_TRUE(curl.Post(GetUrl(TEST_URL_JSONRPC), "{ \"jsonrpc\": \"2.0\", \"method\": \"JSONRPC.Version\", \"id\": 1 }", result));
  ASSERT_FALSE(result.empty());

  // parse the JSON-RPC response
  CVariant resultObj;
  ASSERT_TRUE(CJSONVariantParser::Parse(result, resultObj));
  // make sure it's an object
  ASSERT_TRUE(resultObj.isObject());

  // get the HTTP header details
  const CHttpHeader& httpHeader = curl.GetHttpHeader();

  // Content-Length header must be present
  ASSERT_EQ(1U, httpHeader.GetValues(MHD_HTTP_HEADER_CONTENT_LENGTH).size());
  // Content-Type must be "application/json"
  EXPECT_STREQ("application/json", httpHeader.GetMimeType().c_str());
  // Accept-Ranges must be "none"
  EXPECT_STREQ("none", httpHeader.GetValue(MHD_HTTP_HEADER_ACCEPT_RANGES).c_str());

  // Cache-Control must contain "mag-age=0" and "no-cache"
  std::string cacheControl = httpHeader.GetValue(MHD_HTTP_HEADER_CACHE_CONTROL);
  EXPECT_TRUE(cacheControl.find("max-age=0") != std::string::npos);
  EXPECT_TRUE(cacheControl.find("no-cache") != std::string::npos);

  // uninitialize JSON-RPC
  JSONRPC::CJSONRPC::Cleanup();
}

TEST_F(TestWebServer, CanModifyOverJsonRpcWithHttpPost)
{
  // initialized JSON-RPC
  JSONRPC::CJSONRPC::Initialize();

  std::string result;
  CCurlFile curl;
  curl.SetMimeType("application/json");
  ASSERT_TRUE(curl.Post(GetUrl(TEST_URL_JSONRPC), "{ \"jsonrpc\": \"2.0\", \"method\": \"Input.Left\", \"id\": 1 }", result));
  ASSERT_FALSE(result.empty());

  // parse the JSON-RPC response
  CVariant resultObj;
  ASSERT_TRUE(CJSONVariantParser::Parse(result, resultObj));
  // make sure it's an object
  ASSERT_TRUE(resultObj.isObject());
  // it must contain the "result" property with the "OK" value
  ASSERT_TRUE(resultObj.isMember("result") && resultObj["result"].isString());
  EXPECT_STREQ("OK", resultObj["result"].asString().c_str());

  // get the HTTP header details
  const CHttpHeader& httpHeader = curl.GetHttpHeader();

  // Content-Length header must be present
  ASSERT_EQ(1U, httpHeader.GetValues(MHD_HTTP_HEADER_CONTENT_LENGTH).size());
  // Content-Type must be "application/json"
  EXPECT_STREQ("application/json", httpHeader.GetMimeType().c_str());
  // Accept-Ranges must be "none"
  EXPECT_STREQ("none", httpHeader.GetValue(MHD_HTTP_HEADER_ACCEPT_RANGES).c_str());

  // Cache-Control must contain "mag-age=0" and "no-cache"
  std::string cacheControl = httpHeader.GetValue(MHD_HTTP_HEADER_CACHE_CONTROL);
  EXPECT_TRUE(cacheControl.find("max-age=0") != std::string::npos);
  EXPECT_TRUE(cacheControl.find("no-cache") != std::string::npos);

  // uninitialize JSON-RPC
  JSONRPC::CJSONRPC::Cleanup();
}

TEST_F(TestWebServer, CanNotHeadNonExistingFile)
{
  CCurlFile curl;
  ASSERT_FALSE(curl.Exists(CURL(GetUrlOfTestFile("file_does_not_exist"))));
}

TEST_F(TestWebServer, CanHeadFile)
{
  CCurlFile curl;
  ASSERT_TRUE(curl.Exists(CURL(GetUrlOfTestFile(TEST_FILES_HTML))));

  CheckHtmlTestFileResponse(curl);
}

TEST_F(TestWebServer, CanNotGetNonExistingFile)
{
  CCurlFile curl;
  ASSERT_FALSE(curl.Exists(CURL(GetUrlOfTestFile(("file_does_not_exist")))));
}

TEST_F(TestWebServer, CanGetFile)
{
  std::string result;
  CCurlFile curl;
  curl.SetRequestHeader(MHD_HTTP_HEADER_RANGE, "");
  ASSERT_TRUE(curl.Get(GetUrlOfTestFile(TEST_FILES_HTML), result));
  ASSERT_STREQ(TEST_FILES_DATA, result.c_str());

  CheckHtmlTestFileResponse(curl);
}

TEST_F(TestWebServer, CanGetFileForcingNoCache)
{
  // check non-cacheable HTML with Control-Cache: no-cache
  std::string result;
  CCurlFile curl_html;
  curl_html.SetRequestHeader(MHD_HTTP_HEADER_RANGE, "");
  curl_html.SetRequestHeader(MHD_HTTP_HEADER_CACHE_CONTROL, "no-cache");
  ASSERT_TRUE(curl_html.Get(GetUrlOfTestFile(TEST_FILES_HTML), result));
  EXPECT_STREQ(TEST_FILES_DATA, result.c_str());
  CheckHtmlTestFileResponse(curl_html);

  // check cacheable text file with Control-Cache: no-cache
  result.clear();
  CCurlFile curl_txt;
  curl_txt.SetRequestHeader(MHD_HTTP_HEADER_RANGE, "");
  curl_txt.SetRequestHeader(MHD_HTTP_HEADER_CACHE_CONTROL, "no-cache");
  ASSERT_TRUE(curl_txt.Get(GetUrlOfTestFile(TEST_FILES_RANGES), result));
  EXPECT_STREQ(TEST_FILES_DATA_RANGES, result.c_str());
  CheckRangesTestFileResponse(curl_txt);

  // check cacheable text file with deprecated Pragma: no-cache
  result.clear();
  CCurlFile curl_txt_pragma;
  curl_txt_pragma.SetRequestHeader(MHD_HTTP_HEADER_RANGE, "");
  curl_txt_pragma.SetRequestHeader(MHD_HTTP_HEADER_PRAGMA, "no-cache");
  ASSERT_TRUE(curl_txt_pragma.Get(GetUrlOfTestFile(TEST_FILES_RANGES), result));
  EXPECT_STREQ(TEST_FILES_DATA_RANGES, result.c_str());
  CheckRangesTestFileResponse(curl_txt_pragma);
}

TEST_F(TestWebServer, CanGetCachedFileWithOlderIfModifiedSince)
{
  // get the last modified date of the file
  CDateTime lastModified;
  ASSERT_TRUE(GetLastModifiedOfTestFile(TEST_FILES_RANGES, lastModified));
  CDateTime lastModifiedOlder = lastModified - CDateTimeSpan(1, 0, 0, 0);

  // get the file with an older If-Modified-Since value
  std::string result;
  CCurlFile curl;
  curl.SetRequestHeader(MHD_HTTP_HEADER_RANGE, "");
  curl.SetRequestHeader(MHD_HTTP_HEADER_IF_MODIFIED_SINCE, lastModifiedOlder.GetAsRFC1123DateTime());
  ASSERT_TRUE(curl.Get(GetUrlOfTestFile(TEST_FILES_RANGES), result));
  EXPECT_STREQ(TEST_FILES_DATA_RANGES, result.c_str());
  CheckRangesTestFileResponse(curl);
}

TEST_F(TestWebServer, CanGetCachedFileWithExactIfModifiedSince)
{
  // get the last modified date of the file
  CDateTime lastModified;
  ASSERT_TRUE(GetLastModifiedOfTestFile(TEST_FILES_RANGES, lastModified));

  // get the file with the exact If-Modified-Since value
  std::string result;
  CCurlFile curl;
  curl.SetRequestHeader(MHD_HTTP_HEADER_RANGE, "");
  curl.SetRequestHeader(MHD_HTTP_HEADER_IF_MODIFIED_SINCE, lastModified.GetAsRFC1123DateTime());
  ASSERT_TRUE(curl.Get(GetUrlOfTestFile(TEST_FILES_RANGES), result));
  ASSERT_TRUE(result.empty());
  CheckRangesTestFileResponse(curl, MHD_HTTP_NOT_MODIFIED, true);
}

TEST_F(TestWebServer, CanGetCachedFileWithNewerIfModifiedSince)
{
  // get the last modified date of the file
  CDateTime lastModified;
  ASSERT_TRUE(GetLastModifiedOfTestFile(TEST_FILES_RANGES, lastModified));
  CDateTime lastModifiedNewer = lastModified + CDateTimeSpan(1, 0, 0, 0);

  // get the file with a newer If-Modified-Since value
  std::string result;
  CCurlFile curl;
  curl.SetRequestHeader(MHD_HTTP_HEADER_RANGE, "");
  curl.SetRequestHeader(MHD_HTTP_HEADER_IF_MODIFIED_SINCE,
                        lastModifiedNewer.GetAsRFC1123DateTime());
  ASSERT_TRUE(curl.Get(GetUrlOfTestFile(TEST_FILES_RANGES), result));
  ASSERT_TRUE(result.empty());
  CheckRangesTestFileResponse(curl, MHD_HTTP_NOT_MODIFIED, true);
}

TEST_F(TestWebServer, CanGetCachedFileWithNewerIfModifiedSinceForcingNoCache)
{
  // get the last modified date of the file
  CDateTime lastModified;
  ASSERT_TRUE(GetLastModifiedOfTestFile(TEST_FILES_RANGES, lastModified));
  CDateTime lastModifiedNewer = lastModified + CDateTimeSpan(1, 0, 0, 0);

  // get the file with a newer If-Modified-Since value but forcing no caching
  std::string result;
  CCurlFile curl;
  curl.SetRequestHeader(MHD_HTTP_HEADER_RANGE, "");
  curl.SetRequestHeader(MHD_HTTP_HEADER_IF_MODIFIED_SINCE, lastModifiedNewer.GetAsRFC1123DateTime());
  curl.SetRequestHeader(MHD_HTTP_HEADER_CACHE_CONTROL, "no-cache");
  ASSERT_TRUE(curl.Get(GetUrlOfTestFile(TEST_FILES_RANGES), result));
  EXPECT_STREQ(TEST_FILES_DATA_RANGES, result.c_str());
  CheckRangesTestFileResponse(curl);
}

TEST_F(TestWebServer, CanGetCachedFileWithOlderIfUnmodifiedSince)
{
  // get the last modified date of the file
  CDateTime lastModified;
  ASSERT_TRUE(GetLastModifiedOfTestFile(TEST_FILES_RANGES, lastModified));
  CDateTime lastModifiedOlder = lastModified - CDateTimeSpan(1, 0, 0, 0);

  // get the file with an older If-Unmodified-Since value
  std::string result;
  CCurlFile curl;
  curl.SetRequestHeader(MHD_HTTP_HEADER_RANGE, "");
  curl.SetRequestHeader(MHD_HTTP_HEADER_IF_UNMODIFIED_SINCE, lastModifiedOlder.GetAsRFC1123DateTime());
  ASSERT_FALSE(curl.Get(GetUrlOfTestFile(TEST_FILES_RANGES), result));
}

TEST_F(TestWebServer, CanGetCachedFileWithExactIfUnmodifiedSince)
{
  // get the last modified date of the file
  CDateTime lastModified;
  ASSERT_TRUE(GetLastModifiedOfTestFile(TEST_FILES_RANGES, lastModified));

  // get the file with an older If-Unmodified-Since value
  std::string result;
  CCurlFile curl;
  curl.SetRequestHeader(MHD_HTTP_HEADER_RANGE, "");
  curl.SetRequestHeader(MHD_HTTP_HEADER_IF_UNMODIFIED_SINCE, lastModified.GetAsRFC1123DateTime());
  ASSERT_TRUE(curl.Get(GetUrlOfTestFile(TEST_FILES_RANGES), result));
  EXPECT_STREQ(TEST_FILES_DATA_RANGES, result.c_str());
  CheckRangesTestFileResponse(curl);
}

TEST_F(TestWebServer, CanGetCachedFileWithNewerIfUnmodifiedSince)
{
  // get the last modified date of the file
  CDateTime lastModified;
  ASSERT_TRUE(GetLastModifiedOfTestFile(TEST_FILES_RANGES, lastModified));
  CDateTime lastModifiedNewer = lastModified + CDateTimeSpan(1, 0, 0, 0);

  // get the file with a newer If-Unmodified-Since value
  std::string result;
  CCurlFile curl;
  curl.SetRequestHeader(MHD_HTTP_HEADER_RANGE, "");
  curl.SetRequestHeader(MHD_HTTP_HEADER_IF_UNMODIFIED_SINCE, lastModifiedNewer.GetAsRFC1123DateTime());
  ASSERT_TRUE(curl.Get(GetUrlOfTestFile(TEST_FILES_RANGES), result));
  EXPECT_STREQ(TEST_FILES_DATA_RANGES, result.c_str());
  CheckRangesTestFileResponse(curl);
}

TEST_F(TestWebServer, CanGetRangedFileRange0_)
{
  const std::string rangedFileContent = TEST_FILES_DATA_RANGES;
  const std::string range = "bytes=0-";

  CHttpRanges ranges;
  ASSERT_TRUE(ranges.Parse(range, rangedFileContent.size()));

  // get the whole file but specify the beginning of the range
  std::string result;
  CCurlFile curl;
  curl.SetRequestHeader(MHD_HTTP_HEADER_RANGE, range);
  ASSERT_TRUE(curl.Get(GetUrlOfTestFile(TEST_FILES_RANGES), result));
  CheckRangesTestFileResponse(curl, result, ranges);
}

TEST_F(TestWebServer, CanGetRangedFileRange0_End)
{
  const std::string rangedFileContent = TEST_FILES_DATA_RANGES;
  const std::string range = GenerateRangeHeaderValue(0, rangedFileContent.size());

  CHttpRanges ranges;
  ASSERT_TRUE(ranges.Parse(range, rangedFileContent.size()));

  // get the whole file but specify the whole range
  std::string result;
  CCurlFile curl;
  curl.SetRequestHeader(MHD_HTTP_HEADER_RANGE, range);
  ASSERT_TRUE(curl.Get(GetUrlOfTestFile(TEST_FILES_RANGES), result));
  CheckRangesTestFileResponse(curl, result, ranges);
}

TEST_F(TestWebServer, CanGetRangedFileRange0_2xEnd)
{
  const std::string rangedFileContent = TEST_FILES_DATA_RANGES;
  const std::string range = GenerateRangeHeaderValue(0, rangedFileContent.size() * 2);

  CHttpRanges ranges;
  ASSERT_TRUE(ranges.Parse(range, rangedFileContent.size()));

  // get the whole file but specify a larger range
  std::string result;
  CCurlFile curl;
  curl.SetRequestHeader(MHD_HTTP_HEADER_RANGE, range);
  ASSERT_TRUE(curl.Get(GetUrlOfTestFile(TEST_FILES_RANGES), result));
  CheckRangesTestFileResponse(curl, result, ranges);
}

TEST_F(TestWebServer, CanGetRangedFileRange0_First)
{
  const std::string rangedFileContent = TEST_FILES_DATA_RANGES;
  std::vector<std::string> rangedContent = StringUtils::Split(TEST_FILES_DATA_RANGES, ";");
  const std::string range = GenerateRangeHeaderValue(0, rangedContent.front().size() - 1);

  CHttpRanges ranges;
  ASSERT_TRUE(ranges.Parse(range, rangedFileContent.size()));

  // get the whole file but specify a larger range
  std::string result;
  CCurlFile curl;
  curl.SetRequestHeader(MHD_HTTP_HEADER_RANGE, range);
  ASSERT_TRUE(curl.Get(GetUrlOfTestFile(TEST_FILES_RANGES), result));
  CheckRangesTestFileResponse(curl, result, ranges);
}

TEST_F(TestWebServer, CanGetRangedFileRangeFirst_Second)
{
  const std::string rangedFileContent = TEST_FILES_DATA_RANGES;
  std::vector<std::string> rangedContent = StringUtils::Split(TEST_FILES_DATA_RANGES, ";");
  const std::string range = GenerateRangeHeaderValue(rangedContent.front().size() + 1, rangedContent.front().size() + 1 + rangedContent.at(2).size() - 1);

  CHttpRanges ranges;
  ASSERT_TRUE(ranges.Parse(range, rangedFileContent.size()));

  // get the whole file but specify a larger range
  std::string result;
  CCurlFile curl;
  curl.SetRequestHeader(MHD_HTTP_HEADER_RANGE, range);
  ASSERT_TRUE(curl.Get(GetUrlOfTestFile(TEST_FILES_RANGES), result));
  CheckRangesTestFileResponse(curl, result, ranges);
}

TEST_F(TestWebServer, CanGetRangedFileRange_Last)
{
  const std::string rangedFileContent = TEST_FILES_DATA_RANGES;
  std::vector<std::string> rangedContent = StringUtils::Split(TEST_FILES_DATA_RANGES, ";");
  const std::string range =
      StringUtils::Format("bytes=-{}", static_cast<unsigned int>(rangedContent.back().size()));

  CHttpRanges ranges;
  ASSERT_TRUE(ranges.Parse(range, rangedFileContent.size()));

  // get the whole file but specify a larger range
  std::string result;
  CCurlFile curl;
  curl.SetRequestHeader(MHD_HTTP_HEADER_RANGE, range);
  ASSERT_TRUE(curl.Get(GetUrlOfTestFile(TEST_FILES_RANGES), result));
  CheckRangesTestFileResponse(curl, result, ranges);
}

TEST_F(TestWebServer, CanGetRangedFileRangeFirstSecond)
{
  const std::string rangedFileContent = TEST_FILES_DATA_RANGES;
  std::vector<std::string> rangedContent = StringUtils::Split(TEST_FILES_DATA_RANGES, ";");
  const std::string range = StringUtils::Format(
      "bytes=0-{},{}-{}", static_cast<unsigned int>(rangedContent.front().size() - 1),
      static_cast<unsigned int>(rangedContent.front().size() + 1),
      static_cast<unsigned int>(rangedContent.front().size() + 1) +
          static_cast<unsigned int>(rangedContent.at(1).size() - 1));

  CHttpRanges ranges;
  ASSERT_TRUE(ranges.Parse(range, rangedFileContent.size()));

  // get the whole file but specify a larger range
  std::string result;
  CCurlFile curl;
  curl.SetRequestHeader(MHD_HTTP_HEADER_RANGE, range);
  ASSERT_TRUE(curl.Get(GetUrlOfTestFile(TEST_FILES_RANGES), result));
  CheckRangesTestFileResponse(curl, result, ranges);
}

TEST_F(TestWebServer, CanGetRangedFileRangeFirstSecondLast)
{
  const std::string rangedFileContent = TEST_FILES_DATA_RANGES;
  std::vector<std::string> rangedContent = StringUtils::Split(TEST_FILES_DATA_RANGES, ";");
  const std::string range = StringUtils::Format(
      "bytes=0-{},{}-{},-{}", static_cast<unsigned int>(rangedContent.front().size() - 1),
      static_cast<unsigned int>(rangedContent.front().size() + 1),
      static_cast<unsigned int>(rangedContent.front().size() + 1) +
          static_cast<unsigned int>(rangedContent.at(1).size() - 1),
      static_cast<unsigned int>(rangedContent.back().size()));

  CHttpRanges ranges;
  ASSERT_TRUE(ranges.Parse(range, rangedFileContent.size()));

  // get the whole file but specify a larger range
  std::string result;
  CCurlFile curl;
  curl.SetRequestHeader(MHD_HTTP_HEADER_RANGE, range);
  ASSERT_TRUE(curl.Get(GetUrlOfTestFile(TEST_FILES_RANGES), result));
  CheckRangesTestFileResponse(curl, result, ranges);
}

TEST_F(TestWebServer, CanGetCachedRangedFileWithOlderIfRange)
{
  const std::string rangedFileContent = TEST_FILES_DATA_RANGES;
  const std::string range = "bytes=0-";

  CHttpRanges ranges;
  ASSERT_TRUE(ranges.Parse(range, rangedFileContent.size()));

  // get the last modified date of the file
  CDateTime lastModified;
  ASSERT_TRUE(GetLastModifiedOfTestFile(TEST_FILES_RANGES, lastModified));
  CDateTime lastModifiedOlder = lastModified - CDateTimeSpan(1, 0, 0, 0);

  // get the whole file (but ranged) with an older If-Range value
  std::string result;
  CCurlFile curl;
  curl.SetRequestHeader(MHD_HTTP_HEADER_RANGE, range);
  curl.SetRequestHeader(MHD_HTTP_HEADER_IF_RANGE, lastModifiedOlder.GetAsRFC1123DateTime());
  ASSERT_TRUE(curl.Get(GetUrlOfTestFile(TEST_FILES_RANGES), result));
  EXPECT_STREQ(TEST_FILES_DATA_RANGES, result.c_str());
  CheckRangesTestFileResponse(curl);
}

TEST_F(TestWebServer, CanGetCachedRangedFileWithExactIfRange)
{
  const std::string rangedFileContent = TEST_FILES_DATA_RANGES;
  const std::string range = "bytes=0-";

  CHttpRanges ranges;
  ASSERT_TRUE(ranges.Parse(range, rangedFileContent.size()));

  // get the last modified date of the file
  CDateTime lastModified;
  ASSERT_TRUE(GetLastModifiedOfTestFile(TEST_FILES_RANGES, lastModified));

  // get the whole file (but ranged) with an older If-Range value
  std::string result;
  CCurlFile curl;
  curl.SetRequestHeader(MHD_HTTP_HEADER_RANGE, range);
  curl.SetRequestHeader(MHD_HTTP_HEADER_IF_RANGE, lastModified.GetAsRFC1123DateTime());
  ASSERT_TRUE(curl.Get(GetUrlOfTestFile(TEST_FILES_RANGES), result));
  CheckRangesTestFileResponse(curl, result, ranges);
}

TEST_F(TestWebServer, CanGetCachedRangedFileWithNewerIfRange)
{
  const std::string rangedFileContent = TEST_FILES_DATA_RANGES;
  const std::string range = "bytes=0-";

  CHttpRanges ranges;
  ASSERT_TRUE(ranges.Parse(range, rangedFileContent.size()));

  // get the last modified date of the file
  CDateTime lastModified;
  ASSERT_TRUE(GetLastModifiedOfTestFile(TEST_FILES_RANGES, lastModified));
  CDateTime lastModifiedNewer = lastModified + CDateTimeSpan(1, 0, 0, 0);

  // get the whole file (but ranged) with an older If-Range value
  std::string result;
  CCurlFile curl;
  curl.SetRequestHeader(MHD_HTTP_HEADER_RANGE, range);
  curl.SetRequestHeader(MHD_HTTP_HEADER_IF_RANGE, lastModifiedNewer.GetAsRFC1123DateTime());
  ASSERT_TRUE(curl.Get(GetUrlOfTestFile(TEST_FILES_RANGES), result));
  CheckRangesTestFileResponse(curl, result, ranges);
}

/*!
 \brief Web server requiring authentication, with the credentials held by the password manager
 rather than being part of the requested url.

 This is what adding a source with a username/password gives us: sources.xml holds the plain url
 and passwords.xml holds the credentials, so every request has to pick them up from
 CPasswordManager. Anything talking to the server directly instead of going through CFile has to
 apply them itself, which is what these tests cover.
 */
class TestWebServerAuth : public TestWebServer
{
protected:
  std::string GetServerUsername() const override { return "kodi"; }
  std::string GetServerPassword() const override { return "secret"; }

  void SetUp() override
  {
    CPasswordManager::GetInstance().Clear();
    TestWebServer::SetUp();

    // remember the credentials for the server, as saving a source with a username/password does
    CURL authenticatedUrl{baseUrl};
    authenticatedUrl.SetUserName(GetServerUsername());
    authenticatedUrl.SetPassword(GetServerPassword());
    CPasswordManager::GetInstance().SaveAuthenticatedURL(authenticatedUrl, false);
  }

  void TearDown() override
  {
    CPasswordManager::GetInstance().Clear();

    TestWebServer::TearDown();
  }
};

// The static CCurlFile helpers that talk to the server directly and so have to apply the stored
// credentials themselves. Each is invoked through a uniform bool(const CURL&) wrapper (discarding
// the header/type output and only reporting success) so the same test can cover all of them.
bool InvokeGetMimeType(const CURL& url)
{
  std::string content;
  return CCurlFile::GetMimeType(url, content);
}

bool InvokeGetContentType(const CURL& url)
{
  std::string content;
  return CCurlFile::GetContentType(url, content);
}

bool InvokeGetHttpHeader(const CURL& url)
{
  CHttpHeader headers;
  return CCurlFile::GetHttpHeader(url, headers);
}

struct AuthHelperCase
{
  const char* name;
  bool (*invoke)(const CURL& url);
  bool withCredentials;
};

class TestWebServerAuthHelper : public TestWebServerAuth,
                                public testing::WithParamInterface<AuthHelperCase>
{
};

// Every helper must succeed when the credentials are known to the password manager and fail when
// they are not (proving authentication is really being applied, not that the server lets us in).
TEST_P(TestWebServerAuthHelper, AppliesStoredCredentials)
{
  const AuthHelperCase& helper = GetParam();

  // TestWebServerAuth::SetUp() has seeded the credentials; drop them again for the negative cases
  if (!helper.withCredentials)
    CPasswordManager::GetInstance().Clear();

  // the url deliberately carries no credentials, they have to come from the password manager
  const CURL url{GetUrlOfTestFile(TEST_FILES_RANGES)};
  ASSERT_TRUE(url.GetUserName().empty());

  EXPECT_EQ(helper.withCredentials, helper.invoke(url));
}

INSTANTIATE_TEST_SUITE_P(
    Helpers,
    TestWebServerAuthHelper,
    testing::Values(AuthHelperCase{"GetMimeType_WithCredentials", InvokeGetMimeType, true},
                    AuthHelperCase{"GetMimeType_NoCredentials", InvokeGetMimeType, false},
                    AuthHelperCase{"GetContentType_WithCredentials", InvokeGetContentType, true},
                    AuthHelperCase{"GetContentType_NoCredentials", InvokeGetContentType, false},
                    AuthHelperCase{"GetHttpHeader_WithCredentials", InvokeGetHttpHeader, true},
                    AuthHelperCase{"GetHttpHeader_NoCredentials", InvokeGetHttpHeader, false}),
    [](const testing::TestParamInfo<AuthHelperCase>& info) { return info.param.name; });

TEST_F(TestWebServerAuth, GetMimeTypeReturnsTheCorrectType)
{
  // the url deliberately carries no credentials, they have to come from the password manager
  const CURL url{GetUrlOfTestFile(TEST_FILES_RANGES)};
  ASSERT_TRUE(url.GetUserName().empty());

  std::string mimeType;
  ASSERT_TRUE(CCurlFile::GetMimeType(url, mimeType));
  EXPECT_STREQ("text/plain", mimeType.c_str());
}

TEST_F(TestWebServerAuth, GetContentTypeReturnsTheCorrectType)
{
  const CURL url{GetUrlOfTestFile(TEST_FILES_RANGES)};
  ASSERT_TRUE(url.GetUserName().empty());

  std::string contentType;
  ASSERT_TRUE(CCurlFile::GetContentType(url, contentType));
  EXPECT_TRUE(contentType.find("text/plain") != std::string::npos);
}

TEST_F(TestWebServerAuth, CanStatWithStoredCredentials)
{
  // CFile::Stat() has always applied the stored credentials, unlike the CCurlFile helpers above
  struct __stat64 buffer;
  ASSERT_EQ(0, CFile::Stat(CURL{GetUrlOfTestFile(TEST_FILES_RANGES)}, &buffer));
}
