/*
 * Copyright 2016 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.jbcsrc;

import com.google.common.base.Joiner;
import com.google.common.collect.ImmutableSet;
import com.google.template.soy.SoyFileSetParserBuilder;
import com.google.template.soy.base.SourceLocation;
import com.google.template.soy.exprtree.DataAccessNode;
import com.google.template.soy.exprtree.ExprNode;
import com.google.template.soy.exprtree.FunctionNode;
import com.google.template.soy.exprtree.VarRefNode;
import com.google.template.soy.shared.restricted.SoyFunction;
import com.google.template.soy.soytree.SoytreeUtils;
import com.google.template.soy.soytree.TemplateNode;

import junit.framework.TestCase;

import java.util.Set;

/** Tests for {@link TemplateAnalysis}. */
public final class TemplateAnalysisTest extends TestCase {

  // All the tests are written as soy templates with references to two soy functions 'refed' and
  // 'notrefed'.
  //  if a variable is passed to 'refed' then it means that at that point in the program we expect
  //  that it has definitely already been referenced.
  //  'notrefed' just means the opposite.  at that point in the program there is no guarantee that
  //  it has already been referenced.

  public void testSimpleSequentalAccess() {
    runTest("{@param p : string}", "{notrefed($p)}{refed($p)}");

    runTest(
        "{@param b : bool}",
        "{@param p1 : string}",
        "{@param p2 : string}",
        "{@param p3 : string}",
        "{$b ? $p1 + $p3 : $p2 + $p3}",
        "{refed($b)}",
        "{notrefed($p1)}",
        "{notrefed($p2)}",
        "{refed($p3)}");
  }

  public void testDataAccess() {
    runTest("{@param p : list<string>}", 
        "{notrefed($p[0])}",
        "{refed($p[0])}");

    runTest("{@param p : [field:string]}", 
        "{notrefed($p.field)}",
        "{refed($p.field)}");
  }

  public void testIf() {
    // conditions are refed prior to the blocks they control
    // if there is an {else} then anything refed in all branches is refed after the if
    runTest(
        "{@param p1 : string}",
        "{@param p2 : string}",
        "{@param p3 : string}",
        "{if $p1}",
        "  {refed($p1)}",
        "  {notrefed($p2)}",
        "  {$p3}",
        "{elseif $p2}",
        "  {refed($p1)}",
        "  {refed($p2)}",
        "  {$p3}",
        "{else}",
        "  {refed($p1)}",
        "  {refed($p2)}",
        "  {$p3}",
        "{/if}",
        "{refed($p3)}");

    runTest(
        "{@param p : string}",
        "{@param b1 : bool}",
        "{@param b2 : bool}",
        "{if $b1}",
        "  {$p}",
        "  {refed($b1)}",
        "  {notrefed($b2)}",
        "{elseif $b2}",
        "  {$p}",
        "  {refed($b1)}",
        "  {refed($b2)}",
        "{/if}",
        "{notrefed($p)}");
  }

  public void testSwitch() {
    // empty switch
    runTest("{@param p : int}", "{switch $p}", "{/switch}", "{notrefed($p)}");

    // only default, switch expression still not evaluated
    runTest(
        "{@param p : int}",
        "{@param p2 : int}",
        "{switch $p}",
        "{default}",
        "  {$p2}",
        "{/switch}",
        "{notrefed($p)}",
        "{refed($p2)}");

    // cases
    runTest(
        "{@param p : int}",
        "{@param p2 : int}",
        "{switch $p}",
        "{case $p2}",
        "  {refed($p)}",
        "  {refed($p2)}",
        "{default}",
        "  {$p2}",
        "{/switch}",
        "{refed($p)}",
        "{refed($p2)}");
    // cases
    runTest(
        "{@param p : int}",
        "{@param p2 : int}",
        "{@param p3 : int}",
        "{switch $p}",
        "{case $p2}",
        "  {refed($p)}",
        "  {refed($p2)}",
        "{case $p3}",
        "  {refed($p)}",
        "  {refed($p2)}",
        "  {refed($p3)}",
        "{/switch}",
        "{refed($p)}",
        "{refed($p2)}",
        "{notrefed($p3)}"); // p3 is not refed because it only happens if $p != $p2
  }

