// Copyright (c) 2013, 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.

library mixin_test;

import 'package:csslib/src/messages.dart';
import 'package:test/test.dart';

import 'testing.dart';

compileAndValidate(String input, String generated) {
  var errors = <Message>[];
  var stylesheet = compileCss(input, errors: errors, opts: options);
  expect(stylesheet != null, true);
  expect(errors.isEmpty, true, reason: errors.toString());
  expect(prettyPrint(stylesheet), generated);
}

compilePolyfillAndValidate(String input, String generated) {
  var errors = <Message>[];
  var stylesheet = polyFillCompileCss(input, errors: errors, opts: options);
  expect(stylesheet != null, true);
  expect(errors.isEmpty, true, reason: errors.toString());
  expect(prettyPrint(stylesheet), generated);
}

void topLevelMixin() {
  compileAndValidate(r'''
@mixin silly-links {
  a {
    color: blue;
    background-color: red;
  }
}

@include silly-links;
''', r'''
a {
  color: #00f;
  background-color: #f00;
}''');
}

void topLevelMixinTwoIncludes() {
  compileAndValidate(r'''
@mixin a {
  a {
    color: blue;
    background-color: red;
  }
}
@mixin b {
  span {
    color: black;
    background-color: orange;
  }
}
@include a;
@include b;
''', r'''
a {
  color: #00f;
  background-color: #f00;
}
span {
  color: #000;
  background-color: #ffa500;
}''');
}

/** Tests top-level mixins that includes another mixin. */
void topLevelMixinMultiRulesets() {
  compileAndValidate(r'''
@mixin a {
  a {
    color: blue;
    background-color: red;
  }
}
@mixin b {
  #foo-id {
    border-top: 1px solid red;
    border-bottom: 2px solid green;
  }
}
@mixin c {
  span {
    color: black;
    background-color: orange;
  }
  @include b;
}
@include a;
@include c;
''', r'''
a {
  color: #00f;
  background-color: #f00;
}
span {
  color: #000;
  background-color: #ffa500;
}
#foo-id {
  border-top: 1px solid #f00;
  border-bottom: 2px solid #008000;
}''');
}

void topLevelMixinDeeplyNestedRulesets() {
  compileAndValidate(r'''
@mixin a {
  a {
    color: blue;
    background-color: red;
  }
}
@mixin b {
  #foo-id {
    border-top: 1px solid red;
    border-bottom: 2px solid green;
  }
}
@mixin f {
  #split-bar div {
    border: 1px solid lightgray;
  }
}
@mixin e {
  #split-bar:visited {
    color: gray;
  }
  @include f;
}
@mixin d {
  a:hover {
    cursor: arrow;
  }
  @include e
}
@mixin c {
  @include a;
  span {
    color: black;
    background-color: orange;
  }
  @include b;
  @include d;
}
@include c;
''', r'''
a {
  color: #00f;
  background-color: #f00;
}
span {
  color: #000;
  background-color: #ffa500;
}
#foo-id {
  border-top: 1px solid #f00;
  border-bottom: 2px solid #008000;
}
a:hover {
  cursor: arrow;
}
#split-bar:visited {
  color: #808080;
}
#split-bar div {
  border: 1px solid #d3d3d3;
}''');
}

/** Tests selector groups and other combinators. */
void topLevelMixinSelectors() {
  compileAndValidate(r'''
@mixin a {
  a, b {
    color: blue;
    background-color: red;
  }
  div > span {
    color: black;
    background-color: orange;
  }
}

@include a;
''', r'''
a, b {
  color: #00f;
  background-color: #f00;
}
div > span {
  color: #000;
  background-color: #ffa500;
}''');
}

void declSimpleMixin() {
  compileAndValidate(r'''
@mixin div-border {
  border: 2px dashed red;
}
div {
  @include div-border;
}
''', r'''
div {
  border: 2px dashed #f00;
}''');
}

void declMixinTwoIncludes() {
  compileAndValidate(r'''
@mixin div-border {
  border: 2px dashed red;
}
@mixin div-color {
  color: blue;
}
div {
  @include div-border;
  @include div-color;
}
''', r'''
div {
  border: 2px dashed #f00;
  color: #00f;
}''');
}

void declMixinNestedIncludes() {
  compileAndValidate(r'''
@mixin div-border {
  border: 2px dashed red;
}
@mixin div-padding {
  padding: .5em;
}
@mixin div-margin {
  margin: 5px;
}
@mixin div-color {
  @include div-padding;
  color: blue;
  @include div-margin;
}
div {
  @include div-border;
  @include div-color;
}
''', r'''
div {
  border: 2px dashed #f00;
  padding: .5em;
  color: #00f;
  margin: 5px;
}''');
}

