// Copyright (C) 2022 WYATT GROUP
// Please see the AUTHORS file for details.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.

import 'dart:async';
import 'dart:math';

import 'package:wyatt_authentication_bloc/wyatt_authentication_bloc.dart';
import 'package:wyatt_type_utils/wyatt_type_utils.dart';

class AuthenticationMockDataSourceImpl extends AuthenticationRemoteDataSource {
  AuthenticationMockDataSourceImpl({
    this.idToken = 'fake-id-token',
    this.registeredAccounts,
  });
  Pair<Account, String>? _connectedMock;
  Pair<Account, String>? _registeredMock;
  DateTime _lastSignInTime = DateTime.now();
  final StreamController<Account?> _streamAccount = StreamController()
    ..add(null);

  final List<Pair<Account, String>>? registeredAccounts;
  final String idToken;

  Future<void> _randomDelay() async {
    await Future<void>.delayed(
      Duration(milliseconds: Random().nextInt(400) + 200),
    );
    return;
  }

  @override
  Future<void> confirmPasswordReset({
    required String code,
    required String newPassword,
  }) async {
    await _randomDelay();
  }

  @override
  Future<String> getIdentityToken() async {
    await _randomDelay();
    return idToken;
  }

  @override
  Future<void> refresh() async {
    await _randomDelay();
    if (_connectedMock.isNull) {
      throw RefreshFailureFirebase();
    }
    final refresh = DateTime.now();
    final mock = (_connectedMock?.left as AccountModel?)
        ?.copyWith(lastSignInTime: refresh);
    _connectedMock = _connectedMock?.copyWith(left: mock);
    _streamAccount.add(mock);
  }

  @override
  Future<void> sendEmailVerification() async {
    await _randomDelay();
    if (_connectedMock.isNotNull) {
      final refresh = DateTime.now();
      final mock = (_connectedMock?.left as AccountModel?)?.copyWith(
        emailVerified: false,
        lastSignInTime: refresh,
      );
      _streamAccount.add(mock);
      _connectedMock = _connectedMock?.copyWith(left: mock);
      return;
    }
    throw SendEmailVerificationFailureFirebase();
  }

  @override
  Future<void> sendPasswordResetEmail({required String email}) async {
    await _randomDelay();
    if (registeredAccounts.isNotNull) {
      final accounts =
          registeredAccounts?.where((pair) => pair.left?.email == email);
      if (accounts.isNotNullOrEmpty) {
        return;
      }
    }
    if (_registeredMock.isNotNull) {
      if (_registeredMock?.left?.email != email) {
        throw SendPasswordResetEmailFailureFirebase();
      }
      return;
    }
    throw SendPasswordResetEmailFailureFirebase();
  }

  @override
  Future<Account> signInAnonymously() async {
    await _randomDelay();
    final creation = DateTime.now();
    final mock = AccountModel(
      uid: 'mock-id-anom',
      emailVerified: false,
      isAnonymous: true,
      providerId: 'wyatt-studio.fr',
      creationTime: creation,
      lastSignInTime: creation,
      isNewUser: creation == creation,
    );
    _streamAccount.add(mock);
    _connectedMock = _connectedMock?.copyWith(left: mock);
    _lastSignInTime = DateTime.now();
    return Future.value(mock);
  }

  @override
  Future<Account> signInWithGoogle() async {
    await _randomDelay();
    final creation = DateTime.now();
    final mock = AccountModel(
      uid: 'mock-id-google',
      emailVerified: true,
      isAnonymous: false,
      providerId: 'google.com',
      creationTime: creation,
      lastSignInTime: creation,
      isNewUser: creation == creation,
    );
    _streamAccount.add(mock);
    _connectedMock = _connectedMock?.copyWith(left: mock);
    _lastSignInTime = DateTime.now();
    return Future.value(mock);
  }

