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

// ignore_for_file: avoid_print

import 'dart:io';

import 'package:lcov_parser/lcov_parser.dart' as lcov;
import 'package:meta/meta.dart';

// After you run `flutter test --coverage`, `.../rfw/coverage/lcov.info` will
// represent the latest coverage information for the package. Load that file
// into your IDE's coverage mode to see what lines need coverage.
// In Emacs, that's `M-x coverlay-load-file`, for example.
// (If you're using Emacs, you may need to set the variable `coverlay:base-path`
// first (make sure it has a trailing slash), then load the overlay file, and
// once it is loaded you can call `M-x coverlay-display-stats` to get a summary
// of the files to look at.)

// If Dart coverage increases the number of lines that could be covered, it is
// possible that this package will no longer report 100% coverage even though
// nothing has changed about the package itself. In the event that that happens,
// set this constant to the number of lines currently being covered and file a
// bug, cc'ing the current package owner (Hixie) so that they can add more tests.
const int? targetLines = null;

@immutable
final class LcovLine {
  const LcovLine(this.filename, this.line);
  final String filename;
  final int line;

  @override
  int get hashCode => Object.hash(filename, line);

  @override
  bool operator ==(Object other) {
    return other is LcovLine &&
        other.line == line &&
        other.filename == filename;
  }

  @override
  String toString() {
    return '$filename:$line';
  }
}

Future<void> main(List<String> arguments) async {
  // This script is mentioned in the CONTRIBUTING.md file.

  final Directory coverageDirectory = Directory('coverage');

  if (coverageDirectory.existsSync()) {
    coverageDirectory.deleteSync(recursive: true);
  }

  final ProcessResult result = Process.runSync(
    'flutter',
    <String>[
      'test',
      '--coverage',
      if (arguments.isNotEmpty) ...arguments,
    ],
  );

  if (result.exitCode != 0) {
    print(result.stdout);
    print(result.stderr);
    print('Tests failed.');
    // leave coverage directory around to aid debugging
    exit(1);
  }

  if (Platform.environment.containsKey('CHANNEL') &&
      Platform.environment['CHANNEL'] != 'master' &&
      Platform.environment['CHANNEL'] != 'main') {
    print(
      'Tests passed. (Coverage verification skipped; currently on ${Platform.environment['CHANNEL']} channel.)',
    );
    coverageDirectory.deleteSync(recursive: true);
    exit(0);
  }

  final List<File> libFiles = Directory('lib')
      .listSync(recursive: true)
      .whereType<File>()
      .where((File file) => file.path.endsWith('.dart'))
      .toList();
  final Set<LcovLine> flakyLines = <LcovLine>{};
  final Set<LcovLine> deadLines = <LcovLine>{};
  for (final File file in libFiles) {
    int lineNumber = 0;
    for (final String line in file.readAsLinesSync()) {
      lineNumber += 1;
      if (line.endsWith('// dead code on VM target')) {
        deadLines.add(LcovLine(file.path, lineNumber));
      }
      if (line.endsWith('// https://github.com/dart-lang/sdk/issues/53349')) {
        flakyLines.add(LcovLine(file.path, lineNumber));
      }
    }
  }

  final List<lcov.Record> records = await lcov.Parser.parse(
    'coverage/lcov.info',
  );
  int totalLines = 0;
  int coveredLines = 0;
  bool deadLinesError = false;
  for (final lcov.Record record in records) {
    if (record.lines != null) {
      totalLines += record.lines!.found ?? 0;
      coveredLines += record.lines!.hit ?? 0;
      if (record.file != null && record.lines!.details != null) {
        for (int index = 0; index < record.lines!.details!.length; index += 1) {
          if (record.lines!.details![index].hit != null &&
              record.lines!.details![index].line != null) {
            final LcovLine line = LcovLine(
              record.file!,
              record.lines!.details![index].line!,
            );
            if (flakyLines.contains(line)) {
              totalLines -= 1;
              if (record.lines!.details![index].hit! > 0) {
                coveredLines -= 1;
              }
            }
            if (deadLines.contains(line)) {
              deadLines.remove(line);
              totalLines -= 1;
              if (record.lines!.details![index].hit! > 0) {
                print(
                  '$line: Line is marked as being dead code but was nonetheless covered.',
                );
                deadLinesError = true;
              }
            }
          }
        }
      }
    }
  }
  if (deadLines.isNotEmpty || deadLinesError) {
    for (final LcovLine line in deadLines) {
      print(
        '$line: Line is marked as being undetectably dead code but was not considered reachable.',
      );
    }
    print(
      'Consider removing the "dead code on VM target" comment from affected lines.',
    );
    exit(1);
  }
  if (totalLines <= 0 || totalLines < coveredLines) {
    print('Failed to compute coverage correctly.');
    exit(1);
  }

  final String coveredPercent =
      (100.0 * coveredLines / totalLines).toStringAsFixed(1);

  if (targetLines != null) {
    if (targetLines! < totalLines) {
      print(
        'Warning: test_coverage has an override set to reduce the expected number of covered lines from $totalLines to $targetLines.\n'
        'New tests should be written to cover all lines in the package.',
      );
      totalLines = targetLines!;
    } else if (targetLines == totalLines) {
      print(
        'Warning: test_coverage has a redundant targetLines; it is equal to the actual number of coverable lines ($totalLines).\n'
        'Update test_coverage.dart to set the targetLines constant to null.',
      );
    } else {
      print(
        'Warning: test_coverage has an outdated targetLines ($targetLines) that is above the total number of lines in the package ($totalLines).\n'
        'Update test_coverage.dart to set the targetLines constant to null.',
      );
    }
  }

  if (coveredLines < totalLines) {
    print('');
    print('                  ╭──────────────────────────────╮');
    print('                  │ COVERAGE REGRESSION DETECTED │');
    print('                  ╰──────────────────────────────╯');
    print('');
    print(
      'Coverage has reduced to only $coveredLines lines ($coveredPercent%), out\n'
      'of $totalLines total lines; ${totalLines - coveredLines} lines are not covered by tests.\n'
      'Please add sufficient tests to get coverage back to 100%.',
    );
    print('');
    print(
      'When in doubt, ask @Hixie for advice. Thanks!',
    );
    exit(1);
  }

  coverageDirectory.deleteSync(recursive: true);
}