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

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

/// The base class for tests that test how well the parser recovers from various
/// syntactic errors.
abstract class AbstractRecoveryTest extends FastaParserTestCase {
  void testRecovery(
      String invalidCode, List<ErrorCode>? errorCodes, String validCode,
      {CompilationUnitImpl Function(CompilationUnitImpl unit)?
          adjustValidUnitBeforeComparison,
      List<ErrorCode>? expectedErrorsInValidCode,
      FeatureSet? featureSet}) {
    CompilationUnitImpl validUnit;

    // Assert that the valid code is indeed valid.
    try {
      validUnit = parseCompilationUnit(validCode,
          codes: expectedErrorsInValidCode, featureSet: featureSet);
      validateTokenStream(validUnit.beginToken);
    } catch (e) {
//      print('');
//      print('  Errors in valid code.');
//      print('    Error: $e');
//      print('    Code: $validCode');
//      print('');
      rethrow;
    }

    // Compare the structures before asserting valid errors.
    GatheringErrorListener listener = GatheringErrorListener();
    var invalidUnit =
        parseCompilationUnit2(invalidCode, listener, featureSet: featureSet);
    validateTokenStream(invalidUnit.beginToken);
    if (adjustValidUnitBeforeComparison != null) {
      validUnit = adjustValidUnitBeforeComparison(validUnit);
    }
    ResultComparator.compare(invalidUnit, validUnit);

    // Assert valid errors.
    if (errorCodes != null) {
      listener.assertErrorsWithCodes(errorCodes);
    } else {
      listener.assertNoErrors();
    }
  }

  void validateTokenStream(Token token) {
    while (!token.isEof) {
      Token next = token.next!;
      expect(token.end, lessThanOrEqualTo(next.offset));
      if (next.isSynthetic) {
        if (const [')', ']', '}'].contains(next.lexeme)) {
          expect(next.beforeSynthetic, token);
        }
      }
      token = next;
    }
  }
}

/// An object used to compare to AST structures and cause the test to fail if
/// they differ in any important ways.
class ResultComparator extends AstComparator {
  @override
  bool failDifferentLength(List first, List second) {
    StringBuffer buffer = StringBuffer();
    buffer.writeln('Expected a list of length ${second.length}');
    buffer.writeln('  $second');
    buffer.writeln('But found a list of length ${first.length}');
    buffer.writeln('  $first');
    if (first is NodeList) {
      _safelyWriteNodePath(buffer, first.owner);
    }
    fail(buffer.toString());
  }

  @override
  bool failIfNotNull(Object? first, Object? second) {
    if (second != null) {
      StringBuffer buffer = StringBuffer();
      buffer.write('Expected null; found a ');
      buffer.writeln(second.runtimeType);
      if (second is AstNode) {
        _safelyWriteNodePath(buffer, second);
      }
      fail(buffer.toString());
    }
    return true;
  }

  @override
  bool failIsNull(Object first, Object? second) {
    StringBuffer buffer = StringBuffer();
    buffer.write('Expected a ');
    buffer.write(first.runtimeType);
    buffer.writeln('; found null');
    if (first is AstNode) {
      _safelyWriteNodePath(buffer, first);
    }
    fail(buffer.toString());
  }

  @override
  bool failRuntimeType(Object first, Object second) {
    StringBuffer buffer = StringBuffer();
    buffer.write('Expected a ');
    buffer.writeln(second.runtimeType);
    buffer.write('; found ');
    buffer.writeln(first.runtimeType);
    if (first is AstNode) {
      _safelyWriteNodePath(buffer, first);
    }
    fail(buffer.toString());
  }

  /// Overridden to allow the valid code to contain an explicit identifier where
  /// a synthetic identifier is expected to be inserted by recovery.
  @override
  bool isEqualNodes(AstNode? first, AstNode? second) {
    if (first is SimpleIdentifier && second is SimpleIdentifier) {
      if (first.isSynthetic && second.name == '_s_') {
        return true;
      }
      if (first.token.isKeyword && second.name == '_k_') {
        return true;
      }
    }
    return super.isEqualNodes(first, second);
  }

  /// Overridden to ignore the offsets of tokens because these can legitimately
  /// be different.
  @override
  bool isEqualTokensNotNull(Token first, Token second) =>
      (first.isSynthetic && first.type == second.type) ||
      (first.length == second.length && first.lexeme == second.lexeme);

  void _safelyWriteNodePath(StringBuffer buffer, AstNode? node) {
    buffer.write('  path: ');
    if (node == null) {
      buffer.write(' null');
    } else {
      _writeNodePath(buffer, node);
    }
  }

  void _writeNodePath(StringBuffer buffer, AstNode node) {
    var parent = node.parent;
    if (parent != null) {
      _writeNodePath(buffer, parent);
      buffer.write(', ');
    }
    buffer.write(node.runtimeType);
  }

  /// Compare the [actual] and [expected] nodes, failing the test if they are
  /// different.
  static void compare(AstNode actual, AstNode expected) {
    ResultComparator comparator = ResultComparator();
    if (!comparator.isEqualNodes(actual, expected)) {
      fail('Expected: $expected\n   Found: $actual');
    }
  }
}
