// 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/scheduler.dart';
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';

import 'restoration.dart';

void main() {
  testWidgets('$RestorationManager dispatches memory events', (WidgetTester tester) async {
    await expectLater(
      await memoryEvents(() => RestorationManager().dispose(), RestorationManager),
      areCreateAndDispose,
    );
  });

  group('RestorationManager', () {
    testWidgets('root bucket retrieval', (WidgetTester tester) async {
      final List<MethodCall> callsToEngine = <MethodCall>[];
      final Completer<Map<dynamic, dynamic>> result = Completer<Map<dynamic, dynamic>>();
      tester.binding.defaultBinaryMessenger.setMockMethodCallHandler(SystemChannels.restoration, (
        MethodCall call,
      ) {
        callsToEngine.add(call);
        return result.future;
      });

      final RestorationManager manager = RestorationManager();
      addTearDown(manager.dispose);
      final Future<RestorationBucket?> rootBucketFuture = manager.rootBucket;
      RestorationBucket? rootBucket;
      rootBucketFuture.then((RestorationBucket? bucket) {
        rootBucket = bucket;
      });
      expect(rootBucketFuture, isNotNull);
      expect(rootBucket, isNull);

      // Accessing rootBucket again gives same future.
      expect(manager.rootBucket, same(rootBucketFuture));

      // Engine has only been contacted once.
      expect(callsToEngine, hasLength(1));
      expect(callsToEngine.single.method, 'get');

      // Complete the engine request.
      result.complete(_createEncodedRestorationData1());
      await tester.pump();

      // Root bucket future completed.
      expect(rootBucket, isNotNull);

      // Root bucket contains the expected data.
      expect(rootBucket!.read<int>('value1'), 10);
      expect(rootBucket!.read<String>('value2'), 'Hello');
      final RestorationBucket child = rootBucket!.claimChild('child1', debugOwner: null);
      addTearDown(child.dispose);
      expect(child.read<int>('another value'), 22);

      // Accessing the root bucket again completes synchronously with same bucket.
      RestorationBucket? synchronousBucket;
      manager.rootBucket.then((RestorationBucket? bucket) {
        synchronousBucket = bucket;
      });
      expect(synchronousBucket, isNotNull);
      expect(synchronousBucket, same(rootBucket));
    });

    testWidgets('root bucket received from engine before retrieval', (WidgetTester tester) async {
      SystemChannels.restoration.setMethodCallHandler(null);
      final List<MethodCall> callsToEngine = <MethodCall>[];
      tester.binding.defaultBinaryMessenger.setMockMethodCallHandler(SystemChannels.restoration, (
        MethodCall call,
      ) async {
        callsToEngine.add(call);
        return null;
      });
      final RestorationManager manager = RestorationManager();
      addTearDown(manager.dispose);

      await _pushDataFromEngine(_createEncodedRestorationData1());

      RestorationBucket? rootBucket;
      manager.rootBucket.then((RestorationBucket? bucket) => rootBucket = bucket);
      // Root bucket is available synchronously.
      expect(rootBucket, isNotNull);
      // Engine was never asked.
      expect(callsToEngine, isEmpty);
    });

    testWidgets('root bucket received while engine retrieval is pending', (
      WidgetTester tester,
    ) async {
      SystemChannels.restoration.setMethodCallHandler(null);
      final List<MethodCall> callsToEngine = <MethodCall>[];
      final Completer<Map<dynamic, dynamic>> result = Completer<Map<dynamic, dynamic>>();
      tester.binding.defaultBinaryMessenger.setMockMethodCallHandler(SystemChannels.restoration, (
        MethodCall call,
      ) {
        callsToEngine.add(call);
        return result.future;
      });
      final RestorationManager manager = RestorationManager();
      addTearDown(manager.dispose);

      RestorationBucket? rootBucket;
      manager.rootBucket.then((RestorationBucket? bucket) => rootBucket = bucket);
      expect(rootBucket, isNull);
      expect(callsToEngine.single.method, 'get');

      await _pushDataFromEngine(_createEncodedRestorationData1());
      expect(rootBucket, isNotNull);
      expect(rootBucket!.read<int>('value1'), 10);

      result.complete(_createEncodedRestorationData2());
      await tester.pump();

      RestorationBucket? rootBucket2;
      manager.rootBucket.then((RestorationBucket? bucket) => rootBucket2 = bucket);
      expect(rootBucket2, isNotNull);
      expect(rootBucket2, same(rootBucket));
      expect(rootBucket2!.read<int>('value1'), 10);
      expect(rootBucket2!.contains('foo'), isFalse);
    });

    testWidgets('root bucket is properly replaced when new data is available', (
      WidgetTester tester,
    ) async {
      tester.binding.defaultBinaryMessenger.setMockMethodCallHandler(SystemChannels.restoration, (
        MethodCall call,
      ) async {
        return _createEncodedRestorationData1();
      });
      final RestorationManager manager = RestorationManager();
      addTearDown(manager.dispose);
      RestorationBucket? rootBucket;
      manager.rootBucket.then((RestorationBucket? bucket) {
        rootBucket = bucket;
      });
      await tester.pump();
      expect(rootBucket, isNotNull);
      expect(rootBucket!.read<int>('value1'), 10);
      final RestorationBucket child = rootBucket!.claimChild('child1', debugOwner: null);
      expect(child.read<int>('another value'), 22);

      bool rootReplaced = false;
      RestorationBucket? newRoot;
      manager.addListener(() {
        rootReplaced = true;
        manager.rootBucket.then((RestorationBucket? bucket) {
          newRoot = bucket;
        });
        // The new bucket is available synchronously.
        expect(newRoot, isNotNull);
      });

      // Send new Data.
      await _pushDataFromEngine(_createEncodedRestorationData2());

      expect(rootReplaced, isTrue);
      expect(newRoot, isNot(same(rootBucket)));

      child.dispose();

      expect(newRoot!.read<int>('foo'), 33);
      expect(newRoot!.read<int>('value1'), null);
      final RestorationBucket newChild = newRoot!.claimChild('childFoo', debugOwner: null);
      addTearDown(newChild.dispose);
      expect(newChild.read<String>('bar'), 'Hello');
    });

    testWidgets('returns null as root bucket when restoration is disabled', (
      WidgetTester tester,
    ) async {
      final List<MethodCall> callsToEngine = <MethodCall>[];
      final Completer<Map<dynamic, dynamic>> result = Completer<Map<dynamic, dynamic>>();
      tester.binding.defaultBinaryMessenger.setMockMethodCallHandler(SystemChannels.restoration, (
        MethodCall call,
      ) {
        callsToEngine.add(call);
        return result.future;
      });
      int listenerCount = 0;
      final RestorationManager manager = RestorationManager()
        ..addListener(() {
          listenerCount++;
        });
      addTearDown(manager.dispose);
      RestorationBucket? rootBucket;
      bool rootBucketResolved = false;
      manager.rootBucket.then((RestorationBucket? bucket) {
        rootBucketResolved = true;
        rootBucket = bucket;
      });
      await tester.pump();
      expect(rootBucketResolved, isFalse);
      expect(listenerCount, 0);

      result.complete(_packageRestorationData(enabled: false));
      await tester.pump();
      expect(rootBucketResolved, isTrue);
      expect(rootBucket, isNull);

      // Switch to non-null.
      await _pushDataFromEngine(_createEncodedRestorationData1());
      expect(listenerCount, 1);
      manager.rootBucket.then((RestorationBucket? bucket) {
        rootBucket = bucket;
      });
      expect(rootBucket, isNotNull);

      // Switch to null again.
      await _pushDataFromEngine(_packageRestorationData(enabled: false));
      expect(listenerCount, 2);
      manager.rootBucket.then((RestorationBucket? bucket) {
        rootBucket = bucket;
      });
      expect(rootBucket, isNull);
    });

    testWidgets('flushData', (WidgetTester tester) async {
      final List<MethodCall> callsToEngine = <MethodCall>[];
      final Completer<Map<dynamic, dynamic>> result = Completer<Map<dynamic, dynamic>>();
      tester.binding.defaultBinaryMessenger.setMockMethodCallHandler(SystemChannels.restoration, (
        MethodCall call,
      ) {
        callsToEngine.add(call);
        return result.future;
      });

      final RestorationManager manager = RestorationManager();
      addTearDown(manager.dispose);
      final Future<RestorationBucket?> rootBucketFuture = manager.rootBucket;
      RestorationBucket? rootBucket;
      rootBucketFuture.then((RestorationBucket? bucket) {
        rootBucket = bucket;
      });
      result.complete(_createEncodedRestorationData1());
      await tester.pump();
      expect(rootBucket, isNotNull);
      callsToEngine.clear();

      // Schedule a frame.
      SchedulerBinding.instance.ensureVisualUpdate();
      rootBucket!.write('foo', 1);
      // flushData is no-op because frame is scheduled.
      manager.flushData();
      expect(callsToEngine, isEmpty);
      // Data is flushed at the end of the frame.
      await tester.pump();
      expect(callsToEngine, hasLength(1));
      callsToEngine.clear();

      // flushData without frame sends data directly.
      rootBucket!.write('foo', 2);
      manager.flushData();
      expect(callsToEngine, hasLength(1));
    });

    testWidgets('isReplacing', (WidgetTester tester) async {
      final Completer<Map<dynamic, dynamic>> result = Completer<Map<dynamic, dynamic>>();
      tester.binding.defaultBinaryMessenger.setMockMethodCallHandler(SystemChannels.restoration, (
        MethodCall call,
      ) {
        return result.future;
      });

      final TestRestorationManager manager = TestRestorationManager();
      addTearDown(manager.dispose);
      expect(manager.isReplacing, isFalse);

      RestorationBucket? rootBucket;
      manager.rootBucket.then((RestorationBucket? bucket) {
        rootBucket = bucket;
      });
      result.complete(_createEncodedRestorationData1());
      await tester.idle();
      expect(rootBucket, isNotNull);
      expect(rootBucket!.isReplacing, isFalse);
      expect(manager.isReplacing, isFalse);
      tester.binding.scheduleFrame();
      await tester.pump();
      expect(manager.isReplacing, isFalse);
      expect(rootBucket!.isReplacing, isFalse);

      manager.receiveDataFromEngine(enabled: true, data: null);
      RestorationBucket? rootBucket2;
      manager.rootBucket.then((RestorationBucket? bucket) {
        rootBucket2 = bucket;
      });
      expect(rootBucket2, isNotNull);
      expect(rootBucket2, isNot(same(rootBucket)));
      expect(manager.isReplacing, isTrue);
      expect(rootBucket2!.isReplacing, isTrue);
      await tester.idle();
      expect(manager.isReplacing, isTrue);
      expect(rootBucket2!.isReplacing, isTrue);
      tester.binding.scheduleFrame();
      await tester.pump();
      expect(manager.isReplacing, isFalse);
      expect(rootBucket2!.isReplacing, isFalse);

      manager.receiveDataFromEngine(enabled: false, data: null);
      RestorationBucket? rootBucket3;
      manager.rootBucket.then((RestorationBucket? bucket) {
        rootBucket3 = bucket;
      });
      expect(rootBucket3, isNull);
      expect(manager.isReplacing, isFalse);
      await tester.idle();
      expect(manager.isReplacing, isFalse);
      tester.binding.scheduleFrame();
      await tester.pump();
      expect(manager.isReplacing, isFalse);
    });
  });

  test('debugIsSerializableForRestoration', () {
    expect(debugIsSerializableForRestoration(Object()), isFalse);
    expect(debugIsSerializableForRestoration(Container()), isFalse);

    expect(debugIsSerializableForRestoration(null), isTrue);
    expect(debugIsSerializableForRestoration(147823), isTrue);
    expect(debugIsSerializableForRestoration(12.43), isTrue);
    expect(debugIsSerializableForRestoration('Hello World'), isTrue);
    expect(debugIsSerializableForRestoration(<int>[12, 13, 14]), isTrue);
    expect(debugIsSerializableForRestoration(<String, int>{'v1': 10, 'v2': 23}), isTrue);
    expect(
      debugIsSerializableForRestoration(<String, dynamic>{
        'hello': <int>[12, 12, 12],
        'world': <int, bool>{1: true, 2: false, 4: true},
      }),
      isTrue,
    );
  });
}

