#include "gtest/gtest.h"
#include "gtest/MozGTestBench.h"  // For MOZ_GTEST_BENCH

#include <regex>
#include "json/json.h"
#include "json/reader.h"
#include "nsString.h"
#include "mozilla/net/MozURL.h"
#include "nsCOMPtr.h"
#include "nsDirectoryServiceDefs.h"
#include "nsNetUtil.h"
#include "nsIFile.h"
#include "nsIURI.h"
#include "nsStreamUtils.h"
#include "mozilla/BasePrincipal.h"

using namespace mozilla;
using namespace mozilla::net;

TEST(TestMozURL, Getters)
{
  nsAutoCString href("http://user:pass@example.com/path?query#ref");
  RefPtr<MozURL> url;
  ASSERT_EQ(MozURL::Init(getter_AddRefs(url), href), NS_OK);

  ASSERT_TRUE(url->Scheme().EqualsLiteral("http"));

  ASSERT_TRUE(url->Spec() == href);

  ASSERT_TRUE(url->Username().EqualsLiteral("user"));

  ASSERT_TRUE(url->Password().EqualsLiteral("pass"));

  ASSERT_TRUE(url->Host().EqualsLiteral("example.com"));

  ASSERT_TRUE(url->FilePath().EqualsLiteral("/path"));

  ASSERT_TRUE(url->Query().EqualsLiteral("query"));

  ASSERT_TRUE(url->Ref().EqualsLiteral("ref"));

  url = nullptr;
  ASSERT_EQ(MozURL::Init(getter_AddRefs(url), ""_ns), NS_ERROR_MALFORMED_URI);
  ASSERT_EQ(url, nullptr);
}

TEST(TestMozURL, MutatorChain)
{
  nsAutoCString href("http://user:pass@example.com/path?query#ref");
  RefPtr<MozURL> url;
  ASSERT_EQ(MozURL::Init(getter_AddRefs(url), href), NS_OK);
  nsAutoCString out;

  RefPtr<MozURL> url2;
  ASSERT_EQ(url->Mutate()
                .SetScheme("https"_ns)
                .SetUsername("newuser"_ns)
                .SetPassword("newpass"_ns)
                .SetHostname("test"_ns)
                .SetFilePath("new/file/path"_ns)
                .SetQuery("bla"_ns)
                .SetRef("huh"_ns)
                .Finalize(getter_AddRefs(url2)),
            NS_OK);

  ASSERT_TRUE(url2->Spec().EqualsLiteral(
      "https://newuser:newpass@test/new/file/path?bla#huh"));
}

TEST(TestMozURL, MutatorFinalizeTwice)
{
  nsAutoCString href("http://user:pass@example.com/path?query#ref");
  RefPtr<MozURL> url;
  ASSERT_EQ(MozURL::Init(getter_AddRefs(url), href), NS_OK);
  nsAutoCString out;

  RefPtr<MozURL> url2;
  MozURL::Mutator mut = url->Mutate();
  mut.SetScheme("https"_ns);  // Change the scheme to https
  ASSERT_EQ(mut.Finalize(getter_AddRefs(url2)), NS_OK);
  ASSERT_TRUE(url2->Spec().EqualsLiteral(
      "https://user:pass@example.com/path?query#ref"));

  // Test that a second call to Finalize will result in an error code
  url2 = nullptr;
  ASSERT_EQ(mut.Finalize(getter_AddRefs(url2)), NS_ERROR_NOT_AVAILABLE);
  ASSERT_EQ(url2, nullptr);
}

TEST(TestMozURL, MutatorErrorStatus)
{
  nsAutoCString href("http://user:pass@example.com/path?query#ref");
  RefPtr<MozURL> url;
  ASSERT_EQ(MozURL::Init(getter_AddRefs(url), href), NS_OK);
  nsAutoCString out;

  // Test that trying to set the scheme to a bad value will get you an error
  MozURL::Mutator mut = url->Mutate();
  mut.SetScheme("!@#$%^&*("_ns);
  ASSERT_EQ(mut.GetStatus(), NS_ERROR_MALFORMED_URI);

  // Test that the mutator will not work after one faulty operation
  mut.SetScheme("test"_ns);
  ASSERT_EQ(mut.GetStatus(), NS_ERROR_MALFORMED_URI);
}

