// Copyright 2014 The Flutter Authors. 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:flutter_tools/src/base/context.dart';

import '../../src/common.dart';

void main() {
  group('AppContext', () {
    group('global getter', () {
      late bool called;

      setUp(() {
        called = false;
      });

      test('returns non-null context in the root zone', () {
        expect(context, isNotNull);
      });

      test('returns root context in child of root zone if zone was manually created', () {
        final Zone rootZone = Zone.current;
        final AppContext rootContext = context;
        runZoned<void>(() {
          expect(Zone.current, isNot(rootZone));
          expect(Zone.current.parent, rootZone);
          expect(context, rootContext);
          called = true;
        });
        expect(called, isTrue);
      });

      test('returns child context after run', () async {
        final AppContext rootContext = context;
        await rootContext.run<void>(
          name: 'child',
          body: () {
            expect(context, isNot(rootContext));
            expect(context.name, 'child');
            called = true;
          },
        );
        expect(called, isTrue);
      });

      test('returns grandchild context after nested run', () async {
        final AppContext rootContext = context;
        await rootContext.run<void>(
          name: 'child',
          body: () async {
            final AppContext childContext = context;
            await childContext.run<void>(
              name: 'grandchild',
              body: () {
                expect(context, isNot(rootContext));
                expect(context, isNot(childContext));
                expect(context.name, 'grandchild');
                called = true;
              },
            );
          },
        );
        expect(called, isTrue);
      });

      test('scans up zone hierarchy for first context', () async {
        final AppContext rootContext = context;
        await rootContext.run<void>(
          name: 'child',
          body: () {
            final AppContext childContext = context;
            runZoned<void>(() {
              expect(context, isNot(rootContext));
              expect(context, same(childContext));
              expect(context.name, 'child');
              called = true;
            });
          },
        );
        expect(called, isTrue);
      });
    });

    group('operator[]', () {
      test('still finds values if async code runs after body has finished', () async {
        final outer = Completer<void>();
        final inner = Completer<void>();
        String? value;
        await context.run<void>(
          body: () {
            outer.future.then<void>((_) {
              value = context.get<String>();
              inner.complete();
            });
          },
          fallbacks: <Type, Generator>{String: () => 'value'},
        );
        expect(value, isNull);
        outer.complete();
        await inner.future;
        expect(value, 'value');
      });

      test('caches generated override values', () async {
        var consultationCount = 0;
        String? value;
        await context.run<void>(
          body: () async {
            final buf = StringBuffer(context.get<String>()!);
            buf.write(context.get<String>());
            await context.run<void>(
              body: () {
                buf.write(context.get<String>());
              },
            );
            value = buf.toString();
          },
          overrides: <Type, Generator>{
            String: () {
              consultationCount++;
              return 'v';
            },
          },
        );
        expect(value, 'vvv');
        expect(consultationCount, 1);
      });

      test('caches generated fallback values', () async {
        var consultationCount = 0;
        String? value;
        await context.run(
          body: () async {
            final buf = StringBuffer(context.get<String>()!);
            buf.write(context.get<String>());
            await context.run<void>(
              body: () {
                buf.write(context.get<String>());
              },
            );
            value = buf.toString();
          },
          fallbacks: <Type, Generator>{
            String: () {
              consultationCount++;
              return 'v';
            },
          },
        );
        expect(value, 'vvv');
        expect(consultationCount, 1);
      });

      test('returns null if generated value is null', () async {
        final String? value = await context.run<String?>(
          body: () => context.get<String>(),
          overrides: <Type, Generator>{String: () => null},
        );
        expect(value, isNull);
      });

      test('throws if generator has dependency cycle', () async {
        final Future<String?> value = context.run<String?>(
          body: () async {
            return context.get<String>();
          },
          fallbacks: <Type, Generator>{
            int: () => int.parse(context.get<String>() ?? ''),
            String: () => '${context.get<double>()}',
            double: () => context.get<int>()! * 1.0,
          },
        );
        expect(
          () => value,
          throwsA(
            isA<ContextDependencyCycleException>()
                .having((ContextDependencyCycleException error) => error.cycle, 'cycle', <Type>[
                  String,
                  double,
                  int,
                ])
                .having(
                  (ContextDependencyCycleException error) => error.toString(),
                  'toString()',
                  'Dependency cycle detected: String -> double -> int',
                ),
          ),
        );
      });
    });

    group('run', () {
      test('returns the value returned by body', () async {
        expect(await context.run<int>(body: () => 123), 123);
        expect(await context.run<String>(body: () => 'value'), 'value');
        expect(await context.run<int>(body: () async => 456), 456);
      });

      test('passes name to child context', () async {
        await context.run<void>(
          name: 'child',
          body: () {
            expect(context.name, 'child');
          },
        );
      });

      group('fallbacks', () {
        late bool called;

        setUp(() {
          called = false;
        });

        test('are applied after parent context is consulted', () async {
          final String? value = await context.run<String?>(
            body: () {
              return context.run<String?>(
                body: () {
                  called = true;
                  return context.get<String>();
                },
                fallbacks: <Type, Generator>{String: () => 'child'},
              );
            },
          );
          expect(called, isTrue);
          expect(value, 'child');
        });

        test('are not applied if parent context supplies value', () async {
          var childConsulted = false;
          final String? value = await context.run<String?>(
            body: () {
              return context.run<String?>(
                body: () {
                  called = true;
                  return context.get<String>();
                },
                fallbacks: <Type, Generator>{
                  String: () {
                    childConsulted = true;
                    return 'child';
                  },
                },
              );
            },
            fallbacks: <Type, Generator>{String: () => 'parent'},
          );
          expect(called, isTrue);
          expect(value, 'parent');
          expect(childConsulted, isFalse);
        });

        test('may depend on one another', () async {
          final String? value = await context.run<String?>(
            body: () {
              return context.get<String>();
            },
            fallbacks: <Type, Generator>{int: () => 123, String: () => '-${context.get<int>()}-'},
          );
          expect(value, '-123-');
        });
      });

      group('overrides', () {
        test('intercept consultation of parent context', () async {
          var parentConsulted = false;
          final String? value = await context.run<String?>(
            body: () {
              return context.run<String?>(
                body: () => context.get<String>(),
                overrides: <Type, Generator>{String: () => 'child'},
              );
            },
            fallbacks: <Type, Generator>{
              String: () {
                parentConsulted = true;
                return 'parent';
              },
            },
          );
          expect(value, 'child');
          expect(parentConsulted, isFalse);
        });
      });
    });
  });
}
