// 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:ui';

import 'package:flutter/services.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart';

void main() {
  AppLifecycleListener? listener;

  Future<void> setAppLifeCycleState(AppLifecycleState state) async {
    final ByteData? message = const StringCodec().encodeMessage(state.toString());
    await TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
        .handlePlatformMessage('flutter/lifecycle', message, (_) {});
  }

  Future<void> sendAppExitRequest() async {
    final ByteData message = const JSONMethodCodec().encodeMethodCall(const MethodCall('System.requestAppExit'));
    await TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
        .handlePlatformMessage('flutter/platform', message, (_) {});
  }

  setUp(() async {
    WidgetsFlutterBinding.ensureInitialized();
    WidgetsBinding.instance
      ..resetEpoch()
      ..platformDispatcher.onBeginFrame = null
      ..platformDispatcher.onDrawFrame = null;
    final TestWidgetsFlutterBinding binding = TestWidgetsFlutterBinding.instance;
    binding.readTestInitialLifecycleStateFromNativeWindow();
    // Reset the state to detached. Going to paused first makes it a valid
    // transition from any state, since the intermediate transitions will be
    // generated.
    await setAppLifeCycleState(AppLifecycleState.paused);
    await setAppLifeCycleState(AppLifecycleState.detached);
  });

  tearDown(() {
    listener?.dispose();
    listener = null;
    final TestWidgetsFlutterBinding binding = TestWidgetsFlutterBinding.instance;
    binding.resetLifecycleState();
    binding.platformDispatcher.resetInitialLifecycleState();
    assert(TestAppLifecycleListener.registerCount == 0,
        'There were ${TestAppLifecycleListener.registerCount} listeners that were not disposed of in tests.');
  });

  testWidgets('Default Diagnostics', (WidgetTester tester) async {
    listener = TestAppLifecycleListener(binding: tester.binding);
    expect(listener.toString(),
        equalsIgnoringHashCodes('TestAppLifecycleListener#00000(binding: <AutomatedTestWidgetsFlutterBinding>)'));
  });

  testWidgets('Diagnostics', (WidgetTester tester) async {
    Future<AppExitResponse> handleExitRequested() async {
      return AppExitResponse.cancel;
    }

    listener = TestAppLifecycleListener(
      binding: WidgetsBinding.instance,
      onExitRequested: handleExitRequested,
      onStateChange: (AppLifecycleState _) {},
    );
    expect(
        listener.toString(),
        equalsIgnoringHashCodes(
            'TestAppLifecycleListener#00000(binding: <AutomatedTestWidgetsFlutterBinding>, onStateChange, onExitRequested)'));
  });

  testWidgets('listens to AppLifecycleState', (WidgetTester tester) async {
    final List<AppLifecycleState> states = <AppLifecycleState>[tester.binding.lifecycleState!];
    void stateChange(AppLifecycleState state) {
      states.add(state);
    }

    listener = TestAppLifecycleListener(
      binding: WidgetsBinding.instance,
      onStateChange: stateChange,
    );
    expect(states, equals(<AppLifecycleState>[AppLifecycleState.detached]));
    await setAppLifeCycleState(AppLifecycleState.inactive);
    // "resumed" is generated.
    expect(states,
        equals(<AppLifecycleState>[AppLifecycleState.detached, AppLifecycleState.resumed, AppLifecycleState.inactive]));
    await setAppLifeCycleState(AppLifecycleState.resumed);
    expect(
        states,
        equals(<AppLifecycleState>[
          AppLifecycleState.detached,
          AppLifecycleState.resumed,
          AppLifecycleState.inactive,
          AppLifecycleState.resumed
        ]));
  });

  testWidgets('Triggers correct state transition callbacks', (WidgetTester tester) async {
    final List<String> transitions = <String>[];
    listener = TestAppLifecycleListener(
      binding: WidgetsBinding.instance,
      onDetach: () => transitions.add('detach'),
      onHide: () => transitions.add('hide'),
      onInactive: () => transitions.add('inactive'),
      onPause: () => transitions.add('pause'),
      onRestart: () => transitions.add('restart'),
      onResume: () => transitions.add('resume'),
      onShow: () => transitions.add('show'),
    );

    // Try "standard" sequence
    await setAppLifeCycleState(AppLifecycleState.resumed);
    expect(transitions, equals(<String>['resume']));
    await setAppLifeCycleState(AppLifecycleState.inactive);
    expect(transitions, equals(<String>['resume', 'inactive']));
    await setAppLifeCycleState(AppLifecycleState.hidden);
    expect(transitions, equals(<String>['resume', 'inactive', 'hide']));
    await setAppLifeCycleState(AppLifecycleState.paused);
    expect(transitions, equals(<String>['resume', 'inactive', 'hide', 'pause']));

    // Go back to resume
    transitions.clear();
    await setAppLifeCycleState(AppLifecycleState.hidden);
    expect(transitions, equals(<String>['restart']));
    await setAppLifeCycleState(AppLifecycleState.inactive);
    expect(transitions, equals(<String>['restart', 'show']));
    await setAppLifeCycleState(AppLifecycleState.resumed);
    expect(transitions, equals(<String>['restart', 'show', 'resume']));

    // Generates intermediate states.
    transitions.clear();
    await setAppLifeCycleState(AppLifecycleState.paused);
    expect(transitions, equals(<String>['inactive', 'hide', 'pause']));
    // Wraps around from pause to detach.
    await setAppLifeCycleState(AppLifecycleState.detached);
    expect(transitions, equals(<String>['inactive', 'hide', 'pause', 'detach']));
    await setAppLifeCycleState(AppLifecycleState.resumed);
    expect(transitions, equals(<String>['inactive', 'hide', 'pause', 'detach', 'resume']));
    await setAppLifeCycleState(AppLifecycleState.paused);
    expect(transitions, equals(<String>['inactive', 'hide', 'pause', 'detach', 'resume', 'inactive', 'hide', 'pause']));
    transitions.clear();
    await setAppLifeCycleState(AppLifecycleState.resumed);
    expect(transitions, equals(<String>['restart', 'show', 'resume']));

    // Asserts on bad transitions
    await expectLater(() => setAppLifeCycleState(AppLifecycleState.detached), throwsAssertionError);
    await setAppLifeCycleState(AppLifecycleState.paused);
    await setAppLifeCycleState(AppLifecycleState.detached);
  });

  testWidgets('Receives exit requests', (WidgetTester tester) async {
    bool exitRequested = false;
    Future<AppExitResponse> handleExitRequested() async {
      exitRequested = true;
      return AppExitResponse.cancel;
    }

    listener = TestAppLifecycleListener(
      binding: WidgetsBinding.instance,
      onExitRequested: handleExitRequested,
    );
    await sendAppExitRequest();
    expect(exitRequested, isTrue);
  });

  test('AppLifecycleListener dispatches memory events', () async {
    await expectLater(
      await memoryEvents(
        () => AppLifecycleListener(binding: WidgetsBinding.instance).dispose(),
        AppLifecycleListener,
      ),
      areCreateAndDispose,
    );
  });
}

class TestAppLifecycleListener extends AppLifecycleListener {
  TestAppLifecycleListener({
    super.binding,
    super.onResume,
    super.onInactive,
    super.onHide,
    super.onShow,
    super.onPause,
    super.onRestart,
    super.onDetach,
    super.onExitRequested,
    super.onStateChange,
  }) {
    registerCount += 1;
  }

  static int registerCount = 0;

  @override
  void dispose() {
    super.dispose();
    registerCount -= 1;
  }
}
