// Copyright 2018-2026 the Deno authors. MIT license.
import { assert, assertEquals } from "./test_util.ts";

// Regression test: set/delete on a list with many entries that share a name
// must run in linear time. A quadratic implementation would take many seconds
// (or minutes) at this size; the linear implementation finishes in
// milliseconds. The 5s bound is intentionally loose to avoid CI flakes.
Deno.test(function urlSearchParamsSetDeleteManyDuplicatesIsLinear() {
  const N = 20_000;
  const init: [string, string][] = [];
  for (let i = 0; i < N; i++) init.push(["dup", String(i)]);

  const a = new URLSearchParams(init);
  const t1 = performance.now();
  a.delete("dup");
  assert(performance.now() - t1 < 5_000);
  assertEquals(a.get("dup"), null);

  const b = new URLSearchParams(init);
  const t2 = performance.now();
  b.set("dup", "final");
  assert(performance.now() - t2 < 5_000);
  assertEquals(b.get("dup"), "final");
  assertEquals(b.getAll("dup").length, 1);
});

Deno.test(function urlSearchParamsWithMultipleSpaces() {
  const init = { str: "this string has spaces in it" };
  const searchParams = new URLSearchParams(init).toString();
  assertEquals(searchParams, "str=this+string+has+spaces+in+it");
});

Deno.test(function urlSearchParamsWithExclamation() {
  const init = [
    ["str", "hello, world!"],
  ];
  const searchParams = new URLSearchParams(init).toString();
  assertEquals(searchParams, "str=hello%2C+world%21");
});

Deno.test(function urlSearchParamsWithQuotes() {
  const init = [
    ["str", "'hello world'"],
  ];
  const searchParams = new URLSearchParams(init).toString();
  assertEquals(searchParams, "str=%27hello+world%27");
});

Deno.test(function urlSearchParamsWithBracket() {
  const init = [
    ["str", "(hello world)"],
  ];
  const searchParams = new URLSearchParams(init).toString();
  assertEquals(searchParams, "str=%28hello+world%29");
});

Deno.test(function urlSearchParamsWithTilde() {
  const init = [
    ["str", "hello~world"],
  ];
  const searchParams = new URLSearchParams(init).toString();
  assertEquals(searchParams, "str=hello%7Eworld");
});

Deno.test(function urlSearchParamsInitString() {
  const init = "c=4&a=2&b=3&%C3%A1=1";
  const searchParams = new URLSearchParams(init);
  assert(
    init === searchParams.toString(),
    "The init query string does not match",
  );
});

Deno.test(function urlSearchParamsInitStringWithPlusCharacter() {
  let params = new URLSearchParams("q=a+b");
  assertEquals(params.toString(), "q=a+b");
  assertEquals(params.get("q"), "a b");

  params = new URLSearchParams("q=a+b+c");
  assertEquals(params.toString(), "q=a+b+c");
  assertEquals(params.get("q"), "a b c");
});

Deno.test(function urlSearchParamsInitStringWithMalformedParams() {
  // These test cases are copied from Web Platform Tests
  // https://github.com/web-platform-tests/wpt/blob/54c6d64/url/urlsearchparams-constructor.any.js#L60-L80
  let params = new URLSearchParams("id=0&value=%");
  assert(params != null, "constructor returned non-null value.");
  assert(params.has("id"), 'Search params object has name "id"');
  assert(params.has("value"), 'Search params object has name "value"');
  assertEquals(params.get("id"), "0");
  assertEquals(params.get("value"), "%");

  params = new URLSearchParams("b=%2sf%2a");
  assert(params != null, "constructor returned non-null value.");
  assert(params.has("b"), 'Search params object has name "b"');
  assertEquals(params.get("b"), "%2sf*");

  params = new URLSearchParams("b=%2%2af%2a");
  assert(params != null, "constructor returned non-null value.");
  assert(params.has("b"), 'Search params object has name "b"');
  assertEquals(params.get("b"), "%2*f*");

  params = new URLSearchParams("b=%%2a");
  assert(params != null, "constructor returned non-null value.");
  assert(params.has("b"), 'Search params object has name "b"');
  assertEquals(params.get("b"), "%*");
});

Deno.test(function urlSearchParamsInitIterable() {
  const init = [
    ["a", "54"],
    ["b", "true"],
  ];
  const searchParams = new URLSearchParams(init);
  assertEquals(searchParams.toString(), "a=54&b=true");
});

Deno.test(function urlSearchParamsInitRecord() {
  const init = { a: "54", b: "true" };
  const searchParams = new URLSearchParams(init);
  assertEquals(searchParams.toString(), "a=54&b=true");
});

