// 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.

#include <vector>

#include "src/main/cpp/blaze_util.h"
#include "src/main/cpp/blaze_util_platform.h"
#include "src/main/cpp/option_processor.h"
#include "src/main/cpp/util/file.h"
#include "src/main/cpp/util/file_platform.h"
#include "src/main/cpp/util/strings.h"
#include "src/main/cpp/workspace_layout.h"
#include "googletest/include/gtest/gtest.h"

namespace blaze {
using std::string;
using std::unordered_map;
using std::vector;

class RcOptionsTest : public ::testing::Test {
 protected:
  RcOptionsTest()
      : test_file_dir_(blaze::GetEnv("TEST_TMPDIR")),
        workspace_layout_() {}

  const string test_file_dir_;
  const WorkspaceLayout workspace_layout_;

  void WriteRc(const string& filename, const string& contents) {
    bool success = blaze_util::WriteFile(
        contents, blaze_util::JoinPath(test_file_dir_, filename));
    ASSERT_TRUE(success) << "Failed to write " << filename;
  }

  std::unique_ptr<RcFile> Parse(const string& filename,
                                RcFile::ParseError* error,
                                std::string* error_text) {
    return RcFile::Parse(
        blaze_util::JoinPath(test_file_dir_, filename),
        &workspace_layout_,
        // Set workspace to test_file_dir_ so importing %workspace%/foo works.
        test_file_dir_,
        error,
        error_text);
  }

