// Copyright 2019 The Chromium 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:args/args.dart';
import 'package:flutter_tools/src/context_runner.dart';
import 'package:flutter_tools/src/project.dart';
import 'package:flutter_tools/src/test/coverage_collector.dart';
import 'package:pool/pool.dart';
import 'package:path/path.dart' as path;

final ArgParser argParser = ArgParser()
  ..addOption('output-html',
    defaultsTo: 'coverage/report.html',
    help: 'The output path for the genhtml report.'
  )
  ..addOption('output-lcov',
    defaultsTo: 'coverage/lcov.info',
    help: 'The output path for the lcov data.'
  )
  ..addOption('test-directory',
    defaultsTo: 'test/',
    help: 'The path to the test directory.'
  )
  ..addOption('packages',
    defaultsTo: '.packages',
    help: 'The path to the .packages file.'
  )
  ..addOption('genhtml',
    defaultsTo: 'genhtml',
    help: 'The genhtml executable.');


/// Generates an html coverage report for the flutter_tool.
///
/// Example invocation:
///
///    dart tool/tool_coverage.dart --packages=.packages --test-directory=test
Future<void> main(List<String> arguments) async {
  final ArgResults argResults = argParser.parse(arguments);
  await runInContext(() async {
    final CoverageCollector coverageCollector = CoverageCollector(
      flutterProject: FlutterProject.current(),
    );
    /// A temp directory to create synthetic test files in.
    final Directory tempDirectory = Directory.systemTemp.createTempSync('_flutter_coverage')
      ..createSync();
    final String flutterRoot = File(Platform.script.toFilePath()).parent.parent.parent.parent.path;
    await ToolCoverageRunner(tempDirectory, coverageCollector, flutterRoot, argResults).collectCoverage();
  });
}

class ToolCoverageRunner {
  ToolCoverageRunner(
    this.tempDirectory,
    this.coverageCollector,
    this.flutterRoot,
    this.argResults,
  );

  final ArgResults argResults;
  final Pool pool = Pool(Platform.numberOfProcessors);
  final Directory tempDirectory;
  final CoverageCollector coverageCollector;
  final String flutterRoot;

  Future<void> collectCoverage() async {
    final List<Future<void>> pending = <Future<void>>[];

    final Directory testDirectory = Directory(argResults['test-directory']);
    final List<FileSystemEntity> fileSystemEntities = testDirectory.listSync(recursive: true);
    for (FileSystemEntity fileSystemEntity in fileSystemEntities) {
      if (!fileSystemEntity.path.endsWith('_test.dart')) {
        continue;
      }
      pending.add(_runTest(fileSystemEntity));
    }
    await Future.wait(pending);

    final String lcovData = await coverageCollector.finalizeCoverage();
    final String outputLcovPath = argResults['output-lcov'];
    final String outputHtmlPath = argResults['output-html'];
    final String genHtmlExecutable = argResults['genhtml'];
    File(outputLcovPath)
      ..createSync(recursive: true)
      ..writeAsStringSync(lcovData);
    await Process.run(genHtmlExecutable, <String>[outputLcovPath, '-o', outputHtmlPath], runInShell: true);
  }

  // Creates a synthetic test file to wrap the test main in a group invocation.
  // This will set up several fields used by the test methods on the context. Normally
  // this would be handled automatically by the test runner, but since we're executing
  // the files directly with dart we need to handle it manually.
  String _createTest(File testFile) {
    final File fakeTest = File(path.join(tempDirectory.path, testFile.path))
      ..createSync(recursive: true)
      ..writeAsStringSync('''
import "package:test/test.dart";
import "${path.absolute(testFile.path)}" as entrypoint;

void main() {
  group('', entrypoint.main);
}
''');
    return fakeTest.path;
  }

  Future<void> _runTest(File testFile) async {
    final PoolResource resource = await pool.request();
    final String testPath = _createTest(testFile);
    final int port = await _findPort();
    final Uri coverageUri = Uri.parse('http://127.0.0.1:$port');
    final Completer<void> completer = Completer<void>();
    final String packagesPath = argResults['packages'];
    final Process testProcess = await Process.start(
      Platform.resolvedExecutable,
      <String>[
        '--packages=$packagesPath',
        '--pause-isolates-on-exit',
        '--enable-asserts',
        '--enable-vm-service=${coverageUri.port}',
        testPath,
      ],
      runInShell: true,
      environment: <String, String>{
        'FLUTTER_ROOT': flutterRoot,
      }).timeout(const Duration(seconds: 30));
    testProcess.stdout
      .transform(utf8.decoder)
      .transform(const LineSplitter())
      .listen((String line) {
        print(line);
        if (line.contains('All tests passed') || line.contains('Some tests failed')) {
          completer.complete(null);
        }
      });
    try {
      await completer.future;
      await coverageCollector.collectCoverage(testProcess, coverageUri).timeout(const Duration(seconds: 30));
      testProcess?.kill();
    } on TimeoutException {
      print('Failed to collect coverage for ${testFile.path} after 30 seconds');
    } finally {
      resource.release();
    }
  }

  Future<int> _findPort() async {
    int port = 0;
    ServerSocket serverSocket;
    try {
      serverSocket = await ServerSocket.bind(InternetAddress.loopbackIPv4.address, 0);
      port = serverSocket.port;
    } catch (e) {
      // Failures are signaled by a return value of 0 from this function.
      print('_findPort failed: $e');
    }
    if (serverSocket != null) {
      await serverSocket.close();
    }
    return port;
  }
}
