// Copyright (c) 2015, 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 'dart:io';

import 'package:analyzer/analyzer.dart';
import 'package:analyzer/dart/ast/ast.dart';
import 'package:path/path.dart' as p;
import 'package:source_span/source_span.dart';

import '../backend/metadata.dart';
import '../backend/platform_selector.dart';
import '../frontend/timeout.dart';
import '../util/dart.dart';
import '../utils.dart';

/// Parse the test metadata for the test file at [path].
///
/// The [platformVariables] are the set of variables that are valid for platform
/// selectors in suite metadata, in addition to the built-in variables that are
/// allowed everywhere.
///
/// Throws an [AnalysisError] if parsing fails or a [FormatException] if the
/// test annotations are incorrect.
Metadata parseMetadata(String path, Set<String> platformVariables) =>
    new _Parser(path, platformVariables).parse();

/// A parser for test suite metadata.
class _Parser {
  /// The path to the test suite.
  final String _path;

  /// The set of variables that are valid for platform selectors, in addition to
  /// the built-in variables that are allowed everywhere.
  final Set<String> _platformVariables;

  /// All annotations at the top of the file.
  List<Annotation> _annotations;

  /// All prefixes defined by imports in this file.
  Set<String> _prefixes;

  _Parser(String path, this._platformVariables) : _path = path {
    var contents = new File(path).readAsStringSync();
    var directives = parseDirectives(contents, name: path).directives;
    _annotations = directives.isEmpty ? [] : directives.first.metadata;

    // We explicitly *don't* just look for "package:test" imports here,
    // because it could be re-exported from another library.
    _prefixes = directives
        .map((directive) {
          if (directive is ImportDirective) {
            if (directive.prefix == null) return null;
            return directive.prefix.name;
          } else {
            return null;
          }
        })
        .where((prefix) => prefix != null)
        .toSet();
  }

  /// Parses the metadata.
  Metadata parse() {
    Timeout timeout;
    PlatformSelector testOn;
    dynamic /*String|bool*/ skip;
    Map<PlatformSelector, Metadata> onPlatform;
    Set<String> tags;
    int retry;

    for (var annotation in _annotations) {
      var pair =
          _resolveConstructor(annotation.name, annotation.constructorName);
      var name = pair.first;
      var constructorName = pair.last;

      if (name == 'TestOn') {
        _assertSingle(testOn, 'TestOn', annotation);
        testOn = _parseTestOn(annotation, constructorName);
      } else if (name == 'Timeout') {
        _assertSingle(timeout, 'Timeout', annotation);
        timeout = _parseTimeout(annotation, constructorName);
      } else if (name == 'Skip') {
        _assertSingle(skip, 'Skip', annotation);
        skip = _parseSkip(annotation, constructorName);
      } else if (name == 'OnPlatform') {
        _assertSingle(onPlatform, 'OnPlatform', annotation);
        onPlatform = _parseOnPlatform(annotation, constructorName);
      } else if (name == 'Tags') {
        _assertSingle(tags, 'Tags', annotation);
        tags = _parseTags(annotation, constructorName);
      } else if (name == 'Retry') {
        retry = _parseRetry(annotation, constructorName);
      }
    }

    return new Metadata(
        testOn: testOn,
        timeout: timeout,
        skip: skip == null ? null : true,
        skipReason: skip is String ? skip : null,
        onPlatform: onPlatform,
        tags: tags,
        retry: retry);
  }

  /// Parses a `@TestOn` annotation.
  ///
  /// [annotation] is the annotation. [constructorName] is the name of the named
  /// constructor for the annotation, if any.
  PlatformSelector _parseTestOn(Annotation annotation, String constructorName) {
    _assertConstructorName(constructorName, 'TestOn', annotation);
    _assertArguments(annotation.arguments, 'TestOn', annotation, positional: 1);
    return _parsePlatformSelector(annotation.arguments.arguments.first);
  }