  void SuccessfullyParseRcWithExpectedArgs(
      const string& filename,
      const unordered_map<string, vector<string>>& expected_args_map) {
    RcFile::ParseError error;
    string error_text;
    std::unique_ptr<RcFile> rc = Parse(filename, &error, &error_text);
    EXPECT_EQ(error_text, "");
    ASSERT_EQ(error, RcFile::ParseError::NONE);

    // Test that exactly each command in the expected map was in the results,
    // and that for each of these, exactly the expected args are found, in the
    // correct order. Note that this is not just an exercise in rewritting map
    // equality - the results have type RcOption, and the expected values
    // are just strings. This is ignoring the source_path for convenience.
    const RcFile::OptionMap& result = rc->options();
    ASSERT_EQ(expected_args_map.size(), result.size());
    for (const auto& command_args_pair : expected_args_map) {
      const string& expected_command = command_args_pair.first;
      const vector<string>& expected_args = command_args_pair.second;
      const auto result_args_iter = result.find(expected_command);
      ASSERT_NE(result_args_iter, rc->options().end());
      const std::vector<RcOption>& result_args = result_args_iter->second;
      ASSERT_EQ(result_args.size(), expected_args.size());
      for (size_t i = 0; i < result_args.size(); ++i) {
        EXPECT_EQ(result_args[i].option, expected_args[i]);
      }
    }
  }
};

// Effectively empty file tests

TEST_F(RcOptionsTest, Empty) {
  WriteRc("empty.bazelrc",
          "");
  unordered_map<string, vector<string>> no_expected_args;
  SuccessfullyParseRcWithExpectedArgs("empty.bazelrc", no_expected_args);
}

TEST_F(RcOptionsTest, Whitespace) {
  WriteRc("whitespace.bazelrc",
          "      \n\t      ");
  unordered_map<string, vector<string>> no_expected_args;
  SuccessfullyParseRcWithExpectedArgs("whitespace.bazelrc", no_expected_args);
}

TEST_F(RcOptionsTest, CommentedStartup) {
  WriteRc("commented_startup.bazelrc",
          "# startup foo");
  unordered_map<string, vector<string>> no_expected_args;
  SuccessfullyParseRcWithExpectedArgs("whitespace.bazelrc", no_expected_args);
}

TEST_F(RcOptionsTest, EmptyStartupLine) {
  WriteRc("empty_startup_line.bazelrc",
          "startup");
  unordered_map<string, vector<string>> no_expected_args;
  SuccessfullyParseRcWithExpectedArgs("empty_startup_line.bazelrc",
                                      no_expected_args);
}

TEST_F(RcOptionsTest, StartupWithOnlyCommentedArg) {
  WriteRc("startup_with_comment.bazelrc",
          "startup # bar");
  unordered_map<string, vector<string>> no_expected_args;
  SuccessfullyParseRcWithExpectedArgs("startup_with_comment.bazelrc",
                                      no_expected_args);
}

// Single command tests - testing tokenization and accumulation of arguments.

TEST_F(RcOptionsTest, SingleStartupArg) {
  WriteRc("startup_foo.bazelrc",
          "startup foo");
  SuccessfullyParseRcWithExpectedArgs(
      "startup_foo.bazelrc",
      {{"startup", {"foo"}}});
}

TEST_F(RcOptionsTest, SingleStartupArgWithComment) {
  WriteRc("startup_foo_and_comment.bazelrc",
          "startup foo # comment");
  SuccessfullyParseRcWithExpectedArgs(
      "startup_foo_and_comment.bazelrc",
      {{"startup", {"foo"}}});
}

TEST_F(RcOptionsTest, TwoStartupArgsOnOneLine) {
  WriteRc("startup_foo_bar.bazelrc",
          "startup foo bar");
  SuccessfullyParseRcWithExpectedArgs(
      "startup_foo_bar.bazelrc",
      {{"startup", {"foo", "bar"}}});
}

TEST_F(RcOptionsTest, TwoStartupArgsOnOneLineTabSeparated) {
  WriteRc("startup_with_tabs.bazelrc",
          "startup\tfoo\tbar");
  SuccessfullyParseRcWithExpectedArgs(
      "startup_with_tabs.bazelrc",
      {{"startup", {"foo", "bar"}}});
}

TEST_F(RcOptionsTest, StartupOptWithSimpleValue) {
  WriteRc("startup_opt_with_simple_value.bazelrc",
          "startup --opt=foo");
  SuccessfullyParseRcWithExpectedArgs(
      "startup_opt_with_simple_value.bazelrc",
      {{"startup", {"--opt=foo"}}});
}

TEST_F(RcOptionsTest, StartupQuotedArg) {
  WriteRc("startup_quoted_foo_bar.bazelrc",
          "startup \"foo bar\"");
  SuccessfullyParseRcWithExpectedArgs(
      "startup_quoted_foo_bar.bazelrc",
      {{"startup", {"foo bar"}}});
}

TEST_F(RcOptionsTest, QuotedValueStartupArgAfterEquals) {
  WriteRc("startup_opt_quoted_arg.bazelrc",
          "startup --opt=\"foo bar\"");
  SuccessfullyParseRcWithExpectedArgs(
      "startup_opt_quoted_arg.bazelrc",
      {{"startup", {"--opt=foo bar"}}});
}

TEST_F(RcOptionsTest, QuotedValueStartupArgAfterWhitespace) {
  WriteRc("startup_opt_quoted_arg_as_separate_token.bazelrc",
          "startup --opt \"foo bar\"");
  SuccessfullyParseRcWithExpectedArgs(
      "startup_opt_quoted_arg_as_separate_token.bazelrc",
      {{"startup", {"--opt", "foo bar"}}});
}

TEST_F(RcOptionsTest, QuotedValueStartupArgOnNewLine) {
  WriteRc("startup_opt_quoted_arg_different_line.bazelrc",
          "startup --opt\n"
          "startup \"foo bar\"");
  SuccessfullyParseRcWithExpectedArgs(
      "startup_opt_quoted_arg_different_line.bazelrc",
      {{"startup", {"--opt", "foo bar"}}});
}

TEST_F(RcOptionsTest, TwoOptStartup) {
  WriteRc("startup_two_options.bazelrc",
          "startup --opt1\n"
          "startup --opt2");
  SuccessfullyParseRcWithExpectedArgs(
      "startup_two_options.bazelrc",
      {{"startup", {"--opt1", "--opt2"}}});
}

TEST_F(RcOptionsTest, WhitespaceBeforeStartup) {
  WriteRc("whitespace_before_command.bazelrc",
          "  startup foo\n"
          "        # indented comments\n"
          "startup bar\n"
          "\tstartup     \t baz");
  SuccessfullyParseRcWithExpectedArgs(
      "whitespace_before_command.bazelrc",
      {{"startup", {"foo", "bar", "baz"}}});
}

TEST_F(RcOptionsTest, StartupLineContinuation) {
  WriteRc("startup_line_continuation.bazelrc",
          "startup \\\n"
          "foo\n"
          "startup bar \\\n"
          "baz");
  SuccessfullyParseRcWithExpectedArgs(
      "startup_line_continuation.bazelrc",
      {{"startup", {"foo", "bar", "baz"}}});
}

TEST_F(RcOptionsTest, ManyArgStartup) {
  WriteRc("startup_with_many_args.bazelrc",
          "# Many arguments\n"
          "startup foo # First argument has reasons.\n"
          "startup --opt1   --opt2 # These arguments are split wide\n"
          "#startup --this_is_not_an_arg\n"
          "\n\n\n # A few empty lines for good measure\n"
          "startup\t \"string input Value 123. \" --bar");
  SuccessfullyParseRcWithExpectedArgs(
      "startup_with_many_args.bazelrc",
      {{
          "startup",
          {"foo", "--opt1", "--opt2", "string input Value 123. ", "--bar"}
      }});
}

// Testing which commands different args belong to.

TEST_F(RcOptionsTest, MultipleCommands) {
  WriteRc("multiple_commands_intermixed.bazelrc",
          "startup foo\n"
          "build aaa\n"
          "startup bar baz\n"
          "build bbb\n"
          "build ccc\n");
  SuccessfullyParseRcWithExpectedArgs(
      "multiple_commands_intermixed.bazelrc",
      {{"startup", {"foo", "bar", "baz"}}, {"build", {"aaa", "bbb", "ccc"}}});
}

// Successful import tests

TEST_F(RcOptionsTest, SimpleImportFoo) {
  WriteRc("startup_foo.bazelrc",
          "startup foo");
  WriteRc("import_simple.bazelrc",
          "import %workspace%/startup_foo.bazelrc");
  SuccessfullyParseRcWithExpectedArgs(
      "import_simple.bazelrc",
      {{"startup", {"foo"}}});
}

TEST_F(RcOptionsTest, ImportFooThenAddBar) {
  WriteRc("startup_foo.bazelrc",
          "startup foo");
  WriteRc("import_foo_then_bar.bazelrc",
          "import %workspace%/startup_foo.bazelrc\n"
          "startup bar");
  SuccessfullyParseRcWithExpectedArgs(
      "import_foo_then_bar.bazelrc",
      {{"startup", {"foo", "bar"}}});
}

TEST_F(RcOptionsTest, StartupBarThenImportFoo) {
  WriteRc("startup_foo.bazelrc",
          "startup foo");
  WriteRc("bar_then_import_foo.bazelrc",
          "startup bar\n"
          "import %workspace%/startup_foo.bazelrc");
  SuccessfullyParseRcWithExpectedArgs(
      "bar_then_import_foo.bazelrc",
      {{"startup", {"bar", "foo"}}});
}

// Consider making this an error, or at least a warning - most likely, import
// diamonds like this are unintended, and they might lead to surprising doubled
// values for allow_multiple options.
TEST_F(RcOptionsTest, ImportDiamond) {
  WriteRc("startup_foo.bazelrc",
          "startup foo");
  WriteRc("import_foo_then_bar.bazelrc",
          "import %workspace%/startup_foo.bazelrc\n"
          "startup bar");
  WriteRc("bar_then_import_foo.bazelrc",
          "startup bar\n"
          "import %workspace%/startup_foo.bazelrc");
  WriteRc("import_diamond.bazelrc",
          "import %workspace%/import_foo_then_bar.bazelrc\n"
          "import %workspace%/bar_then_import_foo.bazelrc");
  SuccessfullyParseRcWithExpectedArgs(
      "import_diamond.bazelrc",
      {{"startup", {"foo", "bar", "bar", "foo"}}});
}

// Testing failure modes

TEST_F(RcOptionsTest, ImportCycleFails) {
  WriteRc("import_cycle_1.bazelrc",
          "import %workspace%/import_cycle_2.bazelrc");
  WriteRc("import_cycle_2.bazelrc",
          "import %workspace%/import_cycle_1.bazelrc");

  RcFile::ParseError error;
  string error_text;
  std::unique_ptr<RcFile> rc =
      Parse("import_cycle_1.bazelrc", &error, &error_text);
  EXPECT_EQ(error, RcFile::ParseError::IMPORT_LOOP);
  string expected_error;
  blaze_util::StringPrintf(
      &expected_error, "Import loop detected:\n  %s\n  %s\n  %s\n",
      blaze_util::JoinPath(test_file_dir_, "import_cycle_1.bazelrc").c_str(),
      blaze_util::JoinPath(test_file_dir_, "import_cycle_2.bazelrc").c_str(),
      blaze_util::JoinPath(test_file_dir_, "import_cycle_1.bazelrc").c_str());
  ASSERT_EQ(error_text, expected_error);
}

TEST_F(RcOptionsTest, LongImportCycleFails) {
  WriteRc("chain_to_cycle_1.bazelrc",
          "import %workspace%/chain_to_cycle_2.bazelrc");
  WriteRc("chain_to_cycle_2.bazelrc",
          "import %workspace%/chain_to_cycle_3.bazelrc");
  WriteRc("chain_to_cycle_3.bazelrc",
          "import %workspace%/chain_to_cycle_4.bazelrc");
  WriteRc("chain_to_cycle_4.bazelrc",
          "import %workspace%/import_cycle_1.bazelrc");
  WriteRc("import_cycle_1.bazelrc",
          "import %workspace%/import_cycle_2.bazelrc");
  WriteRc("import_cycle_2.bazelrc",
          "import %workspace%/import_cycle_1.bazelrc");

  RcFile::ParseError error;
  string error_text;
  std::unique_ptr<RcFile> rc =
      Parse("chain_to_cycle_1.bazelrc", &error, &error_text);
  EXPECT_EQ(error, RcFile::ParseError::IMPORT_LOOP);
  string expected_error;
  blaze_util::StringPrintf(
      &expected_error,
      "Import loop detected:\n  %s\n  %s\n  %s\n  %s\n  %s\n  %s\n  %s\n",
      blaze_util::JoinPath(test_file_dir_, "chain_to_cycle_1.bazelrc").c_str(),
      blaze_util::JoinPath(test_file_dir_, "chain_to_cycle_2.bazelrc").c_str(),
      blaze_util::JoinPath(test_file_dir_, "chain_to_cycle_3.bazelrc").c_str(),
      blaze_util::JoinPath(test_file_dir_, "chain_to_cycle_4.bazelrc").c_str(),
      blaze_util::JoinPath(test_file_dir_, "import_cycle_1.bazelrc").c_str(),
      blaze_util::JoinPath(test_file_dir_, "import_cycle_2.bazelrc").c_str(),
      blaze_util::JoinPath(test_file_dir_, "import_cycle_1.bazelrc").c_str());
  ASSERT_EQ(error_text, expected_error);
}

TEST_F(RcOptionsTest, FileDoesNotExist) {
  RcFile::ParseError error;
  string error_text;
  std::unique_ptr<RcFile> rc = Parse("not_a_file.bazelrc", &error, &error_text);
  EXPECT_EQ(error, RcFile::ParseError::UNREADABLE_FILE);
  string expected_error;
  blaze_util::StringPrintf(
      &expected_error, "Unexpected error reading .blazerc file '%s'",
      blaze_util::JoinPath(test_file_dir_, "not_a_file.bazelrc").c_str());
  ASSERT_EQ(error_text, expected_error);
}

TEST_F(RcOptionsTest, ImportedFileDoesNotExist) {
  WriteRc("import_fake_file.bazelrc",
          "import somefile");

  RcFile::ParseError error;
  string error_text;
  std::unique_ptr<RcFile> rc =
      Parse("import_fake_file.bazelrc", &error, &error_text);
  EXPECT_EQ(error, RcFile::ParseError::UNREADABLE_FILE);
  ASSERT_EQ(error_text, "Unexpected error reading .blazerc file 'somefile'");
}

TEST_F(RcOptionsTest, ImportHasTooManyArgs) {
  WriteRc("bad_import.bazelrc",
          "import somefile bar");

  RcFile::ParseError error;
  string error_text;
  std::unique_ptr<RcFile> rc = Parse("bad_import.bazelrc", &error, &error_text);
  EXPECT_EQ(error, RcFile::ParseError::INVALID_FORMAT);

  string expected_error;
  blaze_util::StringPrintf(
      &expected_error,
      "Invalid import declaration in .blazerc file '%s': "
      "'import somefile bar' (are you in your source checkout/WORKSPACE?)",
      blaze_util::JoinPath(test_file_dir_, "bad_import.bazelrc").c_str());
  ASSERT_EQ(error_text, expected_error);
}

// TODO(b/34811299) The tests below identify ways that '\' used as a line
// continuation is broken. This is on top of user-reported cases where an
// unintentional '\' made the command on the following line show up as
// an argument, which lead to cryptic messages. There is no value added by '\',
// since the following line could just repeat the command, so it might be best
// to remove this feature entirely.
//
// For now, these tests serve as documentation of the brokenness, and to keep
// broken behavior consistent before we get around to fixing it.

TEST_F(RcOptionsTest, BadStartupLineContinuation_HasWhitespaceAfterSlash) {
  WriteRc("bad_startup_line_continuation.bazelrc",
          "startup foo \\ \n"
          "bar");
  SuccessfullyParseRcWithExpectedArgs(
      "bad_startup_line_continuation.bazelrc",
      {{"startup", {"foo"}}});  // Does not contain "bar" from the next line.
}

TEST_F(RcOptionsTest, BadStartupLineContinuation_HasErroneousSlash) {
  WriteRc("bad_startup_line_continuation.bazelrc",
          "startup foo \\ bar");
  SuccessfullyParseRcWithExpectedArgs(
      "bad_startup_line_continuation.bazelrc",
      // Whitespace between the slash and bar gets counted as part of the token.
      {{"startup", {"foo", " bar"}}});
}

TEST_F(RcOptionsTest, BadStartupLineContinuation_HasCommentAfterSlash) {
  WriteRc("bad_startup_line_continuation.bazelrc",
          "startup foo \\ # comment\n"
          "bar");
  SuccessfullyParseRcWithExpectedArgs(
      "bad_startup_line_continuation.bazelrc",
      // Whitespace between the slash and comment gets counted as a new token,
      // and the bar on the next line is ignored (it's an argumentless command).
      {{"startup", {"foo", " "}}});
}

TEST_F(RcOptionsTest, BadStartupLineContinuation_InterpretsNextLineAsNewline) {
  WriteRc("bad_startup_line_continuation.bazelrc",
          "startup foo \\ #comment\n"
          "bar baz");
  SuccessfullyParseRcWithExpectedArgs(
      "bad_startup_line_continuation.bazelrc",
      // Whitespace between the slash and comment gets counted as a new token,
      // and the bar on the next line treated as its own command, instead of as
      // a "startup" args.
      {{"startup", {"foo", " "}}, {"bar", {"baz"}}});
}

}  // namespace blaze