Deno.test(function urlSearchParamsInit() {
  const params1 = new URLSearchParams("a=b");
  assertEquals(params1.toString(), "a=b");
  const params2 = new URLSearchParams(params1);
  assertEquals(params2.toString(), "a=b");
});

Deno.test(function urlSearchParamsAppendSuccess() {
  const searchParams = new URLSearchParams();
  searchParams.append("a", "true");
  assertEquals(searchParams.toString(), "a=true");
});

Deno.test(function urlSearchParamsDeleteSuccess() {
  const init = "a=54&b=true";
  const searchParams = new URLSearchParams(init);
  searchParams.delete("b");
  assertEquals(searchParams.toString(), "a=54");
});

Deno.test(function urlSearchParamsGetAllSuccess() {
  const init = "a=54&b=true&a=true";
  const searchParams = new URLSearchParams(init);
  assertEquals(searchParams.getAll("a"), ["54", "true"]);
  assertEquals(searchParams.getAll("b"), ["true"]);
  assertEquals(searchParams.getAll("c"), []);
});

Deno.test(function urlSearchParamsGetSuccess() {
  const init = "a=54&b=true&a=true";
  const searchParams = new URLSearchParams(init);
  assertEquals(searchParams.get("a"), "54");
  assertEquals(searchParams.get("b"), "true");
  assertEquals(searchParams.get("c"), null);
});

Deno.test(function urlSearchParamsHasSuccess() {
  const init = "a=54&b=true&a=true";
  const searchParams = new URLSearchParams(init);
  assert(searchParams.has("a"));
  assert(searchParams.has("b"));
  assert(!searchParams.has("c"));
});

Deno.test(function urlSearchParamsSetReplaceFirstAndRemoveOthers() {
  const init = "a=54&b=true&a=true";
  const searchParams = new URLSearchParams(init);
  searchParams.set("a", "false");
  assertEquals(searchParams.toString(), "a=false&b=true");
});

Deno.test(function urlSearchParamsSetAppendNew() {
  const init = "a=54&b=true&a=true";
  const searchParams = new URLSearchParams(init);
  searchParams.set("c", "foo");
  assertEquals(searchParams.toString(), "a=54&b=true&a=true&c=foo");
});

Deno.test(function urlSearchParamsSortSuccess() {
  const init = "c=4&a=2&b=3&a=1";
  const searchParams = new URLSearchParams(init);
  searchParams.sort();
  assertEquals(searchParams.toString(), "a=2&a=1&b=3&c=4");
});

Deno.test(function urlSearchParamsForEachSuccess() {
  const init = [
    ["a", "54"],
    ["b", "true"],
  ];
  const searchParams = new URLSearchParams(init);
  let callNum = 0;
  searchParams.forEach((value, key, parent) => {
    assertEquals(searchParams, parent);
    assertEquals(value, init[callNum][1]);
    assertEquals(key, init[callNum][0]);
    callNum++;
  });
  assertEquals(callNum, init.length);
});

Deno.test(function urlSearchParamsMissingName() {
  const init = "=4";
  const searchParams = new URLSearchParams(init);
  assertEquals(searchParams.get(""), "4");
  assertEquals(searchParams.toString(), "=4");
});

Deno.test(function urlSearchParamsMissingValue() {
  const init = "4=";
  const searchParams = new URLSearchParams(init);
  assertEquals(searchParams.get("4"), "");
  assertEquals(searchParams.toString(), "4=");
});

Deno.test(function urlSearchParamsMissingEqualSign() {
  const init = "4";
  const searchParams = new URLSearchParams(init);
  assertEquals(searchParams.get("4"), "");
  assertEquals(searchParams.toString(), "4=");
});

Deno.test(function urlSearchParamsMissingPair() {
  const init = "c=4&&a=54&";
  const searchParams = new URLSearchParams(init);
  assertEquals(searchParams.toString(), "c=4&a=54");
});

Deno.test(function urlSearchParamsForShortEncodedChar() {
  const init = { linefeed: "\n", tab: "\t" };
  const searchParams = new URLSearchParams(init);
  assertEquals(searchParams.toString(), "linefeed=%0A&tab=%09");
});

// If pair does not contain exactly two items, then throw a TypeError.
// ref https://url.spec.whatwg.org/#interface-urlsearchparams
Deno.test(function urlSearchParamsShouldThrowTypeError() {
  let hasThrown = 0;

  try {
    new URLSearchParams([["1"]]);
    hasThrown = 1;
  } catch (err) {
    if (err instanceof TypeError) {
      hasThrown = 2;
    } else {
      hasThrown = 3;
    }
  }

  assertEquals(hasThrown, 2);

  try {
    new URLSearchParams([["1", "2", "3"]]);
    hasThrown = 1;
  } catch (err) {
    if (err instanceof TypeError) {
      hasThrown = 2;
    } else {
      hasThrown = 3;
    }
  }

  assertEquals(hasThrown, 2);
});