  /// Parses an [expression] that should contain a string representing a
  /// [PlatformSelector].
  PlatformSelector _parsePlatformSelector(Expression expression) {
    var literal = _parseString(expression);
    return _contextualize(
        literal,
        () => new PlatformSelector.parse(literal.stringValue)
          ..validate(_platformVariables));
  }

  /// Parses a `@Retry` annotation.
  ///
  /// [annotation] is the annotation. [constructorName] is the name of the named
  /// constructor for the annotation, if any.
  int _parseRetry(Annotation annotation, String constructorName) {
    _assertConstructorName(constructorName, 'Retry', annotation);
    _assertArguments(annotation.arguments, 'Retry', annotation, positional: 1);
    return _parseInt(annotation.arguments.arguments.first);
  }

  /// Parses a `@Timeout` annotation.
  ///
  /// [annotation] is the annotation. [constructorName] is the name of the named
  /// constructor for the annotation, if any.
  Timeout _parseTimeout(Annotation annotation, String constructorName) {
    _assertConstructorName(constructorName, 'Timeout', annotation,
        validNames: [null, 'factor', 'none']);

    var description = 'Timeout';
    if (constructorName != null) description += '.$constructorName';

    if (constructorName == 'none') {
      _assertNoArguments(annotation, description);
      return Timeout.none;
    }

    _assertArguments(annotation.arguments, description, annotation,
        positional: 1);

    var args = annotation.arguments.arguments;
    if (constructorName == null) return new Timeout(_parseDuration(args.first));
    return new Timeout.factor(_parseNum(args.first));
  }

  /// Parses a `Timeout` constructor.
  Timeout _parseTimeoutConstructor(InstanceCreationExpression constructor) {
    var name =
        _parseConstructor(constructor, 'Timeout', validNames: [null, 'factor']);

    var description = 'Timeout';
    if (name != null) description += '.$name';

    _assertArguments(constructor.argumentList, description, constructor,
        positional: 1);

    var args = constructor.argumentList.arguments;
    if (name == null) return new Timeout(_parseDuration(args.first));
    return new Timeout.factor(_parseNum(args.first));
  }

  /// Parses a `@Skip` annotation.
  ///
  /// [annotation] is the annotation. [constructorName] is the name of the named
  /// constructor for the annotation, if any.
  ///
  /// Returns either `true` or a reason string.
  _parseSkip(Annotation annotation, String constructorName) {
    _assertConstructorName(constructorName, 'Skip', annotation);
    _assertArguments(annotation.arguments, 'Skip', annotation, optional: 1);

    var args = annotation.arguments.arguments;
    return args.isEmpty ? true : _parseString(args.first).stringValue;
  }

  /// Parses a `Skip` constructor.
  ///
  /// Returns either `true` or a reason string.
  _parseSkipConstructor(InstanceCreationExpression constructor) {
    _parseConstructor(constructor, 'Skip');
    _assertArguments(constructor.argumentList, 'Skip', constructor,
        optional: 1);

    var args = constructor.argumentList.arguments;
    return args.isEmpty ? true : _parseString(args.first).stringValue;
  }

  /// Parses a `@Tags` annotation.
  ///
  /// [annotation] is the annotation. [constructorName] is the name of the named
  /// constructor for the annotation, if any.
  Set<String> _parseTags(Annotation annotation, String constructorName) {
    _assertConstructorName(constructorName, 'Tags', annotation);
    _assertArguments(annotation.arguments, 'Tags', annotation, positional: 1);

    return _parseList(annotation.arguments.arguments.first)
        .map((tagExpression) {
      var name = _parseString(tagExpression).stringValue;
      if (name.contains(anchoredHyphenatedIdentifier)) return name;

      throw new SourceSpanFormatException(
          "Invalid tag name. Tags must be (optionally hyphenated) Dart "
          "identifiers.",
          _spanFor(tagExpression));
    }).toSet();
  }

