// Copyright 2013 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/services.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:shared_preferences_platform_interface/shared_preferences_platform_interface.dart';
import 'package:shared_preferences_platform_interface/types.dart';

void main() {
  TestWidgetsFlutterBinding.ensureInitialized();

  const String testString = 'hello world';
  const bool testBool = true;
  const int testInt = 42;
  const double testDouble = 3.14159;
  const List<String> testList = <String>['foo', 'bar'];
  const Map<String, Object> testValues = <String, Object>{
    'flutter.String': testString,
    'flutter.bool': testBool,
    'flutter.int': testInt,
    'flutter.double': testDouble,
    'flutter.List': testList,
  };

  const String testString2 = 'goodbye world';
  const bool testBool2 = false;
  const int testInt2 = 1337;
  const double testDouble2 = 2.71828;
  const List<String> testList2 = <String>['baz', 'quox'];
  const Map<String, dynamic> testValues2 = <String, dynamic>{
    'flutter.String': testString2,
    'flutter.bool': testBool2,
    'flutter.int': testInt2,
    'flutter.double': testDouble2,
    'flutter.List': testList2,
  };

  late FakeSharedPreferencesStore store;
  late SharedPreferences preferences;

  setUp(() async {
    store = FakeSharedPreferencesStore(testValues);
    SharedPreferencesStorePlatform.instance = store;
    preferences = await SharedPreferences.getInstance();
    store.log.clear();
  });

  test('reading', () async {
    expect(preferences.get('String'), testString);
    expect(preferences.get('bool'), testBool);
    expect(preferences.get('int'), testInt);
    expect(preferences.get('double'), testDouble);
    expect(preferences.get('List'), testList);
    expect(preferences.getString('String'), testString);
    expect(preferences.getBool('bool'), testBool);
    expect(preferences.getInt('int'), testInt);
    expect(preferences.getDouble('double'), testDouble);
    expect(preferences.getStringList('List'), testList);
    expect(store.log, <Matcher>[]);
  });

  test('writing', () async {
    await Future.wait(<Future<bool>>[
      preferences.setString('String', testString2),
      preferences.setBool('bool', testBool2),
      preferences.setInt('int', testInt2),
      preferences.setDouble('double', testDouble2),
      preferences.setStringList('List', testList2)
    ]);
    expect(
      store.log,
      <Matcher>[
        isMethodCall('setValue', arguments: <dynamic>[
          'String',
          'flutter.String',
          testString2,
        ]),
        isMethodCall('setValue', arguments: <dynamic>[
          'Bool',
          'flutter.bool',
          testBool2,
        ]),
        isMethodCall('setValue', arguments: <dynamic>[
          'Int',
          'flutter.int',
          testInt2,
        ]),
        isMethodCall('setValue', arguments: <dynamic>[
          'Double',
          'flutter.double',
          testDouble2,
        ]),
        isMethodCall('setValue', arguments: <dynamic>[
          'StringList',
          'flutter.List',
          testList2,
        ]),
      ],
    );
    store.log.clear();

    expect(preferences.getString('String'), testString2);
    expect(preferences.getBool('bool'), testBool2);
    expect(preferences.getInt('int'), testInt2);
    expect(preferences.getDouble('double'), testDouble2);
    expect(preferences.getStringList('List'), testList2);
    expect(store.log, equals(<MethodCall>[]));
  });

  test('removing', () async {
    const String key = 'testKey';
    await preferences.remove(key);
    expect(
        store.log,
        List<Matcher>.filled(
          1,
          isMethodCall(
            'remove',
            arguments: 'flutter.$key',
          ),
          growable: true,
        ));
  });

  test('containsKey', () async {
    const String key = 'testKey';

    expect(false, preferences.containsKey(key));

    await preferences.setString(key, 'test');
    expect(true, preferences.containsKey(key));
  });

  test('clearing', () async {
    await preferences.clear();
    expect(preferences.getString('String'), null);
    expect(preferences.getBool('bool'), null);
    expect(preferences.getInt('int'), null);
    expect(preferences.getDouble('double'), null);
    expect(preferences.getStringList('List'), null);
    expect(store.log, <Matcher>[isMethodCall('clear', arguments: null)]);
  });

  test('reloading', () async {
    await preferences.setString('String', testString);
    expect(preferences.getString('String'), testString);

    SharedPreferences.setMockInitialValues(testValues2.cast<String, Object>());
    expect(preferences.getString('String'), testString);

    await preferences.reload();
    expect(preferences.getString('String'), testString2);
  });

  test('back to back calls should return same instance.', () async {
    final Future<SharedPreferences> first = SharedPreferences.getInstance();
    final Future<SharedPreferences> second = SharedPreferences.getInstance();
    expect(await first, await second);
  });

  test('string list type is dynamic (usually from method channel)', () async {
    SharedPreferences.setMockInitialValues(<String, Object>{
      'dynamic_list': <dynamic>['1', '2']
    });
    final SharedPreferences prefs = await SharedPreferences.getInstance();
    final List<String>? value = prefs.getStringList('dynamic_list');
    expect(value, <String>['1', '2']);
  });

  group('mocking', () {
    const String key = 'dummy';
    const String prefixedKey = 'flutter.$key';

    test('test 1', () async {
      SharedPreferences.setMockInitialValues(
          <String, Object>{prefixedKey: 'my string'});
      final SharedPreferences prefs = await SharedPreferences.getInstance();
      final String? value = prefs.getString(key);
      expect(value, 'my string');
    });

    test('test 2', () async {
      SharedPreferences.setMockInitialValues(
          <String, Object>{prefixedKey: 'my other string'});
      final SharedPreferences prefs = await SharedPreferences.getInstance();
      final String? value = prefs.getString(key);
      expect(value, 'my other string');
    });
  });

  test('writing copy of strings list', () async {
    final List<String> myList = <String>[];
    await preferences.setStringList('myList', myList);
    myList.add('foobar');

    final List<String> cachedList = preferences.getStringList('myList')!;
    expect(cachedList, <String>[]);

    cachedList.add('foobar2');

    expect(preferences.getStringList('myList'), <String>[]);
  });

  test('calling mock initial values with non-prefixed keys succeeds', () async {
    SharedPreferences.setMockInitialValues(<String, Object>{
      'test': 'foo',
    });
    final SharedPreferences prefs = await SharedPreferences.getInstance();
    final String? value = prefs.getString('test');
    expect(value, 'foo');
  });

  test('getInstance always returns the same instance', () async {
    SharedPreferencesStorePlatform.instance = SlowInitSharedPreferencesStore();

    final Future<SharedPreferences> firstFuture =
        SharedPreferences.getInstance();
    final Future<SharedPreferences> secondFuture =
        SharedPreferences.getInstance();
    expect(identical(await firstFuture, await secondFuture), true);
  });

  test('calling setPrefix after getInstance throws', () async {
    const String newPrefix = 'newPrefix';

    await SharedPreferences.getInstance();
    Object? err;
    try {
      SharedPreferences.setPrefix(newPrefix);
    } catch (e) {
      err = e;
    }
    expect(err, isA<StateError>());
  });

  test('using setPrefix allows setting and getting', () async {
    const String newPrefix = 'newPrefix';

    SharedPreferences.resetStatic();
    SharedPreferences.setPrefix(newPrefix);

    final SharedPreferences prefs = await SharedPreferences.getInstance();

    await prefs.setString('stringKey', 'test');
    await prefs.setBool('boolKey', true);
    await prefs.setDouble('doubleKey', 3.14);

    final String? testString = prefs.getString('stringKey');
    expect(testString, 'test');

    final bool? testBool = prefs.getBool('boolKey');
    expect(testBool, true);

    final double? testDouble = prefs.getDouble('doubleKey');
    expect(testDouble, 3.14);
  });

  test('allowList only gets allowed items', () async {
    const Set<String> allowList = <String>{'stringKey', 'boolKey'};

    SharedPreferences.resetStatic();
    SharedPreferences.setPrefix('', allowList: allowList);

    final SharedPreferences prefs = await SharedPreferences.getInstance();

    await prefs.setString('stringKey', 'test');
    await prefs.setBool('boolKey', true);
    await prefs.setDouble('doubleKey', 3.14);

    await prefs.reload();

    final String? testString = prefs.getString('stringKey');
    expect(testString, 'test');

    final bool? testBool = prefs.getBool('boolKey');
    expect(testBool, true);

    final double? testDouble = prefs.getDouble('doubleKey');
    expect(testDouble, null);
  });

  test('using reload after setPrefix properly reloads the cache', () async {
    const String newPrefix = 'newPrefix';

    SharedPreferences.resetStatic();
    SharedPreferences.setPrefix(newPrefix);

    final SharedPreferences prefs = await SharedPreferences.getInstance();
    String? testString = prefs.getString('stringKey');

    await prefs.setString('stringKey', 'test');
    testString = prefs.getString('stringKey');
    expect(testString, 'test');

    await prefs.reload();
    final String? testStrings = prefs.getString('stringKey');
    expect(testStrings, 'test');
  });

  test('unimplemented errors in withParameters methods are updated', () async {
    final UnimplementedSharedPreferencesStore localStore =
        UnimplementedSharedPreferencesStore();
    SharedPreferencesStorePlatform.instance = localStore;
    SharedPreferences.resetStatic();
    SharedPreferences.setPrefix('');
    Object? err;

    try {
      await SharedPreferences.getInstance();
    } catch (e) {
      err = e;
    }
    expect(err, isA<UnimplementedError>());
    expect(
        err.toString(),
        contains(
            "Shared Preferences doesn't yet support the setPrefix method"));
  });

  test('non-Unimplemented errors pass through withParameters methods correctly',
      () async {
    final ThrowingSharedPreferencesStore localStore =
        ThrowingSharedPreferencesStore();
    SharedPreferencesStorePlatform.instance = localStore;
    SharedPreferences.resetStatic();
    SharedPreferences.setPrefix('');
    Object? err;

    try {
      await SharedPreferences.getInstance();
    } catch (e) {
      err = e;
    }
    expect(err, isA<StateError>());
    expect(err.toString(), contains('State Error'));
  });
}