Deno.test(function urlSearchParamsAppendArgumentsCheck() {
  const methodRequireOneParam = ["delete", "getAll", "get", "has", "forEach"];

  const methodRequireTwoParams = ["append", "set"];

  methodRequireOneParam
    .concat(methodRequireTwoParams)
    .forEach((method: string) => {
      const searchParams = new URLSearchParams();
      let hasThrown = 0;
      try {
        // deno-lint-ignore no-explicit-any
        (searchParams as any)[method]();
        hasThrown = 1;
      } catch (err) {
        if (err instanceof TypeError) {
          hasThrown = 2;
        } else {
          hasThrown = 3;
        }
      }
      assertEquals(hasThrown, 2);
    });

  methodRequireTwoParams.forEach((method: string) => {
    const searchParams = new URLSearchParams();
    let hasThrown = 0;
    try {
      // deno-lint-ignore no-explicit-any
      (searchParams as any)[method]("foo");
      hasThrown = 1;
    } catch (err) {
      if (err instanceof TypeError) {
        hasThrown = 2;
      } else {
        hasThrown = 3;
      }
    }
    assertEquals(hasThrown, 2);
  });
});

// ref: https://github.com/web-platform-tests/wpt/blob/master/url/urlsearchparams-delete.any.js
Deno.test(function urlSearchParamsDeletingAppendedMultiple() {
  const params = new URLSearchParams();
  params.append("first", (1 as unknown) as string);
  assert(params.has("first"));
  assertEquals(params.get("first"), "1");
  params.delete("first");
  assertEquals(params.has("first"), false);
  params.append("first", (1 as unknown) as string);
  params.append("first", (10 as unknown) as string);
  params.delete("first");
  assertEquals(params.has("first"), false);
});

// ref: https://github.com/web-platform-tests/wpt/blob/master/url/urlsearchparams-constructor.any.js#L176-L182
Deno.test(function urlSearchParamsCustomSymbolIterator() {
  const params = new URLSearchParams();
  params[Symbol.iterator] = function* (): URLSearchParamsIterator<
    [string, string]
  > {
    yield ["a", "b"];
  };
  const params1 = new URLSearchParams((params as unknown) as string[][]);
  assertEquals(params1.get("a"), "b");
});

Deno.test(
  function urlSearchParamsCustomSymbolIteratorWithNonStringParams() {
    const params = {};
    // deno-lint-ignore no-explicit-any
    (params as any)[Symbol.iterator] = function* (): IterableIterator<
      [number, number]
    > {
      yield [1, 2];
    };
    const params1 = new URLSearchParams((params as unknown) as string[][]);
    assertEquals(params1.get("1"), "2");
  },
);

// If a class extends URLSearchParams, override one method should not change another's behavior.
Deno.test(
  function urlSearchParamsOverridingAppendNotChangeConstructorAndSet() {
    let overriddenAppendCalled = 0;
    class CustomSearchParams extends URLSearchParams {
      override append(name: string, value: string) {
        ++overriddenAppendCalled;
        super.append(name, value);
      }
    }
    new CustomSearchParams("foo=bar");
    new CustomSearchParams([["foo", "bar"]]);
    new CustomSearchParams(new CustomSearchParams({ foo: "bar" }));
    new CustomSearchParams().set("foo", "bar");
    assertEquals(overriddenAppendCalled, 0);
  },
);

Deno.test(function urlSearchParamsOverridingEntriesNotChangeForEach() {
  class CustomSearchParams extends URLSearchParams {
    override *entries(): URLSearchParamsIterator<[string, string]> {
      yield* [];
    }
  }
  let loopCount = 0;
  const params = new CustomSearchParams({ foo: "bar" });
  params.forEach(() => void ++loopCount);
  assertEquals(loopCount, 1);
});

Deno.test(function urlSearchParamsInitUndefinedIsEmpty() {
  assertEquals(new URLSearchParams(undefined).toString(), "");
  assertEquals(new URLSearchParams().toString(), "");
});

// `null` is a value, not a missing argument: WebIDL union resolution sends it
// to the USVString overload, since `record` is not a dictionary type.
Deno.test(function urlSearchParamsInitNullIsStringified() {
  assertEquals(new URLSearchParams(null!).toString(), "null=");
  assertEquals(new URLSearchParams(null!).get("null"), "");
});