  public void testFor() {
    runTest(
        "{@param limit : int}",
        "{@param p : string}",
        "{for $i in range(0, $limit)}",
        "  {$p}",
        "  {$i}",
        "  {refed($limit)}",
        "{/for}",
        "{refed($limit)}",
        "{notrefed($p)}");
    // In this case we can prove that the loop will execute and thus p will have been referenced
    // after the loop.
    runTest(
        "{@param p : string}",
        "{for $i in range(0, 1)}",
        "  {$p}",
        "  {$i}",
        "{/for}",
        "{refed($p)}");
  }

  public void testForeach() {
    // test special functions for foreach loops. though these all look like references to the loop
    // var, they actually aren't.
    runTest(
        "{@param list : list<?>}",
        "{foreach $item in $list}",
        "  {if isFirst($item)}first{/if}",
        "  {if isLast($item)}last{/if}",
        "  {index($item)}",
        "  {notrefed($item)}",
        "  {refed($list)}",
        "{/foreach}",
        "{refed($list)}");

    // test ifempty blocks
    runTest(
        "{@param list : list<?>}",
        "{@param p: ?}",
        "{@param p2: ?}",
        "{@param p3: ?}",
        "{foreach $item in $list}",
        "  {$p}",
        "  {$p2}",
        "{ifempty}",
        "  {$p}",
        "  {$p3}",
        "{/foreach}",
        "{refed($list)}",
        "{refed($p)}",
        "{notrefed($p2)}",
        "{notrefed($p3)}");
  }

  public void testForeach_literalList() {
    // test literal lists
    // empty list
    runTest(
        "{call .loop data=\"all\"}",
        "  {param list: [] /}",
        "{/call}",
        "{/template}",
        "",
        "{template .loop}",
        "{@param list: list<?>}",
        "{@param p: ?}",
        "{@param p2: ?}",
        "{foreach $item in $list}",
        "  {$p}",
        "{ifempty}",
        "  {$p2}",
        "{/foreach}",
        "{notrefed($p)}",
        "{refed($p2)}");

    // nonempty list
    runTest(
        "{@param p: ?}",
        "{@param p2: ?}",
        "{foreach $item in [1, 2, 3]}",
        "  {$p}",
        "{ifempty}",
        "  {$p2}",
        "{/foreach}",
        "{refed($p)}",
        "{notrefed($p2)}");
  }

  public void testLetVariable() {
    runTest("{@param p: ?}", "{let $l : $p/}", "{notrefed($p)}");
    // referencing $l implies we have refed $p
    runTest("{@param p: ?}", "{let $l : $p/}", "{$l}", "{refed($p)}");
    runTest(
        "{@param p: ?}",
        "{let $l kind=\"text\"}{$p}{/let}",
        "{let $l2 : '' + $l /}",
        "{notrefed($l2)}",
        "{refed($l2)}",
        "{refed($l)}",
        "{refed($p)}");
  }

  public void testRefsInLets() {
    runTest(
        "{@param p: ?}",
        "{let $l kind=\"text\"}{$p}{/let}",
        "{let $l2 : notrefed($l) ? refed($l) : '' /}",
        "{notrefed($p)}",
        "{notrefed($l2)}",
        "{refed($l2)}",
        "{refed($l)}");
  }
  
  public void testMsg() {
    runTest(
        "{@param p : ?}",
        "{msg desc=\"\"}",
        "  Hello {$p}",
        "{/msg}",
        "{refed($p)}");

    runTest(
        "{@param p : ?}",
        "{msg desc=\"\"}",
        "  Hello {$p}",
        "{fallbackmsg desc=\"\"}",
        "  Hello foo",
        "{/msg}",
        "{notrefed($p)}");
    
    runTest(
        "{@param p : ?}",
        "{msg desc=\"\"}",
        "  Hello {$p}",
        "{fallbackmsg desc=\"\"}",
        "  Hello old {$p}",
        "{/msg}",
        "{refed($p)}");
  }
  