void declMixinDeeperNestedIncludes() {
  compileAndValidate(r'''
@mixin div-border {
  border: 2px dashed red;
}
@mixin div-padding {
  padding: .5em;
}
@mixin div-margin {
  margin: 5px;
}
@mixin div-color {
  @include div-padding;
  @include div-margin;
}
div {
  @include div-border;
  @include div-color;
}
''', r'''
div {
  border: 2px dashed #f00;
  padding: .5em;
  margin: 5px;
}''');
}

void mixinArg() {
  compileAndValidate(r'''
@mixin div-border-1 {
  border: 2px dashed red;
}
@mixin div-border-2() {
  border: 22px solid blue;
}
@mixin div-left(@dist) {
  margin-left: @dist;
}
@mixin div-right(var-margin) {
  margin-right: var(margin);
}
div-1 {
  @include div-left(10px);
  @include div-right(100px);
  @include div-border-1;
}
div-2 {
  @include div-left(20em);
  @include div-right(5in);
  @include div-border-2();
}
div-3 {
  @include div-border-1();
}
div-4 {
  @include div-border-2;
}
''', r'''
div-1 {
  margin-left: 10px;
  margin-right: 100px;
  border: 2px dashed #f00;
}
div-2 {
  margin-left: 20em;
  margin-right: 5in;
  border: 22px solid #00f;
}
div-3 {
  border: 2px dashed #f00;
}
div-4 {
  border: 22px solid #00f;
}''');
}

void mixinArgs() {
  compileAndValidate(r'''
@mixin box-shadow(@shadows...) {
  -moz-box-shadow: @shadows;
  -webkit-box-shadow: @shadows;
  box-shadow: var(shadows);
}

.shadows {
  @include box-shadow(0px 4px 5px #666, 2px 6px 10px #999);
}''', r'''
.shadowed {
  -moz-box-shadow: 0px 4px 5px #666, 2px 6px 10px #999;
  -webkit-box-shadow: 0px 4px 5px #666, 2px 6px 10px #999;
  box-shadow: 0px 4px 5px #666, 2px 6px 10px #999;
}
''');
}

void mixinManyArgs() {
  compileAndValidate(r'''
@mixin border(@border-values) {
  border: @border-values
}

.primary {
  @include border(3px solid green);
}
''', r'''
.primary {
  border: 3px solid #008000;
}''');

  compileAndValidate(r'''
@mixin setup(@border-color, @border-style, @border-size, @color) {
  border: @border-size @border-style @border-color;
  color: @color;
}

.primary {
  @include setup(red, solid, 5px, blue);
}
''', r'''
.primary {
  border: 5px solid #f00;
  color: #00f;
}''');

  // Test passing a declaration that is multiple parameters.
  compileAndValidate(r'''
@mixin colors(@text, @background, @border) {
  color: @text;
  background-color: @background;
  border-color: @border;
}

@values: #ff0000, #00ff00, #0000ff;
.primary {
  @include colors(@values);
}
''', r'''
var-values: #f00, #0f0, #00f;

.primary {
  color: #f00;
  background-color: #0f0;
  border-color: #00f;
}''');

  compilePolyfillAndValidate(r'''
@mixin colors(@text, @background, @border) {
  color: @text;
  background-color: @background;
  border-color: @border;
}

@values: #ff0000, #00ff00, #0000ff;
.primary {
  @include colors(@values);
}
''', r'''
.primary {
  color: #f00;
  background-color: #0f0;
  border-color: #00f;
}''');
}

void badDeclarationInclude() {
  final errors = <Message>[];
  final input = r'''
@mixin a {
  #foo-id {
    color: red;
  }
}
@mixin b {
  span {
    border: 2px dashed red;
    @include a;
  }
}
@include b;
''';

  var stylesheet = compileCss(input, errors: errors, opts: options);

  expect(stylesheet != null, true);
  expect(errors.isNotEmpty, true);
  expect(errors.length, 1, reason: errors.toString());
  var error = errors[0];
  expect(error.message, 'Using top-level mixin a as a declaration');
  expect(error.span.start.line, 8);
  expect(error.span.end.offset, 105);
}

void badTopInclude() {
  final errors = <Message>[];
  final input = r'''
@mixin b {
  color: red;
}

@mixin a {
  span {
    border: 2px dashed red;
  }
  @include b;
}

@include a;
  ''';

  var stylesheet = compileCss(input, errors: errors, opts: options);
  expect(stylesheet != null, true);
  expect(errors.length, 1, reason: errors.toString());
  var error = errors[0];
  expect(error.message, 'Using declaration mixin b as top-level mixin');
  expect(error.span.start.line, 8);
  expect(error.span.end.offset, 90);
}

