// 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:convert';
import 'dart:io';

class TestSpecs {

  TestSpecs({
    required this.path,
    required this.startTime,
  });

  final String path;
  int startTime;
  int? _endTime;

  int get milliseconds => endTime - startTime;

  set endTime(int value) {
    _endTime = value;
  }

  int get endTime => _endTime ?? 0;

  String toJson() {
    return json.encode(
      <String, String>{'path': path, 'runtime': milliseconds.toString()}
    );
  }
}

class TestFileReporterResults {
  TestFileReporterResults._({
    required this.allTestSpecs,
    required this.hasFailedTests,
    required this.errors,
  });

  /// Intended to parse the output file of `dart test --file-reporter json:file_name
  factory TestFileReporterResults.fromFile(File metrics) {
    if (!metrics.existsSync()) {
      throw Exception('${metrics.path} does not exist');
    }

    final Map<int, TestSpecs> testSpecs = <int, TestSpecs>{};
    bool hasFailedTests = true;
    final List<String> errors = <String>[];

    for (final String metric in metrics.readAsLinesSync()) {
      /// Using print within a test adds the printed content to the json file report
      /// as \u0000 making the file parsing step fail. The content of the json file
      /// is expected to be a json dictionary per line and the following line removes
      /// all the additional content at the beginning of the line until it finds the
      /// first opening curly bracket.
      // TODO(godofredoc): remove when https://github.com/flutter/flutter/issues/145553 is fixed.
      final String sanitizedMetric = metric.replaceAll(RegExp(r'$.*{'), '{');
      final Map<String, Object?> entry = json.decode(sanitizedMetric) as Map<String, Object?>;
      if (entry.containsKey('suite')) {
        final Map<String, Object?> suite = entry['suite']! as Map<String, Object?>;
        addTestSpec(suite, entry['time']! as int, testSpecs);
      } else if (isMetricDone(entry, testSpecs)) {
        final Map<String, Object?> group = entry['group']! as Map<String, Object?>;
        final int suiteID = group['suiteID']! as int;
        addMetricDone(suiteID, entry['time']! as int, testSpecs);
      } else if (entry.containsKey('error')) {
        final String stackTrace = entry.containsKey('stackTrace') ? entry['stackTrace']! as String : '';
        errors.add('${entry['error']}\n $stackTrace');
      } else if (entry.containsKey('success') && entry['success'] == true) {
        hasFailedTests = false;
      }
    }

    return TestFileReporterResults._(allTestSpecs: testSpecs, hasFailedTests: hasFailedTests, errors: errors);
  }

  final Map<int, TestSpecs> allTestSpecs;
  final bool hasFailedTests;
  final List<String> errors;


  static void addTestSpec(Map<String, Object?> suite, int time, Map<int, TestSpecs> allTestSpecs) {
    allTestSpecs[suite['id']! as int] = TestSpecs(
      path: suite['path']! as String,
      startTime: time,
    );
  }

  static void addMetricDone(int suiteID, int time, Map<int, TestSpecs> allTestSpecs) {
    final TestSpecs testSpec = allTestSpecs[suiteID]!;
    testSpec.endTime = time;
  }

  static bool isMetricDone(Map<String, Object?> entry, Map<int, TestSpecs> allTestSpecs) {
    if (entry.containsKey('group') && entry['type']! as String == 'group') {
      final Map<String, Object?> group = entry['group']! as Map<String, Object?>;
      return allTestSpecs.containsKey(group['suiteID']! as int);
    }
    return false;
  }
}
