/*
 * Copyright 2008 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.template.soy.passes;

import static com.google.common.truth.Truth.assertThat;

import com.google.common.collect.ImmutableList;
import com.google.template.soy.SoyFileSetParserBuilder;
import com.google.template.soy.basetree.SyntaxVersion;
import com.google.template.soy.error.FormattingErrorReporter;
import com.google.template.soy.passes.CheckTemplateParamsVisitor;

import junit.framework.TestCase;

/**
 * Unit tests for {@link CheckTemplateParamsVisitor}.
 *
 */
public final class CheckTemplateParamsVisitorTest extends TestCase {

  public void testMatchingSimple() {
    // ------ No params ------
    String soyDoc = "";
    String templateBody = "Hello world!";
    assertThat(soyDocErrorsForTemplate(soyDoc, templateBody)).isEmpty();

    // ------ Only 'print' statements ------
    soyDoc = "@param boo @param foo @param? goo @param moo";
    templateBody = "{$boo}{$foo.goo |noAutoescape}{2 * $goo[round($moo)]}";
    assertThat(soyDocErrorsForTemplate(soyDoc, templateBody)).isEmpty();

    // ------ Simple 'if' statement with nested 'print' statement ------
    soyDoc =
        "@param boo Something scary.\n" +
        "@param? goo Something slimy.\n";
    templateBody =
        "{if $boo.foo}\n" +
        "  Slimy {$goo}.\n" +
        "{/if}\n";
    assertThat(soyDocErrorsForTemplate(soyDoc, templateBody)).isEmpty();
  }

  public void testMatchingWithAdvancedStmts() {
    // ------ 'if', 'elseif', 'else', '/if' ------
    String soyDoc = "@param boo @param foo";
    String templateBody =
        "{if $boo}\n" +
        "  Scary.\n" +
        "{elseif $foo.goo}\n" +
        "  Slimy.\n" +
        "{else}\n" +
        "  {$foo.moo}\n" +
        "{/if}\n";
    assertThat(soyDocErrorsForTemplate(soyDoc, templateBody)).isEmpty();

    // ------ 'switch', 'case', 'default', '/switch' ------
    soyDoc = "@param boo @param foo @param moo @param too @param zoo";
    templateBody =
        "{switch $boo}\n" +
        "{case $foo.goo}\n" +
        "  Slimy.\n" +
        "{case 0, $moo, $too}\n" +
        "  Soggy.\n" +
        "{default}\n" +
        "  Not {$zoo}.\n" +
        "{/switch}\n";
    assertThat(soyDocErrorsForTemplate(soyDoc, templateBody)).isEmpty();

    // ------ 'foreach', 'ifempty', '/foreach' ------
    soyDoc = "@param moose @param? meese";
    templateBody =
        "{foreach $moo in $moose}\n" +
        "  Cow says {$moo}.\n" +
        "{ifempty}\n" +
        "  No {$meese}.\n" +
        "{/foreach}\n";
    assertThat(soyDocErrorsForTemplate(soyDoc, templateBody)).isEmpty();

    // ------ 'for', '/for' ------
    soyDoc = "@param boo";
    templateBody =
        "{for $i in range(length($boo))}\n" +
        "  {$i + 1}: {$boo[$i]}.\n" +
        "{/for}\n";
    assertThat(soyDocErrorsForTemplate(soyDoc, templateBody)).isEmpty();
  }

