// Copyright 2016 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 'package:coverage/coverage.dart' as coverage;

import '../base/file_system.dart';
import '../base/io.dart';
import '../dart/package_map.dart';
import '../globals.dart';

import 'watcher.dart';

/// A class that's used to collect coverage data during tests.
class CoverageCollector extends TestWatcher {
  Map<String, dynamic> _globalHitmap;

  @override
  Future<Null> onFinishedTests(ProcessEvent event) async {
    printTrace('test ${event.childIndex}: collecting coverage');
    await collectCoverage(event.process, event.observatoryUri);
  }

  void _addHitmap(Map<String, dynamic> hitmap) {
    if (_globalHitmap == null)
      _globalHitmap = hitmap;
    else
      coverage.mergeHitmaps(hitmap, _globalHitmap);
  }

  /// Collects coverage for the given [Process] using the given `port`.
  ///
  /// This should be called when the code whose coverage data is being collected
  /// has been run to completion so that all coverage data has been recorded.
  ///
  /// The returned [Future] completes when the coverage is collected.
  Future<Null> collectCoverage(Process process, Uri observatoryUri) async {
    assert(process != null);
    assert(observatoryUri != null);

    final int pid = process.pid;
    int exitCode;
    // Synchronization is enforced by the API contract. Error handling
    // synchronization is done in the code below where `exitCode` is checked.
    // Callback cannot throw.
    process.exitCode.then<Null>((int code) { // ignore: unawaited_futures
      exitCode = code;
    });
    if (exitCode != null)
      throw new Exception('Failed to collect coverage, process terminated before coverage could be collected.');

    printTrace('pid $pid: collecting coverage data from $observatoryUri...');
    final Map<String, dynamic> data = await coverage
        .collect(observatoryUri, false, false)
        .timeout(
          const Duration(minutes: 2),
          onTimeout: () {
            throw new Exception('Timed out while collecting coverage.');
          },
        );
    printTrace(() {
      final StringBuffer buf = new StringBuffer()
          ..write('pid $pid ($observatoryUri): ')
          ..write(exitCode == null
              ? 'collected coverage data; merging...'
              : 'process terminated prematurely with exit code $exitCode; aborting');
      return buf.toString();
    }());
    if (exitCode != null)
      throw new Exception('Failed to collect coverage, process terminated while coverage was being collected.');
    _addHitmap(coverage.createHitmap(data['coverage']));
    printTrace('pid $pid ($observatoryUri): done merging coverage data into global coverage map.');
  }

  /// Returns a future that will complete with the formatted coverage data
  /// (using [formatter]) once all coverage data has been collected.
  ///
  /// This will not start any collection tasks. It us up to the caller of to
  /// call [collectCoverage] for each process first.
  ///
  /// If [timeout] is specified, the future will timeout (with a
  /// [TimeoutException]) after the specified duration.
  Future<String> finalizeCoverage({
    coverage.Formatter formatter,
    Duration timeout,
  }) async {
    printTrace('formating coverage data');
    if (_globalHitmap == null)
      return null;
    if (formatter == null) {
      final coverage.Resolver resolver = new coverage.Resolver(packagesPath: PackageMap.globalPackagesPath);
      final String packagePath = fs.currentDirectory.path;
      final List<String> reportOn = <String>[fs.path.join(packagePath, 'lib')];
      formatter = new coverage.LcovFormatter(resolver, reportOn: reportOn, basePath: packagePath);
    }
    final String result = await formatter.format(_globalHitmap);
    _globalHitmap = null;
    return result;
  }
}