  /// Parses an `@OnPlatform` annotation.
  ///
  /// [annotation] is the annotation. [constructorName] is the name of the named
  /// constructor for the annotation, if any.
  Map<PlatformSelector, Metadata> _parseOnPlatform(
      Annotation annotation, String constructorName) {
    _assertConstructorName(constructorName, 'OnPlatform', annotation);
    _assertArguments(annotation.arguments, 'OnPlatform', annotation,
        positional: 1);

    return _parseMap(annotation.arguments.arguments.first, key: (key) {
      return _parsePlatformSelector(key);
    }, value: (value) {
      var expressions = <AstNode>[];
      if (value is ListLiteral) {
        expressions = _parseList(value);
      } else if (value is InstanceCreationExpression ||
          value is PrefixedIdentifier) {
        expressions = [value];
      } else {
        throw new SourceSpanFormatException(
            'Expected a Timeout, Skip, or List of those.', _spanFor(value));
      }

      Timeout timeout;
      dynamic skip;
      for (var expression in expressions) {
        if (expression is InstanceCreationExpression) {
          var className = _resolveConstructor(
                  expression.constructorName.type.name,
                  expression.constructorName.name)
              .first;

          if (className == 'Timeout') {
            _assertSingle(timeout, 'Timeout', expression);
            timeout = _parseTimeoutConstructor(expression);
            continue;
          } else if (className == 'Skip') {
            _assertSingle(skip, 'Skip', expression);
            skip = _parseSkipConstructor(expression);
            continue;
          }
        } else if (expression is PrefixedIdentifier &&
            expression.prefix.name == 'Timeout') {
          if (expression.identifier.name != 'none') {
            throw new SourceSpanFormatException(
                'Undefined value.', _spanFor(expression));
          }

          _assertSingle(timeout, 'Timeout', expression);
          timeout = Timeout.none;
          continue;
        }

        throw new SourceSpanFormatException(
            'Expected a Timeout or Skip.', _spanFor(expression));
      }

      return new Metadata.parse(timeout: timeout, skip: skip);
    });
  }

  /// Parses a `const Duration` expression.
  Duration _parseDuration(Expression expression) {
    _parseConstructor(expression, 'Duration');

    var constructor = expression as InstanceCreationExpression;
    var valueExpressions = _assertArguments(
        constructor.argumentList, 'Duration', constructor, named: [
      'days',
      'hours',
      'minutes',
      'seconds',
      'milliseconds',
      'microseconds'
    ]);

    var values = valueExpressions
        .map((key, value) => new MapEntry(key, _parseInt(value)));

    return new Duration(
        days: values["days"] == null ? 0 : values["days"],
        hours: values["hours"] == null ? 0 : values["hours"],
        minutes: values["minutes"] == null ? 0 : values["minutes"],
        seconds: values["seconds"] == null ? 0 : values["seconds"],
        milliseconds:
            values["milliseconds"] == null ? 0 : values["milliseconds"],
        microseconds:
            values["microseconds"] == null ? 0 : values["microseconds"]);
  }

  /// Asserts that [existing] is null.
  ///
  /// [name] is the name of the annotation and [node] is its location, used for
  /// error reporting.
  void _assertSingle(Object existing, String name, AstNode node) {
    if (existing == null) return;
    throw new SourceSpanFormatException(
        "Only a single $name may be used.", _spanFor(node));
  }

  /// Resolves a constructor name from its type [identifier] and its
  /// [constructorName].
  ///
  /// Since the parsed file isn't fully resolved, this is necessary to
  /// disambiguate between prefixed names and named constructors.
  Pair<String, String> _resolveConstructor(
      Identifier identifier, SimpleIdentifier constructorName) {
    // The syntax is ambiguous between named constructors and prefixed
    // annotations, so we need to resolve that ambiguity using the known
    // prefixes. The analyzer parses "new x.y()" as prefix "x", annotation "y",
    // and named constructor null. It parses "new x.y.z()" as prefix "x",
    // annotation "y", and named constructor "z".
    String className;
    String namedConstructor;
    if (identifier is PrefixedIdentifier &&
        !_prefixes.contains(identifier.prefix.name) &&
        constructorName == null) {
      className = identifier.prefix.name;
      namedConstructor = identifier.identifier.name;
    } else {
      className = identifier is PrefixedIdentifier
          ? identifier.identifier.name
          : identifier.name;
      if (constructorName != null) namedConstructor = constructorName.name;
    }
    return new Pair(className, namedConstructor);
  }

