/*
 * Copyright 2011 Google Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.google.common.css.compiler.passes;

import com.google.common.css.compiler.ast.GssParserException;
import com.google.common.css.compiler.passes.testing.PassesTestBase;

/**
 * Unit tests for the {@link ReplaceMixins} compiler pass.
 *
 * @author fbenz@google.com (Florian Benz)
 */
public class ReplaceMixinsTest extends PassesTestBase {

  public void testMixinReplacement1() {
    testTreeConstruction(linesToString(
        "@defmixin test(COLOR) {",
        "  color: COLOR;",
        "}",
        "p {",
        "  @mixin test(#FFF);",
        "}"),

        "[[p]{[color:[[#fff]];]}]");
  }

  public void testMixinReplacement2() {
    testTreeConstruction(linesToString(
        "@defmixin color(COLOR) {",
        "  color: COLOR;",
        "}",
        "@defmixin size(H, W) {",
        "  width: W;",
        "  height: H;",
        "}",
        "p {",
        "  @mixin size(50%, 60%);",
        "  position: absolute;",
        "  @mixin color(red);",
        "}"),

        "[[p]{[width:[[60%]];height:[[50%]];position:[[absolute]];color:[[red]];]}]");
  }

  public void testMixinReplacement3() {
    testTreeConstruction(linesToString(
        "@defmixin size(H, W) {",
        "  width: W;",
        "  height: H;",
        "}",
        "p {",
        "  @mixin size(50%, 60%);",
        "  position: absolute;",
        "}",
        "div {",
        "  @mixin size(100px, 200px);",
        "}"),

        "[[p]{[width:[[60%]];height:[[50%]];position:[[absolute]];]}"
        + "[div]{[width:[[200px]];height:[[100px]];]}]");
  }

  public void testMixinReplacement4() {
    testTreeConstruction(linesToString(
        "@defmixin size(W, H) {",
        "  width: W;",
        "  height: H;",
        "}",
        "@defmixin color(COLOR) {",
        "  color: COLOR;",
        "}",
        "@defmixin position(POS) {",
        "  position: POS;",
        "}",
        "p {",
        "  @mixin color(#fff);",
        "  @mixin size(50%, 60%);",
        "  @mixin position(absolute);",
        "}"),

        "[[p]{[color:[[#fff]];width:[[50%]];height:[[60%]];position:[[absolute]];]}]");
  }

  public void testMixinReplacement5() {
    testTreeConstruction(linesToString(
        "@defmixin empty(X, Y) {",
        "}",
        "p {",
        "  @mixin empty(10, 20);",
        "}"),

        "[[p]{[]}]");
  }

  public void testMixinReplacementTwice() {
    testTreeConstruction(linesToString(
        "@defmixin test(COLOR) {",
        "  color: COLOR;",
        "}",
        "p {",
        "  @mixin test(#FFF);",
        "  @mixin test(#FFF);",
        "}"),

        "[[p]{[color:[[#fff]];color:[[#fff]];]}]");
  }

  public void testMixinReplacementNested1() {
    testTreeConstruction(linesToString(
        "@defmixin width(W) {",
        "  width: W;",
        "}",
        "@defmixin size(W, H) {",
        "  @mixin width(W);",
        "  height: H;",
        "}",
        "p {",
        "  @mixin size(50%, 60%);",
        "  position: absolute;",
        "}"),

        "[[p]{[width:[[50%]];height:[[60%]];position:[[absolute]];]}]");
  }

  public void testMixinReplacementNested2() {
    testTreeConstruction(linesToString(
        "@defmixin width(W2) {",
        "  width: W2;",
        "}",
        "@defmixin size(W, H) {",
        "  @mixin width(W);",
        "  height: H;",
        "}",
        "p {",
        "  @mixin size(50%, 60%);",
        "  @mixin width(10px);",
        "  position: absolute;",
        "}"),

        "[[p]{[width:[[50%]];height:[[60%]];width:[[10px]];position:[[absolute]];]}]");
  }

