// Copyright 2015 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.docgen;

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

import com.google.common.base.Function;
import com.google.common.collect.Iterables;
import com.google.devtools.build.docgen.skylark.SkylarkBuiltinMethodDoc;
import com.google.devtools.build.docgen.skylark.SkylarkJavaMethodDoc;
import com.google.devtools.build.docgen.skylark.SkylarkMethodDoc;
import com.google.devtools.build.docgen.skylark.SkylarkModuleDoc;
import com.google.devtools.build.lib.rules.SkylarkRuleContext;
import com.google.devtools.build.lib.skylark.util.SkylarkTestCase;
import com.google.devtools.build.lib.skylarkinterface.Param;
import com.google.devtools.build.lib.skylarkinterface.SkylarkCallable;
import com.google.devtools.build.lib.skylarkinterface.SkylarkModule;
import com.google.devtools.build.lib.syntax.Environment;
import com.google.devtools.build.lib.syntax.util.EvaluationTestCase;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.TreeMap;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;

/**
 * Tests for Skylark documentation.
 */
@RunWith(JUnit4.class)
public class SkylarkDocumentationTest extends SkylarkTestCase {

  @Before
  public final void createBuildFile() throws Exception {
    scratch.file("foo/BUILD",
        "genrule(name = 'foo',",
        "  cmd = 'dummy_cmd',",
        "  srcs = ['a.txt', 'b.img'],",
        "  tools = ['t.exe'],",
        "  outs = ['c.txt'])");
  }

  @Override
  protected EvaluationTestCase createEvaluationTestCase() {
    return new EvaluationTestCase();
  }

  @Test
  public void testSkylarkRuleClassBuiltInItemsAreDocumented() throws Exception {
    checkSkylarkTopLevelEnvItemsAreDocumented(ev.getEnvironment());
  }

  @Test
  public void testSkylarkRuleImplementationBuiltInItemsAreDocumented() throws Exception {
    // TODO(bazel-team): fix documentation for built in java objects coming from modules.
    checkSkylarkTopLevelEnvItemsAreDocumented(ev.getEnvironment());
  }

  @SuppressWarnings("unchecked")
  private void checkSkylarkTopLevelEnvItemsAreDocumented(Environment env) {
    Map<String, String> docMap = new HashMap<>();
    Map<String, SkylarkModuleDoc> modules = SkylarkDocumentationCollector.collectModules();
    SkylarkModuleDoc topLevel =
        modules.remove(SkylarkDocumentationCollector.getTopLevelModule().name());
    for (Entry<String, SkylarkBuiltinMethodDoc> entry : topLevel.getBuiltinMethods().entrySet()) {
      docMap.put(entry.getKey(), entry.getValue().getDocumentation());
    }
    for (Entry<String, SkylarkModuleDoc> entry : modules.entrySet()) {
      docMap.put(entry.getKey(), entry.getValue().getDocumentation());
    }

    List<String> undocumentedItems = new ArrayList<>();
    // All built in variables are registered in the Skylark global environment.
    for (String varname : env.getVariableNames()) {
      if (docMap.containsKey(varname)) {
        if (docMap.get(varname).isEmpty()) {
          undocumentedItems.add(varname);
        }
      } else {
        undocumentedItems.add(varname);
      }
    }
    assertWithMessage("Undocumented Skylark Environment items: " + undocumentedItems)
        .that(undocumentedItems).isEmpty();
  }

  // TODO(bazel-team): come up with better Skylark specific tests.
  @Test
  public void testDirectJavaMethodsAreGenerated() throws Exception {
    assertThat(collect(SkylarkRuleContext.class)).isNotEmpty();
  }

  /** MockClassA */
  @SkylarkModule(name = "MockClassA", doc = "MockClassA")
  private static class MockClassA {
    @SkylarkCallable(doc = "MockClassA#get")
    public Integer get() {
      return 0;
    }
  }

  /** MockClassB */
  @SkylarkModule(name = "MockClassB", doc = "MockClassB")
  private static class MockClassB {
    @SkylarkCallable(doc = "MockClassB#get")
    public MockClassA get() {
      return new MockClassA();
    }
  }

  /** MockClassC */
  @SkylarkModule(name = "MockClassC", doc = "MockClassC")
  private static class MockClassC extends MockClassA {
    @SkylarkCallable(doc = "MockClassC#get2")
    public Integer get2() {
      return 0;
    }
  }

