// 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 'dart:convert';
import 'dart:io';

import 'package:flutter_devicelab/framework/adb.dart';
import 'package:flutter_devicelab/framework/framework.dart';
import 'package:flutter_devicelab/framework/task_result.dart';
import 'package:flutter_devicelab/framework/utils.dart';
import 'package:path/path.dart' as path;
import 'package:vm_service/vm_service.dart';
import 'package:vm_service/vm_service_io.dart';

void main() {
  task(() async {
    int vmServicePort;

    final Device device = await devices.workingDevice;
    await device.unlock();
    final Directory appDir = dir(path.join(flutterDirectory.path, 'dev/integration_tests/ui'));
    await inDirectory(appDir, () async {
      final Completer<void> ready = Completer<void>();
      bool ok;
      print('run: starting...');
      final Process run = await startProcess(
        path.join(flutterDirectory.path, 'bin', 'flutter'),
        <String>['run', '--verbose', '--no-fast-start', '--no-publish-port', '--disable-service-auth-codes', '-d', device.deviceId, 'lib/main.dart'],
      );
      run.stdout
          .transform<String>(utf8.decoder)
          .transform<String>(const LineSplitter())
          .listen((String line) {
        print('run:stdout: $line');
        if (vmServicePort == null) {
          vmServicePort = parseServicePort(line);
          if (vmServicePort != null) {
            print('service protocol connection available at port $vmServicePort');
            print('run: ready!');
            ready.complete();
            ok ??= true;
          }
        }
      });
      run.stderr
          .transform<String>(utf8.decoder)
          .transform<String>(const LineSplitter())
          .listen((String line) {
        stderr.writeln('run:stderr: $line');
      });
      run.exitCode.then<void>((int exitCode) { ok = false; });
      await Future.any<dynamic>(<Future<dynamic>>[ ready.future, run.exitCode ]);
      if (!ok)
        throw 'Failed to run test app.';

      final VmService client = await vmServiceConnectUri('ws://localhost:$vmServicePort/ws');
      final VM vm = await client.getVM();
      final IsolateRef isolate = vm.isolates.first;

      final StreamController<Event> frameEventsController = StreamController<Event>();
      final StreamController<Event> navigationEventsController = StreamController<Event>();
      try {
        await client.streamListen(EventKind.kExtension);
      } catch (err) {
        // Do nothing on errors.
      }
      client.onExtensionEvent.listen((Event event) {
        if (event.extensionKind == 'Flutter.Frame') {
          frameEventsController.add(event);
        } else if (event.extensionKind == 'Flutter.Navigation') {
          navigationEventsController.add(event);
        }
      });

      final Stream<Event> frameEvents = frameEventsController.stream;
      final Stream<Event> navigationEvents = navigationEventsController.stream;

      print('reassembling app...');
      final Future<Event> frameFuture = frameEvents.first;
      await client.callServiceExtension('ext.flutter.reassemble', isolateId: isolate.id);

      // ensure we get an event
      final Event event = await frameFuture;
      print('${event.kind}: ${event.data}');

      // validate the fields
      // {number: 8, startTime: 0, elapsed: 1437, build: 600, raster: 800}
      print(event.extensionData.data);
      expect(event.extensionData.data['number'] is int);
      expect((event.extensionData.data['number'] as int) >= 0);
      expect(event.extensionData.data['startTime'] is int);
      expect((event.extensionData.data['startTime'] as int) >= 0);
      expect(event.extensionData.data['elapsed'] is int);
      expect((event.extensionData.data['elapsed'] as int) >= 0);
      expect(event.extensionData.data['build'] is int);
      expect((event.extensionData.data['build'] as int) >= 0);
      expect(event.extensionData.data['raster'] is int);
      expect((event.extensionData.data['raster'] as int) >= 0);

      final Future<Event> navigationFuture = navigationEvents.first;
      // This tap triggers a navigation event.
      device.tap(100, 200);

      final Event navigationEvent = await navigationFuture;
      // validate the fields
      expect(navigationEvent.extensionData.data['route'] is Map<dynamic, dynamic>);
      final Map<dynamic, dynamic> route = navigationEvent.extensionData.data['route'] as Map<dynamic, dynamic>;
      expect(route['description'] is String);
      expect(route['settings'] is Map<dynamic, dynamic>);
      final Map<dynamic, dynamic> settings = route['settings'] as Map<dynamic, dynamic>;
      expect(settings.containsKey('name'));

      run.stdin.write('q');
      final int result = await run.exitCode;
      if (result != 0)
        throw 'Received unexpected exit code $result from run process.';
    });
    return TaskResult.success(null);
  });
}

void expect(bool value) {
  if (!value)
    throw 'failed assertion in service extensions test';
}