import 'package:fake_async/fake_async.dart';
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:speech_to_text/speech_recognition_error.dart';
import 'package:speech_to_text/speech_recognition_result.dart';
import 'package:speech_to_text/speech_to_text.dart';

import 'test_speech_channel_handler.dart';

void main() {
  TestWidgetsFlutterBinding.ensureInitialized();

  TestSpeechListener listener;
  TestSpeechChannelHandler speechHandler;
  SpeechToText speech;

  setUp(() {
    listener = TestSpeechListener();
    speech = SpeechToText.withMethodChannel(SpeechToText.speechChannel);
    speechHandler = TestSpeechChannelHandler(speech);
    speech.channel.setMockMethodCallHandler(speechHandler.methodCallHandler);
  });

  tearDown(() {
    speech.channel.setMockMethodCallHandler(null);
  });

  group('hasPermission', () {
    test('true if platform reports true', () async {
      expect(await speech.hasPermission, true);
    });
    test('false if platform reports false', () async {
      speechHandler.hasPermissionResult = false;
      expect(await speech.hasPermission, false);
    });
  });
  group('init', () {
    test('succeeds on platform success', () async {
      expect(await speech.initialize(), true);
      expect(speechHandler.initInvoked, true);
      expect(speech.isAvailable, true);
    });
    test('only invokes once', () async {
      expect(await speech.initialize(), true);
      speechHandler.initInvoked = false;
      expect(await speech.initialize(), true);
      expect(speechHandler.initInvoked, false);
    });
    test('fails on platform failure', () async {
      speechHandler.initResult = false;
      expect(await speech.initialize(), false);
      expect(speech.isAvailable, false);
    });
  });

  group('listen', () {
    test('fails with exception if not initialized', () async {
      try {
        await speech.listen();
        fail("Expected an exception.");
      } on SpeechToTextNotInitializedException {
        // This is a good result
      }
    });
    test('fails with exception if init fails', () async {
      try {
        speechHandler.initResult = false;
        await speech.initialize();
        await speech.listen();
        fail("Expected an exception.");
      } on SpeechToTextNotInitializedException {
        // This is a good result
      }
    });
    test('invokes listen after successful init', () async {
      await speech.initialize();
      await speech.listen();
      expect(speechHandler.listenLocale, isNull);
      expect(speechHandler.listenInvoked, true);
    });
    test('converts platformException to listenFailed', () async {
      await speech.initialize();
      speechHandler.listenException = true;
      try {
        await speech.listen();
        fail("Should have thrown");
      } on ListenFailedException catch (e) {
        expect(e.details, TestSpeechChannelHandler.listenExceptionDetails);
      } catch (wrongE) {
        fail("Should have been ListenFailedException");
      }
    });
    test('stops listen after listenFor duration', () async {
      fakeAsync((fa) {
        speech.initialize();
        fa.flushMicrotasks();
        speech.listen(listenFor: Duration(seconds: 2));
        fa.flushMicrotasks();
        expect(speech.isListening, isTrue);
        fa.elapse(Duration(seconds: 2));
        expect(speech.isListening, isFalse);
      });
    });
    test('stops listen after listenFor duration even with speech event',
        () async {
      fakeAsync((fa) {
        speech.initialize();
        fa.flushMicrotasks();
        speech.listen(listenFor: Duration(seconds: 1));
        speech.processMethodCall(MethodCall(SpeechToText.textRecognitionMethod,
            TestSpeechChannelHandler.firstRecognizedJson));
        fa.flushMicrotasks();
        expect(speech.isListening, isTrue);
        fa.elapse(Duration(seconds: 1));
        expect(speech.isListening, isFalse);
      });
    });
    test('stops listen after pauseFor duration with no speech', () async {
      fakeAsync((fa) {
        speech.initialize();
        fa.flushMicrotasks();
        speech.listen(pauseFor: Duration(seconds: 2));
        fa.flushMicrotasks();
        expect(speech.isListening, isTrue);
        fa.elapse(Duration(seconds: 2));
        expect(speech.isListening, isFalse);
      });
    });
    test('stops listen after pauseFor with longer listenFor duration',
        () async {
      fakeAsync((fa) {
        speech.initialize();
        fa.flushMicrotasks();
        speech.listen(
            pauseFor: Duration(seconds: 1), listenFor: Duration(seconds: 5));
        fa.flushMicrotasks();
        expect(speech.isListening, isTrue);
        fa.elapse(Duration(seconds: 1));
        expect(speech.isListening, isFalse);
      });
    });
    test('stops listen after listenFor with longer pauseFor duration',
        () async {
      fakeAsync((fa) {
        speech.initialize();
        fa.flushMicrotasks();
        speech.listen(
            listenFor: Duration(seconds: 1), pauseFor: Duration(seconds: 5));
        fa.flushMicrotasks();
        expect(speech.isListening, isTrue);
        fa.elapse(Duration(seconds: 1));
        expect(speech.isListening, isFalse);
      });
    });
    test('keeps listening after pauseFor with speech event', () async {
      fakeAsync((fa) {
        speech.initialize();
        fa.flushMicrotasks();
        speech.listen(pauseFor: Duration(seconds: 2));
        fa.flushMicrotasks();
        fa.elapse(Duration(seconds: 1));
        speech.processMethodCall(MethodCall(SpeechToText.textRecognitionMethod,
            TestSpeechChannelHandler.firstRecognizedJson));
        fa.flushMicrotasks();
        fa.elapse(Duration(seconds: 1));
        expect(speech.isListening, isTrue);
      });
    });
    test('uses localeId if provided', () async {
      await speech.initialize();
      await speech.listen(localeId: TestSpeechChannelHandler.localeId1);
      expect(speechHandler.listenInvoked, true);
      expect(speechHandler.listenLocale, TestSpeechChannelHandler.localeId1);
    });
    test('calls speech listener', () async {
      await speech.initialize();
      await speech.listen(onResult: listener.onSpeechResult);
      await speech.processMethodCall(MethodCall(
          SpeechToText.textRecognitionMethod,
          TestSpeechChannelHandler.firstRecognizedJson));
      expect(listener.speechResults, 1);
      expect(
          listener.results, [TestSpeechChannelHandler.firstRecognizedResult]);
      expect(speech.lastRecognizedWords,
          TestSpeechChannelHandler.firstRecognizedWords);
    });
    test('calls speech listener with multiple', () async {
      await speech.initialize();
      await speech.listen(onResult: listener.onSpeechResult);
      await speech.processMethodCall(MethodCall(
          SpeechToText.textRecognitionMethod,
          TestSpeechChannelHandler.firstRecognizedJson));
      await speech.processMethodCall(MethodCall(
          SpeechToText.textRecognitionMethod,
          TestSpeechChannelHandler.secondRecognizedJson));
      expect(listener.speechResults, 2);
      expect(listener.results, [
        TestSpeechChannelHandler.firstRecognizedResult,
        TestSpeechChannelHandler.secondRecognizedResult
      ]);
      expect(speech.lastRecognizedWords,
          TestSpeechChannelHandler.secondRecognizedWords);
    });
  });

  group('status callback', () {
    test('invoked on listen', () async {
      await speech.initialize(
          onError: listener.onSpeechError, onStatus: listener.onSpeechStatus);
      await speech.processMethodCall(MethodCall(
          SpeechToText.notifyStatusMethod, SpeechToText.listeningStatus));
      expect(listener.speechStatus, 1);
      expect(listener.statuses.contains(SpeechToText.listeningStatus), true);
    });
  });

  group('soundLevel callback', () {
    test('invoked on listen', () async {
      await speech.initialize();
      await speech.listen(onSoundLevelChange: listener.onSoundLevel);
      await speech.processMethodCall(MethodCall(
          SpeechToText.soundLevelChangeMethod,
          TestSpeechChannelHandler.level1));
      expect(listener.soundLevel, 1);
      expect(listener.soundLevels, contains(TestSpeechChannelHandler.level1));
    });
    test('sets lastLevel', () async {
      await speech.initialize();
      await speech.listen(onSoundLevelChange: listener.onSoundLevel);
      await speech.processMethodCall(MethodCall(
          SpeechToText.soundLevelChangeMethod,
          TestSpeechChannelHandler.level1));
      expect(speech.lastSoundLevel, TestSpeechChannelHandler.level1);
    });
  });

  group('cancel', () {
    test('does nothing if not initialized', () async {
      speech.cancel();
      expect(speechHandler.cancelInvoked, false);
    });
    test('cancels an active listen', () async {
      await speech.initialize();
      await speech.listen();
      await speech.cancel();
      expect(speechHandler.cancelInvoked, true);
      expect(speech.isListening, isFalse);
    });
  });
  group('stop', () {
    test('does nothing if not initialized', () async {
      speech.stop();
      expect(speechHandler.cancelInvoked, false);
    });
    test('stops an active listen', () async {
      await speech.initialize();
      speech.listen();
      speech.stop();
      expect(speechHandler.stopInvoked, true);
    });
  });
  group('error', () {
    test('notifies handler with transient', () async {
      await speech.initialize(onError: listener.onSpeechError);
      await speech.listen();
      await speech.processMethodCall(MethodCall(SpeechToText.notifyErrorMethod,
          TestSpeechChannelHandler.transientErrorJson));
      expect(listener.speechErrors, 1);
      expect(listener.errors.first.permanent, isFalse);
    });
    test('notifies handler with permanent', () async {
      await speech.initialize(onError: listener.onSpeechError);
      await speech.listen();
      await speech.processMethodCall(MethodCall(SpeechToText.notifyErrorMethod,
          TestSpeechChannelHandler.permanentErrorJson));
      expect(listener.speechErrors, 1);
      expect(listener.errors.first.permanent, isTrue);
    });
    test('continues listening on transient', () async {
      await speech.initialize(onError: listener.onSpeechError);
      await speech.listen();
      await speech.processMethodCall(MethodCall(SpeechToText.notifyErrorMethod,
          TestSpeechChannelHandler.transientErrorJson));
      expect(speech.isListening, isTrue);
    });
    test('continues listening on permanent if cancel not explicitly requested',
        () async {
      await speech.initialize(onError: listener.onSpeechError);
      await speech.listen();
      await speech.processMethodCall(MethodCall(SpeechToText.notifyErrorMethod,
          TestSpeechChannelHandler.permanentErrorJson));
      expect(speech.isListening, isTrue);
    });
    test('stops listening on permanent if cancel explicitly requested',
        () async {
      await speech.initialize(onError: listener.onSpeechError);
      await speech.listen(cancelOnError: true);
      await speech.processMethodCall(MethodCall(SpeechToText.notifyErrorMethod,
          TestSpeechChannelHandler.permanentErrorJson));
      expect(speech.isListening, isFalse);
    });
    test('Error not sent after cancel', () async {
      await speech.initialize(onError: listener.onSpeechError);
      await speech.listen();
      await speech.cancel();
      await speech.processMethodCall(MethodCall(SpeechToText.notifyErrorMethod,
          TestSpeechChannelHandler.permanentErrorJson));
      expect(speech.isListening, isFalse);
      expect(listener.speechErrors, 0);
    });
    test('Error still sent after implicit cancel', () async {
      await speech.initialize(onError: listener.onSpeechError);
      await speech.listen(cancelOnError: true);
      await speech.processMethodCall(MethodCall(SpeechToText.notifyErrorMethod,
          TestSpeechChannelHandler.permanentErrorJson));
      await speech.processMethodCall(MethodCall(SpeechToText.notifyErrorMethod,
          TestSpeechChannelHandler.permanentErrorJson));
      expect(speech.isListening, isFalse);
      expect(listener.speechErrors, 2);
    });
  });
  group('locales', () {
    test('fails with exception if not initialized', () async {
      try {
        await speech.locales();
        fail("Expected an exception.");
      } on SpeechToTextNotInitializedException {
        // This is a good result
      }
    });
    test('system locale null if not initialized', () async {
      LocaleName current;
      try {
        current = await speech.systemLocale();
        fail("Expected an exception.");
      } on SpeechToTextNotInitializedException {
        expect(current, isNull);
      }
    });
    test('handles an empty list', () async {
      await speech.initialize(onError: listener.onSpeechError);
      List<LocaleName> localeNames = await speech.locales();
      expect(speechHandler.localesInvoked, isTrue);
      expect(localeNames, isEmpty);
    });
    test('returns expected locales', () async {
      await speech.initialize(onError: listener.onSpeechError);
      speechHandler.locales.add(TestSpeechChannelHandler.locale1);
      speechHandler.locales.add(TestSpeechChannelHandler.locale2);
      List<LocaleName> localeNames = await speech.locales();
      expect(localeNames, hasLength(speechHandler.locales.length));
      expect(localeNames[0].localeId, TestSpeechChannelHandler.localeId1);
      expect(localeNames[0].name, TestSpeechChannelHandler.name1);
      expect(localeNames[1].localeId, TestSpeechChannelHandler.localeId2);
      expect(localeNames[1].name, TestSpeechChannelHandler.name2);
    });
    test('skips incorrect locales', () async {
      await speech.initialize(onError: listener.onSpeechError);
      speechHandler.locales.add("InvalidJunk");
      speechHandler.locales.add(TestSpeechChannelHandler.locale1);
      List<LocaleName> localeNames = await speech.locales();
      expect(localeNames, hasLength(1));
      expect(localeNames[0].localeId, TestSpeechChannelHandler.localeId1);
      expect(localeNames[0].name, TestSpeechChannelHandler.name1);
    });
    test('system locale matches first returned locale', () async {
      await speech.initialize(onError: listener.onSpeechError);
      speechHandler.locales.add(TestSpeechChannelHandler.locale1);
      speechHandler.locales.add(TestSpeechChannelHandler.locale2);
      LocaleName current = await speech.systemLocale();
      expect(current.localeId, TestSpeechChannelHandler.localeId1);
    });
  });
  group('status', () {
    test('recognized false at start', () async {
      expect(speech.hasRecognized, isFalse);
    });
    test('listening false at start', () async {
      expect(speech.isListening, isFalse);
    });
  });
  test('available false at start', () async {
    expect(speech.isAvailable, isFalse);
  });
  test('hasError false at start', () async {
    expect(speech.hasError, isFalse);
  });
  test('lastError null at start', () async {
    expect(speech.lastError, isNull);
  });
  test('status empty at start', () async {
    expect(speech.lastStatus, isEmpty);
  });
}

class TestSpeechListener {
  int speechResults = 0;
  List<SpeechRecognitionResult> results = [];
  int speechErrors = 0;
  List<SpeechRecognitionError> errors = [];
  int speechStatus = 0;
  List<String> statuses = [];
  int soundLevel = 0;
  List<double> soundLevels = [];

  void onSpeechResult(SpeechRecognitionResult result) {
    ++speechResults;
    results.add(result);
  }

  void onSpeechError(SpeechRecognitionError errorResult) {
    ++speechErrors;
    errors.add(errorResult);
  }

  void onSpeechStatus(String status) {
    ++speechStatus;
    statuses.add(status);
  }

  void onSoundLevel(double level) {
    ++soundLevel;
    soundLevels.add(level);
  }
}
