// Copyright (c) 2014, 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/ast/token.dart';
import 'package:analyzer/source/line_info.dart';
import 'package:analyzer/src/dart/ast/token.dart';
import 'package:analyzer/src/dart/scanner/reader.dart';
import 'package:analyzer/src/dart/scanner/scanner.dart';
import 'package:analyzer/src/generated/source.dart';
import 'package:test/test.dart';
import 'package:test_reflective_loader/test_reflective_loader.dart';

import 'test_support.dart';

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

class CharacterRangeReaderTest extends EngineTestCase {
  void test_advance() {
    CharSequenceReader baseReader = new CharSequenceReader("xyzzy");
    CharacterRangeReader reader = new CharacterRangeReader(baseReader, 1, 4);
    expect(reader.advance(), 0x79);
    expect(reader.advance(), 0x80);
    expect(reader.advance(), 0x80);
    expect(reader.advance(), -1);
    expect(reader.advance(), -1);
  }

  void test_creation() {
    CharSequenceReader baseReader = new CharSequenceReader("xyzzy");
    CharacterRangeReader reader = new CharacterRangeReader(baseReader, 1, 4);
    expect(reader, isNotNull);
  }

  void test_getOffset() {
    CharSequenceReader baseReader = new CharSequenceReader("xyzzy");
    CharacterRangeReader reader = new CharacterRangeReader(baseReader, 1, 2);
    expect(reader.offset, 1);
    reader.advance();
    expect(reader.offset, 2);
    reader.advance();
    expect(reader.offset, 2);
  }

  void test_getString() {
    CharSequenceReader baseReader = new CharSequenceReader("__xyzzy__");
    CharacterRangeReader reader = new CharacterRangeReader(baseReader, 2, 7);
    reader.offset = 5;
    expect(reader.getString(3, 0), "yzz");
    expect(reader.getString(4, 1), "zzy");
  }

  void test_peek() {
    CharSequenceReader baseReader = new CharSequenceReader("xyzzy");
    CharacterRangeReader reader = new CharacterRangeReader(baseReader, 1, 3);
    expect(reader.peek(), 0x79);
    expect(reader.peek(), 0x79);
    reader.advance();
    expect(reader.peek(), 0x80);
    expect(reader.peek(), 0x80);
    reader.advance();
    expect(reader.peek(), -1);
    expect(reader.peek(), -1);
  }

  void test_setOffset() {
    CharSequenceReader baseReader = new CharSequenceReader("xyzzy");
    CharacterRangeReader reader = new CharacterRangeReader(baseReader, 1, 4);
    reader.offset = 2;
    expect(reader.offset, 2);
  }
}

@reflectiveTest
class LineInfoTest extends EngineTestCase {
  void test_lineInfo_multilineComment() {
    String source = "/*\r\n *\r\n */";
    _assertLineInfo(source, [
      new ScannerTest_ExpectedLocation(0, 1, 1),
      new ScannerTest_ExpectedLocation(5, 2, 2),
      new ScannerTest_ExpectedLocation(source.length - 1, 3, 3)
    ]);
  }

  void test_lineInfo_multilineString() {
    String source = "'''a\r\nbc\r\nd'''";
    _assertLineInfo(source, [
      new ScannerTest_ExpectedLocation(0, 1, 1),
      new ScannerTest_ExpectedLocation(7, 2, 2),
      new ScannerTest_ExpectedLocation(source.length - 1, 3, 4)
    ]);
  }

  void test_lineInfo_multilineString_raw() {
    String source = "var a = r'''\nblah\n''';\n\nfoo";
    _assertLineInfo(source, [
      new ScannerTest_ExpectedLocation(0, 1, 1),
      new ScannerTest_ExpectedLocation(14, 2, 2),
      new ScannerTest_ExpectedLocation(source.length - 2, 5, 2)
    ]);
  }

  void test_lineInfo_simpleClass() {
    String source =
        "class Test {\r\n    String s = '...';\r\n    int get x => s.MISSING_GETTER;\r\n}";
    _assertLineInfo(source, [
      new ScannerTest_ExpectedLocation(0, 1, 1),
      new ScannerTest_ExpectedLocation(source.indexOf("MISSING_GETTER"), 3, 20),
      new ScannerTest_ExpectedLocation(source.length - 1, 4, 1)
    ]);
  }

  void test_lineInfo_slashN() {
    String source = "class Test {\n}";
    _assertLineInfo(source, [
      new ScannerTest_ExpectedLocation(0, 1, 1),
      new ScannerTest_ExpectedLocation(source.indexOf("}"), 2, 1)
    ]);
  }

