// Flags: --expose-internals --no-warnings
'use strict';

const common = require('../common');
const assert = require('assert');

const { isErrored, isWritable } = require('stream');

const {
  WritableStream,
  WritableStreamDefaultController,
  WritableStreamDefaultWriter,
  CountQueuingStrategy,
} = require('stream/web');

const {
  kState,
} = require('internal/webstreams/util');

const {
  isPromise,
} = require('util/types');

const {
  kTransfer,
} = require('internal/worker/js_transferable');

const {
  inspect,
} = require('util');

class Sink {
  constructor() {
    this.chunks = [];
  }

  start() {
    this.started = true;
  }

  write(chunk) {
    this.chunks.push(chunk);
  }

  close() {
    this.closed = true;
  }

  abort() {
    this.aborted = true;
  }
}

{
  const stream = new WritableStream();

  assert(stream[kState].controller instanceof WritableStreamDefaultController);
  assert(!stream.locked);

  assert.strictEqual(typeof stream.abort, 'function');
  assert.strictEqual(typeof stream.close, 'function');
  assert.strictEqual(typeof stream.getWriter, 'function');
}

['a', false, 1, null].forEach((sink) => {
  assert.throws(() => new WritableStream(sink), {
    code: 'ERR_INVALID_ARG_TYPE',
  });
});

['a', false, 1].forEach((strategy) => {
  assert.throws(() => new WritableStream({}, strategy), {
    code: 'ERR_INVALID_ARG_TYPE',
  });
});

[1, false, ''].forEach((type) => {
  assert.throws(() => new WritableStream({ type }), {
    code: 'ERR_INVALID_ARG_VALUE',
  });
});

['a', {}].forEach((highWaterMark) => {
  assert.throws(() => new WritableStream({}, { highWaterMark }), {
    code: 'ERR_INVALID_ARG_VALUE',
  });
});

['a', false, {}].forEach((size) => {
  assert.throws(() => new WritableStream({}, { size }), {
    code: 'ERR_INVALID_ARG_TYPE',
  });
});

{
  new WritableStream({});
  new WritableStream([]);
  new WritableStream({}, null);
  new WritableStream({}, {});
  new WritableStream({}, []);
}

{
  const sink = new Sink();
  const stream = new WritableStream(
    sink,
    new CountQueuingStrategy({ highWaterMark: 1 }));

  assert(!stream.locked);
  const writer = stream.getWriter();
  assert(stream.locked);
  assert(writer instanceof WritableStreamDefaultWriter);

  assert(isPromise(writer.closed));
  assert(isPromise(writer.ready));
  assert(typeof writer.desiredSize, 'number');
  assert(typeof writer.abort, 'function');
  assert(typeof writer.close, 'function');
  assert(typeof writer.releaseLock, 'function');
  assert(typeof writer.write, 'function');

  writer.releaseLock();
  assert(!stream.locked);

  const writer2 = stream.getWriter();

  assert(sink.started);

  writer2.closed.then(common.mustCall());
  writer2.ready.then(common.mustCall());

  writer2.close().then(common.mustCall(() => {
    assert.strict(sink.closed);
  }));
}

{
  const sink = new Sink();

  const stream = new WritableStream(
    sink,
    new CountQueuingStrategy({ highWaterMark: 1 }));

  const error = new Error('boom');

  const writer = stream.getWriter();

  assert.rejects(writer.closed, error).then(common.mustCall());

  writer.abort(error).then(common.mustCall(() => {
    assert.strictEqual(stream[kState].state, 'errored');
    assert(sink.aborted);
  }));
}

{
  const sink = new Sink();

  const stream = new WritableStream(
    sink, { highWaterMark: 1 }
  );

  async function write(stream) {
    const writer = stream.getWriter();
    const p = writer.write('hello');
    assert.strictEqual(writer.desiredSize, 0);
    await p;
    assert.strictEqual(writer.desiredSize, 1);
  }

  write(stream).then(common.mustCall(() => {
    assert.deepStrictEqual(['hello'], sink.chunks);
  }));
}

