// 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.

// @dart = 2.8

import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';

import 'restoration.dart';

void main() {
  test('root bucket values', () {
    final MockRestorationManager manager = MockRestorationManager();
    final Map<String, dynamic> rawData = _createRawDataSet();
    final RestorationBucket bucket = RestorationBucket.root(manager: manager, rawData: rawData);

    expect(bucket.restorationId, 'root');
    expect(bucket.debugOwner, manager);

    // Bucket contains expected values from rawData.
    expect(bucket.read<int>('value1'), 10);
    expect(bucket.read<String>('value2'), 'Hello');
    expect(bucket.read<String>('value3'), isNull); // Does not exist.
    expect(manager.updateScheduled, isFalse);

    // Can overwrite existing value.
    bucket.write<int>('value1', 22);
    expect(manager.updateScheduled, isTrue);
    expect(bucket.read<int>('value1'), 22);
    manager.doSerialization();
    expect(rawData[valuesMapKey]['value1'], 22);
    expect(manager.updateScheduled, isFalse);

    // Can add a new value.
    bucket.write<bool>('value3', true);
    expect(manager.updateScheduled, isTrue);
    expect(bucket.read<bool>('value3'), true);
    manager.doSerialization();
    expect(rawData[valuesMapKey]['value3'], true);
    expect(manager.updateScheduled, isFalse);

    // Can remove existing value.
    expect(bucket.remove<int>('value1'), 22);
    expect(manager.updateScheduled, isTrue);
    expect(bucket.read<int>('value1'), isNull); // Does not exist anymore.
    manager.doSerialization();
    expect(rawData[valuesMapKey].containsKey('value1'), isFalse);
    expect(manager.updateScheduled, isFalse);

    // Removing non-existing value is no-op.
    expect(bucket.remove<Object>('value4'), isNull);
    expect(manager.updateScheduled, isFalse);

    // Can store null.
    bucket.write<bool>('value4', null);
    expect(manager.updateScheduled, isTrue);
    expect(bucket.read<int>('value4'), null);
    manager.doSerialization();
    expect(rawData[valuesMapKey].containsKey('value4'), isTrue);
    expect(rawData[valuesMapKey]['value4'], null);
    expect(manager.updateScheduled, isFalse);
  });

  test('child bucket values', () {
    final MockRestorationManager manager = MockRestorationManager();
    final Map<String, dynamic> rootRawData = _createRawDataSet();
    final Object debugOwner = Object();
    final RestorationBucket root = RestorationBucket.root(manager: manager, rawData: rootRawData);
    final RestorationBucket child = RestorationBucket.child(
      restorationId: 'child1',
      parent: root,
      debugOwner: debugOwner,
    );

    expect(child.restorationId, 'child1');
    expect(child.debugOwner, debugOwner);

    // Bucket contains expected values from rawData.
    expect(child.read<int>('foo'), 22);
    expect(child.read<String>('bar'), isNull); // Does not exist.
    expect(manager.updateScheduled, isFalse);

    // Can overwrite existing value.
    child.write<int>('foo', 44);
    expect(manager.updateScheduled, isTrue);
    expect(child.read<int>('foo'), 44);
    manager.doSerialization();
    expect(rootRawData[childrenMapKey]['child1'][valuesMapKey]['foo'], 44);
    expect(manager.updateScheduled, isFalse);

    // Can add a new value.
    child.write<bool>('value3', true);
    expect(manager.updateScheduled, isTrue);
    expect(child.read<bool>('value3'), true);
    manager.doSerialization();
    expect(rootRawData[childrenMapKey]['child1'][valuesMapKey]['value3'], true);
    expect(manager.updateScheduled, isFalse);

    // Can remove existing value.
    expect(child.remove<int>('foo'), 44);
    expect(manager.updateScheduled, isTrue);
    expect(child.read<int>('foo'), isNull); // Does not exist anymore.
    manager.doSerialization();
    expect(rootRawData[childrenMapKey]['child1'].containsKey('foo'), isFalse);
    expect(manager.updateScheduled, isFalse);

    // Removing non-existing value is no-op.
    expect(child.remove<Object>('value4'), isNull);
    expect(manager.updateScheduled, isFalse);

    // Can store null.
    child.write<bool>('value4', null);
    expect(manager.updateScheduled, isTrue);
    expect(child.read<int>('value4'), null);
    manager.doSerialization();
    expect(rootRawData[childrenMapKey]['child1'][valuesMapKey].containsKey('value4'), isTrue);
    expect(rootRawData[childrenMapKey]['child1'][valuesMapKey]['value4'], null);
    expect(manager.updateScheduled, isFalse);
  });

  test('claim child with exisiting data', () {
    final MockRestorationManager manager = MockRestorationManager();
    final Map<String, dynamic> rawData = _createRawDataSet();
    final RestorationBucket bucket = RestorationBucket.root(manager: manager, rawData: rawData);

    final Object debugOwner = Object();
    final RestorationBucket child = bucket.claimChild('child1', debugOwner: debugOwner);

    expect(manager.updateScheduled, isFalse);
    expect(child.restorationId, 'child1');
    expect(child.debugOwner, debugOwner);

    expect(child.read<int>('foo'), 22);
    child.write('bar', 44);
    expect(manager.updateScheduled, isTrue);
    manager.doSerialization();
    expect(rawData[childrenMapKey]['child1'][valuesMapKey]['bar'], 44);
    expect(manager.updateScheduled, isFalse);
  });

  test('claim child with no exisiting data', () {
    final MockRestorationManager manager = MockRestorationManager();
    final Map<String, dynamic> rawData = _createRawDataSet();
    final RestorationBucket bucket = RestorationBucket.root(manager: manager, rawData: rawData);

    expect(rawData[childrenMapKey].containsKey('child2'), isFalse);

    final Object debugOwner = Object();
    final RestorationBucket child = bucket.claimChild('child2', debugOwner: debugOwner);

    expect(manager.updateScheduled, isTrue);
    expect(child.restorationId, 'child2');
    expect(child.debugOwner, debugOwner);

    child.write('foo', 55);
    expect(child.read<int>('foo'), 55);
    manager.doSerialization();

    expect(manager.updateScheduled, isFalse);
    expect(rawData[childrenMapKey].containsKey('child2'), isTrue);
    expect(rawData[childrenMapKey]['child2'][valuesMapKey]['foo'], 55);
  });

  test('claim child that is already claimed throws if not given up', () {
    final MockRestorationManager manager = MockRestorationManager();
    final Map<String, dynamic> rawData = _createRawDataSet();
    final RestorationBucket bucket = RestorationBucket.root(manager: manager, rawData: rawData);

    final RestorationBucket child1 = bucket.claimChild('child1', debugOwner: 'FirstClaim');

    expect(manager.updateScheduled, isFalse);
    expect(child1.restorationId, 'child1');
    expect(child1.read<int>('foo'), 22);

    manager.doSerialization();
    expect(manager.updateScheduled, isFalse);

    final RestorationBucket child2 = bucket.claimChild('child1', debugOwner: 'SecondClaim');
    expect(child2.restorationId, 'child1');
    expect(child2.read<int>('foo'), isNull); // Value does not exist in this child.

    // child1 is not given up before running finalizers.
    try {
      manager.doSerialization();
      fail('expected error');
    } on FlutterError catch (e) {
      expect(
        e.message,
        'Multiple owners claimed child RestorationBuckets with the same IDs.\n'
        'The following IDs were claimed multiple times from the parent RestorationBucket(restorationId: root, owner: MockManager):\n'
        ' * "child1" was claimed by:\n'
        '   * SecondClaim\n'
        '   * FirstClaim (current owner)'
      );
    }
  });

  test('claim child that is already claimed does not throw if given up', () {
    final MockRestorationManager manager = MockRestorationManager();
    final Map<String, dynamic> rawData = _createRawDataSet();
    final RestorationBucket bucket = RestorationBucket.root(manager: manager, rawData: rawData);

    final RestorationBucket child1 = bucket.claimChild('child1', debugOwner: 'FirstClaim');

    expect(manager.updateScheduled, isFalse);
    expect(child1.restorationId, 'child1');
    expect(child1.read<int>('foo'), 22);

    manager.doSerialization();
    expect(manager.updateScheduled, isFalse);

    final RestorationBucket child2 = bucket.claimChild('child1', debugOwner: 'SecondClaim');
    expect(child2.restorationId, 'child1');
    expect(child2.read<int>('foo'), isNull); // Value does not exist in this child.
    child2.write<int>('bar', 55);

    // give up child1.
    child1.dispose();
    manager.doSerialization();
    expect(manager.updateScheduled, isFalse);
    expect(rawData[childrenMapKey]['child1'][valuesMapKey].containsKey('foo'), isFalse);
    expect(rawData[childrenMapKey]['child1'][valuesMapKey]['bar'], 55);
  });

  test('claiming a claimed child twice and only giving it up once throws', () {
    final MockRestorationManager manager = MockRestorationManager();
    final Map<String, dynamic> rawData = _createRawDataSet();
    final RestorationBucket bucket = RestorationBucket.root(manager: manager, rawData: rawData);

    final RestorationBucket child1 = bucket.claimChild('child1', debugOwner: 'FirstClaim');
    expect(child1.restorationId, 'child1');
    final RestorationBucket child2 = bucket.claimChild('child1', debugOwner: 'SecondClaim');
    expect(child2.restorationId, 'child1');
    child1.dispose();
    final RestorationBucket child3 = bucket.claimChild('child1', debugOwner: 'ThirdClaim');
    expect(child3.restorationId, 'child1');
    expect(manager.updateScheduled, isTrue);
    expect(() => manager.doSerialization(), throwsFlutterError);
  });

  test('unclaiming and then claiming same id gives fresh bucket', () {
    final MockRestorationManager manager = MockRestorationManager();
    final Map<String, dynamic> rawData = _createRawDataSet();
    final RestorationBucket bucket = RestorationBucket.root(manager: manager, rawData: rawData);

    final RestorationBucket child1 = bucket.claimChild('child1', debugOwner: 'FirstClaim');
    expect(manager.updateScheduled, isFalse);
    expect(child1.read<int>('foo'), 22);
    child1.dispose();
    expect(manager.updateScheduled, isTrue);
    final RestorationBucket child2 = bucket.claimChild('child1', debugOwner: 'SecondClaim');
    expect(child2.read<int>('foo'), isNull);
  });

  test('cleans up raw data if last value/child is dropped', () {
    final MockRestorationManager manager = MockRestorationManager();
    final Map<String, dynamic> rawData = _createRawDataSet();
    final RestorationBucket root = RestorationBucket.root(manager: manager, rawData: rawData);

    expect(rawData.containsKey(childrenMapKey), isTrue);
    final RestorationBucket child = root.claimChild('child1', debugOwner: 'owner');
    child.dispose();
    expect(manager.updateScheduled, isTrue);
    manager.doSerialization();
    expect(rawData.containsKey(childrenMapKey), isFalse);

    expect(rawData.containsKey(valuesMapKey), isTrue);
    expect(root.remove<int>('value1'), 10);
    expect(root.remove<String>('value2'), 'Hello');
    expect(manager.updateScheduled, isTrue);
    manager.doSerialization();
    expect(rawData.containsKey(valuesMapKey), isFalse);
  });

  test('dispose deletes data', () {
    final MockRestorationManager manager = MockRestorationManager();
    final Map<String, dynamic> rawData = _createRawDataSet();
    final RestorationBucket root = RestorationBucket.root(manager: manager, rawData: rawData);

    final RestorationBucket child1 = root.claimChild('child1', debugOwner: 'owner1');
    child1.claimChild('child1OfChild1', debugOwner: 'owner1.1');
    child1.claimChild('child2OfChild1', debugOwner: 'owner1.2');
    final RestorationBucket child2 = root.claimChild('child2', debugOwner: 'owner2');

    expect(manager.updateScheduled, isTrue);
    manager.doSerialization();
    expect(manager.updateScheduled, isFalse);

    expect(rawData[childrenMapKey].containsKey('child1'), isTrue);
    expect(rawData[childrenMapKey].containsKey('child2'), isTrue);

    child1.dispose();
    expect(manager.updateScheduled, isTrue);
    manager.doSerialization();
    expect(manager.updateScheduled, isFalse);

    expect(rawData[childrenMapKey].containsKey('child1'), isFalse);

    child2.dispose();
    expect(manager.updateScheduled, isTrue);
    manager.doSerialization();
    expect(manager.updateScheduled, isFalse);

    expect(rawData.containsKey(childrenMapKey), isFalse);
  });

  test('rename is no-op if same id', () {
    final MockRestorationManager manager = MockRestorationManager();
    final Map<String, dynamic> rawData = _createRawDataSet();
    final RestorationBucket root = RestorationBucket.root(manager: manager, rawData: rawData);

    final RestorationBucket child = root.claimChild('child1', debugOwner: 'owner1');

    expect(manager.updateScheduled, isFalse);
    expect(child.restorationId, 'child1');
    child.rename('child1');
    expect(manager.updateScheduled, isFalse);
    expect(child.restorationId, 'child1');
    expect(rawData[childrenMapKey].containsKey('child1'), isTrue);
  });

  test('rename to unused id', () {
    final MockRestorationManager manager = MockRestorationManager();
    final Map<String, dynamic> rawData = _createRawDataSet();
    final RestorationBucket root = RestorationBucket.root(manager: manager, rawData: rawData);

    final RestorationBucket child = root.claimChild('child1', debugOwner: 'owner1');
    final Object rawChildData = rawData[childrenMapKey]['child1'];
    expect(rawChildData, isNotNull);

    expect(manager.updateScheduled, isFalse);
    expect(child.restorationId, 'child1');
    child.rename('new-name');
    expect(child.restorationId, 'new-name');

    expect(manager.updateScheduled, isTrue);
    manager.doSerialization();
    expect(manager.updateScheduled, isFalse);

    expect(rawData[childrenMapKey].containsKey('child1'), isFalse);
    expect(rawData[childrenMapKey]['new-name'], rawChildData);
  });

  test('rename to used id throws if id is not given up', () {
    final MockRestorationManager manager = MockRestorationManager();
    final Map<String, dynamic> rawData = _createRawDataSet();
    final RestorationBucket root = RestorationBucket.root(manager: manager, rawData: rawData);

    final RestorationBucket child1 = root.claimChild('child1', debugOwner: 'owner1');
    final RestorationBucket child2 = root.claimChild('child2', debugOwner: 'owner1');
    manager.doSerialization();

    expect(child1.restorationId, 'child1');
    expect(child2.restorationId, 'child2');
    child2.rename('child1');
    expect(child2.restorationId, 'child1');

    expect(manager.updateScheduled, isTrue);
    expect(() => manager.doSerialization(), throwsFlutterError);
  });

  test('rename to used id does not throw if id is given up', () {
    final MockRestorationManager manager = MockRestorationManager();
    final Map<String, dynamic> rawData = _createRawDataSet();
    final RestorationBucket root = RestorationBucket.root(manager: manager, rawData: rawData);

    final RestorationBucket child1 = root.claimChild('child1', debugOwner: 'owner1');
    final RestorationBucket child2 = root.claimChild('child2', debugOwner: 'owner1');
    manager.doSerialization();

    final Object rawChild1Data = rawData[childrenMapKey]['child1'];
    expect(rawChild1Data, isNotNull);
    final Object rawChild2Data = rawData[childrenMapKey]['child2'];
    expect(rawChild2Data, isNotNull);

    expect(child1.restorationId, 'child1');
    expect(child2.restorationId, 'child2');
    child2.rename('child1');
    expect(child2.restorationId, 'child1');
    expect(child1.restorationId, 'child1');

    child1.dispose();

    expect(manager.updateScheduled, isTrue);
    manager.doSerialization();
    expect(manager.updateScheduled, isFalse);

    expect(rawData[childrenMapKey]['child1'], rawChild2Data);
    expect(rawData[childrenMapKey].containsKey('child2'), isFalse);
  });

  test('renaming a to be added child', () {
    final MockRestorationManager manager = MockRestorationManager();
    final Map<String, dynamic> rawData = _createRawDataSet();
    final RestorationBucket root = RestorationBucket.root(manager: manager, rawData: rawData);

    final Object rawChild1Data = rawData[childrenMapKey]['child1'];
    expect(rawChild1Data, isNotNull);

    final RestorationBucket child1 = root.claimChild('child1', debugOwner: 'owner1');
    final RestorationBucket child2 = root.claimChild('child1', debugOwner: 'owner1');

    child2.rename('foo');

    expect(manager.updateScheduled, isTrue);
    manager.doSerialization();
    expect(manager.updateScheduled, isFalse);

    expect(child1.restorationId, 'child1');
    expect(child2.restorationId, 'foo');

    expect(rawData[childrenMapKey]['child1'], rawChild1Data);
    expect(rawData[childrenMapKey]['foo'], isEmpty); // new bucket
  });

  test('adopt is no-op if same parent', () {
    final MockRestorationManager manager = MockRestorationManager();
    final Map<String, dynamic> rawData = _createRawDataSet();
    final RestorationBucket root = RestorationBucket.root(manager: manager, rawData: rawData);

    final RestorationBucket child1 = root.claimChild('child1', debugOwner: 'owner1');

    root.adoptChild(child1);
    expect(manager.updateScheduled, isFalse);
    expect(rawData[childrenMapKey].containsKey('child1'), isTrue);
  });

  test('adopt fresh child', () {
    final MockRestorationManager manager = MockRestorationManager();
    final Map<String, dynamic> rawData = _createRawDataSet();
    final RestorationBucket root = RestorationBucket.root(manager: manager, rawData: rawData);

    final RestorationBucket child = RestorationBucket.empty(restorationId: 'fresh-child', debugOwner: 'owner1');

    root.adoptChild(child);
    expect(manager.updateScheduled, isTrue);

    child.write('value', 22);

    manager.doSerialization();
    expect(manager.updateScheduled, isFalse);

    expect(rawData[childrenMapKey].containsKey('fresh-child'), isTrue);
    expect(rawData[childrenMapKey]['fresh-child'][valuesMapKey]['value'], 22);

    child.write('bar', 'blabla');
    expect(manager.updateScheduled, isTrue);
  });

  test('adopt child that already had a parent', () {
    final MockRestorationManager manager = MockRestorationManager();
    final Map<String, dynamic> rawData = _createRawDataSet();
    final RestorationBucket root = RestorationBucket.root(manager: manager, rawData: rawData);

    final RestorationBucket child = root.claimChild('child1', debugOwner: 'owner1');
    final RestorationBucket childOfChild = child.claimChild('childOfChild', debugOwner: 'owner2');
    childOfChild.write<String>('foo', 'bar');

    expect(manager.updateScheduled, isTrue);
    manager.doSerialization();
    expect(manager.updateScheduled, isFalse);

    final Object childOfChildData = rawData[childrenMapKey]['child1'][childrenMapKey]['childOfChild'];
    expect(childOfChildData, isNotEmpty);

    root.adoptChild(childOfChild);
    expect(manager.updateScheduled, isTrue);
    manager.doSerialization();
    expect(manager.updateScheduled, isFalse);

    expect(rawData[childrenMapKey]['child1'].containsKey(childrenMapKey), isFalse); // child1 has no children anymore.
    expect(rawData[childrenMapKey]['childOfChild'], childOfChildData);
  });

  test('adopting child throws if id is already in use and not given up', () {
    final MockRestorationManager manager = MockRestorationManager();
    final Map<String, dynamic> rawData = _createRawDataSet();
    final RestorationBucket root = RestorationBucket.root(manager: manager, rawData: rawData);

    final RestorationBucket child = root.claimChild('child1', debugOwner: 'owner1');
    final RestorationBucket childOfChild = child.claimChild('child1', debugOwner: 'owner2');
    childOfChild.write<String>('foo', 'bar');

    root.adoptChild(childOfChild);
    expect(manager.updateScheduled, isTrue);
    expect(() => manager.doSerialization(), throwsFlutterError);
  });

  test('adopting child does not throw if id is already in use and given up', () {
    final MockRestorationManager manager = MockRestorationManager();
    final Map<String, dynamic> rawData = _createRawDataSet();
    final RestorationBucket root = RestorationBucket.root(manager: manager, rawData: rawData);

    final RestorationBucket child = root.claimChild('child1', debugOwner: 'owner1');
    final RestorationBucket childOfChild = child.claimChild('child1', debugOwner: 'owner2');
    childOfChild.write<String>('foo', 'bar');

    final Object childOfChildData = rawData[childrenMapKey]['child1'][childrenMapKey]['child1'];
    expect(childOfChildData, isNotEmpty);

    expect(manager.updateScheduled, isTrue);
    manager.doSerialization();
    expect(manager.updateScheduled, isFalse);

    root.adoptChild(childOfChild);
    expect(manager.updateScheduled, isTrue);
    child.dispose();
    manager.doSerialization();
    expect(manager.updateScheduled, isFalse);

    expect(rawData[childrenMapKey]['child1'], childOfChildData);
  });

  test('adopting a to-be-added child under an already in use id', () {
    final MockRestorationManager manager = MockRestorationManager();
    final Map<String, dynamic> rawData = _createRawDataSet();
    final RestorationBucket root = RestorationBucket.root(manager: manager, rawData: rawData);

    final RestorationBucket child1 = root.claimChild('child1', debugOwner: 'owner1');
    final RestorationBucket child2 = root.claimChild('child2', debugOwner: 'owner1');

    expect(manager.updateScheduled, isTrue);
    manager.doSerialization();
    expect(manager.updateScheduled, isFalse);

    final RestorationBucket child1OfChild1 = child1.claimChild('child2', debugOwner: 'owner2');
    child1OfChild1.write<String>('hello', 'world');
    final RestorationBucket child2OfChild1 = child1.claimChild('child2', debugOwner: 'owner2');
    child2OfChild1.write<String>('foo', 'bar');

    root.adoptChild(child2OfChild1);
    child2.dispose();

    expect(manager.updateScheduled, isTrue);
    manager.doSerialization();
    expect(manager.updateScheduled, isFalse);

    expect(rawData[childrenMapKey]['child2'][valuesMapKey]['foo'], 'bar');
    expect(rawData[childrenMapKey]['child1'][childrenMapKey]['child2'][valuesMapKey]['hello'], 'world');
  });

  test('throws when used after dispose', () {
    final RestorationBucket bucket = RestorationBucket.empty(restorationId: 'foo', debugOwner: null);
    bucket.dispose();

    expect(() => bucket.debugOwner, throwsFlutterError);
    expect(() => bucket.restorationId, throwsFlutterError);
    expect(() => bucket.read<int>('foo'), throwsFlutterError);
    expect(() => bucket.write('foo', 10), throwsFlutterError);
    expect(() => bucket.remove<int>('foo'), throwsFlutterError);
    expect(() => bucket.contains('foo'), throwsFlutterError);
    expect(() => bucket.claimChild('child', debugOwner: null), throwsFlutterError);
    final RestorationBucket child = RestorationBucket.empty(restorationId: 'child', debugOwner: null);
    expect(() => bucket.adoptChild(child), throwsFlutterError);
    expect(() => bucket.rename('bar'), throwsFlutterError);
    expect(() => bucket.dispose(), throwsFlutterError);
  });
}

Map<String, dynamic> _createRawDataSet() {
  return <String, dynamic>{
    valuesMapKey: <String, dynamic>{
      'value1' : 10,
      'value2' : 'Hello',
    },
    childrenMapKey: <String, dynamic>{
      'child1' : <String, dynamic>{
        valuesMapKey : <String, dynamic>{
          'foo': 22,
        },
      },
    },
  };
}