  /// Asserts that [constructorName] is a valid constructor name for an AST
  /// node.
  ///
  /// [nodeName] is the name of the class being constructed, and [node] is the
  /// AST node for that class. [validNames], if passed, is the set of valid
  /// constructor names; if an unnamed constructor is valid, it should include
  /// `null`. By default, only an unnamed constructor is allowed.
  void _assertConstructorName(
      String constructorName, String nodeName, AstNode node,
      {Iterable<String> validNames}) {
    if (validNames == null) validNames = [null];
    if (validNames.contains(constructorName)) return;

    if (constructorName == null) {
      throw new SourceSpanFormatException(
          "$nodeName doesn't have an unnamed constructor.", _spanFor(node));
    } else {
      throw new SourceSpanFormatException(
          '$nodeName doesn\'t have a constructor named "$constructorName".',
          _spanFor(node));
    }
  }

  /// Parses a constructor invocation for [className].
  ///
  /// [validNames], if passed, is the set of valid constructor names; if an
  /// unnamed constructor is valid, it should include `null`. By default, only
  /// an unnamed constructor is allowed.
  ///
  /// Returns the name of the named constructor, if any.
  String _parseConstructor(Expression expression, String className,
      {Iterable<String> validNames}) {
    if (validNames == null) validNames = [null];

    if (expression is! InstanceCreationExpression) {
      throw new SourceSpanFormatException(
          "Expected a $className.", _spanFor(expression));
    }

    var constructor = expression as InstanceCreationExpression;
    var pair = _resolveConstructor(constructor.constructorName.type.name,
        constructor.constructorName.name);
    var actualClassName = pair.first;
    var constructorName = pair.last;

    if (actualClassName != className) {
      throw new SourceSpanFormatException(
          "Expected a $className.", _spanFor(constructor));
    }

    if (constructor.keyword.lexeme != "const") {
      throw new SourceSpanFormatException(
          "$className must use a const constructor.", _spanFor(constructor));
    }

    _assertConstructorName(constructorName, className, expression,
        validNames: validNames);
    return constructorName;
  }

  /// Assert that [arguments] is a valid argument list.
  ///
  /// [name] describes the function and [node] is its AST node. [positional] is
  /// the number of required positional arguments, [optional] the number of
  /// optional positional arguments, and [named] the set of valid argument
  /// names.
  ///
  /// The set of parsed named arguments is returned.
  Map<String, Expression> _assertArguments(
      ArgumentList arguments, String name, AstNode node,
      {int positional, int optional, Iterable<String> named}) {
    if (positional == null) positional = 0;
    if (optional == null) optional = 0;
    if (named == null) named = new Set();

    if (arguments == null) {
      throw new SourceSpanFormatException(
          '$name takes arguments.', _spanFor(node));
    }

    var actualNamed = arguments.arguments
        .where((arg) => arg is NamedExpression)
        .map((arg) => arg as NamedExpression)
        .toList();
    if (actualNamed.isNotEmpty && named.isEmpty) {
      throw new SourceSpanFormatException(
          "$name doesn't take named arguments.", _spanFor(actualNamed.first));
    }

    var namedValues = <String, Expression>{};
    for (var argument in actualNamed) {
      var argumentName = argument.name.label.name;
      if (!named.contains(argumentName)) {
        throw new SourceSpanFormatException(
            '$name doesn\'t take an argument named "$argumentName".',
            _spanFor(argument));
      } else if (namedValues.containsKey(argumentName)) {
        throw new SourceSpanFormatException(
            'An argument named "$argumentName" was already passed.',
            _spanFor(argument));
      } else {
        namedValues[argumentName] = argument.expression;
      }
    }

    var actualPositional = arguments.arguments.length - actualNamed.length;
    if (actualPositional < positional) {
      var buffer = new StringBuffer("$name takes ");
      if (optional != 0) buffer.write("at least ");
      buffer.write("$positional argument");
      if (positional > 1) buffer.write("s");
      buffer.write(".");
      throw new SourceSpanFormatException(
          buffer.toString(), _spanFor(arguments));
    }

    if (actualPositional > positional + optional) {
      if (optional + positional == 0) {
        var buffer = new StringBuffer("$name doesn't take ");
        if (named.isNotEmpty) buffer.write("positional ");
        buffer.write("arguments.");
        throw new SourceSpanFormatException(
            buffer.toString(), _spanFor(arguments));
      }

      var buffer = new StringBuffer("$name takes ");
      if (optional != 0) buffer.write("at most ");
      buffer.write("${positional + optional} argument");
      if (positional > 1) buffer.write("s");
      buffer.write(".");
      throw new SourceSpanFormatException(
          buffer.toString(), _spanFor(arguments));
    }

    return namedValues;
  }