TEST(TestMozURL, InitWithBase)
{
  nsAutoCString href("https://example.net/a/b.html");
  RefPtr<MozURL> url;
  ASSERT_EQ(MozURL::Init(getter_AddRefs(url), href), NS_OK);

  ASSERT_TRUE(url->Spec().EqualsLiteral("https://example.net/a/b.html"));

  RefPtr<MozURL> url2;
  ASSERT_EQ(MozURL::Init(getter_AddRefs(url2), "c.png"_ns, url), NS_OK);

  ASSERT_TRUE(url2->Spec().EqualsLiteral("https://example.net/a/c.png"));
}

TEST(TestMozURL, Path)
{
  nsAutoCString href("about:blank");
  RefPtr<MozURL> url;
  ASSERT_EQ(MozURL::Init(getter_AddRefs(url), href), NS_OK);

  ASSERT_TRUE(url->Spec().EqualsLiteral("about:blank"));

  ASSERT_TRUE(url->Scheme().EqualsLiteral("about"));

  ASSERT_TRUE(url->FilePath().EqualsLiteral("blank"));
}

TEST(TestMozURL, HostPort)
{
  nsAutoCString href("https://user:pass@example.net:1234/path?query#ref");
  RefPtr<MozURL> url;
  ASSERT_EQ(MozURL::Init(getter_AddRefs(url), href), NS_OK);

  ASSERT_TRUE(url->HostPort().EqualsLiteral("example.net:1234"));

  RefPtr<MozURL> url2;
  url->Mutate().SetHostPort("test:321"_ns).Finalize(getter_AddRefs(url2));

  ASSERT_TRUE(url2->HostPort().EqualsLiteral("test:321"));
  ASSERT_TRUE(
      url2->Spec().EqualsLiteral("https://user:pass@test:321/path?query#ref"));

  href.Assign("https://user:pass@example.net:443/path?query#ref");
  ASSERT_EQ(MozURL::Init(getter_AddRefs(url), href), NS_OK);
  ASSERT_TRUE(url->HostPort().EqualsLiteral("example.net"));
  ASSERT_EQ(url->Port(), -1);
}

TEST(TestMozURL, Origin)
{
  nsAutoCString href("https://user:pass@example.net:1234/path?query#ref");
  RefPtr<MozURL> url;
  ASSERT_EQ(MozURL::Init(getter_AddRefs(url), href), NS_OK);

  nsAutoCString out;
  url->Origin(out);
  ASSERT_TRUE(out.EqualsLiteral("https://example.net:1234"));

  RefPtr<MozURL> url2;
  ASSERT_EQ(MozURL::Init(getter_AddRefs(url2), "file:///tmp/foo"_ns), NS_OK);
  url2->Origin(out);
  ASSERT_TRUE(out.EqualsLiteral("file:///tmp/foo"));

  RefPtr<MozURL> url3;
  ASSERT_EQ(
      MozURL::Init(getter_AddRefs(url3),
                   nsLiteralCString(
                       "moz-extension://53711a8f-65ed-e742-9671-1f02e267c0bc/"
                       "foo/bar.html")),
      NS_OK);
  url3->Origin(out);
  ASSERT_TRUE(out.EqualsLiteral(
      "moz-extension://53711a8f-65ed-e742-9671-1f02e267c0bc"));

  RefPtr<MozURL> url4;
  ASSERT_EQ(MozURL::Init(getter_AddRefs(url4), "resource://foo/bar.html"_ns),
            NS_OK);
  url4->Origin(out);
  ASSERT_TRUE(out.EqualsLiteral("resource://foo"));

  RefPtr<MozURL> url5;
  ASSERT_EQ(MozURL::Init(getter_AddRefs(url5), "about:home"_ns), NS_OK);
  url5->Origin(out);
  ASSERT_TRUE(out.EqualsLiteral("about:home"));
}

