/*
 * Copyright 2007 The Closure Compiler Authors.
 *
 * 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.javascript.jscomp;

import com.google.common.collect.Sets;
import com.google.javascript.jscomp.type.FlowScope;
import com.google.javascript.jscomp.type.ReverseAbstractInterpreter;
import com.google.javascript.jscomp.type.SemanticReverseAbstractInterpreter;
import com.google.javascript.rhino.Node;
import com.google.javascript.rhino.Token;
import com.google.javascript.rhino.jstype.JSType;

import java.util.Arrays;
import java.util.Collection;

public class SemanticReverseAbstractInterpreterTest
    extends CompilerTypeTestCase {
  private CodingConvention codingConvention = new GoogleCodingConvention();
  private ReverseAbstractInterpreter interpreter;
  private Scope functionScope;

  @Override
  protected void setUp() throws Exception {
    super.setUp();

    interpreter = new SemanticReverseAbstractInterpreter(
        codingConvention, registry);
  }

  public FlowScope newScope() {
    Scope globalScope = Scope.createGlobalScope(new Node(Token.EMPTY));
    functionScope = new Scope(globalScope, new Node(Token.EMPTY));
    return LinkedFlowScope.createEntryLattice(functionScope);
  }

  /**
   * Tests reverse interpretation of a NAME expression.
   */
  public void testNameCondition() throws Exception {
    FlowScope blind = newScope();
    Node condition = createVar(blind, "a", createNullableType(STRING_TYPE));

    // true outcome.
    FlowScope informedTrue = interpreter.
        getPreciserScopeKnowingConditionOutcome(condition, blind, true);
    assertTypeEquals(STRING_TYPE, getVarType(informedTrue, "a"));

    // false outcome.
    FlowScope informedFalse = interpreter.
        getPreciserScopeKnowingConditionOutcome(condition, blind, false);
    assertTypeEquals(createNullableType(STRING_TYPE),
        getVarType(informedFalse, "a"));
  }

  /**
   * Tests reverse interpretation of a NOT(NAME) expression.
   */
  public void testNegatedNameCondition() throws Exception {
    FlowScope blind = newScope();
    Node a = createVar(blind, "a", createNullableType(STRING_TYPE));
    Node condition = new Node(Token.NOT);
    condition.addChildToBack(a);

    // true outcome.
    FlowScope informedTrue = interpreter.
        getPreciserScopeKnowingConditionOutcome(condition, blind, true);
    assertTypeEquals(createNullableType(STRING_TYPE),
        getVarType(informedTrue, "a"));

    // false outcome.
    FlowScope informedFalse = interpreter.
        getPreciserScopeKnowingConditionOutcome(condition, blind, false);
    assertTypeEquals(STRING_TYPE, getVarType(informedFalse, "a"));
  }

  /**
   * Tests reverse interpretation of a ASSIGN expression.
   */
  @SuppressWarnings("unchecked")
  public void testAssignCondition1() throws Exception {
    FlowScope blind = newScope();
    testBinop(blind,
        Token.ASSIGN,
        createVar(blind, "a", createNullableType(OBJECT_TYPE)),
        createVar(blind, "b", createNullableType(OBJECT_TYPE)),
        Sets.newHashSet(
            new TypedName("a", OBJECT_TYPE),
            new TypedName("b", OBJECT_TYPE)),
        Sets.newHashSet(
            new TypedName("a", NULL_TYPE),
            new TypedName("b", NULL_TYPE)));
  }

  /**
   * Tests reverse interpretation of a SHEQ(NAME, NUMBER) expression.
   */
  @SuppressWarnings("unchecked")
  public void testSheqCondition1() throws Exception {
    FlowScope blind = newScope();
    testBinop(blind,
        Token.SHEQ,
        createVar(blind, "a", createUnionType(STRING_TYPE, NUMBER_TYPE)),
        createNumber(56),
        Sets.newHashSet(new TypedName("a", NUMBER_TYPE)),
        Sets.newHashSet(new TypedName("a",
            createUnionType(STRING_TYPE, NUMBER_TYPE))));
  }

  /**
   * Tests reverse interpretation of a SHEQ(NUMBER, NAME) expression.
   */
  @SuppressWarnings("unchecked")
  public void testSheqCondition2() throws Exception {
    FlowScope blind = newScope();
    testBinop(blind,
        Token.SHEQ,
        createNumber(56),
        createVar(blind, "a", createUnionType(STRING_TYPE, NUMBER_TYPE)),
        Sets.newHashSet(new TypedName("a", NUMBER_TYPE)),
        Sets.newHashSet(new TypedName("a",
            createUnionType(STRING_TYPE, NUMBER_TYPE))));
  }

  /**
   * Tests reverse interpretation of a SHEQ(NAME, NAME) expression.
   */
  @SuppressWarnings("unchecked")
  public void testSheqCondition3() throws Exception {
    FlowScope blind = newScope();
    testBinop(blind,
        Token.SHEQ,
        createVar(blind, "b", createUnionType(STRING_TYPE, BOOLEAN_TYPE)),
        createVar(blind, "a", createUnionType(STRING_TYPE, NUMBER_TYPE)),
        Sets.newHashSet(new TypedName("a", STRING_TYPE),
            new TypedName("b", STRING_TYPE)),
        Sets.newHashSet(new TypedName("a",
            createUnionType(STRING_TYPE, NUMBER_TYPE)),
            new TypedName("b",
                createUnionType(STRING_TYPE, BOOLEAN_TYPE))));
  }

  @SuppressWarnings("unchecked")
  public void testSheqCondition4() throws Exception {
    FlowScope blind = newScope();
    testBinop(blind,
        Token.SHEQ,
        createVar(blind, "a", createUnionType(STRING_TYPE, VOID_TYPE)),
        createVar(blind, "b", createUnionType(VOID_TYPE)),
        Sets.newHashSet(new TypedName("a", VOID_TYPE),
            new TypedName("b", VOID_TYPE)),
        Sets.newHashSet(new TypedName("a", STRING_TYPE),
            new TypedName("b", VOID_TYPE)));
  }

  @SuppressWarnings("unchecked")
  public void testSheqCondition5() throws Exception {
    FlowScope blind = newScope();
    testBinop(blind,
        Token.SHEQ,
        createVar(blind, "a", createUnionType(NULL_TYPE, VOID_TYPE)),
        createVar(blind, "b", createUnionType(VOID_TYPE)),
        Sets.newHashSet(new TypedName("a", VOID_TYPE),
            new TypedName("b", VOID_TYPE)),
        Sets.newHashSet(new TypedName("a", NULL_TYPE),
            new TypedName("b", VOID_TYPE)));
  }

  @SuppressWarnings("unchecked")
  public void testSheqCondition6() throws Exception {
    FlowScope blind = newScope();
    testBinop(blind,
        Token.SHEQ,
        createVar(blind, "a", createUnionType(STRING_TYPE, VOID_TYPE)),
        createVar(blind, "b", createUnionType(NUMBER_TYPE, VOID_TYPE)),
        Sets.newHashSet(
            new TypedName("a", VOID_TYPE),
            new TypedName("b", VOID_TYPE)),
        Sets.newHashSet(
            new TypedName("a",
                createUnionType(STRING_TYPE, VOID_TYPE)),
            new TypedName("b",
                createUnionType(NUMBER_TYPE, VOID_TYPE))));
  }

  /**
   * Tests reverse interpretation of a SHNE(NAME, NUMBER) expression.
   */
  @SuppressWarnings("unchecked")
  public void testShneCondition1() throws Exception {
    FlowScope blind = newScope();
    testBinop(blind,
        Token.SHNE,
        createVar(blind, "a", createUnionType(STRING_TYPE, NUMBER_TYPE)),
        createNumber(56),
        Sets.newHashSet(new TypedName("a",
            createUnionType(STRING_TYPE, NUMBER_TYPE))),
        Sets.newHashSet(new TypedName("a", NUMBER_TYPE)));
  }

  /**
   * Tests reverse interpretation of a SHNE(NUMBER, NAME) expression.
   */
  @SuppressWarnings("unchecked")
  public void testShneCondition2() throws Exception {
    FlowScope blind = newScope();
    testBinop(blind,
        Token.SHNE,
        createNumber(56),
        createVar(blind, "a", createUnionType(STRING_TYPE, NUMBER_TYPE)),
        Sets.newHashSet(new TypedName("a",
            createUnionType(STRING_TYPE, NUMBER_TYPE))),
        Sets.newHashSet(new TypedName("a", NUMBER_TYPE)));
  }

  /**
   * Tests reverse interpretation of a SHNE(NAME, NAME) expression.
   */
  @SuppressWarnings("unchecked")
  public void testShneCondition3() throws Exception {
    FlowScope blind = newScope();
    testBinop(blind,
        Token.SHNE,
        createVar(blind, "b", createUnionType(STRING_TYPE, BOOLEAN_TYPE)),
        createVar(blind, "a", createUnionType(STRING_TYPE, NUMBER_TYPE)),
        Sets.newHashSet(new TypedName("a",
            createUnionType(STRING_TYPE, NUMBER_TYPE)),
            new TypedName("b",
                createUnionType(STRING_TYPE, BOOLEAN_TYPE))),
        Sets.newHashSet(new TypedName("a", STRING_TYPE),
            new TypedName("b", STRING_TYPE)));
  }

  @SuppressWarnings("unchecked")
  public void testShneCondition4() throws Exception {
    FlowScope blind = newScope();
    testBinop(blind,
        Token.SHNE,
        createVar(blind, "a", createUnionType(STRING_TYPE, VOID_TYPE)),
        createVar(blind, "b", createUnionType(VOID_TYPE)),
        Sets.newHashSet(new TypedName("a", STRING_TYPE),
            new TypedName("b", VOID_TYPE)),
        Sets.newHashSet(new TypedName("a", VOID_TYPE),
            new TypedName("b", VOID_TYPE)));
  }

  @SuppressWarnings("unchecked")
  public void testShneCondition5() throws Exception {
    FlowScope blind = newScope();
    testBinop(blind,
        Token.SHNE,
        createVar(blind, "a", createUnionType(NULL_TYPE, VOID_TYPE)),
        createVar(blind, "b", createUnionType(NULL_TYPE)),
        Sets.newHashSet(new TypedName("a", VOID_TYPE),
            new TypedName("b", NULL_TYPE)),
        Sets.newHashSet(new TypedName("a", NULL_TYPE),
            new TypedName("b", NULL_TYPE)));
  }

  @SuppressWarnings("unchecked")
  public void testShneCondition6() throws Exception {
    FlowScope blind = newScope();
    testBinop(blind,
        Token.SHNE,
        createVar(blind, "a", createUnionType(STRING_TYPE, VOID_TYPE)),
        createVar(blind, "b", createUnionType(NUMBER_TYPE, VOID_TYPE)),
        Sets.newHashSet(
            new TypedName("a",
                createUnionType(STRING_TYPE, VOID_TYPE)),
            new TypedName("b",
                createUnionType(NUMBER_TYPE, VOID_TYPE))),
        Sets.newHashSet(
            new TypedName("a", VOID_TYPE),
            new TypedName("b", VOID_TYPE)));
  }

  /**
   * Tests reverse interpretation of a EQ(NAME, NULL) expression.
   */
  @SuppressWarnings("unchecked")
  public void testEqCondition1() throws Exception {
    FlowScope blind = newScope();
    testBinop(blind,
        Token.EQ,
        createVar(blind, "a", createUnionType(BOOLEAN_TYPE, VOID_TYPE)),
        createNull(),
        Sets.newHashSet(new TypedName("a", VOID_TYPE)),
        Sets.newHashSet(new TypedName("a", BOOLEAN_TYPE)));
  }

  /**
   * Tests reverse interpretation of a NE(NULL, NAME) expression.
   */
  @SuppressWarnings("unchecked")
  public void testEqCondition2() throws Exception {
    FlowScope blind = newScope();
    testBinop(blind,
        Token.NE,
        createNull(),
        createVar(blind, "a", createUnionType(BOOLEAN_TYPE, VOID_TYPE)),
        Sets.newHashSet(new TypedName("a", BOOLEAN_TYPE)),
        Sets.newHashSet(new TypedName("a", VOID_TYPE)));
  }

  /**
   * Tests reverse interpretation of a EQ(NAME, NULL) expression.
   */
  @SuppressWarnings("unchecked")
  public void testEqCondition3() throws Exception {
    FlowScope blind = newScope();
    // (number,undefined,null)
    JSType nullableOptionalNumber =
        createUnionType(NULL_TYPE, VOID_TYPE, NUMBER_TYPE);
    // (null,undefined)
    JSType nullUndefined =
        createUnionType(VOID_TYPE, NULL_TYPE);
    testBinop(blind,
        Token.EQ,
        createVar(blind, "a", nullableOptionalNumber),
        createNull(),
        Sets.newHashSet(new TypedName("a", nullUndefined)),
        Sets.newHashSet(new TypedName("a", NUMBER_TYPE)));
  }

  /**
   * Tests reverse interpretation of two undefineds.
   */
  @SuppressWarnings("unchecked")
  public void testEqCondition4() throws Exception {
    FlowScope blind = newScope();
    testBinop(blind,
        Token.EQ,
        createVar(blind, "a", VOID_TYPE),
        createVar(blind, "b", VOID_TYPE),
        Sets.newHashSet(
            new TypedName("a", VOID_TYPE),
            new TypedName("b", VOID_TYPE)),
        Sets.newHashSet(
            new TypedName("a", NO_TYPE),
            new TypedName("b", NO_TYPE)));
  }

  /**
   * Tests reverse interpretation of a COMPARE(NAME, NUMBER) expression,
   * where COMPARE can be LE, LT, GE or GT.
   */
  @SuppressWarnings("unchecked")
  public void testInequalitiesCondition1() {
    for (int op : Arrays.asList(Token.LT, Token.GT, Token.LE, Token.GE)) {
      FlowScope blind = newScope();
      testBinop(blind,
          op,
          createVar(blind, "a", createUnionType(STRING_TYPE, VOID_TYPE)),
          createNumber(8),
          Sets.newHashSet(
              new TypedName("a", STRING_TYPE)),
          Sets.newHashSet(new TypedName("a",
              createUnionType(STRING_TYPE, VOID_TYPE))));
    }
  }

  /**
   * Tests reverse interpretation of a COMPARE(NAME, NAME) expression,
   * where COMPARE can be LE, LT, GE or GT.
   */
  @SuppressWarnings("unchecked")
  public void testInequalitiesCondition2() {
    for (int op : Arrays.asList(Token.LT, Token.GT, Token.LE, Token.GE)) {
      FlowScope blind = newScope();
      testBinop(blind,
          op,
          createVar(blind, "a",
              createUnionType(STRING_TYPE, NUMBER_TYPE, VOID_TYPE)),
          createVar(blind, "b",
              createUnionType(NUMBER_TYPE, NULL_TYPE)),
          Sets.newHashSet(
              new TypedName("a",
              createUnionType(STRING_TYPE, NUMBER_TYPE)),
              new TypedName("b",
              createUnionType(NUMBER_TYPE, NULL_TYPE))),
          Sets.newHashSet(
              new TypedName("a",
              createUnionType(STRING_TYPE, NUMBER_TYPE, VOID_TYPE)),
              new TypedName("b",
              createUnionType(NUMBER_TYPE, NULL_TYPE))));
    }
  }

  /**
   * Tests reverse interpretation of a COMPARE(NUMBER-untyped, NAME) expression,
   * where COMPARE can be LE, LT, GE or GT.
   */
  @SuppressWarnings("unchecked")
  public void testInequalitiesCondition3() {
    for (int op : Arrays.asList(Token.LT, Token.GT, Token.LE, Token.GE)) {
      FlowScope blind = newScope();
      testBinop(blind,
          op,
          createUntypedNumber(8),
          createVar(blind, "a", createUnionType(STRING_TYPE, VOID_TYPE)),
          Sets.newHashSet(
              new TypedName("a", STRING_TYPE)),
          Sets.newHashSet(new TypedName("a",
              createUnionType(STRING_TYPE, VOID_TYPE))));
    }
  }

  @SuppressWarnings("unchecked")
  public void testAnd() {
    FlowScope blind = newScope();
    testBinop(blind,
      Token.AND,
      createVar(blind, "b", createUnionType(STRING_TYPE, NULL_TYPE)),
      createVar(blind, "a", createUnionType(NUMBER_TYPE, VOID_TYPE)),
      Sets.newHashSet(new TypedName("a", NUMBER_TYPE),
          new TypedName("b", STRING_TYPE)),
      Sets.newHashSet(new TypedName("a",
          createUnionType(NUMBER_TYPE, VOID_TYPE)),
          new TypedName("b",
          createUnionType(STRING_TYPE, NULL_TYPE))));
  }

  @SuppressWarnings("unchecked")
  public void testTypeof1() {
    FlowScope blind = newScope();
    testBinop(blind,
        Token.EQ,
        new Node(Token.TYPEOF, createVar(blind, "a", OBJECT_TYPE)),
        Node.newString("function"),
        Sets.newHashSet(
            new TypedName("a", U2U_CONSTRUCTOR_TYPE)),
        Sets.newHashSet(
            new TypedName("a", OBJECT_TYPE)));
  }

  @SuppressWarnings("unchecked")
  public void testTypeof2() {
    FlowScope blind = newScope();
    testBinop(blind,
        Token.EQ,
        new Node(Token.TYPEOF, createVar(blind, "a", ALL_TYPE)),
        Node.newString("function"),
        Sets.newHashSet(
            new TypedName("a", U2U_CONSTRUCTOR_TYPE)),
        Sets.newHashSet(
            new TypedName("a", ALL_TYPE)));
  }

  @SuppressWarnings("unchecked")
  public void testTypeof3() {
    FlowScope blind = newScope();
    testBinop(blind,
        Token.EQ,
        new Node(Token.TYPEOF, createVar(
            blind, "a", OBJECT_NUMBER_STRING_BOOLEAN)),
        Node.newString("function"),
        Sets.newHashSet(
            new TypedName("a", U2U_CONSTRUCTOR_TYPE)),
        Sets.newHashSet(
            new TypedName("a", OBJECT_NUMBER_STRING_BOOLEAN)));
  }

  @SuppressWarnings("unchecked")
  public void testTypeof4() {
    FlowScope blind = newScope();
    testBinop(blind,
        Token.EQ,
        new Node(Token.TYPEOF, createVar(
            blind, "a", createUnionType(
                U2U_CONSTRUCTOR_TYPE,NUMBER_STRING_BOOLEAN))),
        Node.newString("function"),
        Sets.newHashSet(
            new TypedName("a", U2U_CONSTRUCTOR_TYPE)),
        Sets.newHashSet(
            new TypedName("a", NUMBER_STRING_BOOLEAN)));
  }

  @SuppressWarnings("unchecked")
  public void testInstanceOf() {
    FlowScope blind = newScope();
    testBinop(blind,
        Token.INSTANCEOF,
        createVar(blind, "x", UNKNOWN_TYPE),
        createVar(blind, "s", STRING_OBJECT_FUNCTION_TYPE),
        Sets.newHashSet(
            new TypedName("x", STRING_OBJECT_TYPE),
            new TypedName("s", STRING_OBJECT_FUNCTION_TYPE)),
        Sets.newHashSet(
            new TypedName("s", STRING_OBJECT_FUNCTION_TYPE)));
  }

  @SuppressWarnings("unchecked")
  public void testInstanceOf2() {
    FlowScope blind = newScope();
    testBinop(blind,
        Token.INSTANCEOF,
        createVar(blind, "x",
            createUnionType(STRING_OBJECT_TYPE, NUMBER_OBJECT_TYPE)),
        createVar(blind, "s", STRING_OBJECT_FUNCTION_TYPE),
        Sets.newHashSet(
            new TypedName("x", STRING_OBJECT_TYPE),
            new TypedName("s", STRING_OBJECT_FUNCTION_TYPE)),
        Sets.newHashSet(
            new TypedName("x", NUMBER_OBJECT_TYPE),
            new TypedName("s", STRING_OBJECT_FUNCTION_TYPE)));
  }

  @SuppressWarnings("unchecked")
  public void testInstanceOf3() {
    FlowScope blind = newScope();
    testBinop(blind,
        Token.INSTANCEOF,
        createVar(blind, "x", OBJECT_TYPE),
        createVar(blind, "s", STRING_OBJECT_FUNCTION_TYPE),
        Sets.newHashSet(
            new TypedName("x", STRING_OBJECT_TYPE),
            new TypedName("s", STRING_OBJECT_FUNCTION_TYPE)),
        Sets.newHashSet(
            new TypedName("x", OBJECT_TYPE),
            new TypedName("s", STRING_OBJECT_FUNCTION_TYPE)));
  }

  @SuppressWarnings("unchecked")
  public void testInstanceOf4() {
    FlowScope blind = newScope();
    testBinop(blind,
        Token.INSTANCEOF,
        createVar(blind, "x", ALL_TYPE),
        createVar(blind, "s", STRING_OBJECT_FUNCTION_TYPE),
        Sets.newHashSet(
            new TypedName("x", STRING_OBJECT_TYPE),
            new TypedName("s", STRING_OBJECT_FUNCTION_TYPE)),
        Sets.newHashSet(
            new TypedName("s", STRING_OBJECT_FUNCTION_TYPE)));
  }

  private void testBinop(FlowScope blind, int binop, Node left, Node right,
      Collection<TypedName> trueOutcome,
      Collection<TypedName> falseOutcome) {
    Node condition = new Node(binop);
    condition.addChildToBack(left);
    condition.addChildToBack(right);

    // true outcome.
    FlowScope informedTrue = interpreter.
        getPreciserScopeKnowingConditionOutcome(condition, blind, true);
    for (TypedName p : trueOutcome) {
      assertTypeEquals(p.name, p.type, getVarType(informedTrue, p.name));
    }

    // false outcome.
    FlowScope informedFalse = interpreter.
        getPreciserScopeKnowingConditionOutcome(condition, blind, false);
    for (TypedName p : falseOutcome) {
      assertTypeEquals(p.type, getVarType(informedFalse, p.name));
    }
  }

  private Node createNull() {
    Node n = new Node(Token.NULL);
    n.setJSType(NULL_TYPE);
    return n;
  }

  private Node createNumber(int n) {
    Node number = createUntypedNumber(n);
    number.setJSType(NUMBER_TYPE);
    return number;
  }

  private Node createUntypedNumber(int n) {
    return Node.newNumber(n);
  }

  private JSType getVarType(FlowScope scope, String name) {
    return scope.getSlot(name).getType();
  }

  private Node createVar(FlowScope scope, String name, JSType type) {
    Node n = Node.newString(Token.NAME, name);
    functionScope.declare(name, n, null, null);
    ((LinkedFlowScope) scope).inferSlotType(name, type);
    n.setJSType(type);
    return n;
  }

  private static class TypedName {
    private final String name;
    private final JSType type;

    private TypedName(String name, JSType type) {
      this.name = name;
      this.type = type;
    }
  }
}
