/*
 * Copyright 2015 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.parsepasses.contextautoesc;

import static org.junit.Assert.assertEquals;

import com.google.common.base.Optional;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;

@RunWith(JUnit4.class)
public class ContextTest {

  private void assertUnionNoop(String dominating, String other) {
    assertUnion(dominating, dominating, other);
  }

  private void assertUnion(String expected, String a, String b) {
    Context aContext = Context.parse(a);
    Context bContext = Context.parse(b);
    Context expectedContext = Context.parse(expected);
    assertEquals("Union of " + aContext + " and " + bContext,
        Optional.of(expectedContext), Context.union(aContext, bContext));
    assertEquals("Reverse union of " + bContext + " and " + aContext,
        Optional.of(expectedContext), Context.union(bContext, aContext));
  }

  private void assertUnionFails(String a, String b) {
    Context aContext = Context.parse(a);
    Context bContext = Context.parse(b);
    assertEquals("Union of " + aContext + " and " + bContext,
        Optional.absent(), Context.union(aContext, bContext));
    assertEquals("Reverse union of " + bContext + " and " + aContext,
        Optional.absent(), Context.union(bContext, aContext));
  }

  @Test public void testObviousUnions() {
    assertUnionNoop("CSS", "CSS");
    assertUnionNoop("HTML_PCDATA", "HTML_PCDATA");
    assertUnionNoop("JS", "JS");
    assertUnionNoop("HTML_TAG_NAME", "HTML_TAG_NAME");
  }

  @Test public void testJsUnions() {
    // Identity cases.
    assertUnionNoop("JS UNKNOWN", "JS UNKNOWN");
    assertUnionNoop("JS DIV_OP", "JS DIV_OP");
    assertUnionNoop("JS REGEX", "JS REGEX");

    // Unknown accepts either.
    assertUnionNoop("JS UNKNOWN", "JS REGEX");
    assertUnionNoop("JS UNKNOWN", "JS DIV_OP");

    // Unknown is the join of the two different JS types.
    assertUnion("JS UNKNOWN", "JS REGEX", "JS DIV_OP");

    // Same, even if it's within a script attribute.
    assertUnion("JS NORMAL SCRIPT DOUBLE_QUOTE UNKNOWN",
        "JS NORMAL SCRIPT DOUBLE_QUOTE REGEX",
        "JS NORMAL SCRIPT DOUBLE_QUOTE DIV_OP");

    // One is in a script, one is not:
    assertUnionFails("JS NORMAL SCRIPT DOUBLE_QUOTE REGEX", "JS REGEX");
  }

  @Test public void testUriPartUnions() {
    // The more well-formed states:
    String start = "URI START NORMAL";
    String maybeScheme = "URI MAYBE_SCHEME NORMAL";
    String authorityOrPath = "URI AUTHORITY_OR_PATH NORMAL";
    String query = "URI QUERY NORMAL";
    String fragment = "URI FRAGMENT NORMAL";
    // The more nebulous and sticky states:
    String maybeVariableScheme = "URI MAYBE_VARIABLE_SCHEME NORMAL";
    String unknownPreFragment = "URI UNKNOWN_PRE_FRAGMENT NORMAL";
    // NOTE: "NONE" needed to disambiguate from JsFollowingSlash.UNKNOWN
    String unknown = "URI NONE NONE NONE NONE UNKNOWN NORMAL";
    String dangerousScheme = "URI DANGEROUS_SCHEME NORMAL";

    // Well-formed states, identity:
    assertUnionNoop(start, start);
    assertUnionNoop(maybeScheme, maybeScheme);
    assertUnionNoop(authorityOrPath, authorityOrPath);
    assertUnionNoop(query, query);
    assertUnionNoop(fragment, fragment);

    // Well-formed states before the fragment:
    assertUnion(unknownPreFragment, start, maybeScheme);
    assertUnion(unknownPreFragment, start, authorityOrPath);
    assertUnion(unknownPreFragment, start, query);
    assertUnion(unknownPreFragment, maybeScheme, authorityOrPath);
    assertUnion(unknownPreFragment, maybeScheme, query);
    assertUnion(unknownPreFragment, authorityOrPath, query);

    // Well-formed states, both before and after fragment:
    assertUnion(unknown, start, fragment);
    assertUnion(unknown, maybeScheme, fragment);
    assertUnion(unknown, authorityOrPath, fragment);
    assertUnion(unknown, query, fragment);

    // Variable at the beginning matched with other parts:
    assertUnionNoop(maybeVariableScheme, start);
    assertUnionNoop(maybeVariableScheme, maybeScheme);
    assertUnionNoop(maybeVariableScheme, authorityOrPath);
    assertUnionNoop(maybeVariableScheme, query);
    assertUnionNoop(maybeVariableScheme, maybeVariableScheme);
    assertUnion(unknown, maybeVariableScheme, fragment);

    // Unknown but before fragment:
    assertUnionNoop(unknownPreFragment, start);
    assertUnionNoop(unknownPreFragment, maybeScheme);
    assertUnionNoop(unknownPreFragment, authorityOrPath);
    assertUnionNoop(unknownPreFragment, query);
    assertUnionNoop(unknownPreFragment, maybeVariableScheme);
    assertUnionNoop(unknownPreFragment, unknownPreFragment);
    assertUnion(unknown, unknownPreFragment, fragment);

    // Either before or after fragment:
    assertUnionNoop(unknown, start);
    assertUnionNoop(unknown, maybeScheme);
    assertUnionNoop(unknown, maybeVariableScheme);
    assertUnionNoop(unknown, authorityOrPath);
    assertUnionNoop(unknown, query);
    assertUnionNoop(unknown, unknownPreFragment);
    assertUnionNoop(unknown, fragment);

    // Poison context:
    assertUnionNoop(dangerousScheme, start);
    assertUnionNoop(dangerousScheme, maybeScheme);
    assertUnionNoop(dangerousScheme, maybeVariableScheme);
    assertUnionNoop(dangerousScheme, authorityOrPath);
    assertUnionNoop(dangerousScheme, query);
    assertUnionNoop(dangerousScheme, unknownPreFragment);
    assertUnionNoop(dangerousScheme, fragment);
    assertUnionNoop(dangerousScheme, unknown);
  }

  @Test public void testUriTypeUnions() {
    assertUnionNoop("URI START NORMAL", "URI START NORMAL");
    assertUnionNoop("URI START MEDIA", "URI START MEDIA");
    // For now, we don't allow unioning these two types.
    assertUnionFails("URI START MEDIA", "URI START NORMAL");
  }

  @Test
  public void testTagStates() {
    // Identity for HTML_TAG:
    assertUnionNoop("HTML_TAG NORMAL", "HTML_TAG NORMAL");
    // Two very different types of tags: {if $x}<img{else}<script{/if}
    assertUnionFails("HTML_TAG NORMAL", "HTML_TAG SCRIPT");

    // Something like: <a{if $x} foo="bar"{/if} or even <a{if $x} {/if}
    assertUnionNoop("HTML_TAG NORMAL", "HTML_TAG_NAME NORMAL");
    assertUnionNoop("HTML_TAG STYLE", "HTML_TAG_NAME STYLE");

    // Incompatible tag contexts: {if $x}<img src="foo"{else}<script{/if}
    assertUnionFails("HTML_TAG NORMAL", "HTML_TAG_NAME SCRIPT");

    // Case: <input {if $x}checked{/if}
    assertUnionNoop("HTML_TAG SCRIPT", "HTML_ATTRIBUTE_NAME SCRIPT");
    // Same but different element types.
    assertUnionFails("HTML_TAG NORMAL", "HTML_ATTRIBUTE_NAME SCRIPT");

    // Something like: <a {if $x}b=foo{/if}
    assertUnionNoop("HTML_TAG NORMAL",
        "HTML_NORMAL_ATTR_VALUE NORMAL PLAIN_TEXT SPACE_OR_TAG_END");
    // Similar, but one side is a script and the other isn't:
    assertUnionFails("HTML_TAG SCRIPT",
        "HTML_NORMAL_ATTR_VALUE NORMAL PLAIN_TEXT SPACE_OR_TAG_END");
    // Or, unclosed quote: <a {if $x}b="foo{/if}
    assertUnionFails("HTML_TAG NORMAL",
        "HTML_NORMAL_ATTR_VALUE NORMAL PLAIN_TEXT SINGLE_QUOTE");
  }

  @Test public void testClearlyFailingUnions() {
    assertUnionFails("TEXT", "HTML_PCDATA");
    assertUnionFails("CSS", "JS");
  }
}
