// 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 'package:analyzer/dart/analysis/features.dart';
import 'package:analyzer/error/error.dart';
import 'package:analyzer/src/dart/ast/ast.dart';
import 'package:analyzer/src/dart/error/syntactic_errors.dart';
import 'package:test/test.dart';

import '../../../../generated/test_support.dart';
import '../recovery_test_support.dart';

typedef AdjustValidUnitBeforeComparison =
    CompilationUnitImpl Function(CompilationUnitImpl unit);

/// A base class that adds support for tests that test how well the parser
/// recovers when the user has entered an incomplete (but otherwise correct)
/// construct (such as a top-level declaration, class member, or statement).
///
/// Because users often add new constructs between two existing constructs,
/// these tests effectively test whether the parser is able to recognize where
/// the partially entered construct ends and where the next fully entered
/// construct begins. (The preceding construct is irrelevant.) Given the large
/// number of following constructs the are valid in most contexts, these tests
/// are designed to programmatically generate tests based on a list of possible
/// following constructs.
abstract class PartialCodeTest extends AbstractRecoveryTest {
  /// A list of suffixes that can be used by tests of class members.
  static final List<TestSuffix> classMemberSuffixes = <TestSuffix>[
    TestSuffix('annotation', '@annotation var f;'),
    TestSuffix('field', 'var f;'),
    TestSuffix('fieldConst', 'const f = 0;'),
    TestSuffix('fieldFinal', 'final f = 0;'),
    TestSuffix('methodNonVoid', 'int a(b) => 0;'),
    TestSuffix('methodVoid', 'void a(b) {}'),
    TestSuffix('getter', 'int get a => 0;'),
    TestSuffix('setter', 'set a(b) {}'),
  ];

  /// A list of suffixes that can be used by tests of top-level constructs that
  /// can validly be followed by any declaration.
  static final List<TestSuffix> declarationSuffixes = <TestSuffix>[
    TestSuffix('class', 'class A {}'),
    TestSuffix('enum', 'enum E { v }'),
    //    new TestSuffix('extension', 'extension E on A {}'),
    TestSuffix('mixin', 'mixin M {}'),
    TestSuffix('typedef', 'typedef A = B Function(C, D);'),
    TestSuffix('functionVoid', 'void f() {}'),
    TestSuffix('functionNonVoid', 'int f() {}'),
    TestSuffix('var', 'var a;'),
    TestSuffix('const', 'const a = 0;'),
    TestSuffix('final', 'final a = 0;'),
    TestSuffix('getter', 'int get a => 0;'),
    TestSuffix('setter', 'set a(b) {}'),
  ];

  /// A list of suffixes that can be used by tests of top-level constructs that
  /// can validly be followed by anything that is valid after a part directive.
  static final List<TestSuffix> postPartSuffixes = <TestSuffix>[
    TestSuffix('part', "part 'a.dart';"),
    ...declarationSuffixes,
  ];

  /// A list of suffixes that can be used by tests of top-level constructs that
  /// can validly be followed by any directive or declaration other than a
  /// library directive.
  static final List<TestSuffix> prePartSuffixes = <TestSuffix>[
    TestSuffix('import', "import 'a.dart';"),
    TestSuffix('export', "export 'a.dart';"),
    ...postPartSuffixes,
  ];

  /// A list of suffixes that can be used by tests of statements.
  static final List<TestSuffix> statementSuffixes = <TestSuffix>[
    TestSuffix('assert', "assert (true);"),
    TestSuffix('block', "{}"),
    TestSuffix('break', "break;"),
    TestSuffix('continue', "continue;"),
    TestSuffix('do', "do {} while (true);"),
    TestSuffix('if', "if (true) {}"),
    TestSuffix('for', "for (var x in y) {}"),
    TestSuffix('labeled', "l: {}"),
    TestSuffix('localFunctionNonVoid', "int f() {}"),
    TestSuffix('localFunctionVoid', "void f() {}"),
    TestSuffix('localVariable', "var x;"),
    TestSuffix('switch', "switch (x) {}"),
    TestSuffix('try', "try {} finally {}"),
    TestSuffix('return', "return;"),
    TestSuffix('while', "while (true) {}"),
  ];

  /// Build a group of tests with the given [groupName]. There will be one test
  /// for every combination of elements in the cross-product of the lists of
  /// [descriptors] and [suffixes], and one additional test for every descriptor
  /// where the suffix is the empty string (to test partial declarations at the
  /// end of the file). In total, there will be
  /// `descriptors.length * (suffixes.length + 1)` tests generated.
  buildTests(
    String groupName,
    List<TestDescriptor> descriptors,
    List<TestSuffix> suffixes, {
    FeatureSet? featureSet,
    String? head,
    bool includeEof = true,
    String? tail,
  }) {
    group(groupName, () {
      for (TestDescriptor descriptor in descriptors) {
        if (includeEof) {
          _buildTestForDescriptorAndSuffix(
            descriptor,
            TestSuffix.eof,
            0,
            head,
            tail,
            featureSet: featureSet,
          );
        }
        for (int i = 0; i < suffixes.length; i++) {
          _buildTestForDescriptorAndSuffix(
            descriptor,
            suffixes[i],
            i + 1,
            head,
            tail,
            featureSet: featureSet,
          );
        }
        var descriptorFailing = descriptor.failing;
        if (descriptorFailing != null) {
          test('${descriptor.name}_failingList', () {
            var failing = Set.of(descriptorFailing);
            if (includeEof) {
              failing.remove('eof');
            }
            failing.removeAll(suffixes.map((suffix) => suffix.name));
            expect(
              failing,
              isEmpty,
              reason:
                  'There are tests marked as failing that are not being run',
            );
          });
        }
      }
    });
  }

