// Copyright 2018 The Bazel Authors. All Rights Reserved.
//
// 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.devtools.build.lib.syntax;

import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.fail;

import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.devtools.build.lib.events.Location;
import com.google.devtools.build.lib.events.Location.LineAndColumn;
import com.google.devtools.build.lib.syntax.Debuggable.ReadyToPause;
import com.google.devtools.build.lib.syntax.Debuggable.Stepping;
import com.google.devtools.build.lib.syntax.Environment.LexicalFrame;
import com.google.devtools.build.lib.vfs.PathFragment;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;

/** Unit tests of {@link Environment}s implementation of {@link Debuggable}. */
@RunWith(JUnit4.class)
public class EnvironmentDebuggingTest {

  private static Environment newEnvironment() {
    Mutability mutability = Mutability.create("test");
    return Environment.builder(mutability).useDefaultSemantics().build();
  }

  /** Enter a dummy function scope with the given name, and the current environment's globals. */
  private static void enterFunctionScope(Environment env, String functionName, Location location) {
    FuncallExpression ast = new FuncallExpression(Identifier.of("test"), ImmutableList.of());
    ast.setLocation(location);
    env.enterScope(
        new BaseFunction(functionName) {},
        LexicalFrame.create(env.mutability()),
        ast,
        env.getGlobals());
  }

  @Test
  public void testListFramesFromGlobalFrame() throws Exception {
    Environment env = newEnvironment();
    env.update("a", 1);
    env.update("b", 2);
    env.update("c", 3);

    ImmutableList<DebugFrame> frames = env.listFrames(Location.BUILTIN);

    assertThat(frames).hasSize(1);
    assertThat(frames.get(0))
        .isEqualTo(
            DebugFrame.builder()
                .setFunctionName("<top level>")
                .setLocation(Location.BUILTIN)
                .setGlobalBindings(ImmutableMap.of("a", 1, "b", 2, "c", 3))
                .build());
  }

  @Test
  public void testListFramesFromChildFrame() throws Exception {
    Environment env = newEnvironment();
    env.update("a", 1);
    env.update("b", 2);
    env.update("c", 3);
    Location funcallLocation =
        Location.fromPathAndStartColumn(
            PathFragment.create("foo/bar"), 0, 0, new LineAndColumn(12, 0));
    enterFunctionScope(env, "function", funcallLocation);
    env.update("a", 4); // shadow parent frame var
    env.update("y", 5);
    env.update("z", 6);

    ImmutableList<DebugFrame> frames = env.listFrames(Location.BUILTIN);

    assertThat(frames).hasSize(2);
    assertThat(frames.get(0))
        .isEqualTo(
            DebugFrame.builder()
                .setFunctionName("function")
                .setLocation(Location.BUILTIN)
                .setLexicalFrameBindings(ImmutableMap.of("a", 4, "y", 5, "z", 6))
                .setGlobalBindings(ImmutableMap.of("a", 1, "b", 2, "c", 3))
                .build());
    assertThat(frames.get(1))
        .isEqualTo(
            DebugFrame.builder()
                .setFunctionName("<top level>")
                .setLocation(funcallLocation)
                .setGlobalBindings(ImmutableMap.of("a", 1, "b", 2, "c", 3))
                .build());
  }

  @Test
  public void testStepIntoFunction() {
    Environment env = newEnvironment();

    ReadyToPause predicate = env.stepControl(Stepping.INTO);
    enterFunctionScope(env, "function", Location.BUILTIN);

    assertThat(predicate.test(env)).isTrue();
  }

  @Test
  public void testStepIntoFallsBackToStepOver() {
    // test that when stepping into, we'll fall back to stopping at the next statement in the
    // current frame
    Environment env = newEnvironment();

    ReadyToPause predicate = env.stepControl(Stepping.INTO);

    assertThat(predicate.test(env)).isTrue();
  }

  @Test
  public void testStepIntoFallsBackToStepOut() {
    // test that when stepping into, we'll fall back to stopping when exiting the current frame
    Environment env = newEnvironment();
    enterFunctionScope(env, "function", Location.BUILTIN);

    ReadyToPause predicate = env.stepControl(Stepping.INTO);
    env.exitScope();

    assertThat(predicate.test(env)).isTrue();
  }

  @Test
  public void testStepOverFunction() {
    Environment env = newEnvironment();

    ReadyToPause predicate = env.stepControl(Stepping.OVER);
    enterFunctionScope(env, "function", Location.BUILTIN);

    assertThat(predicate.test(env)).isFalse();
    env.exitScope();
    assertThat(predicate.test(env)).isTrue();
  }

  @Test
  public void testStepOverFallsBackToStepOut() {
    // test that when stepping over, we'll fall back to stopping when exiting the current frame
    Environment env = newEnvironment();
    enterFunctionScope(env, "function", Location.BUILTIN);

    ReadyToPause predicate = env.stepControl(Stepping.OVER);
    env.exitScope();

    assertThat(predicate.test(env)).isTrue();
  }

  @Test
  public void testStepOutOfInnerFrame() {
    Environment env = newEnvironment();
    enterFunctionScope(env, "function", Location.BUILTIN);

    ReadyToPause predicate = env.stepControl(Stepping.OUT);

    assertThat(predicate.test(env)).isFalse();
    env.exitScope();
    assertThat(predicate.test(env)).isTrue();
  }

  @Test
  public void testStepOutOfOutermostFrame() {
    Environment env = newEnvironment();

    assertThat(env.stepControl(Stepping.OUT)).isNull();
  }

  @Test
  public void testStepControlWithNoSteppingReturnsNull() {
    Environment env = newEnvironment();

    assertThat(env.stepControl(Stepping.NONE)).isNull();
  }

  @Test
  public void testEvaluateVariableInScope() throws Exception {
    Environment env = newEnvironment();
    env.update("a", 1);

    Object result = env.evaluate("a");

    assertThat(result).isEqualTo(1);
  }

  @Test
  public void testEvaluateVariableNotInScopeFails() throws Exception {
    Environment env = newEnvironment();
    env.update("a", 1);

    try {
      env.evaluate("b");
      fail();
    } catch (EvalException e) {
      assertThat(e).hasMessageThat().isEqualTo("name 'b' is not defined");
    }
  }

  @Test
  public void testEvaluateExpressionOnVariableInScope() throws Exception {
    Environment env = newEnvironment();
    env.update("a", "string");

    Object result = env.evaluate("a.startswith(\"str\")");

    assertThat(result).isEqualTo(Boolean.TRUE);
  }
}
