// 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.starlarkdebug.server;

import static com.google.common.truth.Truth.assertThat;

import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.devtools.build.lib.collect.nestedset.Depset;
import com.google.devtools.build.lib.collect.nestedset.NestedSet;
import com.google.devtools.build.lib.collect.nestedset.NestedSetBuilder;
import com.google.devtools.build.lib.starlarkdebugging.StarlarkDebuggingProtos.Value;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
import net.starlark.java.annot.StarlarkMethod;
import net.starlark.java.eval.Printer;
import net.starlark.java.eval.Starlark;
import net.starlark.java.eval.StarlarkValue;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;

/** Unit tests for {@link DebuggerSerialization}. */
@RunWith(JUnit4.class)
public final class DebuggerSerializationTest {

  private final ThreadObjectMap dummyObjectMap = new ThreadObjectMap();

  /**
   * Returns the {@link Value} proto message corresponding to the given object and label. Subsequent
   * calls may return values with different IDs.
   */
  private Value getValueProto(String label, Object value) {
    return DebuggerSerialization.getValueProto(dummyObjectMap, label, value);
  }

  private ImmutableList<Value> getChildren(Value value) {
    Object object = dummyObjectMap.getValue(value.getId());
    return object != null
        ? DebuggerSerialization.getChildren(dummyObjectMap, object)
        : ImmutableList.of();
  }

  @Test
  public void testSimpleNestedSet() {
    Set<String> children = ImmutableSet.of("a", "b");
    Depset set =
        Depset.of(String.class, NestedSetBuilder.<String>stableOrder().addAll(children).build());

    Value value = getValueProto("name", set);

    assertTypeAndDescription(set, value);
    assertThat(value.getHasChildren()).isTrue();
    assertThat(value.getLabel()).isEqualTo("name");

    List<Value> childValues = getChildren(value);

    assertThat(childValues.get(0))
        .isEqualTo(
            Value.newBuilder()
                .setLabel("order")
                .setType("string")
                .setDescription("default")
                .build());
    assertEqualIgnoringTypeDescriptionAndId(childValues.get(1), getValueProto("directs", children));
    assertEqualIgnoringTypeDescriptionAndId(
        childValues.get(2), getValueProto("transitives", ImmutableList.of()));
  }

  @Test
  public void testNestedSetWithNestedChildren() {
    NestedSet<String> innerNestedSet =
        NestedSetBuilder.<String>stableOrder().add("inner1").add("inner2").build();
    ImmutableSet<String> directChildren = ImmutableSet.of("a", "b");
    Depset outerSet =
        Depset.of(
            String.class,
            NestedSetBuilder.<String>linkOrder()
                .addAll(directChildren)
                .addTransitive(innerNestedSet)
                .build());

    Value value = getValueProto("name", outerSet);
    List<Value> childValues = getChildren(value);

    assertTypeAndDescription(outerSet, value);
    assertThat(childValues).hasSize(3);
    assertThat(childValues.get(0))
        .isEqualTo(
            Value.newBuilder()
                .setLabel("order")
                .setType("string")
                .setDescription("topological")
                .build());
    assertEqualIgnoringTypeDescriptionAndId(
        childValues.get(1), getValueProto("directs", directChildren));
    assertEqualIgnoringTypeDescriptionAndId(
        childValues.get(2), getValueProto("transitives", ImmutableList.of(innerNestedSet)));
  }

  @Test
  public void testSimpleMap() {
    Map<String, Integer> map = ImmutableMap.of("a", 1, "b", 2);

    Value value = getValueProto("name", map);
    List<Value> childValues = getChildren(value);

    assertTypeAndDescription(map, value);
    assertThat(childValues).hasSize(2);
    assertThat(childValues.get(0).getLabel()).isEqualTo("[0]");
    assertThat(getChildren(childValues.get(0)))
        .isEqualTo(ImmutableList.of(getValueProto("key", "a"), getValueProto("value", 1)));
    assertThat(childValues.get(1).getLabel()).isEqualTo("[1]");
    assertThat(getChildren(childValues.get(1)))
        .isEqualTo(ImmutableList.of(getValueProto("key", "b"), getValueProto("value", 2)));
  }

  @Test
  public void testNestedMap() {
    Set<String> set = ImmutableSet.of("a", "b");
    Map<String, Object> map = ImmutableMap.of("a", set);

    Value value = getValueProto("name", map);
    List<Value> childValues = getChildren(value);

    assertTypeAndDescription(map, value);
    assertThat(childValues).hasSize(1);
    assertThat(childValues.get(0).getLabel()).isEqualTo("[0]");
    assertThat(clearIds(getChildren(childValues.get(0))))
        .isEqualTo(
            ImmutableList.of(getValueProto("key", "a"), clearId(getValueProto("value", set))));
  }