  public void testMixinReplacementNested3() {
    testTreeConstruction(linesToString(
        "@defmixin width(W2) {",
        "  width: W2;",
        "}",
        "@defmixin size(W, H) {",
        "  @mixin width(W);",
        "  height: H;",
        "  @mixin width(H);",
        "}",
        "p {",
        "  @mixin size(50%, 60%);",
        "  @mixin width(10px);",
        "  position: absolute;",
        "}"),

        "[[p]{[width:[[50%]];height:[[60%]];width:[[60%]];width:[[10px]];"
        + "position:[[absolute]];]}]");
  }

  public void testMixinReplacementNested4() {
    testTreeConstruction(linesToString(
        "@defmixin empty() {",
        "}",
        "@defmixin callEmpty() {",
        "  @mixin empty();",
        "}",
        "p {",
        "  @mixin empty();",
        "  @mixin callEmpty();",
        "  @mixin callEmpty();",
        "  @mixin empty();",
        "}"),

        "[[p]{[]}]");
  }

  public void testMixinReplacementNested5() {
    testTreeConstruction(linesToString(
        "@defmixin empty() {",
        "}",
        "@defmixin callEmpty1() {",
        "  @mixin empty();",
        "}",
        "@defmixin callEmpty2() {",
        "  @mixin callEmpty1();",
        "}",
        "@defmixin callEmpty3() {",
        "  @mixin callEmpty2();",
        "}",
        "p {",
        "  @mixin empty();",
        "  @mixin callEmpty3();",
        "  @mixin empty();",
        "}"),

        "[[p]{[]}]");
  }

  public void testCompositeValues1() {
    testTreeConstruction(linesToString(
        "@defmixin font(RATIO) {",
        "  font: RATIO Arial;",
        "}",
        "p {",
        "  @mixin font(1em/1.3em);",
        "}"),
        // TODO(user): note Arial has been incorrectly parsed here
        // as a GSS variable use rather than a font-family list.
        // There's no comma so no way to tell the difference. It
        // doesn't make any difference though because the ASTs are
        // identical, but someday if we want to
        // expand shorthands we will need to disambiguate these
        // cases in the AST.
        "[[p]{[font:[[[1em]/[1.3em]][Arial]];]}]");
  }

  public void testCompositeValues2() {
    testTreeConstruction(linesToString(
        "@defmixin margin(MARGIN) {",
        "  margin: MARGIN;",
        "}",
        "p {",
        "  @mixin margin(1px 2px 3px 4px);",
        "}"),

        "[[p]{[margin:[[1px][2px][3px][4px]];]}]");
  }

  public void testMixinReplacementComponents() {
    testTreeConstruction(linesToString(
        "@defmixin col(PAR) {",
        "  color: PAR;",
        "  background-color: CHANGES;",
        "}",
        "@abstract_component ABY {",
        "  @def STAYS red;",
        "  @def CHANGES none;",
        "  .ABSTRACT { background: CHANGES; font: STAYS; }",
        "}",
        "@component BLUE_ABY extends ABY {",
        "  @def CHANGES  blue;",
        "  .INCOMP { @mixin col(CHANGES); }",
        "}",
        "@component YELLOW_ABY extends ABY {",
        "  @def CHANGES yellow;",
        "  .INCOMP { @mixin col(green);}",
        "}"),

        "[[.BLUE_ABY-ABSTRACT]{[background:[[blue]];font:[[red]];]}"
        + "[.BLUE_ABY-INCOMP]{[color:[[blue]];background-color:[[blue]];]}"
        + "[.YELLOW_ABY-ABSTRACT]{[background:[[yellow]];font:[[red]];]}"
        + "[.YELLOW_ABY-INCOMP]{[color:[[green]];background-color:[[yellow]];]}]");
  }

