// 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.

// This test builds an integration test from the list of samples in the
// examples/api/lib directory, and then runs it. The tests are just smoke tests,
// designed to start up each example and run it for a couple of frames to make
// sure it doesn't throw an exception or fail to compile.

import 'dart:async';
import 'dart:convert';
import 'dart:io' show Process, ProcessException, exitCode, stderr, stdout;

import 'package:file/file.dart';
import 'package:file/local.dart';
import 'package:path/path.dart' as path;
import 'package:platform/platform.dart';
import 'package:process/process.dart';

FileSystem filesystem = const LocalFileSystem();
ProcessManager processManager = const LocalProcessManager();
Platform platform = const LocalPlatform();

FutureOr<dynamic> main() async {
  if (!platform.isLinux && !platform.isWindows && !platform.isMacOS) {
    stderr.writeln('Example smoke tests are only designed to run on desktop platforms');
    exitCode = 4;
    return;
  }
  final Directory flutterDir = filesystem.directory(
    path.absolute(
      path.dirname(
        path.dirname(
          path.dirname(platform.script.toFilePath()),
        ),
      ),
    ),
  );
  final Directory apiDir = flutterDir.childDirectory('examples').childDirectory('api');
  final File integrationTest = await generateTest(apiDir);
  try {
    await runSmokeTests(flutterDir: flutterDir, integrationTest: integrationTest, apiDir: apiDir);
  } finally {
    await cleanUp(integrationTest);
  }
}

Future<void> cleanUp(File integrationTest) async {
  try {
    await integrationTest.delete();
    // Delete the integration_test directory if it is empty.
    await integrationTest.parent.delete();
  } on FileSystemException {
    // Ignore, there might be other files in there preventing it from
    // being removed, or it might not exist.
  }
}

// Executes the generated smoke test.
Future<void> runSmokeTests({
  required Directory flutterDir,
  required File integrationTest,
  required Directory apiDir,
}) async {
  final File flutterExe =
      flutterDir.childDirectory('bin').childFile(platform.isWindows ? 'flutter.bat' : 'flutter');
  final List<String> cmd = <String>[
    // If we're in a container with no X display, then use the virtual framebuffer.
    if (platform.isLinux &&
        (platform.environment['DISPLAY'] == null ||
         platform.environment['DISPLAY']!.isEmpty)) '/usr/bin/xvfb-run',
    flutterExe.absolute.path,
    'test',
    '--reporter=expanded',
    '--device-id=${platform.operatingSystem}',
    integrationTest.absolute.path,
  ];
  await runCommand(cmd, workingDirectory: apiDir);
}

// A class to hold information related to an example, used to generate names
// from for the tests.
class ExampleInfo {
  ExampleInfo(this.file, Directory examplesLibDir)
      : importPath = _getImportPath(file, examplesLibDir),
        importName = '' {
    importName = importPath.replaceAll(RegExp(r'\.dart$'), '').replaceAll(RegExp(r'\W'), '_');
  }

  final File file;
  final String importPath;
  String importName;

  static String _getImportPath(File example, Directory examplesLibDir) {
    final String relativePath =
        path.relative(example.absolute.path, from: examplesLibDir.absolute.path);
    // So that Windows paths are proper URIs in the import statements.
    return path.toUri(relativePath).toFilePath(windows: false);
  }
}

