// 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 'dart:math';

import 'package:flutter_devicelab/common.dart';
import 'package:flutter_devicelab/framework/devices.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;

void generateMain(Directory appDir, String sentinel) {
  final String mainCode = '''
import 'package:flutter/foundation.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_driver/driver_extension.dart';

class ReassembleListener extends StatefulWidget {
  const ReassembleListener({Key key, this.child})
      : super(key: key);

  final Widget child;

  @override
  _ReassembleListenerState createState() => _ReassembleListenerState();
}

class _ReassembleListenerState extends State<ReassembleListener> {
  @override
  initState() {
    super.initState();
    print('$sentinel');
  }

  @override
  void reassemble() {
    super.reassemble();
    print('$sentinel');
  }

  @override
  Widget build(BuildContext context) {
    return widget.child;
  }
}

void main() {
  runApp(
    ReassembleListener(
      child: Text(
        'Hello, word!',
        textDirection: TextDirection.rtl,
      )
    )
  );
}
''';
  File(path.join(appDir.path, 'lib', 'fuchsia_main.dart'))
    .writeAsStringSync(mainCode, flush: true);
}

void main() {
  deviceOperatingSystem = DeviceOperatingSystem.fuchsia;

  task(() async {
    section('Checking environment variables');

    if (Platform.environment['FUCHSIA_SSH_CONFIG'] == null &&
        Platform.environment['FUCHSIA_BUILD_DIR'] == null) {
      throw Exception('No FUCHSIA_SSH_CONFIG or FUCHSIA_BUILD_DIR set');
    }

    final String flutterBinary = path.join(flutterDirectory.path, 'bin', 'flutter');

    section('Downloading Fuchsia SDK and flutter runner');

    // Download the Fuchsia SDK.
    final int precacheResult = await exec(
      flutterBinary,
      <String>[
        'precache',
        '--fuchsia',
        '--flutter_runner',
      ]
    );

    if (precacheResult != 0) {
      throw Exception('flutter precache failed with exit code $precacheResult');
    }

    final Directory fuchsiaToolDirectory =
      Directory(path.join(flutterDirectory.path, 'bin', 'cache', 'artifacts', 'fuchsia', 'tools'));
    if (!fuchsiaToolDirectory.existsSync()) {
      throw Exception('Expected Fuchsia tool directory at ${fuchsiaToolDirectory.path}');
    }

    final Device device = await devices.workingDevice;
    final Directory appDir = dir(path.join(
      flutterDirectory.path,
      'dev',
      'integration_tests',
      'ui',
    ));

    await inDirectory(appDir, () async {
      final Random random = Random();
      final Map<String, Completer<void>> sentinelMessage = <String, Completer<void>>{
        'sentinel-${random.nextInt(1<<32)}': Completer<void>(),
        'sentinel-${random.nextInt(1<<32)}': Completer<void>(),
      };

      late Process runProcess;
      late Process logsProcess;

      try {
        section('Creating lib/fuchsia_main.dart');

        generateMain(appDir, sentinelMessage.keys.toList()[0]);

        section('Launching `flutter run` in ${appDir.path}');

        runProcess = await startProcess(
          flutterBinary,
          <String>[
            'run',
            '--suppress-analytics',
            '-d', device.deviceId,
            '-t', 'lib/fuchsia_main.dart',
          ],
          isBot: false, // We just want to test the output, not have any debugging info.
        );

        logsProcess = await startProcess(
          flutterBinary,
          <String>['logs', '--suppress-analytics', '-d', device.deviceId],
          isBot: false, // We just want to test the output, not have any debugging info.
        );

        Future<dynamic> eventOrExit(Future<void> event) {
          return Future.any<dynamic>(<Future<dynamic>>[
            event,
            runProcess.exitCode,
            logsProcess.exitCode,
          ]);
        }

        logsProcess.stdout
          .transform<String>(utf8.decoder)
          .transform<String>(const LineSplitter())
          .listen((String log) {
            print('logs:stdout: $log');
            for (final String sentinel in sentinelMessage.keys) {
              if (log.contains(sentinel)) {
                if (sentinelMessage[sentinel]!.isCompleted) {
                  throw Exception(
                    'Expected a single `$sentinel` message in the device log, but found more than one'
                  );
                }
                sentinelMessage[sentinel]!.complete();
                break;
              }
            }
          });

        final Completer<void> hotReloadCompleter = Completer<void>();
        final Completer<void> reloadedCompleter = Completer<void>();
        final RegExp observatoryRegexp = RegExp('An Observatory debugger and profiler on .+ is available at');
        runProcess.stdout
          .transform<String>(utf8.decoder)
          .transform<String>(const LineSplitter())
          .listen((String line) {
            print('run:stdout: $line');
            if (observatoryRegexp.hasMatch(line)) {
              hotReloadCompleter.complete();
            } else if (line.contains('Reloaded')) {
              reloadedCompleter.complete();
            }
          });

        final List<String> runStderr = <String>[];
        runProcess.stderr
          .transform<String>(utf8.decoder)
          .transform<String>(const LineSplitter())
          .listen((String line) {
            runStderr.add(line);
            print('run:stderr: $line');
          });

        section('Waiting for hot reload availability');
        await eventOrExit(hotReloadCompleter.future);

        section('Waiting for Dart VM');
        // Wait for the first message in the log from the Dart VM.
        await eventOrExit(sentinelMessage.values.toList()[0].future);

        // Change the dart file.
        generateMain(appDir, sentinelMessage.keys.toList()[1]);

        section('Hot reload');
        runProcess.stdin.write('r');
        unawaited(runProcess.stdin.flush());
        await eventOrExit(reloadedCompleter.future);

        section('Waiting for Dart VM');
        // Wait for the second message in the log from the Dart VM.
        await eventOrExit(sentinelMessage.values.toList()[1].future);

        section('Quitting flutter run');

        runProcess.stdin.write('q');
        unawaited(runProcess.stdin.flush());

        final int runExitCode = await runProcess.exitCode;
        if (runExitCode != 0 || runStderr.isNotEmpty) {
          throw Exception(
            'flutter run exited with code $runExitCode and errors: ${runStderr.join('\n')}.'
          );
        }
      } finally {
        runProcess.kill();
        logsProcess.kill();
        File(path.join(appDir.path, 'lib', 'fuchsia_main.dart')).deleteSync();
      }

      for (final String sentinel in sentinelMessage.keys) {
        if (!sentinelMessage[sentinel]!.isCompleted) {
          throw Exception('Expected $sentinel in the device logs.');
        }
      }
    });

    return TaskResult.success(null);
  });
}