// Copyright 2020 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.query2.cquery;

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

import com.google.common.collect.ImmutableList;
import com.google.common.eventbus.EventBus;
import com.google.devtools.build.lib.events.Event;
import com.google.devtools.build.lib.events.Reporter;
import com.google.devtools.build.lib.packages.LabelPrinter;
import com.google.devtools.build.lib.query2.PostAnalysisQueryEnvironment;
import com.google.devtools.build.lib.query2.engine.QueryEnvironment.Setting;
import com.google.devtools.build.lib.query2.engine.QueryExpression;
import com.google.devtools.build.lib.query2.engine.QueryParser;
import java.io.ByteArrayOutputStream;
import java.io.PrintStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
import org.junit.Before;
import org.junit.Test;

/** Tests cquery's {@link --output=graph} format. */
public class GraphOutputFormatterCallbackTest extends ConfiguredTargetQueryTest {

  private CqueryOptions options;
  private Reporter reporter;
  private final List<Event> events = new ArrayList<>();

  @Before
  public final void defineSimpleRule() throws Exception {
    writeFile(
        "defs/defs.bzl",
        "def _impl(ctx):",
        "  pass",
        "simple_rule = rule(",
        "  implementation = _impl,",
        "  attrs = {",
        "    'deps': attr.label_list(allow_files = True),",
        "    'host_deps': attr.label_list(cfg = 'host')",
        "  }",
        ")");
    writeFile("defs/BUILD");
  }

  @Before
  public final void setUpCqueryOptions() {
    this.options = new CqueryOptions();
    options.graphNodeStringLimit = 512;
    this.reporter = new Reporter(new EventBus(), events::add);
  }

  private List<String> getOutput(String queryExpression) throws Exception {
    QueryExpression expression = QueryParser.parse(queryExpression, getDefaultFunctions());
    Set<String> targetPatternSet = new LinkedHashSet<>();
    expression.collectTargetPatterns(targetPatternSet);
    helper.setQuerySettings(Setting.NO_IMPLICIT_DEPS);
    PostAnalysisQueryEnvironment<KeyedConfiguredTarget> env =
        ((ConfiguredTargetQueryHelper) helper).getPostAnalysisQueryEnvironment(targetPatternSet);

    ByteArrayOutputStream output = new ByteArrayOutputStream();
    GraphOutputFormatterCallback callback =
        new GraphOutputFormatterCallback(
            reporter,
            options,
            new PrintStream(output),
            getHelper().getSkyframeExecutor(),
            env.getAccessor(),
            ct -> env.getFwdDeps(ImmutableList.of(ct)),
            LabelPrinter.legacy());
    env.evaluateQuery(expression, callback);
    return Arrays.asList(output.toString().split(System.lineSeparator()));
  }

  /** Convenience method for easily injecting a config hash into an expected output sequence. */
  private static List<String> withConfigHash(String configHash, String... pattern) {
    return Arrays.stream(pattern)
        .map(entry -> entry.replace("%s", configHash))
        .collect(Collectors.toList());
  }

  @Test
  public void basicGraph() throws Exception {
    writeFile(
        "test/BUILD",
        "load('//defs:defs.bzl', 'simple_rule')",
        "simple_rule(name = 'a', deps = [':b', ':c'])",
        "simple_rule(name = 'b', deps = [':d'])",
        "simple_rule(name = 'c')",
        "simple_rule(name = 'd')");
    List<String> output = getOutput("deps(//test:a)");
    String firstNode = output.get(2);
    String configHash = firstNode.substring(firstNode.indexOf("(") + 1, firstNode.length() - 2);
    assertThat(getOutput("deps(//test:a)"))
        .isEqualTo(
            withConfigHash(
                configHash,
                "digraph mygraph {",
                "  node [shape=box];",
                "  \"//test:a (%s)\"",
                "  \"//test:a (%s)\" -> \"//test:b (%s)\"",
                "  \"//test:a (%s)\" -> \"//test:c (%s)\"",
                "  \"//test:c (%s)\"",
                "  \"//test:b (%s)\"",
                "  \"//test:b (%s)\" -> \"//test:d (%s)\"",
                "  \"//test:d (%s)\"",
                "}"));
  }