  /** MockClassD */
  @SkylarkModule(name = "MockClassD", doc = "MockClassD")
  private static class MockClassD {
    @SkylarkCallable(
      doc = "MockClassD#test",
      mandatoryPositionals = 1,
      parameters = {
        @Param(name = "b"),
        @Param(name = "c", named = true, positional = false),
        @Param(name = "d", named = true, positional = false, defaultValue = "1"),
      }
    )
    public Integer test(int a, int b, int c, int d) {
      return 0;
    }
  }

  /** MockClassE */
  @SkylarkModule(name = "MockClassE", doc = "MockClassE")
  private static class MockClassE extends MockClassA {
    @Override
    public Integer get() {
      return 1;
    }
  }

  @Test
  public void testSkylarkJavaInterfaceExplorerOnSimpleClass() throws Exception {
    Map<String, SkylarkModuleDoc> objects = collect(MockClassA.class);
    assertThat(extractMethods(Iterables.getOnlyElement(objects.values())
        .getJavaMethods())).containsExactly(MockClassA.class.getMethod("get"));
  }

  @Test
  public void testSkylarkJavaInterfaceExplorerFindsClassFromReturnValue() throws Exception {
    Map<String, SkylarkModuleDoc> objects = collect(MockClassB.class);
    assertThat(extractMethods(
        objects.get("MockClassA").getJavaMethods())).containsExactly(
        MockClassA.class.getMethod("get"));
  }

  @Test
  public void testSkylarkJavaInterfaceExplorerFindsAllMethodsOnSubClass() throws Exception {
    Map<String, SkylarkModuleDoc> objects = collect(MockClassC.class);
    assertThat(extractMethods(Iterables.getOnlyElement(objects.values())
        .getJavaMethods())).containsExactly(
        MockClassA.class.getMethod("get"), MockClassC.class.getMethod("get2"));
  }

  @Test
  public void testSkylarkCallableParameters() throws Exception {
    Map<String, SkylarkModuleDoc> objects = collect(MockClassD.class);
    assertThat(objects).hasSize(1);
    assertThat(objects).containsKey("MockClassD");
    SkylarkModuleDoc moduleDoc = objects.get("MockClassD");
    assertThat(moduleDoc.getDocumentation()).isEqualTo("MockClassD");
    assertThat(moduleDoc.getMethods()).hasSize(1);
    SkylarkMethodDoc methodDoc = moduleDoc.getMethods().iterator().next();
    assertThat(methodDoc.getDocumentation()).isEqualTo("MockClassD#test");
    assertThat(methodDoc.getSignature()).isEqualTo("int MockClassD.test(arg0:int, b, *, c, d=1)");
    assertThat(methodDoc.getParams()).hasSize(3);
  }

  @Test
  public void testSkylarkCallableOverriding() throws Exception {
    Map<String, SkylarkModuleDoc> objects = collect(MockClassE.class);
    assertThat(objects).hasSize(1);
    assertThat(objects).containsKey("MockClassE");
    SkylarkModuleDoc moduleDoc = objects.get("MockClassE");
    assertThat(moduleDoc.getDocumentation()).isEqualTo("MockClassE");
    assertThat(moduleDoc.getMethods()).hasSize(1);
    SkylarkMethodDoc methodDoc = moduleDoc.getMethods().iterator().next();
    assertThat(methodDoc.getDocumentation()).isEqualTo("MockClassA#get");
    assertThat(methodDoc.getSignature()).isEqualTo("int MockClassE.get()");
  }

  private Iterable<Method> extractMethods(Collection<SkylarkJavaMethodDoc> methods) {
    return Iterables.transform(methods, new Function<SkylarkJavaMethodDoc, Method>() {
      @Override
      public Method apply(SkylarkJavaMethodDoc input) {
        return input.getMethod();
      }
    });
  }

  private Map<String, SkylarkModuleDoc> collect(Class<?> classObject) {
    Map<String, SkylarkModuleDoc> modules = new TreeMap<>();
    SkylarkDocumentationCollector.collectJavaObjects(
        classObject.getAnnotation(SkylarkModule.class), classObject, modules);
    return modules;
  }
}