  public void testCalls() {
    String fileContent1 =
        "{namespace boo autoescape=\"deprecated-noncontextual\"}\n" +
        "\n" +
        "/**\n" +
        " * @param? goo @param too @param woo @param? zoo\n" +
        " * @param gee\n" +  // no 'mee' is okay because user may not want to list it (chrisn)
        " * @param maa\n" +  // no 'gaa' is okay because it may be optional in 'baa.faa'
        " * @param transParam \n" +  // okay (not required) because it's used in transitive callee
        " */\n" +
        "{template .foo}\n" +
        "  {call .fee data=\"$goo.moo\" /}\n" +
        "  {call .fee data=\"$too\"}\n" +
        "    {param gee : $woo.hoo /}\n" +
        "    {param mee}\n" +
        "      {$zoo}\n" +
        "    {/param}\n" +
        "  {/call}\n" +
        "  {call .fee data=\"all\" /}\n" +
        "  {call baa.faa data=\"all\" /}\n" +
        "  {call .transitive1 data=\"all\" /}\n" +
        "{/template}\n" +
        "\n" +
        "/** @param gee @param mee */\n" +
        "{template .fee}\n" +
        "  {$gee}{$mee}\n" +
        "{/template}\n" +
        "\n" +
        "/** */\n" +
        "{template .transitive1}\n" +
        "  {call .transitive2 data=\"all\" /}\n" +
        "{/template}\n" +
        "\n" +
        "/** @param transParam */\n" +
        "{template .transitive2}\n" +
        "  {$transParam}\n" +
        "{/template}\n";

    String fileContent2 =
        "{namespace baa autoescape=\"deprecated-noncontextual\"}\n" +
        "\n" +
        "/** @param gaa @param maa */\n" +
        "{template .faa}\n" +
        "  {$gaa}{$maa}\n" +
        "{/template}\n";

    assertThat(soyDocErrorsFor(fileContent1, fileContent2)).isEmpty();
  }

  public void testCallWithMissingParam() {
    String fileContent =
        "{namespace boo autoescape=\"deprecated-noncontextual\"}\n" +
        "\n" +
        "/** @param a */\n" +
        "{template .caller}\n" +
        "  {call .callee}\n" +
        "    {param x: $a /}\n" +
        "  {/call}\n" +
        "{/template}\n" +
        "\n" +
        "/** @param x @param y */\n" +
        "{template .callee}\n" +
        "  {$x}{$y}\n" +
        "{/template}\n";

    // This is actually not reported as an error by this visitor CheckTemplateParams doesn't check
    // calls
    assertThat(soyDocErrorsFor(fileContent)).containsExactly("Call missing required param 'y'.");
  }

  public void testUndeclaredParam() {
    String soyDoc = "@param foo";
    String templateBody = "{$boo.foo}";
    ImmutableList<String> errors = soyDocErrorsForTemplate(soyDoc, templateBody);
    assertThat(errors).hasSize(2);
    assertThat(errors.get(0)).contains("Unknown data key 'boo'.");
    assertThat(errors.get(1)).isEqualTo("Param 'foo' unused in template body.");
  }

  public void testUnusedParam() {
    String soyDoc = "@param boo @param? foo";
    String templateBody = "{$boo.foo}";
    ImmutableList<String> errors = soyDocErrorsForTemplate(soyDoc, templateBody);
    assertThat(errors).containsExactly("Param 'foo' unused in template body.");
  }

  public void testUnusedParamInCallWithAllData() {
    String fileContent =
        "{namespace boo autoescape=\"deprecated-noncontextual\"}\n" +
        "\n" +
        "/**\n" +
        " * @param moo\n" +
        " * @param zoo\n" +  // 'zoo' is not used, even in call to .goo
        " */\n" +
        "{template .foo}\n" +
        "  {call .goo data=\"all\" /}\n" +
        "{/template}\n" +
        "\n" +
        "/** @param moo */\n" +
        "{template .goo}\n" +
        "  {$moo}\n" +
        "{/template}\n";

    ImmutableList<String> errors = soyDocErrorsFor(fileContent);
    assertThat(errors).containsExactly("Param 'zoo' unused in template body.");
  }

  public void testWithExternalCallWithAllData() {
    String fileContent =
        "{namespace boo autoescape=\"deprecated-noncontextual\"}\n" +
        "\n" +
        "/**\n" +
        " * @param zoo\n" +  // 'zoo' is okay because it may be used in 'goo.moo'
        " */\n" +
        "{template .foo}\n" +
        "  {call goo.moo data=\"all\" /}\n" +
        "{/template}\n";

    assertThat(soyDocErrorsFor(fileContent)).isEmpty();
  }

  public void testUnusedParamWithRecursiveCall() {
    String soyDoc = "@param boo @param foo";
    String templateBody = "{call .foo data=\"all\" /}";
    ImmutableList<String> errors = soyDocErrorsForTemplate(soyDoc, templateBody);
    assertThat(errors)
        .containsExactly(
            "Param 'boo' unused in template body.", "Param 'foo' unused in template body.")
        .inOrder();
  }