namespace {

bool OriginMatchesExpectedOrigin(const nsACString& aOrigin,
                                 const nsACString& aExpectedOrigin) {
  if (aExpectedOrigin.Equals("null") &&
      StringBeginsWith(aOrigin, "moz-nullprincipal"_ns)) {
    return true;
  }
  return aOrigin == aExpectedOrigin;
}

void CheckOrigin(const nsACString& aSpec, const nsACString& aBase,
                 const nsACString& aOrigin) {
  nsCOMPtr<nsIURI> baseUri;
  nsresult rv = NS_NewURI(getter_AddRefs(baseUri), aBase);
  ASSERT_EQ(rv, NS_OK);

  nsCOMPtr<nsIURI> uri;
  rv = NS_NewURI(getter_AddRefs(uri), aSpec, nullptr, baseUri);
  ASSERT_EQ(rv, NS_OK);

  OriginAttributes attrs;

  nsCOMPtr<nsIPrincipal> principal =
      BasePrincipal::CreateContentPrincipal(uri, attrs);
  ASSERT_TRUE(principal);

  nsCString origin;
  rv = principal->GetOriginNoSuffix(origin);
  ASSERT_EQ(rv, NS_OK);

  EXPECT_TRUE(OriginMatchesExpectedOrigin(origin, aOrigin));

  nsCString baseDomain;
  rv = principal->GetBaseDomain(baseDomain);

  RefPtr<MozURL> baseUrl;
  ASSERT_EQ(MozURL::Init(getter_AddRefs(baseUrl), aBase), NS_OK);

  RefPtr<MozURL> url;
  ASSERT_EQ(MozURL::Init(getter_AddRefs(url), aSpec, baseUrl), NS_OK);

  url->Origin(origin);

  EXPECT_TRUE(OriginMatchesExpectedOrigin(origin, aOrigin));
}

}  // namespace

TEST(TestMozURL, UrlTestData)
{
  nsCOMPtr<nsIFile> file;
  nsresult rv =
      NS_GetSpecialDirectory(NS_OS_CURRENT_WORKING_DIR, getter_AddRefs(file));
  ASSERT_EQ(rv, NS_OK);

  rv = file->Append(u"urltestdata.json"_ns);
  ASSERT_EQ(rv, NS_OK);

  bool exists;
  rv = file->Exists(&exists);
  ASSERT_EQ(rv, NS_OK);

  ASSERT_TRUE(exists);

  nsCOMPtr<nsIInputStream> stream;
  rv = NS_NewLocalFileInputStream(getter_AddRefs(stream), file);
  ASSERT_EQ(rv, NS_OK);

  nsCOMPtr<nsIInputStream> bufferedStream;
  rv = NS_NewBufferedInputStream(getter_AddRefs(bufferedStream),
                                 stream.forget(), 4096);
  ASSERT_EQ(rv, NS_OK);

  nsCString data;
  rv = NS_ConsumeStream(bufferedStream, UINT32_MAX, data);
  ASSERT_EQ(rv, NS_OK);

  Json::Value root;
  Json::CharReaderBuilder builder;
  std::unique_ptr<Json::CharReader> const reader(builder.newCharReader());
  ASSERT_TRUE(
      reader->parse(data.BeginReading(), data.EndReading(), &root, nullptr));
  ASSERT_TRUE(root.isArray());

  for (auto& item : root) {
    if (!item.isObject()) {
      continue;
    }

    const Json::Value& skip = item["skip"];
    ASSERT_TRUE(skip.isNull() || skip.isBool());
    if (skip.isBool() && skip.asBool()) {
      continue;
    }

    const Json::Value& failure = item["failure"];
    ASSERT_TRUE(failure.isNull() || failure.isBool());
    if (failure.isBool() && failure.asBool()) {
      continue;
    }

    const Json::Value& origin = item["origin"];
    ASSERT_TRUE(origin.isNull() || origin.isString());
    if (origin.isNull()) {
      continue;
    }
    const char* originBegin;
    const char* originEnd;
    origin.getString(&originBegin, &originEnd);

    auto baseCString = nsDependentCString("about:blank");
    const Json::Value& base = item["base"];
    if (!base.isNull()) {
      const char* baseBegin;
      const char* baseEnd;
      base.getString(&baseBegin, &baseEnd);
      baseCString.Assign(nsDependentCSubstring(baseBegin, baseEnd));
    }

    const Json::Value& input = item["input"];
    ASSERT_TRUE(input.isString());
    const char* inputBegin;
    const char* inputEnd;
    input.getString(&inputBegin, &inputEnd);

    CheckOrigin(nsDependentCString(inputBegin, inputEnd), baseCString,
                nsDependentCString(originBegin, originEnd));
  }
}