  @Test
  public void testSimpleIterable() {
    Iterable<Integer> iter = ImmutableList.of(1, 2);

    Value value = getValueProto("name", iter);
    List<Value> childValues = getChildren(value);

    assertTypeAndDescription(iter, value);
    assertThat(childValues).hasSize(2);
    assertThat(childValues.get(0)).isEqualTo(getValueProto("[0]", 1));
    assertThat(childValues.get(1)).isEqualTo(getValueProto("[1]", 2));
  }

  @Test
  public void testNestedIterable() {
    Iterable<Object> iter = ImmutableList.of(ImmutableList.of(1, 2));

    Value value = getValueProto("name", iter);
    List<Value> childValues = getChildren(value);

    assertTypeAndDescription(iter, value);
    assertThat(childValues).hasSize(1);
    assertValuesEqualIgnoringId(childValues.get(0), getValueProto("[0]", ImmutableList.of(1, 2)));
  }

  @Test
  public void testSimpleArray() {
    int[] array = new int[] {1, 2};

    Value value = getValueProto("name", array);
    List<Value> childValues = getChildren(value);

    assertTypeAndDescription(array, value);
    assertThat(childValues).hasSize(2);
    assertThat(childValues.get(0)).isEqualTo(getValueProto("[0]", 1));
    assertThat(childValues.get(1)).isEqualTo(getValueProto("[1]", 2));
  }

  @Test
  public void testNestedArray() {
    Object[] array = new Object[] {1, ImmutableList.of(2, 3)};

    Value value = getValueProto("name", array);
    List<Value> childValues = getChildren(value);

    assertTypeAndDescription(array, value);
    assertThat(childValues).hasSize(2);
    assertThat(childValues.get(0)).isEqualTo(getValueProto("[0]", 1));
    assertValuesEqualIgnoringId(childValues.get(1), getValueProto("[1]", ImmutableList.of(2, 3)));
  }

  @Test
  public void testUnrecognizedObjectOrStarlarkPrimitiveHasNoChildren() {
    assertThat(getValueProto("name", 1).getHasChildren()).isFalse();
    assertThat(getValueProto("name", "string").getHasChildren()).isFalse();
    assertThat(getValueProto("name", new Object()).getHasChildren()).isFalse();
  }

  @Test
  public void testStarlarkValue() {
    DummyType dummy = new DummyType();

    Value value = getValueProto("name", dummy);
    assertTypeAndDescription(dummy, value);
    assertThat(getChildren(value)).containsExactly(getValueProto("bool", true));
  }

  private static class DummyType implements StarlarkValue {
    @Override
    public void repr(Printer printer) {
      printer.append("DummyType");
    }

    @StarlarkMethod(name = "bool", doc = "Returns True", structField = true)
    public boolean bool() {
      return true;
    }

    public boolean anotherMethod() {
      return false;
    }
  }

  @Test
  public void testSkipStarlarkCallableThrowingException() {
    DummyTypeWithException dummy = new DummyTypeWithException();

    Value value = getValueProto("name", dummy);
    assertTypeAndDescription(dummy, value);
    assertThat(getChildren(value)).containsExactly(getValueProto("bool", true));
  }

  private static class DummyTypeWithException implements StarlarkValue {
    @Override
    public void repr(Printer printer) {
      printer.append("DummyTypeWithException");
    }

    @StarlarkMethod(name = "bool", doc = "Returns True", structField = true)
    public boolean bool() {
      return true;
    }

    @StarlarkMethod(name = "invalid", doc = "Throws exception!", structField = true)
    public boolean invalid() {
      throw new IllegalArgumentException();
    }

    public boolean anotherMethod() {
      return false;
    }
  }

  private static void assertTypeAndDescription(Object object, Value value) {
    assertThat(value.getType()).isEqualTo(Starlark.type(object));
    assertThat(value.getDescription()).isEqualTo(Starlark.repr(object));
  }

  // Type, description, and ID are implementation dependent.
  private void assertEqualIgnoringTypeDescriptionAndId(Value value1, Value value2) {
    assertThat(value1.getLabel()).isEqualTo(value2.getLabel());

    List<Value> children1 = getChildren(value1);
    List<Value> children2 = getChildren(value2);

    assertThat(children1).hasSize(children2.size());
    for (int i = 0; i < children1.size(); i++) {
      assertEqualIgnoringTypeDescriptionAndId(children1.get(i), children2.get(i));
    }
  }

  private void assertValuesEqualIgnoringId(Value value1, Value value2) {
    assertThat(clearId(value1)).isEqualTo(clearId(value2));
  }

  private Value clearId(Value value) {
    return value.toBuilder().clearId().build();
  }

  private List<Value> clearIds(List<Value> values) {
    return values.stream().map(this::clearId).collect(Collectors.toList());
  }
}