class FakeSharedPreferencesStore extends SharedPreferencesStorePlatform {
  FakeSharedPreferencesStore(Map<String, Object> data)
      : backend = InMemorySharedPreferencesStore.withData(data);

  final InMemorySharedPreferencesStore backend;
  final List<MethodCall> log = <MethodCall>[];

  @override
  Future<bool> clear() {
    log.add(const MethodCall('clear'));
    return backend.clear();
  }

  @override
  Future<bool> clearWithParameters(ClearParameters parameters) {
    log.add(const MethodCall('clearWithParameters'));
    return backend.clearWithParameters(parameters);
  }

  @override
  Future<Map<String, Object>> getAll() {
    log.add(const MethodCall('getAll'));
    return backend.getAll();
  }

  @override
  Future<Map<String, Object>> getAllWithParameters(
      GetAllParameters parameters) {
    log.add(const MethodCall('getAllWithParameters'));
    return backend.getAllWithParameters(parameters);
  }

  @override
  Future<bool> remove(String key) {
    log.add(MethodCall('remove', key));
    return backend.remove(key);
  }

  @override
  Future<bool> setValue(String valueType, String key, Object value) {
    log.add(MethodCall('setValue', <dynamic>[valueType, key, value]));
    return backend.setValue(valueType, key, value);
  }
}

