// Copyright (c) 2021, the Dart project authors.  Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE filevents.

import 'dart:async';

import 'package:async/async.dart';
import 'package:test/test.dart';

void main() {
  late StreamController controller;
  setUp(() {
    controller = StreamController<void>();
  });

  test('passes through data events', () {
    controller.sink.rejectErrors()
      ..add(1)
      ..add(2)
      ..add(3);
    expect(controller.stream, emitsInOrder([1, 2, 3]));
  });

  test('passes through close events', () {
    controller.sink.rejectErrors()
      ..add(1)
      ..close();
    expect(controller.stream, emitsInOrder([1, emitsDone]));
  });

  test('passes through data events from addStream()', () {
    controller.sink.rejectErrors().addStream(Stream.fromIterable([1, 2, 3]));
    expect(controller.stream, emitsInOrder([1, 2, 3]));
  });

  test('allows multiple addStream() calls', () async {
    var transformed = controller.sink.rejectErrors();
    await transformed.addStream(Stream.fromIterable([1, 2, 3]));
    await transformed.addStream(Stream.fromIterable([4, 5, 6]));
    expect(controller.stream, emitsInOrder([1, 2, 3, 4, 5, 6]));
  });

  group('on addError()', () {
    test('forwards the error to done', () {
      var transformed = controller.sink.rejectErrors();
      transformed.addError('oh no');
      expect(transformed.done, throwsA('oh no'));
    });

    test('closes the underlying sink', () {
      var transformed = controller.sink.rejectErrors();
      transformed.addError('oh no');
      transformed.done.catchError((_) {});

      expect(controller.stream, emitsDone);
    });

    test('ignores further events', () async {
      var transformed = controller.sink.rejectErrors();
      transformed.addError('oh no');
      transformed.done.catchError((_) {});
      expect(controller.stream, emitsDone);

      // Try adding events synchronously and asynchronously and verify that they
      // don't throw and also aren't passed to the underlying sink.
      transformed
        ..add(1)
        ..addError('another');
      await pumpEventQueue();
      transformed
        ..add(2)
        ..addError('yet another');
    });

    test('cancels the current subscription', () async {
      var inputCanceled = false;
      var inputController =
          StreamController(onCancel: () => inputCanceled = true);

      var transformed = controller.sink.rejectErrors()
        ..addStream(inputController.stream);
      inputController.addError('oh no');
      transformed.done.catchError((_) {});

      await pumpEventQueue();
      expect(inputCanceled, isTrue);
    });
  });

  group('when the inner sink\'s done future completes', () {
    test('done completes', () async {
      var completer = Completer<void>();
      var transformed = NullStreamSink(done: completer.future).rejectErrors();

      var doneCompleted = false;
      transformed.done.then((_) => doneCompleted = true);
      await pumpEventQueue();
      expect(doneCompleted, isFalse);

      completer.complete();
      await pumpEventQueue();
      expect(doneCompleted, isTrue);
    });

    test('an outstanding addStream() completes', () async {
      var completer = Completer<void>();
      var transformed = NullStreamSink(done: completer.future).rejectErrors();

      var addStreamCompleted = false;
      transformed
          .addStream(StreamController().stream)
          .then((_) => addStreamCompleted = true);
      await pumpEventQueue();
      expect(addStreamCompleted, isFalse);

      completer.complete();
      await pumpEventQueue();
      expect(addStreamCompleted, isTrue);
    });

    test('an outstanding addStream()\'s subscription is cancelled', () async {
      var completer = Completer<void>();
      var transformed = NullStreamSink(done: completer.future).rejectErrors();

      var addStreamCancelled = false;
      transformed.addStream(
          StreamController(onCancel: () => addStreamCancelled = true).stream);
      await pumpEventQueue();
      expect(addStreamCancelled, isFalse);

      completer.complete();
      await pumpEventQueue();
      expect(addStreamCancelled, isTrue);
    });

    test('forwards an outstanding addStream()\'s cancellation error', () async {
      var completer = Completer<void>();
      var transformed = NullStreamSink(done: completer.future).rejectErrors();

      expect(
          transformed.addStream(
              StreamController(onCancel: () => throw 'oh no').stream),
          throwsA('oh no'));
      completer.complete();
    });

    group('forwards its error', () {
      test('through done', () async {
        expect(NullStreamSink(done: Future.error('oh no')).rejectErrors().done,
            throwsA('oh no'));
      });

      test('through close', () async {
        expect(
            NullStreamSink(done: Future.error('oh no')).rejectErrors().close(),
            throwsA('oh no'));
      });
    });
  });

  group('after closing', () {
    test('throws on add()', () {
      var sink = controller.sink.rejectErrors()..close();
      expect(() => sink.add(1), throwsStateError);
    });

    test('throws on addError()', () {
      var sink = controller.sink.rejectErrors()..close();
      expect(() => sink.addError('oh no'), throwsStateError);
    });

    test('throws on addStream()', () {
      var sink = controller.sink.rejectErrors()..close();
      expect(() => sink.addStream(const Stream.empty()), throwsStateError);
    });

    test('allows close()', () {
      var sink = controller.sink.rejectErrors()..close();
      sink.close(); // Shouldn't throw
    });
  });

  group('during an active addStream()', () {
    test('throws on add()', () {
      var sink = controller.sink.rejectErrors()
        ..addStream(StreamController().stream);
      expect(() => sink.add(1), throwsStateError);
    });

    test('throws on addError()', () {
      var sink = controller.sink.rejectErrors()
        ..addStream(StreamController().stream);
      expect(() => sink.addError('oh no'), throwsStateError);
    });

    test('throws on addStream()', () {
      var sink = controller.sink.rejectErrors()
        ..addStream(StreamController().stream);
      expect(() => sink.addStream(const Stream.empty()), throwsStateError);
    });

    test('throws on close()', () {
      var sink = controller.sink.rejectErrors()
        ..addStream(StreamController().stream);
      expect(() => sink.close(), throwsStateError);
    });
  });
}
