// 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/analyzer.dart';
import 'package:analyzer/error/error.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 CompilationUnit AdjustValidUnitBeforeComparison(CompilationUnit 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>[
    new TestSuffix('annotation', '@annotation var f;'),
    new TestSuffix('field', 'var f;'),
    new TestSuffix('fieldConst', 'const f = 0;'),
    new TestSuffix('fieldFinal', 'final f = 0;'),
    new TestSuffix('methodNonVoid', 'int a(b) => 0;'),
    new TestSuffix('methodVoid', 'void a(b) {}'),
    new TestSuffix('getter', 'int get a => 0;'),
    new 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>[
    new TestSuffix('class', 'class A {}'),
    new TestSuffix('typedef', 'typedef A = B Function(C, D);'),
    new TestSuffix('functionVoid', 'void f() {}'),
    new TestSuffix('functionNonVoid', 'int f() {}'),
    new TestSuffix('var', 'var a;'),
    new TestSuffix('const', 'const a = 0;'),
    new TestSuffix('final', 'final a = 0;'),
    new TestSuffix('getter', 'int get a => 0;'),
    new 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>[
    new TestSuffix('part', "part 'a.dart';")
  ]..addAll(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>[
    new TestSuffix('import', "import 'a.dart';"),
    new TestSuffix('export', "export 'a.dart';")
  ]..addAll(postPartSuffixes);

  /**
   * A list of suffixes that can be used by tests of statements.
   */
  static final List<TestSuffix> statementSuffixes = <TestSuffix>[
    new TestSuffix('assert', "assert (true);"),
    new TestSuffix('block', "{}"),
    new TestSuffix('break', "break;"),
    new TestSuffix('continue', "continue;"),
    new TestSuffix('do', "do {} while (true);"),
    new TestSuffix('if', "if (true) {}"),
    new TestSuffix('for', "for (var x in y) {}"),
    new TestSuffix('labeled', "l: {}"),
    new TestSuffix('localFunctionNonVoid', "int f() {}"),
    new TestSuffix('localFunctionVoid', "void f() {}"),
    new TestSuffix('localVariable', "var x;"),
    new TestSuffix('switch', "switch (x) {}"),
    new TestSuffix('try', "try {} finally {}"),
    new TestSuffix('return', "return;"),
    new 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,
      {String head, bool includeEof: true, String tail}) {
    group(groupName, () {
      for (TestDescriptor descriptor in descriptors) {
        if (includeEof) {
          _buildTestForDescriptorAndSuffix(
              descriptor, TestSuffix.eof, 0, head, tail);
        }
        for (int i = 0; i < suffixes.length; i++) {
          _buildTestForDescriptorAndSuffix(
              descriptor, suffixes[i], i + 1, head, tail);
        }
        if (descriptor.failing != null) {
          test('${descriptor.name}_failingList', () {
            Set<String> failing = new Set.from(descriptor.failing);
            if (includeEof) {
              failing.remove('eof');
            }
            failing.removeAll(suffixes.map((TestSuffix 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) {
    test('${descriptor.name}_${suffix.name}', () {
      //
      // Compose the invalid and valid pieces of code.
      //
      StringBuffer invalid = new StringBuffer();
      StringBuffer valid = new StringBuffer();
      StringBuffer base = new 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.
      //
      GatheringErrorListener listener =
          new GatheringErrorListener(checkRanges: true);
      parseCompilationUnit2(base.toString(), listener);
      var baseErrorCodes = <ErrorCode>[];
      listener.errors.forEach((AnalysisError error) {
        if (error.errorCode == ParserErrorCode.BREAK_OUTSIDE_OF_LOOP ||
            error.errorCode == ParserErrorCode.CONTINUE_OUTSIDE_OF_LOOP ||
            error.errorCode == ParserErrorCode.CONTINUE_WITHOUT_LABEL_IN_CASE) {
          baseErrorCodes.add(error.errorCode);
        }
      });

      var expectedValidCodeErrors = <ErrorCode>[];
      expectedValidCodeErrors.addAll(baseErrorCodes);
      if (descriptor.expectedErrorsInValidCode != null) {
        expectedValidCodeErrors.addAll(descriptor.expectedErrorsInValidCode);
      }

      var expectedInvalidCodeErrors = <ErrorCode>[];
      expectedInvalidCodeErrors.addAll(baseErrorCodes);
      if (descriptor.errorCodes != null) {
        expectedInvalidCodeErrors.addAll(descriptor.errorCodes);
      }
      //
      // Run the test.
      //
      List<String> 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,
              expectedErrorsInValidCode: expectedValidCodeErrors);
          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,
            expectedErrorsInValidCode: expectedValidCodeErrors);
      }
    });
  }
}

/**
 * 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;

  /**
   * Error codes that the parser is expected to produce.
   */
  final List<ErrorCode> errorCodes;

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

  /**
   * Error codes that the parser is expected to produce in the valid code.
   */
  final List<ErrorCode> expectedErrorsInValidCode;

  /**
   * 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.errorCodes, this.valid,
      {this.allFailing: false,
      this.failing,
      this.expectedErrorsInValidCode,
      this.adjustValidUnitBeforeComparison});
}

/**
 * A description of a set of suffixes that are to be used to construct tests.
 */
class TestSuffix {
  static final TestSuffix eof = new 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);
}