class UnimplementedSharedPreferencesStore
    extends SharedPreferencesStorePlatform {
  @override
  Future<bool> clear() {
    throw UnimplementedError();
  }

  @override
  Future<Map<String, Object>> getAll() {
    throw UnimplementedError();
  }

  @override
  Future<bool> remove(String key) {
    throw UnimplementedError();
  }

  @override
  Future<bool> setValue(String valueType, String key, Object value) {
    throw UnimplementedError();
  }
}

class SlowInitSharedPreferencesStore
    extends UnimplementedSharedPreferencesStore {
  @override
  Future<Map<String, Object>> getAll() async {
    await Future<void>.delayed(const Duration(seconds: 1));
    return <String, Object>{};
  }
}

class ThrowingSharedPreferencesStore extends SharedPreferencesStorePlatform {
  @override
  Future<bool> clear() {
    throw UnimplementedError();
  }

  @override
  Future<Map<String, Object>> getAll() {
    throw UnimplementedError();
  }

  @override
  Future<bool> remove(String key) {
    throw UnimplementedError();
  }

  @override
  Future<bool> setValue(String valueType, String key, Object value) {
    throw UnimplementedError();
  }

  @override
  Future<Map<String, Object>> getAllWithParameters(
      GetAllParameters parameters) {
    throw StateError('State Error');
  }
}