// Flags: --no-warnings --expose-internals
'use strict';
const common = require('../common');
const assert = require('assert');
const timers = require('timers');
const { promisify } = require('util');
const child_process = require('child_process');

// TODO(benjamingr) - refactor to use getEventListeners when #35991 lands
const { NodeEventTarget } = require('internal/event_target');

const timerPromises = require('timers/promises');

const setPromiseTimeout = promisify(timers.setTimeout);
const exec = promisify(child_process.exec);

const { setInterval } = timerPromises;

process.on('multipleResolves', common.mustNotCall());

{
  const iterable = setInterval(1, undefined);
  const iterator = iterable[Symbol.asyncIterator]();
  const promise = iterator.next();
  promise.then(common.mustCall((result) => {
    assert.ok(!result.done, 'iterator was wrongly marked as done');
    assert.strictEqual(result.value, undefined);
    return iterator.return();
  })).then(common.mustCall());
}

{
  const iterable = setInterval(1, 'foobar');
  const iterator = iterable[Symbol.asyncIterator]();
  const promise = iterator.next();
  promise.then(common.mustCall((result) => {
    assert.ok(!result.done, 'iterator was wronly marked as done');
    assert.strictEqual(result.value, 'foobar');
    return iterator.return();
  })).then(common.mustCall());
}

{
  const iterable = setInterval(1, 'foobar');
  const iterator = iterable[Symbol.asyncIterator]();
  const promise = iterator.next();
  promise
    .then(common.mustCall((result) => {
      assert.ok(!result.done, 'iterator was wronly marked as done');
      assert.strictEqual(result.value, 'foobar');
      return iterator.next();
    }))
    .then(common.mustCall((result) => {
      assert.ok(!result.done, 'iterator was wrongly marked as done');
      assert.strictEqual(result.value, 'foobar');
      return iterator.return();
    }))
    .then(common.mustCall());
}

{
  const signal = AbortSignal.abort(); // Abort in advance

  const iterable = setInterval(1, undefined, { signal });
  const iterator = iterable[Symbol.asyncIterator]();
  assert.rejects(iterator.next(), /AbortError/).then(common.mustCall());
}

{
  const ac = new AbortController();
  const { signal } = ac;

  const iterable = setInterval(100, undefined, { signal });
  const iterator = iterable[Symbol.asyncIterator]();

  // This promise should take 100 seconds to resolve, so now aborting it should
  // mean we abort early
  const promise = iterator.next();

  ac.abort(); // Abort in after we have a next promise

  assert.rejects(promise, /AbortError/).then(common.mustCall());
}

{
  // Check aborting after getting a value.
  const ac = new AbortController();
  const { signal } = ac;

  const iterable = setInterval(100, undefined, { signal });
  const iterator = iterable[Symbol.asyncIterator]();

  const promise = iterator.next();
  const abortPromise = promise.then(common.mustCall(() => ac.abort()))
    .then(() => iterator.next());
  assert.rejects(abortPromise, /AbortError/).then(common.mustCall());
}

{
  [1, '', Infinity, null, {}].forEach((ref) => {
    const iterable = setInterval(10, undefined, { ref });
    assert.rejects(() => iterable[Symbol.asyncIterator]().next(), /ERR_INVALID_ARG_TYPE/)
      .then(common.mustCall());
  });

  [1, '', Infinity, null, {}].forEach((signal) => {
    const iterable = setInterval(10, undefined, { signal });
    assert.rejects(() => iterable[Symbol.asyncIterator]().next(), /ERR_INVALID_ARG_TYPE/)
      .then(common.mustCall());
  });

  [1, '', Infinity, null, true, false].forEach((options) => {
    const iterable = setInterval(10, undefined, options);
    assert.rejects(() => iterable[Symbol.asyncIterator]().next(), /ERR_INVALID_ARG_TYPE/)
      .then(common.mustCall());
  });
}