Future<void> _pushDataFromEngine(Map<dynamic, dynamic> data) async {
  await TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.handlePlatformMessage(
    'flutter/restoration',
    const StandardMethodCodec().encodeMethodCall(MethodCall('push', data)),
    (_) {},
  );
}

Map<dynamic, dynamic> _createEncodedRestorationData1() {
  final Map<String, dynamic> data = <String, dynamic>{
    valuesMapKey: <String, dynamic>{'value1': 10, 'value2': 'Hello'},
    childrenMapKey: <String, dynamic>{
      'child1': <String, dynamic>{
        valuesMapKey: <String, dynamic>{'another value': 22},
      },
    },
  };
  return _packageRestorationData(data: data);
}

Map<dynamic, dynamic> _createEncodedRestorationData2() {
  final Map<String, dynamic> data = <String, dynamic>{
    valuesMapKey: <String, dynamic>{'foo': 33},
    childrenMapKey: <String, dynamic>{
      'childFoo': <String, dynamic>{
        valuesMapKey: <String, dynamic>{'bar': 'Hello'},
      },
    },
  };
  return _packageRestorationData(data: data);
}

Map<dynamic, dynamic> _packageRestorationData({bool enabled = true, Map<dynamic, dynamic>? data}) {
  final ByteData? encoded = const StandardMessageCodec().encodeMessage(data);
  return <dynamic, dynamic>{
    'enabled': enabled,
    'data': encoded?.buffer.asUint8List(encoded.offsetInBytes, encoded.lengthInBytes),
  };
}

class TestRestorationManager extends RestorationManager {
  void receiveDataFromEngine({required bool enabled, required Uint8List? data}) {
    handleRestorationUpdateFromEngine(enabled: enabled, data: data);
  }
}
