// Copyright 2016 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.runtime;

import static org.junit.Assert.assertEquals;

import com.google.common.collect.Collections2;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import com.google.devtools.build.lib.analysis.BlazeDirectories;
import com.google.devtools.build.lib.analysis.ConfigurationCollectionFactory;
import com.google.devtools.build.lib.analysis.ConfiguredRuleClassProvider;
import com.google.devtools.build.lib.analysis.ServerDirectories;
import com.google.devtools.build.lib.analysis.config.BuildConfiguration;
import com.google.devtools.build.lib.analysis.config.FragmentOptions;
import com.google.devtools.build.lib.cmdline.Label;
import com.google.devtools.build.lib.runtime.BlazeCommandDispatcher.LockingMode;
import com.google.devtools.build.lib.runtime.BlazeCommandDispatcher.ShutdownBlazeServerException;
import com.google.devtools.build.lib.testutil.Scratch;
import com.google.devtools.build.lib.testutil.TestConstants;
import com.google.devtools.build.lib.util.ExitCode;
import com.google.devtools.build.lib.util.io.RecordingOutErr;
import com.google.devtools.common.options.Option;
import com.google.devtools.common.options.OptionsBase;
import com.google.devtools.common.options.OptionsParser;
import com.google.devtools.common.options.OptionsProvider;

import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
import org.mockito.Mockito;

import java.util.List;
import java.util.Map;
import java.util.Set;

/**
 * Tests the handling of rc-options in {@link BlazeCommandDispatcher}.
 */
@RunWith(JUnit4.class)
public class BlazeCommandDispatcherRcoptionsTest {

  /**
   * Example options to be used by the tests.
   */
  public static class FooOptions extends OptionsBase {
    @Option(name = "numoption", defaultValue = "0")
    public int numOption;

    @Option(name = "stringoption", defaultValue = "[unspecified]")
    public String stringOption;
  }

  @Command(
    name = "reportnum",
    options = {FooOptions.class},
    shortDescription = "",
    help = ""
  )
  private static class ReportNumCommand implements BlazeCommand {

    @Override
    public ExitCode exec(CommandEnvironment env, OptionsProvider options)
        throws ShutdownBlazeServerException {
      FooOptions fooOptions = options.getOptions(FooOptions.class);
      env.getReporter().getOutErr().printOut("" + fooOptions.numOption);
      return ExitCode.SUCCESS;
    }

    @Override
    public void editOptions(CommandEnvironment env, OptionsParser optionsParser) {}
  }

  @Command(
    name = "reportall",
    options = {FooOptions.class},
    shortDescription = "",
    help = ""
  )
  private static class ReportAllCommand implements BlazeCommand {

    @Override
    public ExitCode exec(CommandEnvironment env, OptionsProvider options)
        throws ShutdownBlazeServerException {
      FooOptions fooOptions = options.getOptions(FooOptions.class);
      env.getReporter()
          .getOutErr()
          .printOut("" + fooOptions.numOption + " " + fooOptions.stringOption);
      return ExitCode.SUCCESS;
    }

    @Override
    public void editOptions(CommandEnvironment env, OptionsParser optionsParser) {}
  }

  @Command(
    name = "reportallinherited",
    options = {FooOptions.class},
    shortDescription = "",
    help = "",
    inherits = ReportAllCommand.class
  )
  private static class ReportAllInheritedCommand extends ReportAllCommand {
  }


  private final Scratch scratch = new Scratch();
  private final RecordingOutErr outErr = new RecordingOutErr();
  private final ReportNumCommand reportNum = new ReportNumCommand();
  private final ReportAllCommand reportAll = new ReportAllCommand();
  private final ReportAllCommand reportAllInherited = new ReportAllInheritedCommand();
  private BlazeRuntime runtime;