  public void testCall() {
    // The tricky thing about calls is how params are handled
    runTest(
        "{@param p : ?}",
        "{call .foo data=\"$p\"/}",
        "{refed($p)}");
    runTest(
        "{@param p : ?}",
        "{call .foo data=\"all\"/}",
        "{notrefed($p)}");
    runTest(
        "{@param p : ?}",
        "{call .foo}",
        "  {param p1 : notrefed($p) /}",
        "  {param p2 : notrefed($p) /}",
        "{/call}",
        "{notrefed($p)}");
    
    runTest(
        "{@param p : ?}",
        "{$p}",
        "{call .foo}",
        "  {param p1 : refed($p) /}",
        "  {param p2 : refed($p) /}",
        "{/call}",
        "{refed($p)}");
  }

  void runTest(String... lines) {
    TemplateNode template = parseTemplate(lines);
    TemplateAnalysis analysis = TemplateAnalysis.analyze(template);
    for (FunctionNode node : SoytreeUtils.getAllNodesOfType(template, FunctionNode.class)) {
      if (node.getSoyFunction() == NOT_REFED_FUNCTION) {
        checkNotReferenced(analysis, node.getChild(0));
      } else if (node.getSoyFunction() == REFED_FUNCTION) {
        checkReferenced(analysis, node.getChild(0));
      }
    }
  }

  private void checkNotReferenced(TemplateAnalysis analysis, ExprNode child) {
    if (hasDefinitelyAlreadyBeenAccessed(analysis, child)) {
      fail("Expected reference to " + format(child) + " to have not been definitely referenced.");
    }
  }

  private void checkReferenced(TemplateAnalysis analysis, ExprNode child) {
    if (!hasDefinitelyAlreadyBeenAccessed(analysis, child)) {
      fail("Expected reference to " + format(child) + " to have been definitely referenced.");
    }
  }

  private boolean hasDefinitelyAlreadyBeenAccessed(TemplateAnalysis analysis, ExprNode child) {
    if (child instanceof VarRefNode) {
      return analysis.isResolved((VarRefNode) child);
    }
    if (child instanceof DataAccessNode) {
      return analysis.isResolved((DataAccessNode) child);
    }
    return false;
  }

  private String format(ExprNode child) {
    SourceLocation sourceLocation = child.getSourceLocation();
    // subtract 2 from the line number since the boilerplate adds 2 lines above the user content
    return child.toSourceString()
        + " at "
        + (sourceLocation.getLineNumber() - 2)
        + ":"
        + sourceLocation.getBeginColumn();
  }

  private static TemplateNode parseTemplate(String... lines) {
    return SoyFileSetParserBuilder.forFileContents(
        Joiner.on("\n").join(
            "{namespace test}",
            "{template .caller}",
            Joiner.on("\n").join(lines),
            "{/template}",
            "",
            // add an additional template as a callee.
            "{template .foo}",
            "  {@param? p1 : ?}",
            "  {@param? p2 : ?}",
            "  {$p1 + $p2}",
            "{/template}",
            ""))
        .addSoyFunction(REFED_FUNCTION)
        .addSoyFunction(NOT_REFED_FUNCTION)
        .parse()
        .fileSet()
        .getChild(0)
        .getChild(0);
  }

  private static final SoyFunction NOT_REFED_FUNCTION =
      new SoyFunction() {
        @Override
        public String getName() {
          return "notrefed";
        }

        @Override
        public Set<Integer> getValidArgsSizes() {
          return ImmutableSet.of(1);
        }
      };

  private static final SoyFunction REFED_FUNCTION =
      new SoyFunction() {
        @Override
        public String getName() {
          return "refed";
        }

        @Override
        public Set<Integer> getValidArgsSizes() {
          return ImmutableSet.of(1);
        }
      };
}