  @Test
  public void factorEquivalentNodes() throws Exception {
    options.graphFactored = true;
    writeFile(
        "test/BUILD",
        "load('//defs:defs.bzl', 'simple_rule')",
        "simple_rule(name = 'a', deps = [':b', ':c'])",
        "simple_rule(name = 'b', deps = [':d'])",
        "simple_rule(name = 'c', deps = [':d'])",
        "simple_rule(name = 'd')");
    List<String> output = getOutput("deps(//test:a)");
    String firstNode = output.get(2);
    String configHash = firstNode.substring(firstNode.indexOf("(") + 1, firstNode.length() - 2);
    assertThat(getOutput("deps(//test:a)"))
        .isEqualTo(
            withConfigHash(
                configHash,
                "digraph mygraph {",
                "  node [shape=box];",
                "  \"//test:a (%s)\"",
                "  \"//test:a (%s)\" -> \"//test:b (%s)\\n//test:c (%s)\"",
                "  \"//test:b (%s)\\n//test:c (%s)\"",
                "  \"//test:b (%s)\\n//test:c (%s)\" -> \"//test:d (%s)\"",
                "  \"//test:d (%s)\"",
                "}"));
  }

  // TODO(b/203203933): Replace "host" with "exec" throughout this test.
  @Test
  public void nullAndHostDeps() throws Exception {
    writeFile(
        "test/BUILD",
        "load('//defs:defs.bzl', 'simple_rule')",
        "simple_rule(name = 'a', deps = [':b', ':file.src'], host_deps = [':host_dep'])",
        "simple_rule(name = 'b')",
        "simple_rule(name = 'host_dep')");
    writeFile("test/file.src");
    List<String> output = getOutput("deps(//test:a)");
    String firstNode = output.get(2);
    String configHash = firstNode.substring(firstNode.indexOf("(") + 1, firstNode.length() - 2);
    String hostNode = output.get(6);
    String execConfigHash = hostNode.substring(hostNode.indexOf("(") + 1, hostNode.length() - 2);
    assertThat(getOutput("deps(//test:a)"))
        .isEqualTo(
            withConfigHash(
                configHash,
                "digraph mygraph {",
                "  node [shape=box];",
                "  \"//test:a (%s)\"",
                "  \"//test:a (%s)\" -> \"//test:b (%s)\"",
                "  \"//test:a (%s)\" -> \"//test:file.src (null)\"",
                "  \"//test:a (%s)\" -> \"//test:host_dep (" + execConfigHash + ")\"",
                "  \"//test:host_dep (" + execConfigHash + ")\"",
                "  \"//test:file.src (null)\"",
                "  \"//test:b (%s)\"",
                "}"));
  }

  @Test
  public void selectsResolvedAndRemoved() throws Exception {
    writeFile(
        "test/BUILD",
        "load('//defs:defs.bzl', 'simple_rule')",
        "config_setting(name = 'use_a', define_values = {'a': '1'})",
        "simple_rule(name = 'a', deps = select({",
        "    ':use_a': [':dep_with_a'],",
        "    '//conditions:default': [':default_dep'],",
        "}))",
        "simple_rule(name = 'dep_with_a')",
        "simple_rule(name = 'default_dep')");
    getHelper().useConfiguration("--define", "a=1");
    List<String> output = getOutput("deps(//test:a)");
    String firstNode = output.get(2);
    String configHash = firstNode.substring(firstNode.indexOf("(") + 1, firstNode.length() - 2);
    assertThat(getOutput("deps(//test:a)"))
        .isEqualTo(
            withConfigHash(
                configHash,
                "digraph mygraph {",
                "  node [shape=box];",
                "  \"//test:a (%s)\"",
                "  \"//test:a (%s)\" -> \"//test:dep_with_a (%s)\"",
                "  \"//test:a (%s)\" -> \"//test:use_a (%s)\"",
                "  \"//test:use_a (%s)\"",
                "  \"//test:dep_with_a (%s)\"",
                "}"));
  }
}
