'use strict';

// TODO(jsbell): Once ServiceWorker is supported, add arbitrary path coverage.
const kPath = location.pathname.replace(/[^/]+$/, '');

// True when running in a document context as opposed to a worker context
const kHasDocument = typeof document !== 'undefined';

// True when running on unsecured 'http:' rather than secured 'https:'.
const kIsUnsecured = location.protocol !== 'https:';

const kCookieHelperCgi = 'resources/cookie_helper.py';

// Approximate async equivalent to the document.cookie getter but with
// important differences: optional additional getAll arguments are
// forwarded, and an empty cookie jar returns undefined.
//
// This is intended primarily for verification against expected cookie
// jar contents. It should produce more readable messages using
// assert_equals in failing cases than assert_object_equals would
// using parsed cookie jar contents and also allows expectations to be
// written more compactly.
async function getCookieString(...args) {
  const cookies = await cookieStore.getAll(...args);
  return cookies.length
    ? cookies.map(({name, value}) =>
                  (name ? (name + '=') : '') + value).join('; ')
    : undefined;
}

// Approximate async equivalent to the document.cookie getter but from
// the server's point of view. Returns UTF-8 interpretation. Allows
// sub-path to be specified.
//
// Unlike document.cookie, this returns undefined when no cookies are
// present.
async function getCookieStringHttp(extraPath = null) {
  const url =
        kCookieHelperCgi + ((extraPath == null) ? '' : ('/' + extraPath));
  const response = await fetch(url, { credentials: 'include' });
  const text = await response.text();
  assert_equals(
      response.ok,
      true,
      'CGI should have succeeded in getCookieStringHttp\n' + text);
  assert_equals(
      response.headers.get('content-type'),
      'text/plain; charset=utf-8',
      'CGI did not return UTF-8 text in getCookieStringHttp');
  if (text === '')
    return undefined;
  assert_equals(
      text.indexOf('cookie='),
      0,
      'CGI response did not begin with "cookie=" and was not empty: ' + text);
  return decodeURIComponent(text.replace(/^cookie=/, ''));
}

// Approximate async equivalent to the document.cookie getter but from
// the server's point of view. Returns binary string
// interpretation. Allows sub-path to be specified.
//
// Unlike document.cookie, this returns undefined when no cookies are
// present.
async function getCookieBinaryHttp(extraPath = null) {
  const url =
        kCookieHelperCgi +
        ((extraPath == null) ?
         '' :
         ('/' + extraPath)) + '?charset=iso-8859-1';
  const response = await fetch(url, { credentials: 'include' });
  const text = await response.text();
  assert_equals(
      response.ok,
      true,
      'CGI should have succeeded in getCookieBinaryHttp\n' + text);
  assert_equals(
      response.headers.get('content-type'),
      'text/plain; charset=iso-8859-1',
      'CGI did not return ISO 8859-1 text in getCookieBinaryHttp');
  if (text === '')
    return undefined;
  assert_equals(
      text.indexOf('cookie='),
      0,
      'CGI response did not begin with "cookie=" and was not empty: ' + text);
  return unescape(text.replace(/^cookie=/, ''));
}

// Approximate async equivalent to the document.cookie setter but from
// the server's point of view.
async function setCookieStringHttp(setCookie) {
  const encodedSetCookie = encodeURIComponent(setCookie);
  const url = kCookieHelperCgi;
  const headers = new Headers();
  headers.set(
      'content-type',
      'application/x-www-form-urlencoded; charset=utf-8');
  const response = await fetch(
      url,
      {
        credentials: 'include',
        method: 'POST',
        headers: headers,
        body: 'set-cookie=' + encodedSetCookie,
      });
  const text = await response.text();
  assert_equals(
      response.ok,
      true,
      'CGI should have succeeded in setCookieStringHttp set-cookie: ' +
        setCookie + '\n' + text);
  assert_equals(
      response.headers.get('content-type'),
      'text/plain; charset=utf-8',
      'CGI did not return UTF-8 text in setCookieStringHttp');
  assert_equals(
      text,
      'set-cookie=' + encodedSetCookie,
      'CGI did not faithfully echo the set-cookie value');
}

// Approximate async equivalent to the document.cookie setter but from
// the server's point of view. This version sets a binary cookie rather
// than a UTF-8 one.
async function setCookieBinaryHttp(setCookie) {
  const encodedSetCookie = escape(setCookie).split('/').join('%2F');
  const url = kCookieHelperCgi + '?charset=iso-8859-1';
  const headers = new Headers();
  headers.set(
      'content-type',
      'application/x-www-form-urlencoded; charset=iso-8859-1');
  const response = await fetch(url, {
    credentials: 'include',
    method: 'POST',
    headers: headers,
    body: 'set-cookie=' + encodedSetCookie
  });
  const text = await response.text();
  assert_equals(
      response.ok,
      true,
      'CGI should have succeeded in setCookieBinaryHttp set-cookie: ' +
        setCookie + '\n' + text);
  assert_equals(
      response.headers.get('content-type'),
      'text/plain; charset=iso-8859-1',
      'CGI did not return Latin-1 text in setCookieBinaryHttp');
  assert_equals(
      text,
      'set-cookie=' + encodedSetCookie,
      'CGI did not faithfully echo the set-cookie value');
}

// Async document.cookie getter; converts '' to undefined which loses
// information in the edge case where a single ''-valued anonymous
// cookie is visible.
async function getCookieStringDocument() {
  if (!kHasDocument)
    throw 'document.cookie not available in this context';
  return String(document.cookie || '') || undefined;
}

// Async document.cookie setter
async function setCookieStringDocument(setCookie) {
  if (!kHasDocument)
    throw 'document.cookie not available in this context';
  document.cookie = setCookie;
}

// Observe the next 'change' event on the cookieStore. Typical usage:
//
//   const eventPromise = observeNextCookieChangeEvent();
//   await /* something that modifies cookies */
//   await verifyCookieChangeEvent(
//     eventPromise, {changed: [{name: 'name', value: 'value'}]});
//
function observeNextCookieChangeEvent() {
  return new Promise(resolve => {
    cookieStore.addEventListener('change', e => resolve(e), {once: true});
  });
}

async function verifyCookieChangeEvent(eventPromise, expected, description) {
  description = description ? description + ': ' : '';
  expected = Object.assign({changed:[], deleted:[]}, expected);
  const event = await eventPromise;
  assert_equals(event.changed.length, expected.changed.length,
               description + 'number of changed cookies');
  for (let i = 0; i < event.changed.length; ++i) {
    assert_equals(event.changed[i].name, expected.changed[i].name,
                 description + 'changed cookie name');
    assert_equals(event.changed[i].value, expected.changed[i].value,
                 description + 'changed cookie value');
  }
  assert_equals(event.deleted.length, expected.deleted.length,
               description + 'number of deleted cookies');
  for (let i = 0; i < event.deleted.length; ++i) {
    assert_equals(event.deleted[i].name, expected.deleted[i].name,
                 description + 'deleted cookie name');
    assert_equals(event.deleted[i].value, expected.deleted[i].value,
                 description + 'deleted cookie value');
  }
}

// Helper function for promise_test with cookies; cookies
// named in these tests are cleared before/after the test
// body function is executed.
async function cookie_test(func, description) {

  // Wipe cookies used by tests before and after the test.
  async function deleteAllCookies() {
    (await cookieStore.getAll()).forEach(({name, value}) => {
      cookieStore.delete(name);
    });
  }

  return promise_test(async t => {
    await deleteAllCookies();
    try {
      return await func(t);
    } finally {
      await deleteAllCookies();
    }
  }, description);
}
