// Copyright 2017 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.skylark.skylint;

import com.google.common.collect.ImmutableMap;
import com.google.common.truth.Truth;
import com.google.devtools.build.lib.syntax.BuildFileAST;
import com.google.devtools.build.lib.syntax.FunctionDefStatement;
import com.google.devtools.build.lib.syntax.LoadStatement;
import com.google.devtools.build.lib.syntax.Statement;
import com.google.devtools.skylark.skylint.DependencyAnalyzer.DependencyCollector;
import com.google.devtools.skylark.skylint.Linter.FileFacade;
import java.nio.charset.StandardCharsets;
import java.nio.file.NoSuchFileException;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;

/** Tests for {@link com.google.devtools.skylark.skylint.DependencyAnalyzer} */
@RunWith(JUnit4.class)
public class DependencyAnalyzerTest {
  private static final DependencyCollector<List<String>> functionCollector =
      new DependencyCollector<List<String>>() {
        @Override
        public List<String> initInfo(Path path) {
          return new ArrayList<>();
        }

        @Override
        public List<String> loadDependency(
            List<String> currentFileInfo,
            LoadStatement stmt,
            Path loadedPath,
            List<String> loadedFileInfo) {
          for (String name : loadedFileInfo) {
            currentFileInfo.add(loadedPath + " - " + name);
          }
          return currentFileInfo;
        }

        @Override
        public List<String> collectInfo(Path path, BuildFileAST ast, List<String> info) {
          for (Statement stmt : ast.getStatements()) {
            if (stmt instanceof FunctionDefStatement) {
              info.add(((FunctionDefStatement) stmt).getIdentifier().getName());
            }
          }
          return info;
        }
      };

  public static FileFacade toFileFacade(Map<String, String> files) {
    return path -> {
      String contents = files.get(path.toString());
      if (contents == null) {
        throw new NoSuchFileException(path.toString());
      }
      return contents.getBytes(StandardCharsets.ISO_8859_1);
    };
  }

  private static List<String> getFunctionNames(Map<String, String> files, String path) {
    return new DependencyAnalyzer<>(toFileFacade(files), functionCollector)
        .collectTransitiveInfo(Paths.get(path));
  }

  @Test
  public void externalRepositoryDependency() throws Exception {
    Map<String, String> files =
        ImmutableMap.<String, String>builder()
            .put("/WORKSPACE", "")
            .put("/BUILD", "")
            .put("/test.bzl", "load('@some-repo//foo:bar.bzl', 'baz')")
            .build();
    Truth.assertThat(getFunctionNames(files, "/test.bzl")).isEmpty();
  }

  @Test
  public void nonexistentDependency() throws Exception {
    Map<String, String> files =
        ImmutableMap.<String, String>builder()
            .put("/WORKSPACE", "")
            .put("/BUILD", "")
            .put("/test.bzl", "load(':does_not_exist.bzl', 'baz')")
            .build();
    Truth.assertThat(getFunctionNames(files, "/test.bzl")).isEmpty();
  }

  @Test
  public void samePackageDependency() throws Exception {
    Map<String, String> files =
        ImmutableMap.<String, String>builder()
            .put("/WORKSPACE", "")
            .put("/BUILD", "")
            .put("/test.bzl", "load(':bar.bzl', 'baz')\nload('//:bar.bzl', 'baz')")
            .put("/bar.bzl", "def baz(): pass")
            .build();
    Truth.assertThat(getFunctionNames(files, "/test.bzl"))
        .isEqualTo(Arrays.asList("/bar.bzl - baz", "/bar.bzl - baz"));
  }

  @Test
  public void samePackageDependencyWithoutBuildFile() throws Exception {
    Map<String, String> files =
        ImmutableMap.<String, String>builder()
            .put("/WORKSPACE", "")
            .put("/test.bzl", "load(':bar.bzl', 'baz')")
            .put("/bar.bzl", "def baz(): pass")
            .build();
    Truth.assertThat(getFunctionNames(files, "/test.bzl")).isEmpty();
  }

  @Test
  public void subpackageDependency() throws Exception {
    Map<String, String> files =
        ImmutableMap.<String, String>builder()
            .put("/WORKSPACE", "")
            .put("/BUILD", "")
            .put("/test.bzl", "load('subpackage:foo.bzl', 'foo')")
            .put("/subpackage/BUILD", "")
            .put("/subpackage/foo.bzl", "def foo(): pass")
            .build();
    Truth.assertThat(getFunctionNames(files, "/test.bzl"))
        .isEqualTo(Collections.singletonList("/subpackage/foo.bzl - foo"));
  }