{
  const stream = new WritableStream();
  assert.strictEqual(isWritable(stream), true);
  stream.close().then(common.mustCall(() => {
    assert.strictEqual(isWritable(stream), false);
  }));
}

{
  const stream = new WritableStream({
    write: common.mustCall((chunk, controller) => {
      controller.error(new Error());
    })
  });
  const writer = stream.getWriter();
  assert.strictEqual(isErrored(stream), false);
  writer.write().then(common.mustCall(() => {
    assert.strictEqual(isErrored(stream), true);
  }));
}

{
  assert.throws(() => Reflect.get(WritableStream.prototype, 'locked', {}), {
    code: 'ERR_INVALID_THIS',
  });
  assert.rejects(() => WritableStream.prototype.abort({}), {
    code: 'ERR_INVALID_THIS',
  }).then(common.mustCall());
  assert.rejects(() => WritableStream.prototype.close({}), {
    code: 'ERR_INVALID_THIS',
  }).then(common.mustCall());
  assert.throws(() => WritableStream.prototype.getWriter.call(), {
    code: 'ERR_INVALID_THIS',
  });
  assert.throws(() => WritableStream.prototype[kTransfer].call(), {
    code: 'ERR_INVALID_THIS',
  });
  assert.rejects(
    Reflect.get(WritableStreamDefaultWriter.prototype, 'closed'), {
      code: 'ERR_INVALID_THIS',
    }).then(common.mustCall());
  assert.rejects(
    Reflect.get(WritableStreamDefaultWriter.prototype, 'ready'), {
      code: 'ERR_INVALID_THIS',
    }).then(common.mustCall());
  assert.throws(
    () => Reflect.get(WritableStreamDefaultWriter.prototype, 'desiredSize'), {
      code: 'ERR_INVALID_THIS',
    });
  assert.rejects(WritableStreamDefaultWriter.prototype.abort({}), {
    code: 'ERR_INVALID_THIS',
  }).then(common.mustCall());
  assert.rejects(WritableStreamDefaultWriter.prototype.close({}), {
    code: 'ERR_INVALID_THIS',
  }).then(common.mustCall());
  assert.rejects(WritableStreamDefaultWriter.prototype.write({}), {
    code: 'ERR_INVALID_THIS',
  }).then(common.mustCall());
  assert.throws(() => WritableStreamDefaultWriter.prototype.releaseLock({}), {
    code: 'ERR_INVALID_THIS',
  });

  assert.throws(() => {
    Reflect.get(WritableStreamDefaultController.prototype, 'signal', {});
  }, {
    code: 'ERR_INVALID_THIS',
  });

  assert.throws(() => {
    WritableStreamDefaultController.prototype.error({});
  }, {
    code: 'ERR_INVALID_THIS',
  });
}

{
  let controller;
  const writable = new WritableStream({
    start(c) { controller = c; }
  });
  assert.strictEqual(
    inspect(writable),
    'WritableStream { locked: false, state: \'writable\' }');
  assert.strictEqual(
    inspect(writable, { depth: null }),
    'WritableStream { locked: false, state: \'writable\' }');
  assert.strictEqual(
    inspect(writable, { depth: 0 }),
    'WritableStream [Object]');

  const writer = writable.getWriter();
  assert.match(
    inspect(writer),
    /WritableStreamDefaultWriter/);
  assert.match(
    inspect(writer, { depth: null }),
    /WritableStreamDefaultWriter/);
  assert.match(
    inspect(writer, { depth: 0 }),
    /WritableStreamDefaultWriter \[/);

  assert.match(
    inspect(controller),
    /WritableStreamDefaultController/);
  assert.match(
    inspect(controller, { depth: null }),
    /WritableStreamDefaultController/);
  assert.match(
    inspect(controller, { depth: 0 }),
    /WritableStreamDefaultController \[/);

  writer.abort(new Error('boom'));

  assert.strictEqual(writer.desiredSize, null);
  setImmediate(common.mustCall(() => assert.strictEqual(writer.desiredSize, null)));
}
