'use strict';

const common = require('../common');
const { Readable, addAbortSignal } = require('stream');
const assert = require('assert');

{
  const read = new Readable({
    read() {}
  });
  read.resume();

  read.on('close', common.mustCall());

  read.destroy();
  assert.strictEqual(read.destroyed, true);
}

{
  const read = new Readable({
    read() {}
  });
  read.resume();

  const expected = new Error('kaboom');

  read.on('end', common.mustNotCall('no end event'));
  read.on('close', common.mustCall());
  read.on('error', common.mustCall((err) => {
    assert.strictEqual(err, expected);
  }));

  read.destroy(expected);
  assert.strictEqual(read.destroyed, true);
}

{
  const read = new Readable({
    read() {}
  });

  read._destroy = common.mustCall(function(err, cb) {
    assert.strictEqual(err, expected);
    cb(err);
  });

  const expected = new Error('kaboom');

  read.on('end', common.mustNotCall('no end event'));
  read.on('close', common.mustCall());
  read.on('error', common.mustCall((err) => {
    assert.strictEqual(err, expected);
  }));

  read.destroy(expected);
  assert.strictEqual(read.destroyed, true);
}

{
  const read = new Readable({
    read() {},
    destroy: common.mustCall(function(err, cb) {
      assert.strictEqual(err, expected);
      cb();
    })
  });

  const expected = new Error('kaboom');

  read.on('end', common.mustNotCall('no end event'));

  // Error is swallowed by the custom _destroy
  read.on('error', common.mustNotCall('no error event'));
  read.on('close', common.mustCall());

  read.destroy(expected);
  assert.strictEqual(read.destroyed, true);
}

{
  const read = new Readable({
    read() {}
  });

  read._destroy = common.mustCall(function(err, cb) {
    assert.strictEqual(err, null);
    cb();
  });

  read.destroy();
  assert.strictEqual(read.destroyed, true);
}

{
  const read = new Readable({
    read() {}
  });
  read.resume();

  read._destroy = common.mustCall(function(err, cb) {
    assert.strictEqual(err, null);
    process.nextTick(() => {
      this.push(null);
      cb();
    });
  });

  const fail = common.mustNotCall('no end event');

  read.on('end', fail);
  read.on('close', common.mustCall());

  read.destroy();

  read.removeListener('end', fail);
  read.on('end', common.mustNotCall());
  assert.strictEqual(read.destroyed, true);
}

{
  const read = new Readable({
    read() {}
  });

  const expected = new Error('kaboom');

  read._destroy = common.mustCall(function(err, cb) {
    assert.strictEqual(err, null);
    cb(expected);
  });

  let ticked = false;
  read.on('end', common.mustNotCall('no end event'));
  read.on('error', common.mustCall((err) => {
    assert.strictEqual(ticked, true);
    assert.strictEqual(read._readableState.errorEmitted, true);
    assert.strictEqual(read._readableState.errored, expected);
    assert.strictEqual(err, expected);
  }));

  read.destroy();
  assert.strictEqual(read._readableState.errorEmitted, false);
  assert.strictEqual(read._readableState.errored, expected);
  assert.strictEqual(read.destroyed, true);
  ticked = true;
}

{
  const read = new Readable({
    read() {}
  });
  read.resume();

  read.destroyed = true;
  assert.strictEqual(read.destroyed, true);

  // The internal destroy() mechanism should not be triggered
  read.on('end', common.mustNotCall());
  read.destroy();
}

{
  function MyReadable() {
    assert.strictEqual(this.destroyed, false);
    this.destroyed = false;
    Readable.call(this);
  }

  Object.setPrototypeOf(MyReadable.prototype, Readable.prototype);
  Object.setPrototypeOf(MyReadable, Readable);

  new MyReadable();
}

{
  // Destroy and destroy callback
  const read = new Readable({
    read() {}
  });
  read.resume();

  const expected = new Error('kaboom');

  let ticked = false;
  read.on('close', common.mustCall(() => {
    assert.strictEqual(read._readableState.errorEmitted, true);
    assert.strictEqual(ticked, true);
  }));
  read.on('error', common.mustCall((err) => {
    assert.strictEqual(err, expected);
  }));

  assert.strictEqual(read._readableState.errored, null);
  assert.strictEqual(read._readableState.errorEmitted, false);

  read.destroy(expected, common.mustCall(function(err) {
    assert.strictEqual(read._readableState.errored, expected);
    assert.strictEqual(err, expected);
  }));
  assert.strictEqual(read._readableState.errorEmitted, false);
  assert.strictEqual(read._readableState.errored, expected);
  ticked = true;
}

{
  const readable = new Readable({
    destroy: common.mustCall(function(err, cb) {
      process.nextTick(cb, new Error('kaboom 1'));
    }),
    read() {}
  });

  let ticked = false;
  readable.on('close', common.mustCall(() => {
    assert.strictEqual(ticked, true);
    assert.strictEqual(readable._readableState.errorEmitted, true);
  }));
  readable.on('error', common.mustCall((err) => {
    assert.strictEqual(ticked, true);
    assert.strictEqual(err.message, 'kaboom 1');
    assert.strictEqual(readable._readableState.errorEmitted, true);
  }));

  readable.destroy();
  assert.strictEqual(readable.destroyed, true);
  assert.strictEqual(readable._readableState.errored, null);
  assert.strictEqual(readable._readableState.errorEmitted, false);

  // Test case where `readable.destroy()` is called again with an error before
  // the `_destroy()` callback is called.
  readable.destroy(new Error('kaboom 2'));
  assert.strictEqual(readable._readableState.errorEmitted, false);
  assert.strictEqual(readable._readableState.errored, null);

  ticked = true;
}

{
  const read = new Readable({
    read() {}
  });

  read.destroy();
  read.push('hi');
  read.on('data', common.mustNotCall());
}

{
  const read = new Readable({
    read: common.mustNotCall(function() {})
  });
  read.destroy();
  assert.strictEqual(read.destroyed, true);
  read.read();
}

{
  const read = new Readable({
    autoDestroy: false,
    read() {
      this.push(null);
      this.push('asd');
    }
  });

  read.on('error', common.mustCall(() => {
    assert(read._readableState.errored);
  }));
  read.resume();
}

{
  const controller = new AbortController();
  const read = addAbortSignal(controller.signal, new Readable({
    read() {
      this.push('asd');
    },
  }));

  read.on('error', common.mustCall((e) => {
    assert.strictEqual(e.name, 'AbortError');
  }));
  controller.abort();
  read.on('data', common.mustNotCall());
}

{
  const controller = new AbortController();
  const read = new Readable({
    signal: controller.signal,
    read() {
      this.push('asd');
    },
  });

  read.on('error', common.mustCall((e) => {
    assert.strictEqual(e.name, 'AbortError');
  }));
  controller.abort();
  read.on('data', common.mustNotCall());
}

{
  const controller = new AbortController();
  const read = addAbortSignal(controller.signal, new Readable({
    objectMode: true,
    read() {
      return false;
    }
  }));
  read.push('asd');

  read.on('error', common.mustCall((e) => {
    assert.strictEqual(e.name, 'AbortError');
  }));
  assert.rejects((async () => {
    // eslint-disable-next-line no-unused-vars, no-empty
    for await (const chunk of read) { }
  })(), /AbortError/);
  setTimeout(() => controller.abort(), 0);
}