  @override
  Future<Account> signInWithEmailAndPassword({
    required String email,
    required String password,
  }) async {
    await _randomDelay();
    if (registeredAccounts.isNotNull) {
      final accounts =
          registeredAccounts?.where((pair) => pair.left?.email == email);
      if (accounts.isNotNullOrEmpty) {
        final account = accounts?.first;
        if (account?.right != password) {
          throw SignInWithCredentialFailureFirebase.fromCode('wrong-password');
        }
        _streamAccount.add(account!.left);
        _connectedMock = account.copyWith();
        return account.left!;
      }
    }
    if (_registeredMock.isNotNull) {
      if (_registeredMock?.left?.email != email) {
        throw SignInWithCredentialFailureFirebase.fromCode('user-not-found');
      }
      if (_registeredMock?.right != password) {
        throw SignInWithCredentialFailureFirebase.fromCode('wrong-password');
      }
      _streamAccount.add(_registeredMock!.left);
      _connectedMock = _registeredMock!.copyWith();
      _lastSignInTime = DateTime.now();
      return _registeredMock!.left!;
    }
    throw SignInWithCredentialFailureFirebase();
  }

  @override
  Future<void> signOut() async {
    _connectedMock = null;
    _streamAccount.add(null);
  }

  @override
  Future<Account> signUp({
    required String email,
    required String password,
  }) async {
    await _randomDelay();
    if (registeredAccounts.isNotNull) {
      final accounts =
          registeredAccounts?.where((pair) => pair.left?.email == email);
      if (accounts.isNotNullOrEmpty) {
        throw SignUpWithEmailAndPasswordFailureFirebase.fromCode(
          'email-already-in-use',
        );
      }
    }
    if (_registeredMock?.left?.email == email) {
      throw SignUpWithEmailAndPasswordFailureFirebase.fromCode(
        'email-already-in-use',
      );
    }
    final creation = DateTime.now();
    final mock = AccountModel(
      uid: 'mock-id-email',
      emailVerified: false,
      isAnonymous: false,
      providerId: 'wyatt',
      email: email,
      creationTime: creation,
      lastSignInTime: creation,
      isNewUser: creation == creation,
    );
    _streamAccount.add(mock);
    _registeredMock = Pair(mock, password);
    _connectedMock = _registeredMock!.copyWith();
    _lastSignInTime = DateTime.now();
    return Future.value(mock);
  }

  @override
  Stream<Account?> streamAccount() => _streamAccount.stream.asBroadcastStream();

  @override
  Future<bool> verifyPasswordResetCode({required String code}) async {
    await _randomDelay();
    return true;
  }

  @override
  Future<Account> reauthenticateWithCredential() async {
    await _randomDelay();
    if (_connectedMock.isNull) {
      throw ReauthenticateFailureFirebase();
    }
    await refresh();
    _lastSignInTime = DateTime.now();
    return Future.value(_connectedMock?.left);
  }

  @override
  Future<Account> updateEmail({required String email}) {
    final before = DateTime.now().subtract(const Duration(seconds: 10));
    if (_lastSignInTime.isBefore(before)) {
      throw UpdateEmailFailureFirebase('requires-recent-login');
    }
    final refresh = DateTime.now();
    final mock = (_connectedMock?.left as AccountModel?)
        ?.copyWith(lastSignInTime: refresh, email: email);
    _connectedMock = _connectedMock?.copyWith(left: mock);
    _registeredMock = _registeredMock?.copyWith(left: mock);
    _streamAccount.add(mock);
    return Future.value(_connectedMock?.left);
  }

  @override
  Future<Account> updatePassword({required String password}) {
    final before = DateTime.now().subtract(const Duration(seconds: 10));
    if (_lastSignInTime.isBefore(before)) {
      throw UpdatePasswordFailureFirebase('requires-recent-login');
    }
    final refresh = DateTime.now();
    final mock = (_connectedMock?.left as AccountModel?)
        ?.copyWith(lastSignInTime: refresh);
    _connectedMock = _connectedMock?.copyWith(left: mock, right: password);
    _registeredMock = _registeredMock?.copyWith(left: mock, right: password);
    _streamAccount.add(mock);
    return Future.value(_connectedMock?.left);
  }
}