  public void testMixinReplacementGradient1() {
    testTreeConstruction(linesToString(
        "@def GRADIENT_POS top;",
        "@def BASE_COLOR #cc0000;",
        "@def FONT_SIZE_SMALL 80%;",
        "@defmixin gradient(HSL1, HSL2, HSL3, COLOR) {",
        "  background-color: COLOR;",
        "  background-image:",
        "      -webkit-linear-gradient(GRADIENT_POS, hsl(HSL1, HSL2, HSL3),",
        "          COLOR);",
        "}",
        ".SOME_CLASS {",
        "  @mixin gradient(0%, 50%, 70%, BASE_COLOR);",
        "  font-size: FONT_SIZE_SMALL;",
        "}"),

        "[[.SOME_CLASS]{[background-color:[[#cc0000]];"
        + "background-image:"
        + "[-webkit-linear-gradient(top,hsl(0%,50%,70%),#cc0000)];"
        + "font-size:[[80%]];]}]");
  }

  public void testMixinReplacementGradient2() {
    testTreeConstruction(linesToString(
        "@defmixin grad(A, B, C, D, E) {",
        "  background-image:-webkit-linear-gradient(A, B, C, D, E);",
        "}",
        ".SOME_CLASS {",
        "  @mixin grad(bottom left, red 20px, yellow, green, blue 90%);",
        "}"),

        "[[.SOME_CLASS]{["
        + "background-image:[-webkit-linear-gradient("
        + "bottom left,red 20px,yellow,green,blue 90%)];]}]");
  }

  public void testMixinReplacementGradient3() {
    testTreeConstruction(linesToString(
        "@defmixin grad(A, B, C, D, E) {",
        "  background-image:-webkit-linear-gradient(A, B, C, D, E);",
        "}",
        ".SOME_CLASS {",
        "  @mixin grad( bottom  left,  red  20px, yellow,  green , blue  90%);",
        "}"),

        "[[.SOME_CLASS]{["
        + "background-image:[-webkit-linear-gradient("
        + "bottom left,red 20px,yellow,green,blue 90%)];]}]");
  }

  public void testMixinReplacementOnCompositeValueNodeWithNestedValues() {
    testTreeConstruction(linesToString(
        "@defmixin foo(A, B, C) {",
        "  background: rgba(A, B, C, .2), rgba(A, B, C, .2);",
        "}",
        ".a { @mixin foo(255, 255, 255); }",
        ".b { @mixin foo(0, 0, 0); }"),
        "[[.a]{[background:[[rgba(255,255,255,.2),rgba(255,255,255,.2)]];]}"
        + "[.b]{[background:[[rgba(0,0,0,.2),rgba(0,0,0,.2)]];]}]");
  }

  public void testMixinReplacementGradientYouTube() {
    testTreeConstruction(linesToString(
        "@def _VR_MAIN rgba(0, 0, 0, .12), rgba(0, 0, 0, .08) 1px,"
        + " rgba(0, 0, 0, .08) 1px, rgba(0, 0, 0, 0) 30px, transparent 100%;",
        "@defmixin rule_main(ORIENTATION, GRADIENT_PARAMS) {",
        "/* @alternate */ background: -webkit-linear-gradient(ORIENTATION,",
        "GRADIENT_PARAMS);",
        "}",
        "",
        ".test {",
        "@mixin rule_main(left, _VR_MAIN);",
        "}"),

        "[[.test]{[[/* @alternate */]background:[-webkit-linear-gradient(left,"
        + "[rgba(0,0,0,.12),rgba(0,0,0,.08)] [[1px],rgba(0,0,0,.08)]"
        + " [[1px],rgba(0,0,0,0)] [[30px],[transparent]] 100%)];]}]");
  }

  public void testNoMatchingMixinDefinition() throws GssParserException {
    parseAndRun("@defmixin test() {} h1 { @mixin unkown(); }",
        ReplaceMixins.NO_MATCHING_MIXIN_DEFINITION_ERROR_MESSAGE);
  }

  public void testDifferentArgumentCount1() throws GssParserException {
    parseAndRun("@defmixin test(PAR1) {} h1 { @mixin test(10px, 20px); }",
        ReplaceMixins.ARGUMENT_MISMATCH_ERROR_MESSAGE);
  }

  public void testDifferentArgumentCount2() throws GssParserException {
    parseAndRun("@defmixin test(PAR1, PAR2) {} h1 { @mixin test(10px); }",
        ReplaceMixins.ARGUMENT_MISMATCH_ERROR_MESSAGE);
  }