{
  // Check that timer adding signals does not leak handlers
  const signal = new NodeEventTarget();
  signal.aborted = false;
  const iterator = setInterval(1, undefined, { signal });
  iterator.next().then(common.mustCall(() => {
    assert.strictEqual(signal.listenerCount('abort'), 1);
    iterator.return();
  })).finally(common.mustCall(() => {
    assert.strictEqual(signal.listenerCount('abort'), 0);
  }));
}

{
  // Check that break removes the signal listener
  const signal = new NodeEventTarget();
  signal.aborted = false;
  async function tryBreak() {
    const iterator = setInterval(10, undefined, { signal });
    let i = 0;
    // eslint-disable-next-line no-unused-vars
    for await (const _ of iterator) {
      if (i === 0) {
        assert.strictEqual(signal.listenerCount('abort'), 1);
      }
      i++;
      if (i === 2) {
        break;
      }
    }
    assert.strictEqual(i, 2);
    assert.strictEqual(signal.listenerCount('abort'), 0);
  }

  tryBreak().then(common.mustCall());
}

{
  exec(`${process.execPath} -pe "const assert = require('assert');` +
    'const interval = require(\'timers/promises\')' +
    '.setInterval(1000, null, { ref: false });' +
    'interval[Symbol.asyncIterator]().next()' +
    '.then(assert.fail)"').then(common.mustCall(({ stderr }) => {
    assert.strictEqual(stderr, '');
  }));
}

{
  async function runInterval(fn, intervalTime, signal) {
    const input = 'foobar';
    const interval = setInterval(intervalTime, input, { signal });
    let iteration = 0;
    for await (const value of interval) {
      assert.strictEqual(value, input);
      iteration++;
      await fn(iteration);
    }
  }

  {
    // Check that we call the correct amount of times.
    const controller = new AbortController();
    const { signal } = controller;

    let loopCount = 0;
    const delay = 20;
    const timeoutLoop = runInterval(() => {
      loopCount++;
      if (loopCount === 5) controller.abort();
      if (loopCount > 5) throw new Error('ran too many times');
    }, delay, signal);

    assert.rejects(timeoutLoop, /AbortError/).then(common.mustCall(() => {
      assert.strictEqual(loopCount, 5);
    }));
  }

  {
    // Check that if we abort when we have some unresolved callbacks,
    // we actually call them.
    const controller = new AbortController();
    const { signal } = controller;
    const delay = 10;
    let totalIterations = 0;
    const timeoutLoop = runInterval(async (iterationNumber) => {
      await setPromiseTimeout(delay * 4);
      if (iterationNumber <= 2) {
        assert.strictEqual(signal.aborted, false);
      }
      if (iterationNumber === 2) {
        controller.abort();
      }
      if (iterationNumber > 2) {
        assert.strictEqual(signal.aborted, true);
      }
      if (iterationNumber > totalIterations) {
        totalIterations = iterationNumber;
      }
    }, delay, signal);

    timeoutLoop.catch(common.mustCall(() => {
      assert.ok(totalIterations >= 3, `iterations was ${totalIterations} < 3`);
    }));
  }
}

{
  // Check that the timing is correct
  let pre = false;
  let post = false;

  const time_unit = 50;
  Promise.all([
    setPromiseTimeout(1).then(() => pre = true),
    new Promise((res) => {
      const iterable = timerPromises.setInterval(time_unit * 2);
      const iterator = iterable[Symbol.asyncIterator]();

      iterator.next().then(() => {
        assert.ok(pre, 'interval ran too early');
        assert.ok(!post, 'interval ran too late');
        return iterator.next();
      }).then(() => {
        assert.ok(post, 'second interval ran too early');
        return iterator.return();
      }).then(res);
    }),
    setPromiseTimeout(time_unit * 3).then(() => post = true),
  ]).then(common.mustCall());
}

(async () => {
  const signal = AbortSignal.abort('boom');
  try {
    const iterable = timerPromises.setInterval(2, undefined, { signal });
    // eslint-disable-next-line no-unused-vars, no-empty
    for await (const _ of iterable) { }
    assert.fail('should have failed');
  } catch (err) {
    assert.strictEqual(err.cause, 'boom');
  }
})().then(common.mustCall());