  @Test
  public void dependencyInSiblingPackage() throws Exception {
    Map<String, String> files =
        ImmutableMap.<String, String>builder()
            .put("/WORKSPACE", "")
            .put("/pkg1/BUILD", "")
            .put("/pkg1/test.bzl", "load('//pkg2:bar.bzl')")
            .put("/pkg2/BUILD", "")
            .put("/pkg2/bar.bzl", "def bar(): pass")
            .build();
    Truth.assertThat(getFunctionNames(files, "/pkg1/test.bzl"))
        .isEqualTo(Collections.singletonList("/pkg2/bar.bzl - bar"));
  }

  @Test
  public void dependencyInSiblingPackageWithBuildDotBazelFile() throws Exception {
    Map<String, String> files =
        ImmutableMap.<String, String>builder()
            .put("/WORKSPACE", "")
            .put("/pkg1/BUILD.bazel", "")
            .put("/pkg1/test.bzl", "load('//pkg2:bar.bzl')")
            .put("/pkg2/BUILD.bazel", "")
            .put("/pkg2/bar.bzl", "def bar(): pass")
            .build();
    Truth.assertThat(getFunctionNames(files, "/pkg1/test.bzl"))
        .isEqualTo(Collections.singletonList("/pkg2/bar.bzl - bar"));
  }

  @Test
  public void dependencyLabelWithoutPrefix() throws Exception {
    Map<String, String> files =
        ImmutableMap.<String, String>builder()
            .put("/WORKSPACE", "")
            .put("/test/test.bzl", "load('bar.bzl', 'baz')")
            .put("/bar.bzl", "def baz(): pass")
            .build();
    Truth.assertThat(getFunctionNames(files, "/test/test.bzl"))
        .isEqualTo(Collections.singletonList("/bar.bzl - baz"));
  }

  @Test
  public void transitiveDependencies() throws Exception {
    Map<String, String> files =
        ImmutableMap.<String, String>builder()
            .put("/WORKSPACE", "")
            .put("/BUILD", "")
            .put("/test.bzl", "load(':foo.bzl', 'foo')")
            .put("/foo.bzl", "load(':bar.bzl', foo = 'bar')")
            .put("/bar.bzl", "def bar(): pass")
            .build();
    Truth.assertThat(getFunctionNames(files, "/test.bzl"))
        .isEqualTo(Collections.singletonList("/foo.bzl - /bar.bzl - bar"));
  }

  @Test
  public void diamondDependencies() throws Exception {
    Map<String, String> files =
        ImmutableMap.<String, String>builder()
            .put("/WORKSPACE", "")
            .put("/BUILD", "")
            .put("/test.bzl", "load(':foo.bzl', 'foo')\nload(':bar.bzl', 'bar')")
            .put("/foo.bzl", "load(':base.bzl', foo = 'base')")
            .put("/bar.bzl", "load(':base.bzl', bar = 'base')")
            .put("/base.bzl", "def base(): pass")
            .build();
    Truth.assertThat(getFunctionNames(files, "/test.bzl"))
        .isEqualTo(Arrays.asList("/foo.bzl - /base.bzl - base", "/bar.bzl - /base.bzl - base"));
  }

  @Test
  public void cyclicDependenciesAreHandledGracefully() throws Exception {
    Map<String, String> files =
        ImmutableMap.<String, String>builder()
            .put("/WORKSPACE", "")
            .put("/BUILD", "")
            .put("/test.bzl", "load(':test.bzl', 'foo')\ndef test(): pass")
            .build();
    Truth.assertThat(getFunctionNames(files, "/test.bzl"))
        .isEqualTo(Collections.singletonList("test"));

    files =
        ImmutableMap.<String, String>builder()
            .put("/WORKSPACE", "")
            .put("/BUILD", "")
            .put("/test.bzl", "load(':foo.bzl', 'baz')\ndef test(): pass")
            .put("/foo.bzl", "load(':bar.bzl', 'baz')")
            .put("/bar.bzl", "load(':foo.bzl', 'baz')")
            .build();
    Truth.assertThat(getFunctionNames(files, "/test.bzl"))
        .isEqualTo(Collections.singletonList("test"));
  }
}