  public void testDifferentArgumentCount3() throws GssParserException {
    parseAndRun("@defmixin test() {} h1 { @mixin test(10px, 20px); }",
        ReplaceMixins.ARGUMENT_MISMATCH_ERROR_MESSAGE);
  }

  public void testDifferentArgumentCount4() throws GssParserException {
    parseAndRun("@defmixin test(PAR1) {} h1 { @mixin test(); }",
        ReplaceMixins.ARGUMENT_MISMATCH_ERROR_MESSAGE);
  }

  public void testDifferentArgumentCount5() throws GssParserException {
    parseAndRun(linesToString(
        "@defmixin someMixin(A, C, D, E) {",
        "  background-image:some-function(A, C, D, E);",
        "}",
        ".SOME_CLASS {",
        "  @mixin someMixin(bottom left, red 20px, yellow, green, blue 90%);",
        "}"),
        "GSS constant not defined: A",
        "GSS constant not defined: C",
        "GSS constant not defined: D",
        "GSS constant not defined: E",
        ReplaceMixins.ARGUMENT_MISMATCH_ERROR_MESSAGE);
  }

  public void testNestedBad() throws GssParserException {
    parseAndRun(linesToString(
        "@defmixin color(COL) {",
        "  width: COLOR;",
        "}",
        "@defmixin box(W, H, COLOR) {",
        "  width: W;",
        "  height: H;",
        "  @mixin color(COLOR);",
        "}",
        "p {",
        "  @mixin box(50%, 60%, red);",
        "}"),

        "GSS constant not defined: COLOR");
  }

  public void testCycle() throws GssParserException {
    parseAndRun(linesToString(
        "@defmixin a(A) {",
        "  @mixin c(A);",
        "  height: A;",
        "}",
        "@defmixin b(B) {",
        "  @mixin a(B);",
        "  width: B;",
        "}",
        "@defmixin c(C) {",
        "  @mixin b(C);",
        "}",
        "div {",
        "  @mixin a(10px);",
        "}"),

        ReplaceMixins.CYCLE_ERROR_MESSAGE);
  }

  public void testIdSeqInFontShorthandUsingDefMixin() throws Exception {
    testTreeConstruction(linesToString(
        "@defmixin myfont(FONTSTYLE, FONTFACEA, FONTFACEB) {",
        "  font: FONTSTYLE FONTFACEA, FONTFACEB;",
        "}",
        ".foo { @mixin myfont(bold, arial, serif); }"),

        "[[.foo]{[font:[[bold][[arial],[serif]]];]}]");
  }

  @Override
  protected void runPass() {
    // This passes have to run before.
    new CreateMixins(tree.getMutatingVisitController(), errorManager).runPass();
    new CreateConstantReferences(tree.getMutatingVisitController()).runPass();

    // This passes should run before to produce the expected behavior.
    // They are needed for testMixinReplacementComponents.
    new CreateDefinitionNodes(tree.getMutatingVisitController(),
        errorManager).runPass();
    new CreateComponentNodes(tree.getMutatingVisitController(),
        errorManager).runPass();

    // The passes tested here.
    CollectMixinDefinitions collectDefinitions = new CollectMixinDefinitions(
        tree.getMutatingVisitController(), errorManager);
    collectDefinitions.runPass();
    new ReplaceMixins(tree.getMutatingVisitController(), errorManager,
        collectDefinitions.getDefinitions()).runPass();

    // This passes should run afterwards to produce the expected behavior.
    // They are needed for testMixinReplacementComponents.
    new ProcessComponents<Object>(tree.getMutatingVisitController(),
        errorManager).runPass();
    CollectConstantDefinitions collectConstantDefinitionsPass =
        new CollectConstantDefinitions(tree);
    collectConstantDefinitionsPass.runPass();
    ReplaceConstantReferences replaceConstantReferences =
        new ReplaceConstantReferences(tree,
            collectConstantDefinitionsPass.getConstantDefinitions(),
            true /* removeDefs */, errorManager,
            false /* allowUndefinedConstants */);
    replaceConstantReferences.runPass();
  }
}
