// Copyright (c) 2017, 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:io' as io;

import 'package:analyzer/dart/ast/ast.dart';
import 'package:analyzer/dart/ast/visitor.dart';
import 'package:front_end/src/testing/package_root.dart' as package_root;
import 'package:path/path.dart' as path;
import 'package:test/test.dart';
import 'package:test_reflective_loader/test_reflective_loader.dart';
import 'package:yaml/yaml.dart';

import '../../generated/parser_fasta_test.dart';

main() {
  defineReflectiveSuite(() {
    defineReflectiveTests(AbstractRecoveryTest);
  });
}

@reflectiveTest
class AbstractRecoveryTest extends FastaParserTestCase {
  /**
   * Given the path to the file containing the declaration of the fasta Parser,
   * return a set containing the names of all the messages and templates that
   * are referenced (presumably because they are being generated) within that
   * file.
   */
  Set<String> getGeneratedNames(String parserPath) {
    String content = new io.File(parserPath).readAsStringSync();
    CompilationUnit unit = parseCompilationUnit(content);
    expect(unit, isNotNull);
    GeneratedCodesVisitor visitor = new GeneratedCodesVisitor();
    unit.accept(visitor);
    return visitor.generatedNames;
  }

  /**
   * Given the path to the file 'message.yaml', return a list of the top-level
   * keys defined in that file that define an 'analyzerCode'.
   */
  List<String> getMappedCodes(String messagesPath) {
    String content = new io.File(messagesPath).readAsStringSync();
    YamlDocument document = loadYamlDocument(content);
    expect(document, isNotNull);
    Set<String> codes = new Set<String>();
    YamlNode contents = document.contents;
    if (contents is YamlMap) {
      for (String name in contents.keys) {
        Object value = contents[name];
        if (value is YamlMap) {
          if (value['analyzerCode'] != null) {
            codes.add(name);
          }
        }
      }
    }
    return codes.toList();
  }

  /**
   * Given the path to the file 'message.yaml', return a list of the analyzer
   * codes defined in that file.
   */
  List<String> getReferencedCodes(String messagesPath) {
    String content = new io.File(messagesPath).readAsStringSync();
    YamlDocument document = loadYamlDocument(content);
    expect(document, isNotNull);
    Set<String> codes = new Set<String>();
    YamlNode contents = document.contents;
    if (contents is YamlMap) {
      for (String name in contents.keys) {
        Object value = contents[name];
        if (value is YamlMap) {
          String code = value['analyzerCode']?.toString();
          if (code != null) {
            codes.add(code);
          }
        }
      }
    }
    return codes.toList();
  }

  /**
   * Given the path to the file containing the declaration of the AstBuilder,
   * return a list of the analyzer codes that are translated by the builder.
   */
  List<String> getTranslatedCodes(String astBuilderPath) {
    String content = new io.File(astBuilderPath).readAsStringSync();
    CompilationUnit unit = parseCompilationUnit(content);
    ClassDeclaration astBuilder = unit.declarations[0];
    expect(astBuilder, isNotNull);
    MethodDeclaration method = astBuilder.members.firstWhere(
        (x) => x is MethodDeclaration && x.name.name == 'reportMessage',
        orElse: () => null);
    expect(method, isNotNull);
    SwitchStatement statement = (method.body as BlockFunctionBody)
        .block
        .statements
        .firstWhere((x) => x is SwitchStatement, orElse: () => null);
    expect(statement, isNotNull);
    List<String> codes = <String>[];
    for (SwitchMember member in statement.members) {
      if (member is SwitchCase) {
        codes.add((member.expression as StringLiteral).stringValue);
      }
    }
    return codes;
  }

  @failingTest
  test_mappedMessageCoverage() {
    String frontEndPath = path.join(package_root.packageRoot, 'front_end');
    String parserPath =
        path.join(frontEndPath, 'lib', 'src', 'fasta', 'parser', 'parser.dart');
    Set<String> generatedNames = getGeneratedNames(parserPath);

    String messagesPath = path.join(frontEndPath, 'messages.yaml');
    List<String> mappedCodes = getMappedCodes(messagesPath);

    generatedNames.removeAll(mappedCodes);
    if (generatedNames.isEmpty) {
      return;
    }
    List<String> sortedNames = generatedNames.toList()..sort();
    StringBuffer buffer = new StringBuffer();
    buffer.writeln('Generated parser errors without analyzer codes:');
    for (String name in sortedNames) {
      buffer.write('  ');
      buffer.writeln(name);
    }
    fail(buffer.toString());
  }

  @failingTest
  test_translatedMessageCoverage() {
    String analyzerPath = path.join(package_root.packageRoot, 'analyzer');
    String astBuilderPath =
        path.join(analyzerPath, 'lib', 'src', 'fasta', 'error_converter.dart');
    List<String> translatedCodes = getTranslatedCodes(astBuilderPath);

    String messagesPath =
        path.join(path.dirname(analyzerPath), 'front_end', 'messages.yaml');
    List<String> referencedCodes = getReferencedCodes(messagesPath);

    List<String> untranslated = <String>[];
    for (String referencedCode in referencedCodes) {
      if (!translatedCodes.contains(referencedCode)) {
        untranslated.add(referencedCode);
      }
    }
    StringBuffer buffer = new StringBuffer();
    if (untranslated.isNotEmpty) {
      buffer
          .writeln('Analyzer codes used in messages.yaml but not translated:');
      for (String code in untranslated) {
        buffer.write('  ');
        buffer.writeln(code);
      }
      buffer.write(
          'Add a case for these codes to FastaErrorReporter.reportError.');
    }

    List<String> unreferenced = <String>[];
    for (String translatedCode in translatedCodes) {
      if (!referencedCodes.contains(translatedCode)) {
        unreferenced.add(translatedCode);
      }
    }
    if (untranslated.isNotEmpty) {
      if (buffer.isNotEmpty) {
        buffer.writeln();
        buffer.writeln();
      }
      buffer.writeln(
          'Analyzer codes that are translated but not used in messages.yaml:');
      for (String code in unreferenced) {
        buffer.write('  ');
        buffer.writeln(code);
      }
      buffer.write('Remove the cases for these codes from '
          'FastaErrorReporter.reportMessage.');
    }
    if (buffer.isNotEmpty) {
      fail(buffer.toString());
    }
  }
}

/**
 * A visitor that gathers the names of all the message codes that are generated
 * in the visited AST. This assumes that the codes are accessed via the prefix
 * 'fasta'.
 */
class GeneratedCodesVisitor extends RecursiveAstVisitor {
  /**
   * The names of the message codes that are generated in the visited AST.
   */
  Set<String> generatedNames = new Set<String>();

  @override
  visitPrefixedIdentifier(PrefixedIdentifier node) {
    if (node.prefix.name == 'fasta') {
      String name = node.identifier.name;
      if (name.startsWith('message')) {
        name = name.substring(7);
      } else if (name.startsWith('template')) {
        name = name.substring(8);
      } else {
        return;
      }
      generatedNames.add(name);
    }
  }
}