  public void testUnusedParamInDelegateTemplate() {
    String fileContent =
        "{namespace boo autoescape=\"deprecated-noncontextual\"}\n" +
        "\n" +
        "/**\n" +
        " * @param zoo\n" +  // 'zoo' may be needed in other implementations of the same delegate
        " */\n" +
        "{deltemplate MagicButton}\n" +
        "  blah\n" +
        "{/deltemplate}\n";

    assertThat(soyDocErrorsFor(fileContent)).isEmpty();
  }

  public void testDelegateCallVariant() {
    String fileContent = "" +
        "{namespace boo autoescape=\"deprecated-noncontextual\"}\n" +
        "\n" +
        "/**\n" +
        " * @param variant\n" +
        " */\n" +
        "{template .foo}\n" +
        "  {delcall MagicButton variant=\"$variant\" /}\n" +
        "{/template}\n";

    assertThat(soyDocErrorsFor(fileContent)).isEmpty();
  }

  public void testOnlyCheckFilesInV2() {
    String fileContent0 =
        "{namespace boo0 autoescape=\"deprecated-noncontextual\"}\n" +
        "\n" +  // template is tagged as v1
        "{template .foo0 deprecatedV1=\"true\"}\n" +
        "  {$goo0.moo0}\n" +
        "{/template}\n";

    String fileContent1 =
        "{namespace boo1 autoescape=\"deprecated-noncontextual\"}\n" +
        "\n" +
        "/** Template 1 */\n" +
        "{template .foo1}\n" +
        "  {$goo1}\n" +
        "  {$goo1.moo1()}\n" +  // file is not all V2 syntax due to this expression
        "{/template}\n";

    String fileContent2 =
        "{namespace boo2 autoescape=\"deprecated-noncontextual\"}\n" +
        "\n" +
        "/** Template 2 */\n" +
        "{template .foo2}\n" +
        "  {$goo2.moo2}\n" +
        "{/template}\n";

    ImmutableList<String> errors = soyDocErrorsFor(fileContent0, fileContent1, fileContent2);
    assertThat(errors).hasSize(1);
    assertThat(errors.get(0)).contains("Unknown data key 'goo2'.");
  }

  public void testWithHeaderParams() {
    String fileContent =
        "{namespace boo autoescape=\"deprecated-noncontextual\"}\n" +
        "\n" +
        "/** */\n" +
        "{template .foo}\n" +
        "  {@param goo: string}\n" +
        "  {@inject zoo: string}\n" +
        "  {$goo}\n" +
        "  {$zoo}\n" +
        "{/template}\n";

    assertThat(soyDocErrorsFor(fileContent)).isEmpty();

    fileContent =
        "{namespace boo autoescape=\"deprecated-noncontextual\"}\n" +
        "\n" +
        "/** */\n" +
        "{template .foo}\n" +
        "  {@param goo: string}\n" +
        "  {@inject zoo: string}\n" +
        "{/template}\n";

    ImmutableList<String> errors = soyDocErrorsFor(fileContent);
    assertThat(errors)
        .containsExactly(
            "Param 'goo' unused in template body.", "Param 'zoo' unused in template body.")
        .inOrder();
  }

  private static ImmutableList<String> soyDocErrorsForTemplate(String soyDoc, String templateBody) {
    String testFileContent =
        "{namespace boo autoescape=\"deprecated-noncontextual\"}\n" +
        "\n" +
        "/** " + soyDoc + " */\n" +
        "{template .foo}\n" +
        templateBody + "\n" +
        "{/template}\n";

    return soyDocErrorsFor(testFileContent);
  }

  private static ImmutableList<String> soyDocErrorsFor(String... soyFileContents) {
    FormattingErrorReporter errorReporter = new FormattingErrorReporter();
    SoyFileSetParserBuilder.forFileContents(soyFileContents)
        .declaredSyntaxVersion(SyntaxVersion.V1_0)
        .errorReporter(errorReporter)
        .parse();
    return errorReporter.getErrorMessages();
  }
}