  /// Assert that [annotation] (described by [name]) has no argument list.
  void _assertNoArguments(Annotation annotation, String name) {
    if (annotation.arguments == null) return;
    throw new SourceSpanFormatException(
        "$name doesn't take arguments.", _spanFor(annotation));
  }

  /// Parses a Map literal.
  ///
  /// By default, returns [Expression] keys and values. These can be overridden
  /// with the [key] and [value] parameters.
  Map<K, V> _parseMap<K, V>(Expression expression,
      {K key(Expression expression), V value(Expression expression)}) {
    if (key == null) key = (expression) => expression as K;
    if (value == null) value = (expression) => expression as V;

    if (expression is! MapLiteral) {
      throw new SourceSpanFormatException(
          "Expected a Map.", _spanFor(expression));
    }

    var map = expression as MapLiteral;
    if (map.constKeyword == null) {
      throw new SourceSpanFormatException(
          "Map literals must be const.", _spanFor(map));
    }

    return new Map.fromIterables(map.entries.map((e) => key(e.key)),
        map.entries.map((e) => value(e.value)));
  }

  /// Parses a List literal.
  List<Expression> _parseList(Expression expression) {
    if (expression is! ListLiteral) {
      throw new SourceSpanFormatException(
          "Expected a List.", _spanFor(expression));
    }

    var list = expression as ListLiteral;
    if (list.constKeyword == null) {
      throw new SourceSpanFormatException(
          "List literals must be const.", _spanFor(list));
    }

    return list.elements;
  }

  /// Parses a constant number literal.
  num _parseNum(Expression expression) {
    if (expression is IntegerLiteral) return expression.value;
    if (expression is DoubleLiteral) return expression.value;
    throw new SourceSpanFormatException(
        "Expected a number.", _spanFor(expression));
  }

  /// Parses a constant int literal.
  int _parseInt(Expression expression) {
    if (expression is IntegerLiteral) return expression.value;
    throw new SourceSpanFormatException(
        "Expected an integer.", _spanFor(expression));
  }

  /// Parses a constant String literal.
  StringLiteral _parseString(Expression expression) {
    if (expression is StringLiteral) return expression;
    throw new SourceSpanFormatException(
        "Expected a String.", _spanFor(expression));
  }

  /// Creates a [SourceSpan] for [node].
  SourceSpan _spanFor(AstNode node) {
    // Load a SourceFile from scratch here since we're only ever going to emit
    // one error per file anyway.
    var contents = new File(_path).readAsStringSync();
    return new SourceFile.fromString(contents, url: p.toUri(_path))
        .span(node.offset, node.end);
  }

  /// Runs [fn] and contextualizes any [SourceSpanFormatException]s that occur
  /// in it relative to [literal].
  T _contextualize<T>(StringLiteral literal, T fn()) {
    try {
      return fn();
    } on SourceSpanFormatException catch (error) {
      var file = new SourceFile.fromString(new File(_path).readAsStringSync(),
          url: p.toUri(_path));
      var span = contextualizeSpan(error.span, literal, file);
      if (span == null) rethrow;
      throw new SourceSpanFormatException(error.message, span);
    }
  }
}
