// Copyright (c) 2023, the Dart project authors. Please see the AUTHORS file
// for details. 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 'package:analyzer/dart/analysis/analysis_context_collection.dart';
import 'package:analyzer/dart/analysis/results.dart';
import 'package:analyzer/dart/analysis/session.dart';
import 'package:analyzer/dart/ast/ast.dart';
import 'package:analyzer/dart/ast/visitor.dart';
import 'package:analyzer/file_system/file_system.dart';
import 'package:analyzer/file_system/physical_file_system.dart';
import 'package:analyzer_testing/package_root.dart';
import 'package:http/http.dart' as http;
import 'package:test/test.dart';
import 'package:test_reflective_loader/test_reflective_loader.dart';

Future<void> main() async {
  var tests = <AnnotatedTest>[];

  // Note: Unauthenticated GitHub API calls are limited to 60 per hour
  // so this script cannot be run often and will fail if there are over
  // 60 unique issues listed.

  await findFailingTestAnnotations(tests, packagePath: 'analysis_server');
  await findFailingTestAnnotations(tests, packagePath: 'analyzer');
  await findFailingTestAnnotations(tests, packagePath: 'analyzer_cli');
  await findFailingTestAnnotations(tests, packagePath: 'analyzer_plugin');

  var issueUris = tests.map((test) => test.issueUri).toSet();
  print('Found ${tests.length} with ${issueUris.length} unique issues.');
  print('Fetching test statuses from GitHub...');

  var closedIssues = <Uri>{};
  for (var issueUri in issueUris) {
    var response = await http.get(issueUri);
    if (response.statusCode != 200) {
      throw 'Failed to call GitHub API: ${response.statusCode} - ${response.reasonPhrase}\n${response.body}';
    }
    var issueData = jsonDecode(response.body) as Map<String, Object?>;
    if (issueData['state'] != 'open') {
      closedIssues.add(issueUri);
    }
  }

  print('Found ${closedIssues.length} closed issues:');
  for (var tests in tests.where(
    (test) => closedIssues.contains(test.issueUri),
  )) {
    var relativePath = pathContext.relative(tests.file.path, from: packageRoot);
    print('$relativePath ${tests.testName} ${_formatUri(tests.issueUri)}');
  }
}

final pathContext = provider.pathContext;

final provider = PhysicalResourceProvider.INSTANCE;

Future<void> findFailingTestAnnotations(
  List<AnnotatedTest> tests, {
  required String packagePath,
}) async {
  var pkgRootPath = pathContext.normalize(packageRoot);
  var testsPath = pathContext.join(pkgRootPath, packagePath, 'test');

  var collection = AnalysisContextCollection(
    includedPaths: <String>[testsPath],
    resourceProvider: provider,
  );
  var contexts = collection.contexts;
  if (contexts.length != 1) {
    fail('The directory $testsPath contains multiple analysis contexts.');
  }

  var session = contexts[0].currentSession;
  var directory = provider.getFolder(testsPath);

  print('Searching for FailedTest/SkippedTest annotations in $packagePath...');
  await _findFailingTestAnnotationsIn(session, testsPath, directory, tests);
}

Future<void> _findFailingTestAnnotationsIn(
  AnalysisSession session,
  String testDirPath,
  Folder directory,
  List<AnnotatedTest> tests,
) async {
  var children = directory.getChildren();
  children.sort((first, second) => first.shortName.compareTo(second.shortName));
  for (var child in children) {
    if (child is Folder) {
      await _findFailingTestAnnotationsIn(session, testDirPath, child, tests);
    } else if (child is File && child.shortName.endsWith('_test.dart')) {
      var path = child.path;

      var result = session.getParsedUnit(path);
      if (result is! ParsedUnitResult) {
        fail('Could not parse $path');
      }
      var unit = result.unit;
      var diagnostics = result.diagnostics;
      if (diagnostics.isNotEmpty) {
        fail('Errors found when parsing $path');
      }
      var tracker = FailingTestAnnotationTracker(child);
      unit.accept(tracker);
      tests.addAll(tracker.annotatedTests);
    }
  }
}

String _formatUri(Uri uri) =>
    uri.path.replaceAll('/repos/', '').replaceAll('/issues/', '#');

class AnnotatedTest {
  final File file;
  final String testName;
  final Uri issueUri;

  new(this.file, this.testName, this.issueUri);
}

/// A [RecursiveAstVisitor] that tracks nodes annotated with [FailingTest] or
/// [SkippedTest].
class FailingTestAnnotationTracker extends RecursiveAstVisitor<void> {
  final annotatedTests = <AnnotatedTest>[];
  final File file;

  new(this.file);

  @override
  void visitAnnotation(Annotation node) {
    if (node.name.name == 'FailingTest' || node.name.name == 'SkippedTest') {
      var issue = node.arguments?.arguments
          .whereType<NamedArgument>()
          .where((arg) => arg.name.lexeme == 'issue')
          .firstOrNull;
      var issueUrl = (issue?.argumentExpression as SimpleStringLiteral?)?.value;
      if (issueUrl != null && issueUrl.startsWith('https://github.com/')) {
        var issueUri = Uri.parse(issueUrl);
        var apiUri = issueUri.replace(
          host: 'api.github.com',
          pathSegments: ['repos', ...issueUri.pathSegments],
        );
        var method = node.parent as MethodDeclaration;
        annotatedTests.add(AnnotatedTest(file, method.name.lexeme, apiUri));
      }
    }
    super.visitAnnotation(node);
  }
}
