// Copyright (c) 2017, 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 file.

import 'dart:async';

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

import 'utils.dart';

void main() {
  for (var outerType in streamTypes) {
    for (var innerType in streamTypes) {
      group('Outer type: [$outerType], Inner type: [$innerType]', () {
        late StreamController<int> first;
        late StreamController<int> second;
        late StreamController<int> third;
        late StreamController<Stream<int>> outer;

        late List<int> emittedValues;
        late bool firstCanceled;
        late bool outerCanceled;
        late bool isDone;
        late List<String> errors;
        late StreamSubscription<int> subscription;

        setUp(() async {
          firstCanceled = false;
          outerCanceled = false;
          outer = createController(outerType)
            ..onCancel = () {
              outerCanceled = true;
            };
          first = createController(innerType)
            ..onCancel = () {
              firstCanceled = true;
            };
          second = createController(innerType);
          third = createController(innerType);
          emittedValues = [];
          errors = [];
          isDone = false;
          subscription = outer.stream
              .switchLatest()
              .listen(emittedValues.add, onError: errors.add, onDone: () {
            isDone = true;
          });
        });

        test('forwards events', () async {
          outer.add(first.stream);
          await Future(() {});
          first
            ..add(1)
            ..add(2);
          await Future(() {});

          outer.add(second.stream);
          await Future(() {});
          second
            ..add(3)
            ..add(4);
          await Future(() {});

          expect(emittedValues, [1, 2, 3, 4]);
        });

        test('forwards errors from outer Stream', () async {
          outer.addError('error');
          await Future(() {});
          expect(errors, ['error']);
        });

        test('forwards errors from inner Stream', () async {
          outer.add(first.stream);
          await Future(() {});
          first.addError('error');
          await Future(() {});
          expect(errors, ['error']);
        });

        test('closes when final stream is done', () async {
          outer.add(first.stream);
          await Future(() {});

          outer.add(second.stream);
          await Future(() {});

          await outer.close();
          expect(isDone, false);

          await second.close();
          expect(isDone, true);
        });

        test(
            'closes when outer stream closes if latest inner stream already '
            'closed', () async {
          outer.add(first.stream);
          await Future(() {});
          await first.close();
          expect(isDone, false);

          await outer.close();
          expect(isDone, true);
        });

        test('cancels listeners on previous streams', () async {
          outer.add(first.stream);
          await Future(() {});

          outer.add(second.stream);
          await Future(() {});
          expect(firstCanceled, true);
        });

        if (innerType != 'broadcast') {
          test('waits for cancel before listening to subsequent stream',
              () async {
            var cancelWork = Completer<void>();
            first.onCancel = () => cancelWork.future;
            outer.add(first.stream);
            await Future(() {});

            var cancelDone = false;
            second.onListen = expectAsync0(() {
              expect(cancelDone, true);
            });
            outer.add(second.stream);
            await Future(() {});
            cancelWork.complete();
            cancelDone = true;
          });

          test('all streams are listened to, even while cancelling', () async {
            var cancelWork = Completer<void>();
            first.onCancel = () => cancelWork.future;
            outer.add(first.stream);
            await Future(() {});

            var cancelDone = false;
            second.onListen = expectAsync0(() {
              expect(cancelDone, true);
            });
            third.onListen = expectAsync0(() {
              expect(cancelDone, true);
            });
            outer
              ..add(second.stream)
              ..add(third.stream);
            await Future(() {});
            cancelWork.complete();
            cancelDone = true;
          });
        }

        if (outerType != 'broadcast' && innerType != 'broadcast') {
          test('pausing while cancelling an inner stream is respected',
              () async {
            var cancelWork = Completer<void>();
            first.onCancel = () => cancelWork.future;
            outer.add(first.stream);
            await Future(() {});

            var cancelDone = false;
            second.onListen = expectAsync0(() {
              expect(cancelDone, true);
            });
            outer.add(second.stream);
            await Future(() {});
            subscription.pause();
            cancelWork.complete();
            cancelDone = true;
            await Future(() {});
            expect(second.isPaused, true);
            subscription.resume();
          });
        }

        test('cancels listener on current and outer stream on cancel',
            () async {
          outer.add(first.stream);
          await Future(() {});
          await subscription.cancel();

          await Future(() {});
          expect(outerCanceled, true);
          expect(firstCanceled, true);
        });
      });
    }
  }

  group('switchMap', () {
    test('uses map function', () async {
      var outer = StreamController<List<int>>();

      var values = <int>[];
      outer.stream.switchMap(Stream.fromIterable).listen(values.add);

      outer.add([1, 2, 3]);
      await Future(() {});
      outer.add([4, 5, 6]);
      await Future(() {});
      expect(values, [1, 2, 3, 4, 5, 6]);
    });

    test('can create a broadcast stream', () async {
      var outer = StreamController<int>.broadcast();

      var transformed =
          outer.stream.switchMap((_) => const Stream<int>.empty());

      expect(transformed.isBroadcast, true);
    });

    test('forwards errors from the convert callback', () async {
      var errors = <String>[];
      var source = Stream.fromIterable([1, 2, 3]);
      source.switchMap<int>((i) {
        // ignore: only_throw_errors
        throw 'Error: $i';
      }).listen((_) {}, onError: errors.add);
      await Future<void>(() {});
      expect(errors, ['Error: 1', 'Error: 2', 'Error: 3']);
    });
  });
}
