// 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:path/path.dart' as path;

import '../framework/devices.dart';
import '../framework/framework.dart';
import '../framework/task_result.dart';
import '../framework/utils.dart';

const String _packageName = 'package_with_native_assets';

const List<String> _buildModes = <String>[
  'debug',
  'profile',
  'release',
];

TaskFunction createNativeAssetsTest({
  String? deviceIdOverride,
  bool checkAppRunningOnLocalDevice = true,
  bool isIosSimulator = false,
}) {
  return () async {
    if (deviceIdOverride == null) {
      final Device device = await devices.workingDevice;
      await device.unlock();
      deviceIdOverride = device.deviceId;
    }

    await enableNativeAssets();

    for (final String buildMode in _buildModes) {
      if (buildMode != 'debug' && isIosSimulator) {
        continue;
      }
      final TaskResult buildModeResult = await inTempDir((Directory tempDirectory) async {
        final Directory packageDirectory = await createTestProject(_packageName, tempDirectory);
        final Directory exampleDirectory = dir(packageDirectory.uri.resolve('example/').toFilePath());

        final List<String> options = <String>[
          '-d',
          deviceIdOverride!,
          '--no-android-gradle-daemon',
          '--no-publish-port',
          '--verbose',
          '--uninstall-first',
          '--$buildMode',
        ];
        int transitionCount = 0;
        bool done = false;

        await inDirectory<void>(exampleDirectory, () async {
          final int runFlutterResult = await runFlutter(
            options: options,
            onLine: (String line, Process process) {
              if (done) {
                return;
              }
              switch (transitionCount) {
                case 0:
                  if (!line.contains('Flutter run key commands.')) {
                    return;
                  }
                  if (buildMode == 'debug') {
                    // Do a hot reload diff on the initial dill file.
                    process.stdin.writeln('r');
                  } else {
                    done = true;
                    process.stdin.writeln('q');
                  }
                case 1:
                  if (!line.contains('Reloaded')) {
                    return;
                  }
                  process.stdin.writeln('R');
                case 2:
                  // Do a hot restart, pushing a new complete dill file.
                  if (!line.contains('Restarted application')) {
                    return;
                  }
                  // Do another hot reload, pushing a diff to the second dill file.
                  process.stdin.writeln('r');
                case 3:
                  if (!line.contains('Reloaded')) {
                    return;
                  }
                  done = true;
                  process.stdin.writeln('q');
              }
              transitionCount += 1;
            },
          );
          if (runFlutterResult != 0) {
            print('Flutter run returned non-zero exit code: $runFlutterResult.');
          }
        });

        final int expectedNumberOfTransitions = buildMode == 'debug' ? 4 : 1;
        if (transitionCount != expectedNumberOfTransitions) {
          return TaskResult.failure(
            'Did not get expected number of transitions: $transitionCount '
            '(expected $expectedNumberOfTransitions)',
          );
        }
        return TaskResult.success(null);
      });
      if (buildModeResult.failed) {
        return buildModeResult;
      }
    }
    return TaskResult.success(null);
  };
}

Future<int> runFlutter({
  required List<String> options,
  required void Function(String, Process) onLine,
}) async {
  final Process process = await startFlutter(
    'run',
    options: options,
  );

  final Completer<void> stdoutDone = Completer<void>();
  final Completer<void> stderrDone = Completer<void>();
  process.stdout.transform<String>(utf8.decoder).transform<String>(const LineSplitter()).listen((String line) {
    onLine(line, process);
    print('stdout: $line');
  }, onDone: stdoutDone.complete);

  process.stderr.transform<String>(utf8.decoder).transform<String>(const LineSplitter()).listen(
        (String line) => print('stderr: $line'),
        onDone: stderrDone.complete,
      );

  await Future.wait<void>(<Future<void>>[stdoutDone.future, stderrDone.future]);
  final int exitCode = await process.exitCode;
  return exitCode;
}

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

Future<void> enableNativeAssets() async {
  print('Enabling configs for native assets...');
  final int configResult = await exec(
      _flutterBin,
      <String>[
        'config',
        '-v',
        '--enable-native-assets',
      ],
      canFail: true);
  if (configResult != 0) {
    print('Failed to enable configuration, tasks may not run.');
  }
}

Future<Directory> createTestProject(
  String packageName,
  Directory tempDirectory,
) async {
  await exec(
    _flutterBin,
    <String>[
      'create',
      '--no-pub',
      '--template=package_ffi',
      packageName,
    ],
    workingDirectory: tempDirectory.path,
  );

  final Directory packageDirectory = Directory(
    path.join(tempDirectory.path, packageName),
  );
  await _pinDependencies(
    File(path.join(packageDirectory.path, 'pubspec.yaml')),
  );
  await _pinDependencies(
    File(path.join(packageDirectory.path, 'example', 'pubspec.yaml')),
  );

  await exec(
    _flutterBin,
    <String>[
      'pub',
      'get',
    ],
    workingDirectory: packageDirectory.path,
  );

  return packageDirectory;
}

Future<void> _pinDependencies(File pubspecFile) async {
  final String oldPubspec = await pubspecFile.readAsString();
  final String newPubspec = oldPubspec.replaceAll(': ^', ': ');
  await pubspecFile.writeAsString(newPubspec);
}


Future<T> inTempDir<T>(Future<T> Function(Directory tempDirectory) fun) async {
  final Directory tempDirectory = dir(Directory.systemTemp.createTempSync().resolveSymbolicLinksSync());
  try {
    return await fun(tempDirectory);
  } finally {
    tempDirectory.deleteSync(recursive: true);
  }
}