// Generates the combined smoke test.
Future<File> generateTest(Directory apiDir) async {
  final Directory examplesLibDir = apiDir.childDirectory('lib');

  // Get files from git, to avoid any non-repo files that might be in someone's
  // workspace.
  final List<String> gitFiles = (await runCommand(
    <String>['git', 'ls-files', '**/*.dart'],
    workingDirectory: examplesLibDir,
    quiet: true,
  )).replaceAll(r'\', '/')
    .trim()
    .split('\n');
  final Iterable<File> examples = gitFiles.map<File>((String examplePath) {
    return filesystem.file(path.join(examplesLibDir.absolute.path, examplePath));
  });

  // Collect the examples, and import them all as separate symbols.
  final List<String> imports = <String>[];
  imports.add('''import 'package:flutter/widgets.dart';''');
  imports.add('''import 'package:flutter/scheduler.dart';''');
  imports.add('''import 'package:flutter_test/flutter_test.dart';''');
  imports.add('''import 'package:integration_test/integration_test.dart';''');
  final List<ExampleInfo> infoList = <ExampleInfo>[];
  for (final File example in examples) {
    final ExampleInfo info = ExampleInfo(example, examplesLibDir);
    infoList.add(info);
    imports.add('''import 'package:flutter_api_samples/${info.importPath}' as ${info.importName};''');
  }
  imports.sort();
  infoList.sort((ExampleInfo a, ExampleInfo b) => a.importPath.compareTo(b.importPath));

  final StringBuffer buffer = StringBuffer();
  buffer.writeln('// Temporary generated file. Do not commit.');
  buffer.writeln("import 'dart:io';");
  buffer.writeAll(imports, '\n');
  buffer.writeln(r'''


import '../../../dev/manual_tests/test/mock_image_http.dart';

void main() {
  IntegrationTestWidgetsFlutterBinding? binding;
  try {
    binding = IntegrationTestWidgetsFlutterBinding.ensureInitialized() as IntegrationTestWidgetsFlutterBinding;
  } catch (e) {
    stderr.writeln('Unable to initialize binding${binding == null ? '' : ' $binding'}: $e');
    exitCode = 128;
    return;
  }

''');
  for (final ExampleInfo info in infoList) {
    buffer.writeln('''
  testWidgets(
    'Smoke test ${info.importPath}',
    (WidgetTester tester) async {
      final ErrorWidgetBuilder originalBuilder = ErrorWidget.builder;
      try {
        HttpOverrides.runZoned(() {
          ${info.importName}.main();
        }, createHttpClient: (SecurityContext? context) => FakeHttpClient(context));
        await tester.pump();
        await tester.pump();
        expect(find.byType(WidgetsApp), findsOneWidget);
      } finally {
        ErrorWidget.builder = originalBuilder;
        timeDilation = 1.0;
      }
    },
  );
''');
  }
  buffer.writeln('}');

  final File integrationTest =
      apiDir.childDirectory('integration_test').childFile('smoke_integration_test.dart');
  integrationTest.createSync(recursive: true);
  integrationTest.writeAsStringSync(buffer.toString());
  return integrationTest;
}

// Run a command, and optionally stream the output as it runs, returning the
// stdout.
Future<String> runCommand(
  List<String> cmd, {
  required Directory workingDirectory,
  bool quiet = false,
  List<String>? output,
  Map<String, String>? environment,
}) async {
  final List<int> stdoutOutput = <int>[];
  final List<int> combinedOutput = <int>[];
  final Completer<void> stdoutComplete = Completer<void>();
  final Completer<void> stderrComplete = Completer<void>();

  late Process process;
  Future<int> allComplete() async {
    await stderrComplete.future;
    await stdoutComplete.future;
    return process.exitCode;
  }

  try {
    process = await processManager.start(
      cmd,
      workingDirectory: workingDirectory.absolute.path,
      environment: environment,
    );
    process.stdout.listen(
      (List<int> event) {
        stdoutOutput.addAll(event);
        combinedOutput.addAll(event);
        if (!quiet) {
          stdout.add(event);
        }
      },
      onDone: () async => stdoutComplete.complete(),
    );
    process.stderr.listen(
      (List<int> event) {
        combinedOutput.addAll(event);
        if (!quiet) {
          stderr.add(event);
        }
      },
      onDone: () async => stderrComplete.complete(),
    );
  } on ProcessException catch (e) {
    stderr.writeln('Running "${cmd.join(' ')}" in ${workingDirectory.path} '
        'failed with:\n$e');
    exitCode = 2;
    return utf8.decode(stdoutOutput);
  } on ArgumentError catch (e) {
    stderr.writeln('Running "${cmd.join(' ')}" in ${workingDirectory.path} '
        'failed with:\n$e');
    exitCode = 3;
    return utf8.decode(stdoutOutput);
  }

  final int processExitCode = await allComplete();
  if (processExitCode != 0) {
    stderr.writeln('Running "${cmd.join(' ')}" in ${workingDirectory.path} exited with code $processExitCode');
    exitCode = processExitCode;
  }

  if (output != null) {
    output.addAll(utf8.decode(combinedOutput).split('\n'));
  }

  return utf8.decode(stdoutOutput);
}