// Copyright 2013 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "url/gurl.h"

#include <stddef.h>

#include <array>

#include "base/strings/string_number_conversions.h"
#include "base/strings/utf_string_conversions.h"
#include "base/test/scoped_feature_list.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "url/gurl_abstract_tests.h"
#include "url/gurl_debug.h"
#include "url/origin.h"
#include "url/url_canon.h"
#include "url/url_test_utils.h"

namespace url {

class GURLTest : public testing::Test {
 public:
  GURLTest() = default;

 protected:
  struct ResolveCase {
    std::string_view base;
    std::string_view relative;
    bool expected_valid;
    std::optional<std::string_view> expected;
  };

  using ApplyReplacementsFunc = GURL(const GURL&);

  struct ReplaceCase {
    std::string_view base;
    ApplyReplacementsFunc* apply_replacements;
    std::string_view expected;
  };

  struct ReplaceHostCase {
    std::string_view base;
    std::string_view replacement_host;
    std::string_view expected;
  };

  struct ReplacePathCase {
    std::string_view base;
    std::string_view replacement_path;
    std::string_view expected;
  };

  // Returns the canonicalized string for the given URL string for the
  // GURLTest.Types test.
  std::string TypesTestCase(const char* src) {
    GURL gurl(src);
    return gurl.possibly_invalid_spec();
  }

  void TestResolve(const ResolveCase& resolve_case) {
    // 8-bit code path.
    GURL input(resolve_case.base);
    GURL output = input.Resolve(resolve_case.relative);
    EXPECT_EQ(resolve_case.expected_valid, output.is_valid());
    EXPECT_EQ(resolve_case.expected, output.spec());
    EXPECT_EQ(output.SchemeIsFileSystem(), output.inner_url() != nullptr);

    // Wide code path.
    GURL inputw(base::UTF8ToUTF16(resolve_case.base));
    GURL outputw = input.Resolve(base::UTF8ToUTF16(resolve_case.relative));
    EXPECT_EQ(resolve_case.expected_valid, outputw.is_valid());
    EXPECT_EQ(resolve_case.expected, outputw.spec());
    EXPECT_EQ(outputw.SchemeIsFileSystem(), outputw.inner_url() != nullptr);
  }

  void TestReplace(const ReplaceCase& replace) {
    GURL output = replace.apply_replacements(GURL(replace.base));
    EXPECT_EQ(output.spec(), replace.expected);
    EXPECT_EQ(output.SchemeIsFileSystem(), output.inner_url() != nullptr);
    if (output.SchemeIsFileSystem()) {
      // TODO(mmenke): inner_url()->spec() is currently the same as the spec()
      // for the GURL itself.  This should be fixed.
      // See https://crbug.com/619596
      EXPECT_EQ(output.inner_url()->spec(), replace.expected);
    }
  }

  void TestReplaceHost(const ReplaceHostCase& replace) {
    GURL url(replace.base);
    GURL::Replacements replacements;
    replacements.SetHostStr(replace.replacement_host);
    GURL output = url.ReplaceComponents(replacements);
    EXPECT_EQ(output.spec(), replace.expected);
  }

