const kValidAvailabilities =
    ['unavailable', 'downloadable', 'downloading', 'available'];
const kAvailableAvailabilities = ['downloadable', 'downloading', 'available'];

const kTestPrompt = 'Please write a sentence in English.';
const kTestContext = 'This is a test; this is only a test.';

const getId = (() => {
  let idCount = 0;
  return () => idCount++;
})();

// Takes an array of dictionaries mapping keys to value arrays, e.g.:
//   [ {Shape: ["Square", "Circle", undefined]}, {Count: [1, 2]} ]
// Returns an array of dictionaries with all value combinations, i.e.:
//  [ {Shape: "Square", Count: 1}, {Shape: "Square", Count: 2},
//    {Shape: "Circle", Count: 1}, {Shape: "Circle", Count: 2},
//    {Shape: undefined, Count: 1}, {Shape: undefined, Count: 2} ]
// Omits dictionary members when the value is undefined; supports array values.
function generateOptionCombinations(optionsSpec) {
  // 1. Extract keys from the input specification.
  const keys = optionsSpec.map(o => Object.keys(o)[0]);
  // 2. Extract the arrays of possible values for each key.
  const valueArrays = optionsSpec.map(o => Object.values(o)[0]);
  // 3. Compute the Cartesian product of the value arrays using reduce.
  const valueCombinations = valueArrays.reduce((accumulator, currentValues) => {
    // Init the empty accumulator (first iteration), with single-element
    // arrays.
    if (accumulator.length === 0) {
      return currentValues.map(value => [value]);
    }
    // Otherwise, expand existing combinations with current values.
    return accumulator.flatMap(
        existingCombo => currentValues.map(
            currentValue => [...existingCombo, currentValue]));
  }, []);

  // 4. Map each value combination to a result dictionary, skipping
  // undefined.
  return valueCombinations.map(combination => {
    const result = {};
    keys.forEach((key, index) => {
      if (combination[index] !== undefined) {
        result[key] = combination[index];
      }
    });
    return result;
  });
}

// The method should take the AbortSignal as an option and return a promise.
async function testAbortPromise(t, method) {
  // Test abort signal without custom error.
  {
    const controller = new AbortController();
    const promise = method(controller.signal);
    controller.abort();
    await promise_rejects_dom(t, 'AbortError', promise);

    // Using the same aborted controller will get the `AbortError` as well.
    const anotherPromise = method(controller.signal);
    await promise_rejects_dom(t, 'AbortError', anotherPromise);
  }

  // Test abort signal with custom error.
  {
    const err = new Error('test');
    const controller = new AbortController();
    const promise = method(controller.signal);
    controller.abort(err);
    await promise_rejects_exactly(t, err, promise);

    // Using the same aborted controller will get the same error as well.
    const anotherPromise = method(controller.signal);
    await promise_rejects_exactly(t, err, anotherPromise);
  }
};

async function testCreateMonitorWithAbortAt(
    t, loadedToAbortAt, method, options = {}) {
  const {promise: eventPromise, resolve} = Promise.withResolvers();
  let hadEvent = false;
  function monitor(m) {
    m.addEventListener('downloadprogress', e => {
      if (e.loaded != loadedToAbortAt) {
        return;
      }

      if (hadEvent) {
        assert_unreached(
            'This should never be reached since LanguageDetector.create() was aborted.');
        return;
      }

      resolve();
      hadEvent = true;
    });
  }

  const controller = new AbortController();

  const createPromise =
      method({...options, monitor, signal: controller.signal});

  await eventPromise;

  const err = new Error('test');
  controller.abort(err);
  await promise_rejects_exactly(t, err, createPromise);
}

async function testCreateMonitorWithAbort(t, method, options = {}) {
  await testCreateMonitorWithAbortAt(t, 0, method, options);
  await testCreateMonitorWithAbortAt(t, 1, method, options);
}

// The method should take the AbortSignal as an option and return a
// ReadableStream.
async function testAbortReadableStream(t, method) {
  // Test abort signal without custom error.
  {
    const controller = new AbortController();
    const stream = method(controller.signal);
    controller.abort();
    let writableStream = new WritableStream();
    await promise_rejects_dom(t, 'AbortError', stream.pipeTo(writableStream));

    // Using the same aborted controller will get the `AbortError` as well.
    await promise_rejects_dom(t, 'AbortError', new Promise(() => {
                                method(controller.signal);
                              }));
  }

  // Test abort signal with custom error.
  {
    const error = new DOMException('test', 'VersionError');
    const controller = new AbortController();
    const stream = method(controller.signal);
    controller.abort(error);
    let writableStream = new WritableStream();
    await promise_rejects_exactly(t, error, stream.pipeTo(writableStream));

    // Using the same aborted controller will get the same error.
    await promise_rejects_exactly(t, error, new Promise(() => {
                                    method(controller.signal);
                                  }));
  }
};

async function testMonitor(createFunc, options = {}) {
  let created = false;
  const progressEvents = [];
  function monitor(m) {
    m.addEventListener('downloadprogress', e => {
      // No progress events should be fired after `createFunc` resolves.
      assert_false(created);

      progressEvents.push(e);
    });
  }

  result = await createFunc({...options, monitor});
  created = true;

  assert_greater_than_equal(progressEvents.length, 2);
  assert_equals(progressEvents.at(0).loaded, 0);
  assert_equals(progressEvents.at(-1).loaded, 1);

  let lastProgressEventLoaded = -1;
  for (const progressEvent of progressEvents) {
    assert_equals(progressEvent.lengthComputable, true);
    assert_equals(progressEvent.total, 1);
    assert_less_than_equal(progressEvent.loaded, progressEvent.total);

    // `loaded` must be rounded to the nearest 0x10000th.
    assert_equals(progressEvent.loaded % (1 / 0x10000), 0);

    // Progress events should have monotonically increasing `loaded` values.
    assert_greater_than(progressEvent.loaded, lastProgressEventLoaded);
    lastProgressEventLoaded = progressEvent.loaded;
  }
  return result;
}

function run_iframe_test(iframe, test_name) {
  const id = getId();
  iframe.contentWindow.postMessage({id, type: test_name}, '*');
  const {promise, resolve, reject} = Promise.withResolvers();
  window.onmessage = message => {
    if (message.data.id !== id) {
      return;
    }
    if (message.data.success) {
      resolve(message.data.success);
    } else {
      reject(message.data.err)
    }
  };
  return promise;
}

function load_iframe(src, permission_policy) {
  let iframe = document.createElement('iframe');
  const {promise, resolve} = Promise.withResolvers();
  iframe.onload = () => {
    resolve(iframe);
  };
  iframe.src = src;
  iframe.allow = permission_policy;
  document.body.appendChild(iframe);
  return promise;
}

async function createSummarizer(options = {}) {
  await test_driver.bless();
  return await Summarizer.create(options);
}

async function createWriter(options = {}) {
  await test_driver.bless();
  return await Writer.create(options);
}

async function createRewriter(options = {}) {
  await test_driver.bless();
  return await Rewriter.create(options);
}