  /// Build a single test based on the given [descriptor] and [suffix].
  _buildTestForDescriptorAndSuffix(
    TestDescriptor descriptor,
    TestSuffix suffix,
    int suffixIndex,
    String? head,
    String? tail, {
    FeatureSet? featureSet,
  }) {
    test('${descriptor.name}_${suffix.name}', () {
      //
      // Compose the invalid and valid pieces of code.
      //
      StringBuffer invalid = StringBuffer();
      StringBuffer valid = StringBuffer();
      StringBuffer base = StringBuffer();
      if (head != null) {
        invalid.write(head);
        valid.write(head);
        base.write(head);
      }
      invalid.write(descriptor.invalid);
      valid.write(descriptor.valid);
      if (suffix.text.isNotEmpty) {
        invalid.write(' ');
        invalid.write(suffix.text);
        valid.write(' ');
        valid.write(suffix.text);
        base.write(' ');
        base.write(suffix.text);
      }
      if (tail != null) {
        invalid.write(tail);
        valid.write(tail);
        base.write(tail);
      }
      //
      // Determine the existing errors in the code without either valid or
      // invalid code.
      //
      GatheringDiagnosticListener listener = GatheringDiagnosticListener();
      parseCompilationUnit2(base.toString(), listener, featureSet: featureSet);
      var baseDiagnosticCodes = <DiagnosticCode>[];
      for (var diagnostic in listener.diagnostics) {
        if (diagnostic.diagnosticCode == ParserErrorCode.breakOutsideOfLoop ||
            diagnostic.diagnosticCode ==
                ParserErrorCode.continueOutsideOfLoop ||
            diagnostic.diagnosticCode ==
                ParserErrorCode.continueWithoutLabelInCase) {
          baseDiagnosticCodes.add(diagnostic.diagnosticCode);
        }
      }

      var expectedValidCodeDiagnostics = <DiagnosticCode>[
        ...baseDiagnosticCodes,
        ...?descriptor.expectedDiagnosticsInValidCode,
      ];

      var expectedInvalidCodeErrors = <DiagnosticCode>[
        ...baseDiagnosticCodes,
        ...?descriptor.diagnosticCodes,
      ];
      //
      // Run the test.
      //
      var failing = descriptor.failing;
      if (descriptor.allFailing ||
          (failing != null && failing.contains(suffix.name))) {
        bool failed = false;
        try {
          testRecovery(
            invalid.toString(),
            expectedInvalidCodeErrors,
            valid.toString(),
            adjustValidUnitBeforeComparison:
                descriptor.adjustValidUnitBeforeComparison,
            expectedDiagnosticsInValidCode: expectedValidCodeDiagnostics,
          );
          failed = true;
        } catch (e) {
          // Expected to fail.
        }
        if (failed) {
          fail('Expected to fail, but passed');
        }
      } else {
        testRecovery(
          invalid.toString(),
          expectedInvalidCodeErrors,
          valid.toString(),
          adjustValidUnitBeforeComparison:
              descriptor.adjustValidUnitBeforeComparison,
          expectedDiagnosticsInValidCode: expectedValidCodeDiagnostics,
          featureSet: featureSet,
        );
      }
    });
  }
}

/// A description of a set of tests that are to be built.
class TestDescriptor {
  /// The name of the test.
  final String name;

  /// Invalid code that the parser is expected to recover from.
  final String invalid;

  /// Diagnostic codes that the parser is expected to produce.
  final List<DiagnosticCode>? diagnosticCodes;

  /// Valid code that is equivalent to what the parser should produce as part of
  /// recovering from the invalid code.
  final String valid;

  /// Diagnostic codes that the parser is expected to produce in the valid code.
  final List<DiagnosticCode>? expectedDiagnosticsInValidCode;

  /// A flag indicating whether all of the tests are expected to fail.
  final bool allFailing;

  /// A list containing the names of the suffixes for which the test is expected
  /// to fail.
  final List<String>? failing;

  /// A function that modifies the valid compilation unit before it is compared
  /// with the invalid compilation unit, or `null` if no modification needed.
  AdjustValidUnitBeforeComparison? adjustValidUnitBeforeComparison;

  /// Initialize a newly created test descriptor.
  TestDescriptor(
    this.name,
    this.invalid,
    this.diagnosticCodes,
    this.valid, {
    this.allFailing = false,
    this.failing,
    this.expectedDiagnosticsInValidCode,
    this.adjustValidUnitBeforeComparison,
  });
}

/// A description of a set of suffixes that are to be used to construct tests.
class TestSuffix {
  static final TestSuffix eof = TestSuffix('eof', '');

  /// The name of the suffix.
  final String name;

  /// The code to be appended to the test code.
  final String text;

  /// Initialize a newly created suffix.
  TestSuffix(this.name, this.text);
}