  @Before
  public final void initializeRuntime() throws Exception {
    String productName = TestConstants.PRODUCT_NAME;
    ServerDirectories serverDirectories =
        new ServerDirectories(scratch.dir("install_base"), scratch.dir("output_base"));
    this.runtime =
        new BlazeRuntime.Builder()
            .setProductName(productName)
            .setServerDirectories(serverDirectories)
            .setStartupOptionsProvider(
                OptionsParser.newOptionsParser(BlazeServerStartupOptions.class))
            .addBlazeModule(
                new BlazeModule() {
                  @Override
                  public void initializeRuleClasses(ConfiguredRuleClassProvider.Builder builder) {
                    // We must add these options so that the defaults package can be created.
                    builder.addConfigurationOptions(BuildConfiguration.Options.class);
                    // The defaults package asserts that it is not empty, so we provide options.
                    builder.addConfigurationOptions(MockFragmentOptions.class);
                    // The tools repository is needed for createGlobals
                    builder.setToolsRepository(TestConstants.TOOLS_REPOSITORY);
                    builder.setConfigurationCollectionFactory(
                        Mockito.mock(ConfigurationCollectionFactory.class));
                  }
                })
            .build();

    BlazeDirectories directories =
        new BlazeDirectories(serverDirectories, scratch.dir("pkg"), productName);
    this.runtime.initWorkspace(directories, /*binTools=*/null);
  }

  @Test
  public void testCommonUsed() throws Exception {
    List<String> blazercOpts =
        ImmutableList.of(
            "--rc_source=/home/jrluser/.blazerc", "--default_override=0:common=--numoption=99");

    BlazeCommandDispatcher dispatch = new BlazeCommandDispatcher(runtime, reportNum);
    List<String> cmdLine = Lists.newArrayList("reportnum");
    cmdLine.addAll(blazercOpts);

    dispatch.exec(cmdLine, LockingMode.ERROR_OUT, "test", outErr);
    String out = outErr.outAsLatin1();
    assertEquals("Common options should be used", "99", out);
  }

  @Test
  public void testSpecificOptionsWin() throws Exception {
    List<String> blazercOpts =
        ImmutableList.of(
            "--rc_source=/home/jrluser/.blazerc",
            "--default_override=0:reportnum=--numoption=42",
            "--default_override=0:common=--numoption=99");

    BlazeCommandDispatcher dispatch = new BlazeCommandDispatcher(runtime, reportNum);
    List<String> cmdLine = Lists.newArrayList("reportnum");
    cmdLine.addAll(blazercOpts);

    dispatch.exec(cmdLine, LockingMode.ERROR_OUT, "test", outErr);
    String out = outErr.outAsLatin1();
    assertEquals("Specific options should dominate common options", "42", out);
  }

  @Test
  public void testSpecificOptionsWinOtherOrder() throws Exception {
    List<String> blazercOpts =
        ImmutableList.of(
            "--rc_source=/home/jrluser/.blazerc",
            "--default_override=0:common=--numoption=99",
            "--default_override=0:reportnum=--numoption=42");

    BlazeCommandDispatcher dispatch = new BlazeCommandDispatcher(runtime, reportNum);
    List<String> cmdLine = Lists.newArrayList("reportnum");
    cmdLine.addAll(blazercOpts);

    dispatch.exec(cmdLine, LockingMode.ERROR_OUT, "test", outErr);
    String out = outErr.outAsLatin1();
    assertEquals("Specific options should dominate common options", "42", out);
  }

  @Test
  public void testOptionsCombined() throws Exception {
    List<String> blazercOpts =
        ImmutableList.of(
            "--rc_source=/etc/bazelrc",
            "--default_override=0:common=--stringoption=foo",
            "--rc_source=/home/jrluser/.blazerc",
            "--default_override=1:common=--numoption=99");

    BlazeCommandDispatcher dispatch = new BlazeCommandDispatcher(runtime, reportNum, reportAll);
    List<String> cmdLine = Lists.newArrayList("reportall");
    cmdLine.addAll(blazercOpts);

    dispatch.exec(cmdLine, LockingMode.ERROR_OUT, "test", outErr);
    String out = outErr.outAsLatin1();
    assertEquals("Options should get accumulated over different rc files", "99 foo", out);
  }