  void TestReplacePath(const ReplacePathCase& replace) {
    GURL url(replace.base);
    GURL::Replacements replacements;
    replacements.SetPathStr(replace.replacement_path);
    GURL output = url.ReplaceComponents(replacements);
    EXPECT_EQ(output.spec(), replace.expected);
  }
};

// Different types of URLs should be handled differently, and handed off to
// different canonicalizers.
TEST_F(GURLTest, Types) {
  // URLs with unknown schemes should be treated as path URLs, even when they
  // have things like "://".
  EXPECT_EQ("something:///HOSTNAME.com/",
            TypesTestCase("something:///HOSTNAME.com/"));

  // Conversely, URLs with known schemes should always trigger standard URL
  // handling.
  EXPECT_EQ("http://hostname.com/", TypesTestCase("http:HOSTNAME.com"));
  EXPECT_EQ("http://hostname.com/", TypesTestCase("http:/HOSTNAME.com"));
  EXPECT_EQ("http://hostname.com/", TypesTestCase("http://HOSTNAME.com"));
  EXPECT_EQ("http://hostname.com/", TypesTestCase("http:///HOSTNAME.com"));

#ifdef WIN32
  // URLs that look like Windows absolute path specs.
  EXPECT_EQ("file:///C:/foo.txt", TypesTestCase("c:\\foo.txt"));
  EXPECT_EQ("file:///Z:/foo.txt", TypesTestCase("Z|foo.txt"));
  EXPECT_EQ("file://server/foo.txt", TypesTestCase("\\\\server\\foo.txt"));
  EXPECT_EQ("file://server/foo.txt", TypesTestCase("//server/foo.txt"));
#endif
}

// Test the basic creation and querying of components in a GURL. We assume that
// the parser is already tested and works, so we are mostly interested if the
// object does the right thing with the results.
TEST_F(GURLTest, Components) {
  GURL empty_url(u"");
  EXPECT_TRUE(empty_url.is_empty());
  EXPECT_FALSE(empty_url.is_valid());

  GURL url(u"http://user:pass@google.com:99/foo;bar?q=a#ref");
  EXPECT_FALSE(url.is_empty());
  EXPECT_TRUE(url.is_valid());
  EXPECT_TRUE(url.SchemeIs("http"));
  EXPECT_FALSE(url.SchemeIsFile());

  // This is the narrow version of the URL, which should match the wide input.
  EXPECT_EQ("http://user:pass@google.com:99/foo;bar?q=a#ref", url.spec());

  EXPECT_EQ("http", url.GetScheme());
  EXPECT_EQ("user", url.GetUsername());
  EXPECT_EQ("pass", url.GetPassword());
  EXPECT_EQ("google.com", url.GetHost());
  EXPECT_EQ("99", url.GetPort());
  EXPECT_EQ(99, url.IntPort());
  EXPECT_EQ("/foo;bar", url.GetPath());
  EXPECT_EQ("q=a", url.GetQuery());
  EXPECT_EQ("ref", url.GetRef());

  // Test parsing userinfo with special characters.
  GURL url_special_pass("http://user:%40!$&'()*+,;=:@google.com:12345");
  EXPECT_TRUE(url_special_pass.is_valid());
  // GURL canonicalizes some delimiters.
  EXPECT_EQ("%40!$&%27()*+,%3B%3D%3A", url_special_pass.GetPassword());
  EXPECT_EQ("google.com", url_special_pass.GetHost());
  EXPECT_EQ("12345", url_special_pass.GetPort());

  // Test path collapsing.
  GURL url_path_collapse("http://example.com/a/./b/c/d/../../e");
  EXPECT_EQ("/a/b/e", url_path_collapse.GetPath());

  // Test an IDNA (Internationalizing Domain Names in Applications) host.
  GURL url_idna("http://Bücher.exAMple/");
  EXPECT_EQ("xn--bcher-kva.example", url_idna.GetHost());

  // Test non-ASCII characters, outside of the host (IDNA).
  GURL url_non_ascii("http://example.com/foo/aβc%2Etxt?q=r🙂s");
  EXPECT_EQ("/foo/a%CE%B2c%2Etxt", url_non_ascii.GetPath());
  EXPECT_EQ("q=r%F0%9F%99%82s", url_non_ascii.GetQuery());

  // Test already percent-escaped strings.
  // %2E case preserved (/./ is navigation, other %2E stay encoded).
  GURL url_percent_escaped("http://example.com/a/./%2e/i%2E%2F%2fj?q=r%2Es");
  EXPECT_EQ("/a/i%2E%2F%2fj", url_percent_escaped.GetPath());
  EXPECT_EQ("q=r%2Es", url_percent_escaped.GetQuery());
}

TEST_F(GURLTest, Empty) {
  GURL url;
  EXPECT_FALSE(url.is_valid());
  EXPECT_EQ("", url.spec());

  EXPECT_EQ("", url.GetScheme());
  EXPECT_EQ("", url.GetUsername());
  EXPECT_EQ("", url.GetPassword());
  EXPECT_EQ("", url.GetHost());
  EXPECT_EQ("", url.GetPort());
  EXPECT_EQ(PORT_UNSPECIFIED, url.IntPort());
  EXPECT_EQ("", url.GetPath());
  EXPECT_EQ("", url.GetQuery());
  EXPECT_EQ("", url.GetRef());
}

TEST_F(GURLTest, Copy) {
  GURL url(u"http://user:pass@google.com:99/foo;bar?q=a#ref");

  GURL url2(url);
  EXPECT_TRUE(url2.is_valid());

  EXPECT_EQ("http://user:pass@google.com:99/foo;bar?q=a#ref", url2.spec());
  EXPECT_EQ("http", url2.GetScheme());
  EXPECT_EQ("user", url2.GetUsername());
  EXPECT_EQ("pass", url2.GetPassword());
  EXPECT_EQ("google.com", url2.GetHost());
  EXPECT_EQ("99", url2.GetPort());
  EXPECT_EQ(99, url2.IntPort());
  EXPECT_EQ("/foo;bar", url2.GetPath());
  EXPECT_EQ("q=a", url2.GetQuery());
  EXPECT_EQ("ref", url2.GetRef());

  // Copying of invalid URL should be invalid
  GURL invalid;
  GURL invalid2(invalid);
  EXPECT_FALSE(invalid2.is_valid());
  EXPECT_EQ("", invalid2.spec());
  EXPECT_EQ("", invalid2.GetScheme());
  EXPECT_EQ("", invalid2.GetUsername());
  EXPECT_EQ("", invalid2.GetPassword());
  EXPECT_EQ("", invalid2.GetHost());
  EXPECT_EQ("", invalid2.GetPort());
  EXPECT_EQ(PORT_UNSPECIFIED, invalid2.IntPort());
  EXPECT_EQ("", invalid2.GetPath());
  EXPECT_EQ("", invalid2.GetQuery());
  EXPECT_EQ("", invalid2.GetRef());
}

TEST_F(GURLTest, Assign) {
  GURL url(u"http://user:pass@google.com:99/foo;bar?q=a#ref");

  GURL url2;
  url2 = url;
  EXPECT_TRUE(url2.is_valid());

  EXPECT_EQ("http://user:pass@google.com:99/foo;bar?q=a#ref", url2.spec());
  EXPECT_EQ("http", url2.GetScheme());
  EXPECT_EQ("user", url2.GetUsername());
  EXPECT_EQ("pass", url2.GetPassword());
  EXPECT_EQ("google.com", url2.GetHost());
  EXPECT_EQ("99", url2.GetPort());
  EXPECT_EQ(99, url2.IntPort());
  EXPECT_EQ("/foo;bar", url2.GetPath());
  EXPECT_EQ("q=a", url2.GetQuery());
  EXPECT_EQ("ref", url2.GetRef());

  // Assignment of invalid URL should be invalid
  GURL invalid;
  GURL invalid2;
  invalid2 = invalid;
  EXPECT_FALSE(invalid2.is_valid());
  EXPECT_EQ("", invalid2.spec());
  EXPECT_EQ("", invalid2.GetScheme());
  EXPECT_EQ("", invalid2.GetUsername());
  EXPECT_EQ("", invalid2.GetPassword());
  EXPECT_EQ("", invalid2.GetHost());
  EXPECT_EQ("", invalid2.GetPort());
  EXPECT_EQ(PORT_UNSPECIFIED, invalid2.IntPort());
  EXPECT_EQ("", invalid2.GetPath());
  EXPECT_EQ("", invalid2.GetQuery());
  EXPECT_EQ("", invalid2.GetRef());
}

// This is a regression test for http://crbug.com/309975.
TEST_F(GURLTest, SelfAssign) {
  GURL a("filesystem:http://example.com/temporary/");
  // This should not crash.
  a = *&a;  // The *& defeats Clang's -Wself-assign warning.
}

TEST_F(GURLTest, CopyFileSystem) {
  GURL url(u"filesystem:https://user:pass@google.com:99/t/foo;bar?q=a#ref");

  GURL url2(url);
  EXPECT_TRUE(url2.is_valid());

  EXPECT_EQ("filesystem:https://google.com:99/t/foo;bar?q=a#ref", url2.spec());
  EXPECT_EQ("filesystem", url2.GetScheme());
  EXPECT_EQ("", url2.GetUsername());
  EXPECT_EQ("", url2.GetPassword());
  EXPECT_EQ("", url2.GetHost());
  EXPECT_EQ("", url2.GetPort());
  EXPECT_EQ(PORT_UNSPECIFIED, url2.IntPort());
  EXPECT_EQ("/foo;bar", url2.GetPath());
  EXPECT_EQ("q=a", url2.GetQuery());
  EXPECT_EQ("ref", url2.GetRef());

  const GURL* inner = url2.inner_url();
  ASSERT_TRUE(inner);
  EXPECT_EQ("https", inner->GetScheme());
  EXPECT_EQ("", inner->GetUsername());
  EXPECT_EQ("", inner->GetPassword());
  EXPECT_EQ("google.com", inner->GetHost());
  EXPECT_EQ("99", inner->GetPort());
  EXPECT_EQ(99, inner->IntPort());
  EXPECT_EQ("/t", inner->GetPath());
  EXPECT_EQ("", inner->GetQuery());
  EXPECT_EQ("", inner->GetRef());
}

TEST_F(GURLTest, IsValid) {
  static constexpr auto valid_cases = std::to_array<const char*>({
      "http://google.com",
      "unknown://google.com",
      "http://user:pass@google.com",
      "http://google.com:12345",
      "http://google.com:0",  // 0 is a valid port
      "http://google.com/path",
      "http://google.com//path",
      "http://google.com?k=v#fragment",
      "http://user:pass@google.com:12345/path?k=v#fragment",
      "http:/path",
      "http:path",
  });
  for (const char* valid_case : valid_cases) {
    EXPECT_TRUE(GURL(valid_case).is_valid()) << "Case: " << valid_case;
  }

  static constexpr auto invalid_cases = std::to_array<const char*>({
      "http://?k=v",
      "http:://google.com",
      "http//google.com",
      "http://google.com:12three45",
      "file://server:123",  // file: URLs cannot have a port
      "file://server:0",
      "://google.com",
      "path",
  });
  for (const char* invalid_case : invalid_cases) {
    EXPECT_FALSE(GURL(invalid_case).is_valid()) << "Case: " << invalid_case;
  }
}

TEST_F(GURLTest, ExtraSlashesBeforeAuthority) {
  // According to RFC3986, the hierarchical part for URI with an authority
  // must use only two slashes; GURL intentionally just ignores extra slashes
  // if there are more than 2, and parses the following part as an authority.
  GURL url("http:///host");
  EXPECT_EQ("host", url.GetHost());
  EXPECT_EQ("/", url.GetPath());
}

// Given invalid URLs, we should still get most of the components.
TEST_F(GURLTest, ComponentGettersWorkEvenForInvalidURL) {
  constexpr struct InvalidURLTestExpectations {
    const char* url;
    const char* spec;
    const char* scheme;
    const char* host;
    const char* port;
    const char* path;
    // Extend as needed...
  } expectations[] = {
      {
          "http:google.com:foo",
          "http://google.com:foo/",
          "http",
          "google.com",
          "foo",
          "/",
      },
      {
          "https:google.com:foo",
          "https://google.com:foo/",
          "https",
          "google.com",
          "foo",
          "/",
      },
  };

  for (const auto& e : expectations) {
    const GURL url(e.url);
    EXPECT_FALSE(url.is_valid());
    EXPECT_EQ(e.spec, url.possibly_invalid_spec());
    EXPECT_EQ(e.scheme, url.GetScheme());
    EXPECT_EQ("", url.GetUsername());
    EXPECT_EQ("", url.GetPassword());
    EXPECT_EQ(e.host, url.GetHost());
    EXPECT_EQ(e.port, url.GetPort());
    EXPECT_EQ(PORT_INVALID, url.IntPort());
    EXPECT_EQ(e.path, url.GetPath());
    EXPECT_EQ("", url.GetQuery());
    EXPECT_EQ("", url.GetRef());
  }
}

TEST_F(GURLTest, Resolve) {
  // The tricky cases for relative URL resolving are tested in the
  // canonicalizer unit test. Here, we just test that the GURL integration
  // works properly.
  static constexpr ResolveCase resolve_cases[] = {
      {"http://www.google.com/", "foo.html", true,
       "http://www.google.com/foo.html"},
      {"http://www.google.com/foo/", "bar", true,
       "http://www.google.com/foo/bar"},
      {"http://www.google.com/foo/", "/bar", true, "http://www.google.com/bar"},
      {"http://www.google.com/foo", "bar", true, "http://www.google.com/bar"},
      {"http://www.google.com/", "http://images.google.com/foo.html", true,
       "http://images.google.com/foo.html"},
      {"http://www.google.com/", "http://images.\tgoogle.\ncom/\rfoo.html",
       true, "http://images.google.com/foo.html"},
      {"http://www.google.com/blah/bloo?c#d", "../../../hello/./world.html?a#b",
       true, "http://www.google.com/hello/world.html?a#b"},
      {"http://www.google.com/foo#bar", "#com", true,
       "http://www.google.com/foo#com"},
      {"http://www.google.com/", "Https:images.google.com", true,
       "https://images.google.com/"},
      // An opaque path URL can be replaced with a special absolute URL.
      {"data:blahblah", "http://google.com/", true, "http://google.com/"},
      {"data:blahblah", "http:google.com", true, "http://google.com/"},
      {"data:blahblah", "https:google.com", true, "https://google.com/"},
      // An opaque path URL can not be replaced with a relative URL.
      {"git:opaque", "", false, ""},
      {"git:opaque", "path", false, ""},
      // A non-special URL which doesn't have a host can be replaced with a
      // relative URL.
      {"git:/a", "b", true, "git:/b"},
      // Filesystem URLs have different paths to test.
      {"filesystem:http://www.google.com/type/", "foo.html", true,
       "filesystem:http://www.google.com/type/foo.html"},
      {"filesystem:http://www.google.com/type/", "../foo.html", true,
       "filesystem:http://www.google.com/type/foo.html"},
      // https://crbug.com/530123 - scheme validation (e.g. are "10.0.0.7:"
      // or "x1:" valid schemes) when deciding if |relative| is an absolute url.
      {"file:///some/dir/ip-relative.html", "10.0.0.7:8080/foo.html", true,
       "file:///some/dir/10.0.0.7:8080/foo.html"},
      {"file:///some/dir/", "1://host", true, "file:///some/dir/1://host"},
      {"file:///some/dir/", "x1://host", true, "x1://host"},
      {"file:///some/dir/", "X1://host", true, "x1://host"},
      {"file:///some/dir/", "x.://host", true, "x.://host"},
      {"file:///some/dir/", "x+://host", true, "x+://host"},
      {"file:///some/dir/", "x-://host", true, "x-://host"},
      {"file:///some/dir/", "x!://host", true, "file:///some/dir/x!://host"},
      {"file:///some/dir/", "://host", true, "file:///some/dir/://host"},

      // Non-special base URLs whose paths are empty.
      {"git://host", "", true, "git://host"},
      {"git://host", ".", true, "git://host/"},
      {"git://host", "..", true, "git://host/"},
      {"git://host", "a", true, "git://host/a"},
      {"git://host", "/a", true, "git://host/a"},

      // Non-special base URLs whose paths are "/".
      {"git://host/", "", true, "git://host/"},
      {"git://host/", ".", true, "git://host/"},
      {"git://host/", "..", true, "git://host/"},
      {"git://host/", "a", true, "git://host/a"},
      {"git://host/", "/a", true, "git://host/a"},

      // Non-special base URLs whose hosts and paths are non-empty.
      {"git://host/b", "a", true, "git://host/a"},
      {"git://host/b/c", "a", true, "git://host/b/a"},
      {"git://host/b/c", "../a", true, "git://host/a"},

      // An opaque path can be specified.
      {"git://host", "git:opaque", true, "git:opaque"},
      {"git://host/path#ref", "git:opaque", true, "git:opaque"},
      {"git:/path", "git:opaque", true, "git:opaque"},
      {"https://host/path", "git:opaque", true, "git:opaque"},

      // Path-only base URLs should remain path-only URLs unless a host is
      // specified.
      {"git:/", "", true, "git:/"},
      {"git:/", ".", true, "git:/"},
      {"git:/", "..", true, "git:/"},
      {"git:/", "a", true, "git:/a"},
      {"git:/", "/a", true, "git:/a"},
      {"git:/#ref", "", true, "git:/"},
      {"git:/#ref", "a", true, "git:/a"},

      // Non-special base URLs whose hosts and path are both empty. The
      // result's host should remain empty unless a relative URL specify a
      // host.
      {"git://", "", true, "git://"},
      {"git://", ".", true, "git:///"},
      {"git://", "..", true, "git:///"},
      {"git://", "a", true, "git:///a"},
      {"git://", "/a", true, "git:///a"},

      // Non-special base URLs whose hosts are empty, but with non-empty path.
      {"git:///", "", true, "git:///"},
      {"git:///", ".", true, "git:///"},
      {"git:///", "..", true, "git:///"},
      {"git:///", "a", true, "git:///a"},
      {"git:///", "/a", true, "git:///a"},
      {"git:///#ref", "", true, "git:///"},
      {"git:///#ref", "a", true, "git:///a"},

      // Relative URLs can specify empty hosts for non-special base URLs.
      // e.g. "///path"
      {"git://host/", "//", true, "git://"},
      {"git://host/", "//a", true, "git://a"},
      {"git://host/", "///", true, "git:///"},
      {"git://host/", "////", true, "git:////"},
      {"git://host/", "////..", true, "git:///"},
      {"git://host/", "////../..", true, "git:///"},
      {"git://host/", "////../../..", true, "git:///"},
  };

  for (const ResolveCase& c : resolve_cases) {
    TestResolve(c);
  }
}

TEST_F(GURLTest, GetOrigin) {
  struct TestCase {
    const char* input;
    const char* expected;
  };
  static constexpr auto cases = std::to_array<TestCase>({
      {"http://www.google.com", "http://www.google.com/"},
      {"javascript:window.alert(\"hello,world\");", ""},
      {"http://user:pass@www.google.com:21/blah#baz",
       "http://www.google.com:21/"},
      {"http://user@www.google.com", "http://www.google.com/"},
      {"http://:pass@www.google.com", "http://www.google.com/"},
      {"http://:@www.google.com", "http://www.google.com/"},
      {"filesystem:http://www.google.com/temp/foo?q#b",
       "http://www.google.com/"},
      {"filesystem:http://user:pass@google.com:21/blah#baz",
       "http://google.com:21/"},
      {"blob:null/guid-goes-here", ""},
      {"blob:http://origin/guid-goes-here", "" /* should be http://origin/ */},
  });

  for (const TestCase& c : cases) {
    GURL url(c.input);
    GURL origin = url.DeprecatedGetOriginAsURL();
    EXPECT_EQ(c.expected, origin.spec());
  }
}

TEST_F(GURLTest, GetAsReferrer) {
  struct TestCase {
    const char* input;
    const char* expected;
  };
  static constexpr auto cases = std::to_array<TestCase>({
      {"http://www.google.com", "http://www.google.com/"},
      {"http://user:pass@www.google.com:21/blah#baz",
       "http://www.google.com:21/blah"},
      {"http://user@www.google.com", "http://www.google.com/"},
      {"http://:pass@www.google.com", "http://www.google.com/"},
      {"http://:@www.google.com", "http://www.google.com/"},
      {"http://www.google.com/temp/foo?q#b",
       "http://www.google.com/temp/foo?q"},
      {"not a url", ""},
      {"unknown-scheme://foo.html", ""},
      {"file:///tmp/test.html", ""},
      {"https://www.google.com", "https://www.google.com/"},
  });
  for (const TestCase& c : cases) {
    GURL url(c.input);
    GURL origin = url.GetAsReferrer();
    EXPECT_EQ(c.expected, origin.spec());
  }
}

TEST_F(GURLTest, GetWithEmptyPath) {
  struct TestCase {
    const char* input;
    const char* expected;
  };
  static constexpr auto cases = std::to_array<TestCase>({
      {"http://www.google.com", "http://www.google.com/"},
      {"javascript:window.alert(\"hello, world\");", ""},
      {"http://www.google.com/foo/bar.html?baz=22", "http://www.google.com/"},
      {"filesystem:http://www.google.com/temporary/bar.html?baz=22",
       "filesystem:http://www.google.com/temporary/"},
      {"filesystem:file:///temporary/bar.html?baz=22",
       "filesystem:file:///temporary/"},
  });
  for (const auto& c : cases) {
    GURL url(c.input);
    GURL empty_path = url.GetWithEmptyPath();
    EXPECT_EQ(c.expected, empty_path.spec());
  }
}

TEST_F(GURLTest, GetWithoutFilename) {
  struct TestCase {
    const char* input;
    const char* expected;
  };
  static constexpr auto cases = std::to_array<TestCase>({
      // Common Standard URLs.
      {"https://www.google.com", "https://www.google.com/"},
      {"https://www.google.com/", "https://www.google.com/"},
      {"https://www.google.com/maps.htm", "https://www.google.com/"},
      {"https://www.google.com/maps/", "https://www.google.com/maps/"},
      {"https://www.google.com/index.html", "https://www.google.com/"},
      {"https://www.google.com/index.html?q=maps", "https://www.google.com/"},
      {"https://www.google.com/index.html#maps/", "https://www.google.com/"},
      {"https://foo:bar@www.google.com/maps.htm",
       "https://foo:bar@www.google.com/"},
      {"https://www.google.com/maps/au/index.html",
       "https://www.google.com/maps/au/"},
      {"https://www.google.com/maps/au/north",
       "https://www.google.com/maps/au/"},
      {"https://www.google.com/maps/au/north/",
       "https://www.google.com/maps/au/north/"},
      {"https://www.google.com/maps/au/index.html?q=maps#fragment/",
       "https://www.google.com/maps/au/"},
      {"http://www.google.com:8000/maps/au/index.html?q=maps#fragment/",
       "http://www.google.com:8000/maps/au/"},
      {"https://www.google.com/maps/au/north/?q=maps#fragment",
       "https://www.google.com/maps/au/north/"},
      {"https://www.google.com/maps/au/north?q=maps#fragment",
       "https://www.google.com/maps/au/"},
      // Less common standard URLs.
      {"filesystem:http://www.google.com/temporary/bar.html?baz=22",
       "filesystem:http://www.google.com/temporary/"},
      {"file:///temporary/bar.html?baz=22", "file:///temporary/"},
      {"ftp://foo/test/index.html", "ftp://foo/test/"},
      {"gopher://foo/test/index.html", "gopher://foo/test/"},
      {"ws://foo/test/index.html", "ws://foo/test/"},
      // Non-standard, hierarchical URLs.
      {"chrome://foo/bar.html", "chrome://foo/"},
      {"httpa://foo/test/index.html", "httpa://foo/test/"},
      // Non-standard, non-hierarchical URLs.
      {"blob:https://foo.bar/test/index.html", ""},
      {"about:blank", ""},
      {"data:foobar", ""},
      {"scheme:opaque_data", ""},
      // Invalid URLs.
      {"foobar", ""},
  });

  for (const auto& c : cases) {
    GURL url(c.input);
    GURL without_filename = url.GetWithoutFilename();
    EXPECT_EQ(c.expected, without_filename.spec());
  }
}

TEST_F(GURLTest, GetWithoutRef) {
  struct TestCase {
    const char* input;
    const char* expected;
  };
  static constexpr auto cases = std::to_array<TestCase>({
      // Common Standard URLs.
      {"https://www.google.com/index.html",
       "https://www.google.com/index.html"},
      {"https://www.google.com/index.html#maps/",
       "https://www.google.com/index.html"},

      {"https://foo:bar@www.google.com/maps.htm",
       "https://foo:bar@www.google.com/maps.htm"},
      {"https://foo:bar@www.google.com/maps.htm#fragment",
       "https://foo:bar@www.google.com/maps.htm"},

      {"https://www.google.com/maps/au/index.html?q=maps",
       "https://www.google.com/maps/au/index.html?q=maps"},
      {"https://www.google.com/maps/au/index.html?q=maps#fragment/",
       "https://www.google.com/maps/au/index.html?q=maps"},

      {"http://www.google.com:8000/maps/au/index.html?q=maps",
       "http://www.google.com:8000/maps/au/index.html?q=maps"},
      {"http://www.google.com:8000/maps/au/index.html?q=maps#fragment/",
       "http://www.google.com:8000/maps/au/index.html?q=maps"},

      {"https://www.google.com/maps/au/north/?q=maps",
       "https://www.google.com/maps/au/north/?q=maps"},
      {"https://www.google.com/maps/au/north?q=maps#fragment",
       "https://www.google.com/maps/au/north?q=maps"},

      // Less common standard URLs.
      {"filesystem:http://www.google.com/temporary/bar.html?baz=22",
       "filesystem:http://www.google.com/temporary/bar.html?baz=22"},
      {"file:///temporary/bar.html?baz=22#fragment",
       "file:///temporary/bar.html?baz=22"},

      {"ftp://foo/test/index.html", "ftp://foo/test/index.html"},
      {"ftp://foo/test/index.html#fragment", "ftp://foo/test/index.html"},

      {"gopher://foo/test/index.html", "gopher://foo/test/index.html"},
      {"gopher://foo/test/index.html#fragment", "gopher://foo/test/index.html"},

      {"ws://foo/test/index.html", "ws://foo/test/index.html"},
      {"ws://foo/test/index.html#fragment", "ws://foo/test/index.html"},

      // Non-standard, hierarchical URLs.
      {"chrome://foo/bar.html", "chrome://foo/bar.html"},
      {"chrome://foo/bar.html#fragment", "chrome://foo/bar.html"},

      {"httpa://foo/test/index.html", "httpa://foo/test/index.html"},
      {"httpa://foo/test/index.html#fragment", "httpa://foo/test/index.html"},

      // Non-standard, non-hierarchical URLs.
      {"blob:https://foo.bar/test/index.html",
       "blob:https://foo.bar/test/index.html"},
      {"blob:https://foo.bar/test/index.html#fragment",
       "blob:https://foo.bar/test/index.html"},

      {"about:blank", "about:blank"},
      {"about:blank#ref", "about:blank"},

      {"data:foobar", "data:foobar"},
      {"scheme:opaque_data", "scheme:opaque_data"},
      // Invalid URLs.
      {"foobar", ""},
  });

  for (const auto& i : cases) {
    GURL url(i.input);
    GURL without_ref = url.GetWithoutRef();
    EXPECT_EQ(i.expected, without_ref.spec());
  }
}

TEST_F(GURLTest, Replacements) {
  // The URL canonicalizer replacement test will handle most of these case.
  // The most important thing to do here is to check that the proper
  // canonicalizer gets called based on the scheme of the input.
  static constexpr ReplaceCase replace_cases[] = {
      {.base = "http://www.google.com/foo/bar.html?foo#bar",
       .apply_replacements =
           +[](const GURL& url) {
             GURL::Replacements replacements;
             replacements.SetPathStr("/");
             replacements.ClearQuery();
             replacements.ClearRef();
             return url.ReplaceComponents(replacements);
           },
       .expected = "http://www.google.com/"},
      {.base = "file:///C:/foo/bar.txt",
       .apply_replacements =
           +[](const GURL& url) {
             GURL::Replacements replacements;
             replacements.SetSchemeStr("http");
             replacements.SetHostStr("www.google.com");
             replacements.SetPortStr("99");
             replacements.SetPathStr("/foo");
             replacements.SetQueryStr("search");
             replacements.SetRefStr("ref");
             return url.ReplaceComponents(replacements);
           },
       .expected = "http://www.google.com:99/foo?search#ref"},
#ifdef WIN32
      {.base = "http://www.google.com/foo/bar.html?foo#bar",
       .apply_replacements =
           +[](const GURL& url) {
             GURL::Replacements replacements;
             replacements.SetSchemeStr("file");
             replacements.ClearUsername();
             replacements.ClearPassword();
             replacements.ClearHost();
             replacements.ClearPort();
             replacements.SetPathStr("c:\\");
             replacements.ClearQuery();
             replacements.ClearRef();
             return url.ReplaceComponents(replacements);
           },
       .expected = "file:///C:/"},
#endif
      {.base = "filesystem:http://www.google.com/foo/bar.html?foo#bar",
       .apply_replacements =
           +[](const GURL& url) {
             GURL::Replacements replacements;
             replacements.SetPathStr("/");
             replacements.ClearQuery();
             replacements.ClearRef();
             return url.ReplaceComponents(replacements);
           },
       .expected = "filesystem:http://www.google.com/foo/"},
      // Lengthen the URL instead of shortening it, to test creation of
      // inner_url.
      {.base = "filesystem:http://www.google.com/foo/",
       .apply_replacements =
           +[](const GURL& url) {
             GURL::Replacements replacements;
             replacements.SetPathStr("bar.html");
             replacements.SetQueryStr("foo");
             replacements.SetRefStr("bar");
             return url.ReplaceComponents(replacements);
           },
       .expected = "filesystem:http://www.google.com/foo/bar.html?foo#bar"},
      // Regression test for https://crbug.com/375660989.
      //
      // "steam:" is explicitly registered as an opaque non-special scheme for
      // compatibility reasons. See SchemeRegistry::opaque_non_special_schemes.
      {.base = "steam:a",
       .apply_replacements =
           +[](const GURL& url) {
             GURL::Replacements replacements;
             replacements.SetPathStr("b");
             return url.ReplaceComponents(replacements);
           },
       .expected = "steam:b"},

      // Test cases that Chromium used to parse incorrectly.
      {.base = "git://a1/a2?a3=a4#a5",
       .apply_replacements =
           +[](const GURL& url) {
             GURL::Replacements replacements;
             replacements.SetHostStr("b1");
             replacements.SetPortStr("99");
             replacements.SetPathStr("b2");
             replacements.SetQueryStr("b3=b4");
             replacements.SetRefStr("b5");
             return url.ReplaceComponents(replacements);
           },
       .expected = "git://b1:99/b2?b3=b4#b5"},
      // URL Standard: https://url.spec.whatwg.org/#dom-url-username
      // > 1. If this’s URL cannot have a username/password/port, then return.
      {.base = "git:///",
       .apply_replacements =
           +[](const GURL& url) {
             GURL::Replacements replacements;
             replacements.SetUsernameStr("x");
             return url.ReplaceComponents(replacements);
           },
       .expected = "git:///"},
      // URL Standard: https://url.spec.whatwg.org/#dom-url-password
      // > 1. If this’s URL cannot have a username/password/port, then return.
      {.base = "git:///",
       .apply_replacements =
           +[](const GURL& url) {
             GURL::Replacements replacements;
             replacements.SetPasswordStr("x");
             return url.ReplaceComponents(replacements);
           },
       .expected = "git:///"},
      // URL Standard: https://url.spec.whatwg.org/#dom-url-port
      // > 1. If this’s URL cannot have a username/password/port, then return.
      {.base = "git:///",
       .apply_replacements =
           +[](const GURL& url) {
             GURL::Replacements replacements;
             replacements.SetPortStr("80");
             return url.ReplaceComponents(replacements);
           },
       .expected = "git:///"}};

  for (const ReplaceCase& c : replace_cases) {
    TestReplace(c);
  }

  static constexpr ReplaceHostCase replace_host_cases[] = {
      {"git:/", "host", "git://host/"},
      {"git:/a", "host", "git://host/a"},
      {"git://", "host", "git://host"},
      {"git:///", "host", "git://host/"},
      {"git://h/a", "host", "git://host/a"},
      // The following behavior is different from Web-facing URL APIs
      // because DOMURLUtils::setHostname disallows setting an empty host.
      //
      // Web-facing URL API behavior is:
      // > const url = new URL("git://u:p@h:80/");
      // > url.hostname = "";
      // > assertEquals(url.href, "git://u:p@h:80/");
      {"git://u:p@h:80/", "", "git:///"}};
  for (const ReplaceHostCase& c : replace_host_cases) {
    TestReplaceHost(c);
  }

  static constexpr ReplacePathCase replace_path_cases[] = {
      {"git:/", "a", "git:/a"},
      {"git:/", "", "git:/"},
      {"git:/", "//a", "git:/.//a"},
      {"git:/", "/.//a", "git:/.//a"},
      {"git://", "a", "git:///a"},
      {"git:///", "a", "git:///a"},
      {"git://host", "a", "git://host/a"},
      {"git://host/b", "a", "git://host/a"}};
  for (const ReplacePathCase& c : replace_path_cases) {
    TestReplacePath(c);
  }
}

TEST(GURLTypedTest, ClearFragmentOnDataUrl) {
  // http://crbug.com/291747 - a data URL may legitimately have trailing
  // whitespace in the spec after the ref is cleared. Test this does not trigger
  // the Parsed importing validation DCHECK in GURL.
  GURL url(" data: one # two ");
  EXPECT_TRUE(url.is_valid());

  // By default the trailing whitespace will have been stripped.
  EXPECT_EQ("data: one #%20two", url.spec());

  // Clear the URL's ref and observe the trailing whitespace.
  GURL::Replacements repl;
  repl.ClearRef();
  GURL url_no_ref = url.ReplaceComponents(repl);
  EXPECT_TRUE(url_no_ref.is_valid());
  EXPECT_EQ("data: one ", url_no_ref.spec());

  // Importing a parsed URL via this constructor overload will retain trailing
  // whitespace.
  GURL import_url(url_no_ref.spec(),
                  url_no_ref.parsed_for_possibly_invalid_spec(),
                  url_no_ref.is_valid());
  EXPECT_TRUE(import_url.is_valid());
  EXPECT_EQ(url_no_ref, import_url);
  EXPECT_EQ("data: one ", import_url.spec());
  EXPECT_EQ(" one ", import_url.GetPath());

  // For completeness, test that re-parsing the same URL rather than importing
  // it trims the trailing whitespace.
  GURL reparsed_url(url_no_ref.spec());
  EXPECT_TRUE(reparsed_url.is_valid());
  EXPECT_EQ("data: one", reparsed_url.spec());
}

TEST_F(GURLTest, PathForRequest) {
  struct TestCase {
    const char* input;
    const char* expected;
    const char* inner_expected;
  };
  static constexpr auto cases = std::to_array<TestCase>({
      {"http://www.google.com", "/", nullptr},
      {"http://www.google.com/", "/", nullptr},
      {"http://www.google.com/foo/bar.html?baz=22", "/foo/bar.html?baz=22",
       nullptr},
      {"http://www.google.com/foo/bar.html#ref", "/foo/bar.html", nullptr},
      {"http://www.google.com/foo/bar.html?query#ref", "/foo/bar.html?query",
       nullptr},
      {"filesystem:http://www.google.com/temporary/foo/bar.html?query#ref",
       "/foo/bar.html?query", "/temporary"},
      {"filesystem:http://www.google.com/temporary/foo/bar.html?query",
       "/foo/bar.html?query", "/temporary"},
  });

  for (const auto& i : cases) {
    GURL url(i.input);
    EXPECT_EQ(i.expected, url.PathForRequest());
    EXPECT_EQ(i.expected, url.PathForRequestPiece());
    EXPECT_EQ(i.inner_expected == nullptr, url.inner_url() == nullptr);
    if (url.inner_url() && i.inner_expected) {
      EXPECT_EQ(i.inner_expected, url.inner_url()->PathForRequest());
      EXPECT_EQ(i.inner_expected, url.inner_url()->PathForRequestPiece());
    }
  }
}

TEST_F(GURLTest, EffectiveIntPort) {
  struct PortTest {
    const char* spec;
    int expected_int_port;
  };
  static constexpr auto port_tests = std::to_array<PortTest>({
      // http
      {"http://www.google.com/", 80},
      {"http://www.google.com:80/", 80},
      {"http://www.google.com:443/", 443},

      // https
      {"https://www.google.com/", 443},
      {"https://www.google.com:443/", 443},
      {"https://www.google.com:80/", 80},

      // ftp
      {"ftp://www.google.com/", 21},
      {"ftp://www.google.com:21/", 21},
      {"ftp://www.google.com:80/", 80},

      // file - no port
      {"file://www.google.com/", PORT_UNSPECIFIED},
      {"file://www.google.com:443/", PORT_UNSPECIFIED},

      // data - no port
      {"data:www.google.com:90", PORT_UNSPECIFIED},
      {"data:www.google.com", PORT_UNSPECIFIED},

      // filesystem - no port
      {"filesystem:http://www.google.com:90/t/foo", PORT_UNSPECIFIED},
      {"filesystem:file:///t/foo", PORT_UNSPECIFIED},
  });

  for (const auto& port_test : port_tests) {
    GURL url(port_test.spec);
    EXPECT_EQ(port_test.expected_int_port, url.EffectiveIntPort());
  }
}

TEST_F(GURLTest, IPAddress) {
  struct IPTest {
    const char* spec;
    bool expected_ip;
  };
  static constexpr auto ip_tests = std::to_array<IPTest>({
      {"http://www.google.com/", false},
      {"http://192.168.9.1/", true},
      {"http://192.168.9.1.2/", false},
      {"http://192.168.m.1/", false},
      {"http://2001:db8::1/", false},
      {"http://[2001:db8::1]/", true},
      {"", false},
      {"some random input!", false},
  });

  for (const auto& ip_test : ip_tests) {
    GURL url(ip_test.spec);
    EXPECT_EQ(ip_test.expected_ip, url.HostIsIPAddress());
  }
}

TEST_F(GURLTest, HostNoBrackets) {
  struct TestCase {
    const char* input;
    const char* expected_host;
    const char* expected_plainhost;
  };
  static constexpr auto cases = std::to_array<TestCase>({
      {"http://www.google.com", "www.google.com", "www.google.com"},
      {"http://[2001:db8::1]/", "[2001:db8::1]", "2001:db8::1"},
      {"http://[::]/", "[::]", "::"},

      // Don't require a valid URL, but don't crash either.
      {"http://[]/", "[]", ""},
      {"http://[x]/", "[x]", "x"},
      {"http://[x/", "[x", "[x"},
      {"http://x]/", "x]", "x]"},
      {"http://[/", "[", "["},
      {"http://]/", "]", "]"},
      {"", "", ""},
  });
  for (const auto& i : cases) {
    GURL url(i.input);
    EXPECT_EQ(i.expected_host, url.GetHost());
    EXPECT_EQ(i.expected_plainhost, url.HostNoBrackets());
    EXPECT_EQ(i.expected_plainhost, url.HostNoBracketsPiece());
  }
}

TEST_F(GURLTest, DomainIs) {
  GURL url_1("http://google.com/foo");
  EXPECT_TRUE(url_1.DomainIs("google.com"));

  // Subdomain and port are ignored.
  GURL url_2("http://www.google.com:99/foo");
  EXPECT_TRUE(url_2.DomainIs("google.com"));

  // Different top-level domain.
  GURL url_3("http://www.google.com.cn/foo");
  EXPECT_FALSE(url_3.DomainIs("google.com"));

  // Different host name.
  GURL url_4("http://www.iamnotgoogle.com/foo");
  EXPECT_FALSE(url_4.DomainIs("google.com"));

  // The input must be lower-cased otherwise DomainIs returns false.
  GURL url_5("http://www.google.com/foo");
  EXPECT_FALSE(url_5.DomainIs("Google.com"));

  // If the URL is invalid, DomainIs returns false.
  GURL invalid_url("google.com");
  EXPECT_FALSE(invalid_url.is_valid());
  EXPECT_FALSE(invalid_url.DomainIs("google.com"));

  GURL url_with_escape_chars("https://www.,.test");
  EXPECT_TRUE(url_with_escape_chars.is_valid());
  EXPECT_EQ(url_with_escape_chars.GetHost(), "www.,.test");
  EXPECT_TRUE(url_with_escape_chars.DomainIs(",.test"));
}

TEST_F(GURLTest, DomainIsTerminatingDotBehavior) {
  // If the host part ends with a dot, it matches input domains
  // with or without a dot.
  GURL url_with_dot("http://www.google.com./foo");
  EXPECT_TRUE(url_with_dot.DomainIs("google.com"));
  EXPECT_TRUE(url_with_dot.DomainIs("google.com."));
  EXPECT_TRUE(url_with_dot.DomainIs(".com"));
  EXPECT_TRUE(url_with_dot.DomainIs(".com."));

  // But, if the host name doesn't end with a dot and the input
  // domain does, then it's considered to not match.
  GURL url_without_dot("http://google.com/foo");
  EXPECT_FALSE(url_without_dot.DomainIs("google.com."));

  // If the URL ends with two dots, it doesn't match.
  GURL url_with_two_dots("http://www.google.com../foo");
  EXPECT_FALSE(url_with_two_dots.DomainIs("google.com"));
}

TEST_F(GURLTest, DomainIsWithFilesystemScheme) {
  GURL url_1("filesystem:http://www.google.com:99/foo/");
  EXPECT_TRUE(url_1.DomainIs("google.com"));

  GURL url_2("filesystem:http://www.iamnotgoogle.com/foo/");
  EXPECT_FALSE(url_2.DomainIs("google.com"));
}

// Newlines should be stripped from inputs.
TEST_F(GURLTest, Newlines) {
  // Constructor.
  GURL url_1(" \t ht\ntp://\twww.goo\rgle.com/as\ndf \n ");
  EXPECT_EQ("http://www.google.com/asdf", url_1.spec());
  EXPECT_FALSE(
      url_1.parsed_for_possibly_invalid_spec().potentially_dangling_markup);

  // Relative path resolver.
  GURL url_2 = url_1.Resolve(" \n /fo\to\r ");
  EXPECT_EQ("http://www.google.com/foo", url_2.spec());
  EXPECT_FALSE(
      url_2.parsed_for_possibly_invalid_spec().potentially_dangling_markup);

  // Constructor.
  GURL url_3(" \t ht\ntp://\twww.goo\rgle.com/as\ndf< \n ");
  EXPECT_EQ("http://www.google.com/asdf%3C", url_3.spec());
  EXPECT_TRUE(
      url_3.parsed_for_possibly_invalid_spec().potentially_dangling_markup);

  // Relative path resolver.
  GURL url_4 = url_1.Resolve(" \n /fo\to<\r ");
  EXPECT_EQ("http://www.google.com/foo%3C", url_4.spec());
  EXPECT_TRUE(
      url_4.parsed_for_possibly_invalid_spec().potentially_dangling_markup);

  // Note that newlines are NOT stripped from ReplaceComponents.
}

TEST_F(GURLTest, IsStandard) {
  GURL a("http:foo/bar");
  EXPECT_TRUE(a.IsStandard());

  GURL b("foo:bar/baz");
  EXPECT_FALSE(b.IsStandard());

  GURL c("foo://bar/baz");
  EXPECT_FALSE(c.IsStandard());

  GURL d("cid:bar@baz");
  EXPECT_FALSE(d.IsStandard());
}

TEST_F(GURLTest, SchemeIsHTTPOrHTTPS) {
  EXPECT_TRUE(GURL("http://bar/").SchemeIsHTTPOrHTTPS());
  EXPECT_TRUE(GURL("HTTPS://BAR").SchemeIsHTTPOrHTTPS());
  EXPECT_FALSE(GURL("ftp://bar/").SchemeIsHTTPOrHTTPS());
}

TEST_F(GURLTest, SchemeIsHTTPOrHTTPSAfterSwap) {
  GURL url1("http://example.com");
  GURL url2("ftp://example.com");

  EXPECT_TRUE(url1.SchemeIsHTTPOrHTTPS());
  EXPECT_FALSE(url2.SchemeIsHTTPOrHTTPS());

  url1.Swap(&url2);

  EXPECT_FALSE(url1.SchemeIsHTTPOrHTTPS());
  EXPECT_TRUE(url2.SchemeIsHTTPOrHTTPS());
}

TEST_F(GURLTest, SchemeIsHTTPOrHTTPSAfterReplaceComponents) {
  GURL url("http://example.com");
  EXPECT_TRUE(url.SchemeIsHTTPOrHTTPS());

  GURL::Replacements replacements;
  replacements.SetSchemeStr("ftp");
  GURL new_url = url.ReplaceComponents(replacements);

  EXPECT_FALSE(new_url.SchemeIsHTTPOrHTTPS());
}

TEST_F(GURLTest, SchemeIsHTTPOrHTTPSAfterResolve) {
  GURL url("http://example.com/foo");
  EXPECT_TRUE(url.SchemeIsHTTPOrHTTPS());

  GURL new_url = url.Resolve("bar");
  EXPECT_TRUE(new_url.SchemeIsHTTPOrHTTPS());

  GURL new_url2 = url.Resolve("ftp://elsewhere.com");
  EXPECT_FALSE(new_url2.SchemeIsHTTPOrHTTPS());
}

TEST_F(GURLTest, SchemeIsWSOrWSS) {
  EXPECT_TRUE(GURL("WS://BAR/").SchemeIsWSOrWSS());
  EXPECT_TRUE(GURL("wss://bar/").SchemeIsWSOrWSS());
  EXPECT_FALSE(GURL("http://bar/").SchemeIsWSOrWSS());
}

TEST_F(GURLTest, SchemeIsCryptographic) {
  EXPECT_TRUE(GURL("https://foo.bar.com/").SchemeIsCryptographic());
  EXPECT_TRUE(GURL("HTTPS://foo.bar.com/").SchemeIsCryptographic());
  EXPECT_TRUE(GURL("HtTpS://foo.bar.com/").SchemeIsCryptographic());

  EXPECT_TRUE(GURL("wss://foo.bar.com/").SchemeIsCryptographic());
  EXPECT_TRUE(GURL("WSS://foo.bar.com/").SchemeIsCryptographic());
  EXPECT_TRUE(GURL("WsS://foo.bar.com/").SchemeIsCryptographic());

  EXPECT_FALSE(GURL("http://foo.bar.com/").SchemeIsCryptographic());
  EXPECT_FALSE(GURL("ws://foo.bar.com/").SchemeIsCryptographic());
}

TEST_F(GURLTest, SchemeIsCryptographicStatic) {
  EXPECT_TRUE(GURL::SchemeIsCryptographic("https"));
  EXPECT_TRUE(GURL::SchemeIsCryptographic("wss"));
  EXPECT_FALSE(GURL::SchemeIsCryptographic("http"));
  EXPECT_FALSE(GURL::SchemeIsCryptographic("ws"));
  EXPECT_FALSE(GURL::SchemeIsCryptographic("ftp"));
}

TEST_F(GURLTest, SchemeIsBlob) {
  EXPECT_TRUE(GURL("BLOB://BAR/").SchemeIsBlob());
  EXPECT_TRUE(GURL("blob://bar/").SchemeIsBlob());
  EXPECT_FALSE(GURL("http://bar/").SchemeIsBlob());
}

TEST_F(GURLTest, SchemeIsLocal) {
  EXPECT_TRUE(GURL("BLOB://BAR/").SchemeIsLocal());
  EXPECT_TRUE(GURL("blob://bar/").SchemeIsLocal());
  EXPECT_TRUE(GURL("DATA:TEXT/HTML,BAR").SchemeIsLocal());
  EXPECT_TRUE(GURL("data:text/html,bar").SchemeIsLocal());
  EXPECT_TRUE(GURL("ABOUT:BAR").SchemeIsLocal());
  EXPECT_TRUE(GURL("about:bar").SchemeIsLocal());
  EXPECT_TRUE(GURL("FILESYSTEM:HTTP://FOO.EXAMPLE/BAR").SchemeIsLocal());
  EXPECT_TRUE(GURL("filesystem:http://foo.example/bar").SchemeIsLocal());

  EXPECT_FALSE(GURL("http://bar/").SchemeIsLocal());
  EXPECT_FALSE(GURL("file:///bar").SchemeIsLocal());
}

// Tests that the 'content' of the URL is properly extracted. This can be
// complex in cases such as multiple schemes (view-source:http:) or for
// javascript URLs. See GURL::GetContent for more details.
TEST_F(GURLTest, ContentForNonStandardURLs) {
  static constexpr struct TestCase {
    const char* url;
    const char* expected;
  } cases[] = {
      {"null", ""},
      {"not-a-standard-scheme:this is arbitrary content",
       "this is arbitrary content"},

      // When there are multiple schemes, only the first is excluded from the
      // content. Note also that for e.g. 'http://', the '//' is part of the
      // content not the scheme.
      {"view-source:http://example.com/path", "http://example.com/path"},
      {"blob:http://example.com/GUID", "http://example.com/GUID"},
      {"blob:http://user:password@example.com/GUID",
       "http://user:password@example.com/GUID"},

      // The octothorpe character ('#') marks the end of the URL content, and
      // the start of the fragment. It should not be included in the content.
      {"http://www.example.com/GUID#ref", "www.example.com/GUID"},
      {"http://me:secret@example.com/GUID/#ref", "me:secret@example.com/GUID/"},
      {"data:text/html,Question?<div style=\"color: #bad\">idea</div>",
       "text/html,Question?%3Cdiv%20style=%22color:%20"},

      // TODO(mkwst): This seems like a bug. https://crbug.com/513600
      {"filesystem:http://example.com/path", "/"},

      // Javascript URLs include '#' symbols in their content.
      {"javascript:#", "#"},
      {"javascript:alert('#');", "alert('#');"},

      // Test cases which Chromium used to handle wrongly.
      {"blob://http://example.com/GUID", "http//example.com/GUID"},
      {"git://host/path#fragment", "host/path"},
  };

  for (const auto& test : cases) {
    GURL url(test.url);
    EXPECT_EQ(test.expected, url.GetContent()) << test.url;
    EXPECT_EQ(test.expected, url.GetContentPiece()) << test.url;
  }
}

// Tests that the URL path is properly extracted for unusual URLs. This can be
// complex in cases such as multiple schemes (view-source:http:) or when
// octothorpes ('#') are involved.
TEST_F(GURLTest, PathForNonStandardURLs) {
  static constexpr struct TestCase {
    const char* url;
    const char* expected;
  } cases[] = {
      {"null", ""},
      {"not-a-standard-scheme:this is arbitrary content",
       "this is arbitrary content"},
      {"view-source:http://example.com/path", "http://example.com/path"},
      {"blob:http://example.com/GUID", "http://example.com/GUID"},
      {"blob:http://user:password@example.com/GUID",
       "http://user:password@example.com/GUID"},

      {"http://www.example.com/GUID#ref", "/GUID"},
      {"http://me:secret@example.com/GUID/#ref", "/GUID/"},
      {"data:text/html,Question?<div style=\"color: #bad\">idea</div>",
       "text/html,Question"},

      // Test cases which Chromium used to handle wrongly.
      {"blob://http://example.com/GUID", "//example.com/GUID"},
      {"git://host/path#fragment", "/path"},

      // TODO(mkwst): This seems like a bug. https://crbug.com/513600
      {"filesystem:http://example.com/path", "/"},
  };

  for (const auto& test : cases) {
    GURL url(test.url);
    EXPECT_EQ(test.expected, url.GetPath()) << test.url;
  }
}

TEST_F(GURLTest, EqualsIgnoringRef) {
  static constexpr struct {
    const char* url_a;
    const char* url_b;
    bool are_equals;
  } kTestCases[] = {
      // No ref.
      {"http://a.com", "http://a.com", true},
      {"http://a.com", "http://b.com", false},

      // Same Ref.
      {"http://a.com#foo", "http://a.com#foo", true},
      {"http://a.com#foo", "http://b.com#foo", false},

      // Different Refs.
      {"http://a.com#foo", "http://a.com#bar", true},
      {"http://a.com#foo", "http://b.com#bar", false},

      // One has a ref, the other doesn't.
      {"http://a.com#foo", "http://a.com", true},
      {"http://a.com#foo", "http://b.com", false},

      // Empty refs.
      {"http://a.com#", "http://a.com#", true},
      {"http://a.com#", "http://a.com", true},

      // URLs that differ only by their last character.
      {"http://aaa", "http://aab", false},
      {"http://aaa#foo", "http://aab#foo", false},

      // Different size of the part before the ref.
      {"http://123#a", "http://123456#a", false},

      // Blob URLs
      {"blob:http://a.com#foo", "blob:http://a.com#foo", true},
      {"blob:http://a.com#foo", "blob:http://a.com#bar", true},
      {"blob:http://a.com#foo", "blob:http://b.com#bar", false},

      // Filesystem URLs
      {"filesystem:http://a.com#foo", "filesystem:http://a.com#foo", true},
      {"filesystem:http://a.com#foo", "filesystem:http://a.com#bar", true},
      {"filesystem:http://a.com#foo", "filesystem:http://b.com#bar", false},

      // Data URLs
      {"data:text/html,a#foo", "data:text/html,a#bar", true},
      {"data:text/html,a#foo", "data:text/html,a#foo", true},
      {"data:text/html,a#foo", "data:text/html,b#foo", false},
  };

  for (const auto& test_case : kTestCases) {
    SCOPED_TRACE(testing::Message()
                 << std::endl
                 << "url_a = " << test_case.url_a << std::endl
                 << "url_b = " << test_case.url_b << std::endl);
    // A versus B.
    EXPECT_EQ(test_case.are_equals,
              GURL(test_case.url_a).EqualsIgnoringRef(GURL(test_case.url_b)));
    // B versus A.
    EXPECT_EQ(test_case.are_equals,
              GURL(test_case.url_b).EqualsIgnoringRef(GURL(test_case.url_a)));
  }
}

TEST_F(GURLTest, DebugAlias) {
  GURL url("https://foo.com/bar");
  DEBUG_ALIAS_FOR_GURL(url_debug_alias, url);
  EXPECT_STREQ("https://foo.com/bar", url_debug_alias);
}

TEST_F(GURLTest, InvalidHost) {
  // This contains an invalid percent escape (%T%) and also a valid
  // percent escape that's not 7-bit ascii (%ae), so that the unescaped
  // host contains both an invalid percent escape and invalid UTF-8.
  GURL url("http://%T%Ae");

  EXPECT_FALSE(url.is_valid());
  EXPECT_TRUE(url.SchemeIs(url::kHttpScheme));

  // The invalid percent escape becomes an escaped percent sign (%25), and the
  // invalid UTF-8 character becomes REPLACEMENT CHARACTER' (U+FFFD) encoded as
  // UTF-8.
  EXPECT_EQ(url.host(), "%25t%EF%BF%BD");
}

TEST_F(GURLTest, PortZero) {
  GURL port_zero_url("http://127.0.0.1:0/blah");

  // https://url.spec.whatwg.org/#port-state says that the port 1) consists of
  // ASCII digits (this excludes negative numbers) and 2) cannot be greater than
  // 2^16-1.  This means that port=0 should be valid.
  EXPECT_TRUE(port_zero_url.is_valid());
  EXPECT_EQ("0", port_zero_url.GetPort());
  EXPECT_EQ("127.0.0.1", port_zero_url.GetHost());
  EXPECT_EQ("http", port_zero_url.GetScheme());

  // https://crbug.com/1065532: SchemeHostPort would previously incorrectly
  // consider port=0 to be invalid.
  SchemeHostPort scheme_host_port(port_zero_url);
  EXPECT_TRUE(scheme_host_port.IsValid());
  EXPECT_EQ(port_zero_url.GetScheme(), scheme_host_port.scheme());
  EXPECT_EQ(port_zero_url.GetHost(), scheme_host_port.host());
  EXPECT_EQ(port_zero_url.GetPort(),
            base::NumberToString(scheme_host_port.port()));

  // https://crbug.com/1065532: The SchemeHostPort problem above would lead to
  // bizarre results below - resolved origin would incorrectly be returned as an
  // opaque origin derived from |another_origin|.
  url::Origin another_origin = url::Origin::Create(GURL("http://other.com"));
  url::Origin resolved_origin =
      url::Origin::Resolve(port_zero_url, another_origin);
  EXPECT_FALSE(resolved_origin.opaque());
  EXPECT_EQ(port_zero_url.GetScheme(), resolved_origin.scheme());
  EXPECT_EQ(port_zero_url.GetHost(), resolved_origin.host());
  EXPECT_EQ(port_zero_url.GetPort(),
            base::NumberToString(resolved_origin.port()));

  // port=0 and default HTTP port are different.
  GURL default_port("http://127.0.0.1/foo");
  EXPECT_EQ(0, SchemeHostPort(port_zero_url).port());
  EXPECT_EQ(80, SchemeHostPort(default_port).port());
  url::Origin default_port_origin = url::Origin::Create(default_port);
  EXPECT_FALSE(default_port_origin.IsSameOriginWith(resolved_origin));
}

class GURLTestTraits {
 public:
  using UrlType = GURL;

  static UrlType CreateUrlFromString(std::string_view s) { return GURL(s); }
  static bool IsAboutBlank(const UrlType& url) { return url.IsAboutBlank(); }
  static bool IsAboutSrcdoc(const UrlType& url) { return url.IsAboutSrcdoc(); }

  // Only static members.
  GURLTestTraits() = delete;
};

INSTANTIATE_TYPED_TEST_SUITE_P(GURL, AbstractUrlTest, GURLTestTraits);

}  // namespace url
