// Copyright (c) 2016, 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:test/test.dart';

import 'package:boolean_selector/src/scanner.dart';
import 'package:boolean_selector/src/token.dart';

void main() {
  group("peek()", () {
    test("returns the next token without consuming it", () {
      var scanner = new Scanner("( )");
      expect(scanner.peek().type, equals(TokenType.leftParen));
      expect(scanner.peek().type, equals(TokenType.leftParen));
      expect(scanner.peek().type, equals(TokenType.leftParen));
    });

    test("returns an end-of-file token at the end of a file", () {
      var scanner = new Scanner("( )");
      scanner.next();
      scanner.next();

      var token = scanner.peek();
      expect(token.type, equals(TokenType.endOfFile));
      expect(token.span.start.offset, equals(3));
      expect(token.span.end.offset, equals(3));
    });

    test("throws a StateError if called after end-of-file was consumed", () {
      var scanner = new Scanner("( )");
      scanner.next();
      scanner.next();
      scanner.next();
      expect(() => scanner.peek(), throwsStateError);
    });
  });

  group("next()", () {
    test("consumes and returns the next token", () {
      var scanner = new Scanner("( )");
      expect(scanner.next().type, equals(TokenType.leftParen));
      expect(scanner.peek().type, equals(TokenType.rightParen));
      expect(scanner.next().type, equals(TokenType.rightParen));
    });

    test("returns an end-of-file token at the end of a file", () {
      var scanner = new Scanner("( )");
      scanner.next();
      scanner.next();

      var token = scanner.next();
      expect(token.type, equals(TokenType.endOfFile));
      expect(token.span.start.offset, equals(3));
      expect(token.span.end.offset, equals(3));
    });

    test("throws a StateError if called after end-of-file was consumed", () {
      var scanner = new Scanner("( )");
      scanner.next();
      scanner.next();
      scanner.next();
      expect(() => scanner.next(), throwsStateError);
    });
  });

  group("scan()", () {
    test("consumes a matching token and returns true", () {
      var scanner = new Scanner("( )");
      expect(scanner.scan(TokenType.leftParen), isTrue);
      expect(scanner.peek().type, equals(TokenType.rightParen));
    });

    test("doesn't consume a matching token and returns false", () {
      var scanner = new Scanner("( )");
      expect(scanner.scan(TokenType.questionMark), isFalse);
      expect(scanner.peek().type, equals(TokenType.leftParen));
    });

    test("throws a StateError called after end-of-file was consumed", () {
      var scanner = new Scanner("( )");
      scanner.next();
      scanner.next();
      scanner.next();
      expect(() => scanner.scan(TokenType.endOfFile), throwsStateError);
    });
  });

  group("scans a simple token:", () {
    test("left paren", () => _expectSimpleScan("(", TokenType.leftParen));
    test("right paren", () => _expectSimpleScan(")", TokenType.rightParen));
    test("or", () => _expectSimpleScan("||", TokenType.or));
    test("and", () => _expectSimpleScan("&&", TokenType.and));
    test("not", () => _expectSimpleScan("!", TokenType.not));
    test("question mark", () => _expectSimpleScan("?", TokenType.questionMark));
    test("colon", () => _expectSimpleScan(":", TokenType.colon));
  });

  group("scans an identifier that", () {
    test("is simple", () {
      var token = _scan("   foo  ");
      expect(token.name, equals("foo"));
      expect(token.span.text, equals("foo"));
      expect(token.span.start.offset, equals(3));
      expect(token.span.end.offset, equals(6));
    });

    test("is a single character", () {
      var token = _scan("f");
      expect(token.name, equals("f"));
    });

    test("has a leading underscore", () {
      var token = _scan("_foo");
      expect(token.name, equals("_foo"));
    });

    test("has a leading dash", () {
      var token = _scan("-foo");
      expect(token.name, equals("-foo"));
    });

    test("contains an underscore", () {
      var token = _scan("foo_bar");
      expect(token.name, equals("foo_bar"));
    });

    test("contains a dash", () {
      var token = _scan("foo-bar");
      expect(token.name, equals("foo-bar"));
    });

    test("is capitalized", () {
      var token = _scan("FOO");
      expect(token.name, equals("FOO"));
    });

    test("contains numbers", () {
      var token = _scan("foo123");
      expect(token.name, equals("foo123"));
    });
  });

  test("scans an empty selector", () {
    expect(_scan("").type, equals(TokenType.endOfFile));
  });

  test("scans multiple tokens", () {
    var scanner = new Scanner("(foo && bar)");

    var token = scanner.next();
    expect(token.type, equals(TokenType.leftParen));
    expect(token.span.start.offset, equals(0));
    expect(token.span.end.offset, equals(1));

    token = scanner.next();
    expect(token.type, equals(TokenType.identifier));
    expect((token as IdentifierToken).name, equals("foo"));
    expect(token.span.start.offset, equals(1));
    expect(token.span.end.offset, equals(4));

    token = scanner.next();
    expect(token.type, equals(TokenType.and));
    expect(token.span.start.offset, equals(5));
    expect(token.span.end.offset, equals(7));

    token = scanner.next();
    expect(token.type, equals(TokenType.identifier));
    expect((token as IdentifierToken).name, equals("bar"));
    expect(token.span.start.offset, equals(8));
    expect(token.span.end.offset, equals(11));

    token = scanner.next();
    expect(token.type, equals(TokenType.rightParen));
    expect(token.span.start.offset, equals(11));
    expect(token.span.end.offset, equals(12));

    token = scanner.next();
    expect(token.type, equals(TokenType.endOfFile));
    expect(token.span.start.offset, equals(12));
    expect(token.span.end.offset, equals(12));
  });

  group("ignores", () {
    test("a single-line comment", () {
      var scanner = new Scanner("( // &&\n// ||\n)");
      expect(scanner.next().type, equals(TokenType.leftParen));
      expect(scanner.next().type, equals(TokenType.rightParen));
      expect(scanner.next().type, equals(TokenType.endOfFile));
    });

    test("a single-line comment without a trailing newline", () {
      var scanner = new Scanner("( // &&");
      expect(scanner.next().type, equals(TokenType.leftParen));
      expect(scanner.next().type, equals(TokenType.endOfFile));
    });

    test("a multi-line comment", () {
      var scanner = new Scanner("( /* && * /\n|| */\n)");
      expect(scanner.next().type, equals(TokenType.leftParen));
      expect(scanner.next().type, equals(TokenType.rightParen));
      expect(scanner.next().type, equals(TokenType.endOfFile));
    });

    test("a multi-line nested comment", () {
      var scanner = new Scanner("(/* && /* ? /* || */ : */ ! */)");
      expect(scanner.next().type, equals(TokenType.leftParen));
      expect(scanner.next().type, equals(TokenType.rightParen));
      expect(scanner.next().type, equals(TokenType.endOfFile));
    });

    test("Dart's notion of whitespace", () {
      var scanner = new Scanner("( \t \n)");
      expect(scanner.next().type, equals(TokenType.leftParen));
      expect(scanner.next().type, equals(TokenType.rightParen));
      expect(scanner.next().type, equals(TokenType.endOfFile));
    });
  });

  group("disallows", () {
    test("a single |", () {
      expect(() => _scan("|"), throwsFormatException);
    });

    test('"| |"', () {
      expect(() => _scan("| |"), throwsFormatException);
    });

    test("a single &", () {
      expect(() => _scan("&"), throwsFormatException);
    });

    test('"& &"', () {
      expect(() => _scan("& &"), throwsFormatException);
    });

    test("an unknown operator", () {
      expect(() => _scan("=="), throwsFormatException);
    });

    test("unicode", () {
      expect(() => _scan("öh"), throwsFormatException);
    });

    test("an unclosed multi-line comment", () {
      expect(() => _scan("/*"), throwsFormatException);
    });

    test("an unopened multi-line comment", () {
      expect(() => _scan("*/"), throwsFormatException);
    });
  });
}

/// Asserts that the first token scanned from [selector] has type [type],
/// and that that token's span is exactly [selector].
void _expectSimpleScan(String selector, TokenType type) {
  // Complicate the selector to test that the span covers it correctly.
  var token = _scan("   $selector  ");
  expect(token.type, equals(type));
  expect(token.span.text, equals(selector));
  expect(token.span.start.offset, equals(3));
  expect(token.span.end.offset, equals(3 + selector.length));
}

/// Scans a single token from [selector].
dynamic _scan(String selector) => new Scanner(selector).next();
