// 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 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';
import 'package:flutter_test/flutter_test.dart';

void main() {
  testWidgets('Nested TickerMode cannot turn tickers back on', (WidgetTester tester) async {
    int outerTickCount = 0;
    int innerTickCount = 0;

    Widget nestedTickerModes({required bool innerEnabled, required bool outerEnabled}) {
      return Directionality(
        textDirection: TextDirection.rtl,
        child: TickerMode(
          enabled: outerEnabled,
          child: Row(
            children: <Widget>[
              _TickingWidget(
                onTick: () {
                  outerTickCount++;
                },
              ),
              TickerMode(
                enabled: innerEnabled,
                child: _TickingWidget(
                  onTick: () {
                    innerTickCount++;
                  },
                ),
              ),
            ],
          ),
        ),
      );
    }

    await tester.pumpWidget(
      nestedTickerModes(
        outerEnabled: false,
        innerEnabled: true,
      ),
    );

    expect(outerTickCount, 0);
    expect(innerTickCount, 0);
    await tester.pump(const Duration(seconds: 1));
    await tester.pump(const Duration(seconds: 1));
    await tester.pump(const Duration(seconds: 1));
    await tester.pump(const Duration(seconds: 1));
    expect(outerTickCount, 0);
    expect(innerTickCount, 0);

    await tester.pumpWidget(
      nestedTickerModes(
        outerEnabled: true,
        innerEnabled: false,
      ),
    );
    outerTickCount = 0;
    innerTickCount = 0;
    await tester.pump(const Duration(seconds: 1));
    await tester.pump(const Duration(seconds: 1));
    await tester.pump(const Duration(seconds: 1));
    await tester.pump(const Duration(seconds: 1));
    expect(outerTickCount, 4);
    expect(innerTickCount, 0);

    await tester.pumpWidget(
      nestedTickerModes(
        outerEnabled: true,
        innerEnabled: true,
      ),
    );
    outerTickCount = 0;
    innerTickCount = 0;
    await tester.pump(const Duration(seconds: 1));
    await tester.pump(const Duration(seconds: 1));
    await tester.pump(const Duration(seconds: 1));
    await tester.pump(const Duration(seconds: 1));
    expect(outerTickCount, 4);
    expect(innerTickCount, 4);

    await tester.pumpWidget(
      nestedTickerModes(
        outerEnabled: false,
        innerEnabled: false,
      ),
    );
    outerTickCount = 0;
    innerTickCount = 0;
    await tester.pump(const Duration(seconds: 1));
    await tester.pump(const Duration(seconds: 1));
    await tester.pump(const Duration(seconds: 1));
    await tester.pump(const Duration(seconds: 1));
    expect(outerTickCount, 0);
    expect(innerTickCount, 0);
  });

  testWidgets('Changing TickerMode does not rebuild widgets with SingleTickerProviderStateMixin', (WidgetTester tester) async {
    Widget widgetUnderTest({required bool tickerEnabled}) {
      return TickerMode(
        enabled: tickerEnabled,
        child: const _TickingWidget(),
      );
    }
    _TickingWidgetState state() => tester.state<_TickingWidgetState>(find.byType(_TickingWidget));

    await tester.pumpWidget(widgetUnderTest(tickerEnabled: true));
    expect(state().ticker.isTicking, isTrue);
    expect(state().buildCount, 1);

    await tester.pumpWidget(widgetUnderTest(tickerEnabled: false));
    expect(state().ticker.isTicking, isFalse);
    expect(state().buildCount, 1);

    await tester.pumpWidget(widgetUnderTest(tickerEnabled: true));
    expect(state().ticker.isTicking, isTrue);
    expect(state().buildCount, 1);
  });

  testWidgets('Changing TickerMode does not rebuild widgets with TickerProviderStateMixin', (WidgetTester tester) async {
    Widget widgetUnderTest({required bool tickerEnabled}) {
      return TickerMode(
        enabled: tickerEnabled,
        child: const _MultiTickingWidget(),
      );
    }
    _MultiTickingWidgetState state() => tester.state<_MultiTickingWidgetState>(find.byType(_MultiTickingWidget));

    await tester.pumpWidget(widgetUnderTest(tickerEnabled: true));
    expect(state().ticker.isTicking, isTrue);
    expect(state().buildCount, 1);

    await tester.pumpWidget(widgetUnderTest(tickerEnabled: false));
    expect(state().ticker.isTicking, isFalse);
    expect(state().buildCount, 1);

    await tester.pumpWidget(widgetUnderTest(tickerEnabled: true));
    expect(state().ticker.isTicking, isTrue);
    expect(state().buildCount, 1);
  });

  testWidgets('Moving widgets with SingleTickerProviderStateMixin to a new TickerMode ancestor works', (WidgetTester tester) async {
    final GlobalKey tickingWidgetKey = GlobalKey();
    Widget widgetUnderTest({required LocalKey tickerModeKey, required bool tickerEnabled}) {
      return TickerMode(
        key: tickerModeKey,
        enabled: tickerEnabled,
        child: _TickingWidget(key: tickingWidgetKey),
      );
    }
    // Using different local keys to simulate changing TickerMode ancestors.
    await tester.pumpWidget(widgetUnderTest(tickerEnabled: true, tickerModeKey: UniqueKey()));
    final State tickerModeState = tester.state(find.byType(TickerMode));
    final _TickingWidgetState tickingState = tester.state<_TickingWidgetState>(find.byType(_TickingWidget));
    expect(tickingState.ticker.isTicking, isTrue);

    await tester.pumpWidget(widgetUnderTest(tickerEnabled: false, tickerModeKey: UniqueKey()));
    expect(tester.state(find.byType(TickerMode)), isNot(same(tickerModeState)));
    expect(tickingState, same(tester.state<_TickingWidgetState>(find.byType(_TickingWidget))));
    expect(tickingState.ticker.isTicking, isFalse);
  });

  testWidgets('Moving widgets with TickerProviderStateMixin to a new TickerMode ancestor works', (WidgetTester tester) async {
    final GlobalKey tickingWidgetKey = GlobalKey();
    Widget widgetUnderTest({required LocalKey tickerModeKey, required bool tickerEnabled}) {
      return TickerMode(
        key: tickerModeKey,
        enabled: tickerEnabled,
        child: _MultiTickingWidget(key: tickingWidgetKey),
      );
    }
    // Using different local keys to simulate changing TickerMode ancestors.
    await tester.pumpWidget(widgetUnderTest(tickerEnabled: true, tickerModeKey: UniqueKey()));
    final State tickerModeState = tester.state(find.byType(TickerMode));
    final _MultiTickingWidgetState tickingState = tester.state<_MultiTickingWidgetState>(find.byType(_MultiTickingWidget));
    expect(tickingState.ticker.isTicking, isTrue);

    await tester.pumpWidget(widgetUnderTest(tickerEnabled: false, tickerModeKey: UniqueKey()));
    expect(tester.state(find.byType(TickerMode)), isNot(same(tickerModeState)));
    expect(tickingState, same(tester.state<_MultiTickingWidgetState>(find.byType(_MultiTickingWidget))));
    expect(tickingState.ticker.isTicking, isFalse);
  });

  testWidgets('Ticking widgets in old route do not rebuild when new route is pushed', (WidgetTester tester) async {
    await tester.pumpWidget(MaterialApp(
      routes: <String, WidgetBuilder>{
        '/foo' : (BuildContext context) => const Text('New route'),
      },
      home: Row(
        children: const <Widget>[
          _TickingWidget(),
          _MultiTickingWidget(),
          Text('Old route'),
        ],
      ),
    ));

    _MultiTickingWidgetState multiTickingState() => tester.state<_MultiTickingWidgetState>(find.byType(_MultiTickingWidget, skipOffstage: false));
    _TickingWidgetState tickingState() => tester.state<_TickingWidgetState>(find.byType(_TickingWidget, skipOffstage: false));

    expect(find.text('Old route'), findsOneWidget);
    expect(find.text('New route'), findsNothing);

    expect(multiTickingState().ticker.isTicking, isTrue);
    expect(multiTickingState().buildCount, 1);
    expect(tickingState().ticker.isTicking, isTrue);
    expect(tickingState().buildCount, 1);

    tester.state<NavigatorState>(find.byType(Navigator)).pushNamed('/foo');
    await tester.pumpAndSettle();
    expect(find.text('Old route'), findsNothing);
    expect(find.text('New route'), findsOneWidget);

    expect(multiTickingState().ticker.isTicking, isFalse);
    expect(multiTickingState().buildCount, 1);
    expect(tickingState().ticker.isTicking, isFalse);
    expect(tickingState().buildCount, 1);
  });
}

