/*
 * 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.base.Preconditions.checkNotNull;
import static com.google.common.base.Preconditions.checkState;
import static com.google.common.collect.ImmutableList.toImmutableList;
import static com.google.common.truth.Truth.assertAbout;

import com.google.common.base.Joiner;
import com.google.common.collect.ImmutableList;
import com.google.common.truth.FailureMetadata;
import com.google.common.truth.Subject;
import com.google.javascript.rhino.Node;
import javax.annotation.CheckReturnValue;
import javax.annotation.Nullable;

/**
 * A Truth Subject for the AbstractScope class. Usage:
 *
 * <pre>
 *   import static com.google.javascript.jscomp.testing.ScopeSubject.assertScope;
 *   ...
 *   assertScope(scope).declares("somevar");
 *   assertScope(scope).declares("otherVar").directly();
 *   assertScope(scope).declares("yetAnotherVar").onClosestContainerScope();
 * </pre>
 */
public final class ScopeSubject extends Subject<ScopeSubject, AbstractScope<?, ?>> {
  @CheckReturnValue
  public static ScopeSubject assertScope(AbstractScope<?, ?> scope) {
    return assertAbout(ScopeSubject::new).that(scope);
  }

  private ScopeSubject(FailureMetadata failureMetadata, AbstractScope<?, ?> scope) {
    super(failureMetadata, scope);
  }

  public void doesNotDeclare(String name) {
    AbstractVar<?, ?> var = getVar(name);
    if (var != null) {
      failWithBadResults("does not declare", name, "declared", var);
    }
  }

  public DeclarationSubject declares(String name) {
    AbstractVar<?, ?> var = getVar(name);
    if (var == null) {
      ImmutableList<AbstractVar<?, ?>> declared =
          ImmutableList.copyOf(actual().getAllAccessibleVariables());
      ImmutableList<String> names =
          declared.stream().map(AbstractVar::getName).collect(toImmutableList());
      if (names.size() > 10) {
        names =
            ImmutableList.<String>builder()
                .addAll(names.subList(0, 9))
                .add("and " + (names.size() - 9) + " others")
                .build();
      }
      failWithBadResults("declares", name, "declares", Joiner.on(", ").join(names));
    }
    return new DeclarationSubject(var);
  }

  private AbstractVar<?, ?> getVar(String name) {
    return actual().hasSlot(name) ? checkNotNull(actual().getVar(name)) : null;
  }

  public final class DeclarationSubject {
    private final AbstractVar<?, ?> var;

    private DeclarationSubject(AbstractVar<?, ?> var) {
      this.var = checkNotNull(var);
    }

    /**
     * Expects the variable to be defined on the given {@code scope}. The {@code preposition} is
     * either "on" or "", depending on whether it is needed for grammatical correctness. The {@code
     * expected} object is displayed in brackets, in parallel to the actual scope that the variable
     * is defined on.
     */
    private void expectScope(String preposition, Object expected, AbstractScope<?, ?> scope) {
      if (var.getScope() != scope) {
        failWithBadResults(
            "declares " + var.getName() + (!preposition.isEmpty() ? " " : "") + preposition,
            expected,
            "declares it on",
            var.getScope());
      }
    }

    /** Expects the declared variable to be declared on the subject scope. */
    public void directly() {
      expectScope("", "directly", actual());
    }

    /** Expects the declared variable to be declared on the given scope. */
    public void on(AbstractScope<?, ?> scope) {
      checkState(
          scope != actual(),
          "It doesn't make sense to pass the scope already being asserted about. Use .directly()");
      expectScope("on", scope, scope);
    }

    /** Expects the declared variable to be declared on the closest container scope. */
    public void onClosestContainerScope() {
      expectScope("on", "the closest container scope", actual().getClosestContainerScope());
    }

    /** Expects the declared variable to be declared on the closest hoist scope. */
    public void onClosestHoistScope() {
      expectScope("on", "the closest hoist scope", actual().getClosestHoistScope());
    }

    /** Expects the declared variable to be declared on the global scope. */
    public void globally() {
      expectScope("", "globally", actual().getGlobalScope());
    }

    /** Expects the declared variable to be declared on any scope other than the subject. */
    public void onSomeParent() {
      if (var != null && var.getScope() == actual()) {
        failWithBadResults(
            "declares " + var.getName(), "on a parent scope", "declares it", "directly");
      }
    }

    /** Expects the declared variable to be declared on some scope with the given label. */
    public void onScopeLabeled(String expectedLabel) {
      checkNotNull(expectedLabel);
      String actualLabel = getLabel(var.getScopeRoot());
      if (actualLabel == null) {
        failWithBadResults(
            "declares " + var.getName(),
            "on a scope labeled \"" + expectedLabel + "\"",
            "declares it",
            "on an unlabeled scope");
      } else if (!actualLabel.equals(expectedLabel)) {
        failWithBadResults(
            "declares " + var.getName(),
            "on a scope labeled \"" + expectedLabel + "\"",
            "declares it",
            "on a scope labeled \"" + actualLabel + "\"");
      }
    }
  }

  /** Returns the name of the label applied to n, or null if none exists. */
  @Nullable
  private String getLabel(Node n) {
    // If the node is labeled it will be the second child of a LABEL and the first child
    // will be a LABEL_NAME.
    Node labelNameNode = n.getPrevious();
    if (labelNameNode != null && labelNameNode.isLabelName()) {
      checkState(labelNameNode.getParent().isLabel());
      return labelNameNode.getString();
    } else {
      return null;
    }
  }
}

@@ -6243,29 +6243,22 @@
    Node 
-labelNameNode
+parent
  = n.get
@@ -6262,15 +6262,13 @@
 getP
+a
 re
-vious
+nt
 ();%0A
@@ -6275,16 +6275,65 @@
     if (
+parent != null && parent.isLabel()) %7B%0A      Node 
 labelNam
@@ -6338,27 +6338,59 @@
 ameNode 
-!
 = 
-null && 
+parent.getFirstChild();%0A      checkState(
 labelNam
@@ -6408,19 +6408,33 @@
 elName()
-) %7B
+, labelNameNode);
 %0A      c
@@ -6464,26 +6464,22 @@
 .get
-Parent().isLabel()
+Next() == n, n
 );%0A 

Fix NPE in ScopeSubject.getLabel()