/*
 * Copyright 2015 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 static com.google.common.truth.Truth.assertWithMessage;
import static com.google.javascript.jscomp.TypeMatchingStrategy.EXACT;
import static com.google.javascript.jscomp.TypeMatchingStrategy.LOOSE;
import static com.google.javascript.jscomp.TypeMatchingStrategy.STRICT_NULLABILITY;
import static com.google.javascript.jscomp.TypeMatchingStrategy.SUBTYPES;

import com.google.common.base.Joiner;
import com.google.common.collect.ImmutableList;
import com.google.javascript.jscomp.TypeMatchingStrategy.MatchResult;
import com.google.javascript.rhino.Node;
import com.google.javascript.rhino.jstype.JSType;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;

@RunWith(JUnit4.class)
public final class TypeMatchingStrategyTest {

  private static final String EXTERNS =
      Joiner.on("\n")
          .join(
              "/** @constructor */",
              "var OtherType = function() {};",
              "/** @constructor */",
              "var SuperType = function() {};",
              "/** @constructor @extends {SuperType} */",
              "var SubType = function() {};");

  @Test
  public void testMatch_default() {
    assertMatch(LOOSE, "!SuperType", "!SuperType", true, false);
    assertMatch(LOOSE, "!SuperType", "?SuperType", true, false);
    assertMatch(LOOSE, "!SuperType", "SuperType|undefined", true, false);
    assertMatch(LOOSE, "!SuperType", "SuperType|void", true, false);
    assertMatch(LOOSE, "!SuperType", "!SubType", true, false);
    assertMatch(LOOSE, "!SuperType", "?SubType", true, false);
    assertMatch(LOOSE, "!SuperType", "SubType|undefined", true, false);
    assertMatch(LOOSE, "!SuperType", "SubType|void", true, false);
    assertMatch(LOOSE, "!SuperType", "number", false, false);
    assertMatch(LOOSE, "!SuperType", "*", true, true);
    assertMatch(LOOSE, "!SuperType", "?", true, true);
    assertMatch(LOOSE, "?", "string", true, false);
  }

  @Test
  public void testMatch_respectNullability() {
    assertMatch(STRICT_NULLABILITY, "!SuperType", "!SuperType", true, false);
    assertMatch(STRICT_NULLABILITY, "!SuperType", "?SuperType", false, false);
    assertMatch(STRICT_NULLABILITY, "!SuperType", "SuperType|undefined", false, false);
    assertMatch(STRICT_NULLABILITY, "!SuperType", "SuperType|void", false, false);
    assertMatch(STRICT_NULLABILITY, "!SuperType", "!SubType", true, false);
    assertMatch(STRICT_NULLABILITY, "!SuperType", "?SubType", false, false);
    assertMatch(STRICT_NULLABILITY, "!SuperType", "SubType|undefined", false, false);
    assertMatch(STRICT_NULLABILITY, "!SuperType", "SubType|void", false, false);
    assertMatch(STRICT_NULLABILITY, "!SuperType", "number", false, false);
    assertMatch(STRICT_NULLABILITY, "!SuperType", "*", true, true);
    assertMatch(STRICT_NULLABILITY, "!SuperType", "?", true, true);
    assertMatch(STRICT_NULLABILITY, "?", "string", true, false);
  }

  @Test
  public void testMatch_subtypes() {
    assertMatch(SUBTYPES, "!SuperType", "!SuperType", true, false);
    assertMatch(SUBTYPES, "!SuperType", "?SuperType", false, false);
    assertMatch(SUBTYPES, "!SuperType", "!OtherType", false, false);
    assertMatch(SUBTYPES, "!SuperType", "SuperType|undefined", false, false);
    assertMatch(SUBTYPES, "!SuperType", "SuperType|void", false, false);
    assertMatch(SUBTYPES, "!SuperType", "!SubType", true, false);
    assertMatch(SUBTYPES, "!SuperType", "?SubType", false, false);
    assertMatch(SUBTYPES, "!SuperType", "SubType|undefined", false, false);
    assertMatch(SUBTYPES, "!SuperType", "SubType|void", false, false);
    assertMatch(SUBTYPES, "!SuperType", "number", false, false);
    assertMatch(SUBTYPES, "!SuperType", "*", false, false);
    assertMatch(SUBTYPES, "!SuperType", "?", false, false);
    assertMatch(SUBTYPES, "?SuperType", "!SuperType", true, false);
    assertMatch(SUBTYPES, "?SuperType", "?SuperType", true, false);
    assertMatch(SUBTYPES, "?SuperType", "?OtherType", false, false);
    assertMatch(SUBTYPES, "?SuperType", "SuperType|undefined", false, false);
    assertMatch(SUBTYPES, "?SuperType", "SuperType|void", false, false);
    assertMatch(SUBTYPES, "?SuperType", "SuperType|OtherType", false, false);
    assertMatch(SUBTYPES, "?SuperType", "!SubType", true, false);
    assertMatch(SUBTYPES, "?SuperType", "?SubType", true, false);
    assertMatch(SUBTYPES, "?SuperType", "SubType|undefined", false, false);
    assertMatch(SUBTYPES, "?SuperType", "SubType|void", false, false);
    assertMatch(SUBTYPES, "?SuperType", "number", false, false);
    assertMatch(SUBTYPES, "?SuperType", "*", false, false);
    assertMatch(SUBTYPES, "?SuperType", "?", false, false);
    assertMatch(SUBTYPES, "?", "string", true, false);
    assertMatch(SUBTYPES, "?", "?", true, false);
    assertMatch(SUBTYPES, "?", "*", true, false);
    assertMatch(SUBTYPES, "*", "?", false, false);
  }

  @Test
  public void testMatch_exact() {
    assertMatch(EXACT, "!SuperType", "!SuperType", true, false);
    assertMatch(EXACT, "!SuperType", "?SuperType", false, false);
    assertMatch(EXACT, "!SuperType", "SuperType|undefined", false, false);
    assertMatch(EXACT, "!SuperType", "SuperType|void", false, false);
    assertMatch(EXACT, "!SuperType", "!SubType", false, false);
    assertMatch(EXACT, "!SuperType", "?SubType", false, false);
    assertMatch(EXACT, "!SuperType", "SubType|undefined", false, false);
    assertMatch(EXACT, "!SuperType", "SubType|void", false, false);
    assertMatch(EXACT, "!SuperType", "number", false, false);
    assertMatch(EXACT, "!SuperType", "*", false, false);
    assertMatch(EXACT, "!SuperType", "?", false, false);
    assertMatch(EXACT, "?", "string", true, false);
  }

  private static void assertMatch(
      TypeMatchingStrategy typeMatchingStrategy,
      String templateType,
      String type,
      boolean isMatch,
      boolean isLooseMatch) {

    // It's important that the test uses the same compiler to compile the template type and the
    // type to be matched. Otherwise, equal types won't be considered equal.
    Compiler compiler = new Compiler();
    compiler.disableThreads();
    CompilerOptions options = new CompilerOptions();
    options.setCheckTypes(true);
    options.setChecksOnly(true);

    compiler.compile(
        ImmutableList.of(SourceFile.fromCode("externs", EXTERNS)),
        ImmutableList.of(
            SourceFile.fromCode(
                "test",
                String.format(
                    "/** @type {%s} */ var x; /** @type {%s} */ var y;", templateType, type))),
        options);
    Node script = compiler.getRoot().getLastChild().getFirstChild();
    Node xNode = script.getFirstChild();
    Node yNode = script.getLastChild();
    JSType templateJsType = xNode.getFirstChild().getJSType();
    JSType jsType = yNode.getFirstChild().getJSType();

    MatchResult matchResult = typeMatchingStrategy.match(templateJsType, jsType);
    assertWithMessage(
            isMatch
                ? "'" + templateJsType + "' should match '" + jsType + "'"
                : "'" + templateType + "' should not match '" + type + "'")
        .that(matchResult.isMatch())
        .isEqualTo(isMatch);
    assertWithMessage(
            isLooseMatch
                ? "'" + templateType + "' should loosely match '" + type + "'"
                : "'" + templateType + "' should not loosely match '" + type + "'")
        .that(matchResult.isLooseMatch())
        .isEqualTo(isLooseMatch);
  }
}