class _TickingWidget extends StatefulWidget {
  const _TickingWidget({super.key, this.onTick});

  final VoidCallback? onTick;

  @override
  State<_TickingWidget> createState() => _TickingWidgetState();
}

class _TickingWidgetState extends State<_TickingWidget> with SingleTickerProviderStateMixin {
  late Ticker ticker;
  int buildCount = 0;

  @override
  void initState() {
    super.initState();
    ticker = createTicker((Duration _) {
      widget.onTick?.call();
    })..start();
  }

  @override
  Widget build(BuildContext context) {
    buildCount += 1;
    return Container();
  }

  @override
  void dispose() {
    ticker.dispose();
    super.dispose();
  }
}

class _MultiTickingWidget extends StatefulWidget {
  const _MultiTickingWidget({super.key});

  @override
  State<_MultiTickingWidget> createState() => _MultiTickingWidgetState();
}

class _MultiTickingWidgetState extends State<_MultiTickingWidget> with TickerProviderStateMixin {
  late Ticker ticker;
  int buildCount = 0;

  @override
  void initState() {
    super.initState();
    ticker = createTicker((Duration _) {
    })..start();
  }

  @override
  Widget build(BuildContext context) {
    buildCount += 1;
    return Container();
  }

  @override
  void dispose() {
    ticker.dispose();
    super.dispose();
  }
}