void emptyMixin() {
  final errors = <Message>[];
  final input = r'''
@mixin a {
}
@mixin b {
  border: 2px dashed red;
  @include a;
}
div {
  @include b;
}
  ''';

  var generated = r'''
div {
  border: 2px dashed #f00;
}''';

  var stylesheet = compileCss(input, errors: errors, opts: options);

  expect(stylesheet != null, true);
  expect(errors.isEmpty, true, reason: errors.toString());
  expect(prettyPrint(stylesheet), generated);
}

void undefinedTopLevel() {
  final errors = <Message>[];
  final input = r'''
@mixin a {
  @include b;
}
@mixin b {
  span {
    border: 2px dashed red;
  }
  @include a;
}

@include b;

  ''';

  var stylesheet = compileCss(input, errors: errors, opts: options);

  expect(stylesheet != null, true);
  expect(errors.isNotEmpty, true);
  expect(errors.length, 1, reason: errors.toString());
  var error = errors[0];
  expect(error.message, 'Undefined mixin b');
  expect(error.span.start.line, 1);
  expect(error.span.start.offset, 14);
}

void undefinedDeclaration() {
  final errors = <Message>[];
  final input = r'''
@mixin a {
  @include b;
}
@mixin b {
  border: 2px dashed red;
  @include a;
}
div {
  @include b;
}
  ''';

  var stylesheet = compileCss(input, errors: errors, opts: options);

  expect(stylesheet != null, true);
  expect(errors.isNotEmpty, true);
  expect(errors.length, 1, reason: errors.toString());
  var error = errors[0];
  expect(error.message, 'Undefined mixin b');
  expect(error.span.start.line, 1);
  expect(error.span.start.offset, 14);
}

void includeGrammar() {
  compileAndValidate(r'''
@mixin a {
  foo { color: red }
}

@mixin b {
  @include a;
  @include a;
}

@include b;
''', r'''
foo {
  color: #f00;
}
foo {
  color: #f00;
}''');

  compileAndValidate(r'''
@mixin a {
  color: red
}

foo {
  @include a;
  @include a
}
''', r'''
foo {
  color: #f00;
  color: #f00;
}''');

  var errors = <Message>[];
  var input = r'''
@mixin a {
  foo { color: red }
}

@mixin b {
  @include a
  @include a
}

@include b
''';

  var stylesheet = compileCss(input, errors: errors, opts: options);

  expect(stylesheet != null, true);

  expect(errors.isNotEmpty, true);
  expect(errors.length, 6, reason: errors.toString());
  var error = errors[0];
  expect(error.message, 'parsing error expected ;');
  expect(error.span.start.line, 6);
  expect(error.span.end.offset, 69);
  error = errors[1];
  expect(error.message, 'expected :, but found }');
  expect(error.span.start.line, 7);
  expect(error.span.end.offset, 73);
  error = errors[2];
  expect(error.message, 'parsing error expected }');
  expect(error.span.start.line, 9);
  expect(error.span.end.offset, 83);
  error = errors[3];
  expect(error.message, 'expected {, but found end of file()');
  expect(error.span.start.line, 9);
  expect(error.span.end.offset, 86);
  error = errors[4];
  expect(error.message, 'expected }, but found end of file()');
  expect(error.span.start.line, 10);
  expect(error.span.end.offset, 86);
  error = errors[5];
  expect(error.message, 'Using top-level mixin a as a declaration');
  expect(error.span.start.line, 5);
  expect(error.span.end.offset, 56);
}

main() {
  group('Basic mixin', () {
    test('include grammar', includeGrammar);
    test('empty mixin content', emptyMixin);
  });

  group('Top-level mixin', () {
    test('simple mixin', topLevelMixin);
    test('mixin with two @includes', topLevelMixinTwoIncludes);
    test('multi rulesets', topLevelMixinMultiRulesets);
    test('multi rulesets and nesting', topLevelMixinDeeplyNestedRulesets);
    test('selector groups', topLevelMixinSelectors);
  });

  group('Declaration mixin', () {
    test('simple', declSimpleMixin);
    test('with two @includes', declMixinTwoIncludes);
    test('with includes', declMixinNestedIncludes);
    test('with deeper nesting', declMixinDeeperNestedIncludes);
  });

  group('Mixin arguments', () {
    test('simple arg', mixinArg);
    test('multiple args and var decls as args', mixinManyArgs);
  });

  group('Mixin warnings', () {
    test('undefined top-level', undefinedTopLevel);
    test('undefined declaration', undefinedDeclaration);
    test('detect bad top-level as declaration', badDeclarationInclude);
    test('detect bad declaration as top-level', badTopInclude);
  });
}