  void test_linestarts() {
    String source = "var\r\ni\n=\n1;\n";
    GatheringErrorListener listener = new GatheringErrorListener();
    Scanner scanner =
        new Scanner(null, new CharSequenceReader(source), listener);
    var token = scanner.tokenize();
    expect(token.lexeme, 'var');
    var lineStarts = scanner.lineStarts;
    expect(lineStarts, orderedEquals([0, 5, 7, 9, 12]));
  }

  void test_translate_missing_closing_gt_error() {
    // Ensure that the UnmatchedToken error for missing '>' is translated
    // to the correct analyzer error code.
    // See https://github.com/dart-lang/sdk/issues/30320
    String source = '<!-- @Component(';
    GatheringErrorListener listener = new GatheringErrorListener();
    _scanWithListener(source, listener);
    listener.assertErrorsWithCodes(const [
      ScannerErrorCode.EXPECTED_TOKEN,
      ScannerErrorCode.EXPECTED_TOKEN,
    ]);
  }

  void _assertLineInfo(
      String source, List<ScannerTest_ExpectedLocation> expectedLocations) {
    GatheringErrorListener listener = new GatheringErrorListener();
    _scanWithListener(source, listener);
    listener.assertNoErrors();
    LineInfo info = listener.getLineInfo(new TestSource());
    expect(info, isNotNull);
    int count = expectedLocations.length;
    for (int i = 0; i < count; i++) {
      ScannerTest_ExpectedLocation expectedLocation = expectedLocations[i];
      CharacterLocation location = info.getLocation(expectedLocation._offset);
      expect(location.lineNumber, expectedLocation._lineNumber,
          reason: 'Line number in location $i');
      expect(location.columnNumber, expectedLocation._columnNumber,
          reason: 'Column number in location $i');
    }
  }

  Token _scanWithListener(String source, GatheringErrorListener listener,
      {bool lazyAssignmentOperators: false}) {
    Scanner scanner =
        new Scanner(null, new CharSequenceReader(source), listener);
    scanner.scanLazyAssignmentOperators = lazyAssignmentOperators;
    Token result = scanner.tokenize();
    listener.setLineInfo(new TestSource(), scanner.lineStarts);
    return result;
  }
}

/**
 * An `ExpectedLocation` encodes information about the expected location of a
 * given offset in source code.
 */
class ScannerTest_ExpectedLocation {
  final int _offset;

  final int _lineNumber;

  final int _columnNumber;

  ScannerTest_ExpectedLocation(
      this._offset, this._lineNumber, this._columnNumber);
}

/**
 * A `TokenStreamValidator` is used to validate the correct construction of a
 * stream of tokens.
 */
class TokenStreamValidator {
  /**
   * Validate that the stream of tokens that starts with the given [token] is
   * correct.
   */
  void validate(Token token) {
    StringBuffer buffer = new StringBuffer();
    _validateStream(buffer, token);
    if (buffer.length > 0) {
      fail(buffer.toString());
    }
  }

  void _validateStream(StringBuffer buffer, Token token) {
    if (token == null) {
      return;
    }
    Token previousToken = null;
    int previousEnd = -1;
    Token currentToken = token;
    while (currentToken != null && currentToken.type != TokenType.EOF) {
      _validateStream(buffer, currentToken.precedingComments);
      TokenType type = currentToken.type;
      if (type == TokenType.OPEN_CURLY_BRACKET ||
          type == TokenType.OPEN_PAREN ||
          type == TokenType.OPEN_SQUARE_BRACKET ||
          type == TokenType.STRING_INTERPOLATION_EXPRESSION) {
        if (currentToken is! BeginToken) {
          buffer.write("\r\nExpected BeginToken, found ");
          buffer.write(currentToken.runtimeType.toString());
          buffer.write(" ");
          _writeToken(buffer, currentToken);
        }
      }
      int currentStart = currentToken.offset;
      int currentLength = currentToken.length;
      int currentEnd = currentStart + currentLength - 1;
      if (currentStart <= previousEnd) {
        buffer.write("\r\nInvalid token sequence: ");
        _writeToken(buffer, previousToken);
        buffer.write(" followed by ");
        _writeToken(buffer, currentToken);
      }
      previousEnd = currentEnd;
      previousToken = currentToken;
      currentToken = currentToken.next;
    }
  }

  void _writeToken(StringBuffer buffer, Token token) {
    buffer.write("[");
    buffer.write(token.type);
    buffer.write(", '");
    buffer.write(token.lexeme);
    buffer.write("', ");
    buffer.write(token.offset);
    buffer.write(", ");
    buffer.write(token.length);
    buffer.write("]");
  }
}