  @Test
  public void testOptionsCombinedWithOverride() throws Exception {
    List<String> blazercOpts =
        ImmutableList.of(
            "--rc_source=/etc/bazelrc",
            "--default_override=0:common=--stringoption=foo",
            "--default_override=0:common=--numoption=42",
            "--rc_source=/home/jrluser/.blazerc",
            "--default_override=1:common=--numoption=99");

    BlazeCommandDispatcher dispatch = new BlazeCommandDispatcher(runtime, reportNum, reportAll);
    List<String> cmdLine = Lists.newArrayList("reportall");
    cmdLine.addAll(blazercOpts);

    dispatch.exec(cmdLine, LockingMode.ERROR_OUT, "test", outErr);
    String out = outErr.outAsLatin1();
    assertEquals("The more specific rc-file should override", "99 foo", out);
  }

  @Test
  public void testOptionsCombinedWithOverrideOtherName() throws Exception {
    List<String> blazercOpts =
        ImmutableList.of(
            "--rc_source=/home/jrluser/.blazerc",
            "--default_override=0:common=--stringoption=foo",
            "--default_override=0:common=--numoption=42",
            "--rc_source=/etc/bazelrc",
            "--default_override=1:common=--numoption=99");

    BlazeCommandDispatcher dispatch = new BlazeCommandDispatcher(runtime, reportNum, reportAll);
    List<String> cmdLine = Lists.newArrayList("reportall");
    cmdLine.addAll(blazercOpts);

    dispatch.exec(cmdLine, LockingMode.ERROR_OUT, "test", outErr);
    String out = outErr.outAsLatin1();
    assertEquals("The more specific rc-file should override irrespective of name", "99 foo", out);
  }

  @Test
  public void testInheritedOptionsWithSpecificOverride() throws Exception {
    ImmutableList<ImmutableList<String>> blazercOpts =
        ImmutableList.of(
            ImmutableList.of(
                "--rc_source=/doesnt/matter/0/bazelrc",
                "--default_override=0:common=--stringoption=common",
                "--default_override=0:common=--numoption=42"),
            ImmutableList.of(
                "--rc_source=/doesnt/matter/1/bazelrc",
                "--default_override=0:reportall=--stringoption=reportall"),
            ImmutableList.of(
                "--rc_source=/doesnt/matter/2/bazelrc",
                "--default_override=0:reportallinherited=--stringoption=reportallinherited"));

    for (List<ImmutableList<String>> e : Collections2.permutations(blazercOpts)) {
      outErr.reset();
      BlazeCommandDispatcher dispatch =
          new BlazeCommandDispatcher(runtime, reportNum, reportAll, reportAllInherited);
      List<String> cmdLine = Lists.newArrayList("reportallinherited");
      List<String> orderedOpts = ImmutableList.copyOf(Iterables.concat(e));
      cmdLine.addAll(orderedOpts);

      dispatch.exec(cmdLine, LockingMode.ERROR_OUT, "test", outErr);
      String out = outErr.outAsLatin1();
      assertEquals(
          String.format(
              "The more specific option should override, irrespective of source file or order. %s",
              orderedOpts),
          "42 reportallinherited",
          out);
    }
  }

  /** Options class for testing, so that defaults package has some content. */
  public static class MockFragmentOptions extends FragmentOptions {
    public MockFragmentOptions() {}

    @Option(name = "fake_opt", defaultValue = "false")
    public boolean fakeOpt;

    @Override
    public Map<String, Set<Label>> getDefaultsLabels(BuildConfiguration.Options commonOptions) {
      return ImmutableMap.<String, Set<Label>>of(
          "mock_target", ImmutableSet.of(Label.parseAbsoluteUnchecked("//mock:target")));
    }
  }
}
