// getAndExpireCookiesForDefaultPathTest is a helper method to get and delete
// cookies using echo-cookie.html.
async function getAndExpireCookiesForDefaultPathTest() {
  return new Promise((resolve, reject) => {
    try {
      const iframe = document.createElement('iframe');
      iframe.style = 'display: none';
      iframe.src = '/cookies/resources/echo-cookie.html';
      iframe.addEventListener('load', (e) => {
        const win = e.target.contentWindow;
        const iframeCookies = win.getCookies();
        win.expireCookies().then(() => {
          document.documentElement.removeChild(iframe);
          resolve(iframeCookies);
        });
      }, {once: true});
      document.documentElement.appendChild(iframe);
    } catch (e) {
      reject(e);
    }
  });
}

// getAndExpireCookiesForRedirectTest is a helper method to get and delete
// cookies that were set from a Location header redirect.
async function getAndExpireCookiesForRedirectTest(location) {
  return new Promise((resolve, reject) => {
    try {
      const iframe = document.createElement('iframe');
      iframe.style = 'display: none';
      iframe.src = location;
      const listener = (e) => {
        if (typeof e.data == 'object' && 'cookies' in e.data) {
          window.removeEventListener('message', listener);
          document.documentElement.removeChild(iframe);
          resolve(e.data.cookies);
        }
      };
      window.addEventListener('message', listener);
      iframe.addEventListener('load', (e) => {
        e.target.contentWindow.postMessage('getAndExpireCookiesForRedirectTest', '*');
      }, {once: true});
      document.documentElement.appendChild(iframe);
    } catch (e) {
      reject(e);
    }
  });
}

// httpCookieTest sets a `cookie` (via HTTP), then asserts it was or was not set
// via `expectedValue` (via the DOM). Then cleans it up (via test driver). Most
// tests do not set a Path attribute, so `defaultPath` defaults to true. If the
// cookie values are expected to cause the HTTP request or response to fail, the
// test can be made to pass when this happens via `allowFetchFailure`, which
// defaults to false.
//
// `cookie` may be a single cookie string, or an array of cookie strings, where
// the order of the array items represents the order of the Set-Cookie headers
// sent by the server.
//
// Note: this function has a dependency on testdriver.js. Any test files calling
// it should include testdriver.js and testdriver-vendor.js
function httpCookieTest(cookie, expectedValue, name, defaultPath = true,
                        allowFetchFailure = false) {
  return promise_test((t) => {
    var skipAssertions = false;
    return new Promise(async (resolve, reject) => {
      // The result is ignored as we're expiring cookies for cleaning here.
      await getAndExpireCookiesForDefaultPathTest();
      await test_driver.delete_all_cookies();
      t.add_cleanup(test_driver.delete_all_cookies);

      let encodedCookie = encodeURIComponent(JSON.stringify(cookie));
      try {
        await fetch(`/cookies/resources/cookie.py?set=${encodedCookie}`);
      } catch {
        if (allowFetchFailure) {
          skipAssertions = true;
        } else {
          reject('Failed to fetch /cookies/resources/cookie.py');
        }
      }
      let cookies = document.cookie;
      if (defaultPath) {
        // for the tests where a Path is set from the request-uri
        // path, we need to go look for cookies in an iframe at that
        // default path.
        cookies = await getAndExpireCookiesForDefaultPathTest();
      }
      resolve(cookies);
    }).then((cookies) => {
      if (skipAssertions) {
        return;
      }
      if (Boolean(expectedValue)) {
        assert_equals(cookies, expectedValue, 'The cookie was set as expected.');
      } else {
        assert_equals(cookies, expectedValue, 'The cookie was rejected.');
      }
    });
  }, name);
}

// This is a variation on httpCookieTest, where a redirect happens via
// the Location header and we check to see if cookies are sent via
// getRedirectedCookies
//
// Note: the locations targeted by this function have a dependency on
// path-redirect-shared.js and should be sure to include it.
function httpRedirectCookieTest(cookie, expectedValue, name, location) {
  return promise_test(async (t) => {
    // The result is ignored as we're expiring cookies for cleaning here.
    await getAndExpireCookiesForRedirectTest(location);

    const encodedCookie = encodeURIComponent(JSON.stringify(cookie));
    const encodedLocation = encodeURIComponent(location);
    const setParams = `?set=${encodedCookie}&location=${encodedLocation}`;
    await fetch(`/cookies/resources/cookie.py${setParams}`);
    // for the tests where a redirect happens, we need to head
    // to that URI to get the cookies (and then delete them there)
    const cookies = await getAndExpireCookiesForRedirectTest(location);
    if (Boolean(expectedValue)) {
      assert_equals(cookies, expectedValue, 'The cookie was set as expected.');
    } else {
      assert_equals(cookies, expectedValue, 'The cookie was rejected.');
    }
  }, name);
}

// Sets a `cookie` via the DOM, checks it against `expectedValue` via the DOM,
// then cleans it up via the DOM. This is needed in cases where going through
// HTTP headers may modify the cookie line (e.g. by stripping control
// characters).
//
// Note: this function has a dependency on testdriver.js. Any test files calling
// it should include testdriver.js and testdriver-vendor.js
function domCookieTest(cookie, expectedValue, name) {
  return promise_test(async (t) => {
    await test_driver.delete_all_cookies();
    t.add_cleanup(test_driver.delete_all_cookies);

    if (typeof cookie === "string") {
      document.cookie = cookie;
    } else if (Array.isArray(cookie)) {
      for (const singlecookie of cookie) {
        document.cookie = singlecookie;
      }
    } else {
      throw new Error('Unexpected type passed into domCookieTest as cookie: ' + typeof cookie);
    }
    let cookies = document.cookie;
    assert_equals(cookies, expectedValue, Boolean(expectedValue) ?
                                          'The cookie was set as expected.' :
                                          'The cookie was rejected.');
  }, name);
}

// Returns an array of control characters along with their ASCII codes. Control
// characters are defined by RFC 5234 to be %x00-1F / %x7F.
function getCtlCharacters() {
  const ctlCodes = [...Array(0x20).keys()]
                       .concat([0x7F]);
  return ctlCodes.map(i => ({ code: i, chr: String.fromCharCode(i) }))
}

// Returns a cookie string with name set to "t" * nameLength and value
// set to "1" * valueLength. Passing in 0 for either allows for creating
// a name- or value-less cookie.
//
// Note: Cookie length checking should ignore the "=".
function cookieStringWithNameAndValueLengths(nameLength, valueLength) {
  return `${"t".repeat(nameLength)}=${"1".repeat(valueLength)}`;
}

// Finds the root window.top.opener and directs test_driver commands to it.
//
// If you see a message like: "Error: Tried to run in a non-testharness window
// without a call to set_test_context." then you probably need to call this.
function setTestContextUsingRootWindow() {
  let test_window = window.top;
  while (test_window.opener && !test_window.opener.closed) {
    test_window = test_window.opener.top;
  }
  test_driver.set_test_context(test_window);
}
