//===-- ConfigCompileTests.cpp --------------------------------------------===//
//
// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
// See https://llvm.org/LICENSE.txt for license information.
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
//
//===----------------------------------------------------------------------===//

#include "Config.h"
#include "ConfigFragment.h"
#include "ConfigTesting.h"
#include "Diagnostics.h"
#include "Feature.h"
#include "TestFS.h"
#include "clang/Basic/DiagnosticSema.h"
#include "llvm/ADT/StringRef.h"
#include "llvm/Support/Path.h"
#include "llvm/Support/SourceMgr.h"
#include "gmock/gmock.h"
#include "gtest/gtest.h"
#include <optional>
#include <string>

namespace clang {
namespace clangd {
namespace config {
namespace {
using ::testing::AllOf;
using ::testing::Contains;
using ::testing::ElementsAre;
using ::testing::IsEmpty;
using ::testing::SizeIs;
using ::testing::StartsWith;
using ::testing::UnorderedElementsAre;

class ConfigCompileTests : public ::testing::Test {
protected:
  CapturedDiags Diags;
  Config Conf;
  Fragment Frag;
  Params Parm;

  bool compileAndApply() {
    Conf = Config();
    Diags.Diagnostics.clear();
    auto Compiled = std::move(Frag).compile(Diags.callback());
    return Compiled(Parm, Conf);
  }
};

TEST_F(ConfigCompileTests, Condition) {
  // No condition.
  Frag = {};
  Frag.CompileFlags.Add.emplace_back("X");
  EXPECT_TRUE(compileAndApply()) << "Empty config";
  EXPECT_THAT(Diags.Diagnostics, IsEmpty());
  EXPECT_THAT(Conf.CompileFlags.Edits, SizeIs(1));

  // Regex with no file.
  Frag = {};
  Frag.If.PathMatch.emplace_back("fo*");
  EXPECT_FALSE(compileAndApply());
  EXPECT_THAT(Diags.Diagnostics, IsEmpty());
  EXPECT_THAT(Conf.CompileFlags.Edits, SizeIs(0));

  // Following tests have a file path set.
  Parm.Path = "bar";

  // Non-matching regex.
  Frag = {};
  Frag.If.PathMatch.emplace_back("fo*");
  EXPECT_FALSE(compileAndApply());
  EXPECT_THAT(Diags.Diagnostics, IsEmpty());

  // Matching regex.
  Frag = {};
  Frag.If.PathMatch.emplace_back("fo*");
  Frag.If.PathMatch.emplace_back("ba*r");
  EXPECT_TRUE(compileAndApply());
  EXPECT_THAT(Diags.Diagnostics, IsEmpty());

  // Excluded regex.
  Frag = {};
  Frag.If.PathMatch.emplace_back("b.*");
  Frag.If.PathExclude.emplace_back(".*r");
  EXPECT_FALSE(compileAndApply()) << "Included but also excluded";
  EXPECT_THAT(Diags.Diagnostics, IsEmpty());

  // Invalid regex.
  Frag = {};
  Frag.If.PathMatch.emplace_back("**]@theu");
  EXPECT_TRUE(compileAndApply());
  EXPECT_THAT(Diags.Diagnostics, SizeIs(1));
  EXPECT_THAT(Diags.Diagnostics.front().Message, StartsWith("Invalid regex"));

  // Valid regex and unknown key.
  Frag = {};
  Frag.If.HasUnrecognizedCondition = true;
  Frag.If.PathMatch.emplace_back("ba*r");
  EXPECT_FALSE(compileAndApply());
  EXPECT_THAT(Diags.Diagnostics, IsEmpty());

  // Only matches case-insensitively.
  Frag = {};
  Frag.If.PathMatch.emplace_back("B.*R");
  EXPECT_THAT(Diags.Diagnostics, IsEmpty());
#ifdef CLANGD_PATH_CASE_INSENSITIVE
  EXPECT_TRUE(compileAndApply());
#else
  EXPECT_FALSE(compileAndApply());
#endif

  Frag = {};
  Frag.If.PathExclude.emplace_back("B.*R");
  EXPECT_THAT(Diags.Diagnostics, IsEmpty());
#ifdef CLANGD_PATH_CASE_INSENSITIVE
  EXPECT_FALSE(compileAndApply());
#else
  EXPECT_TRUE(compileAndApply());
#endif
}

TEST_F(ConfigCompileTests, CompileCommands) {
  Frag.CompileFlags.Compiler.emplace("tpc.exe");
  Frag.CompileFlags.Add.emplace_back("-foo");
  Frag.CompileFlags.Remove.emplace_back("--include-directory=");
  std::vector<std::string> Argv = {"clang", "-I", "bar/", "--", "a.cc"};
  EXPECT_TRUE(compileAndApply());
  EXPECT_THAT(Conf.CompileFlags.Edits, SizeIs(3));
  for (auto &Edit : Conf.CompileFlags.Edits)
    Edit(Argv);
  EXPECT_THAT(Argv, ElementsAre("tpc.exe", "-foo", "--", "a.cc"));
}

TEST_F(ConfigCompileTests, CompilationDatabase) {
  Frag.CompileFlags.CompilationDatabase.emplace("None");
  EXPECT_TRUE(compileAndApply());
  EXPECT_EQ(Conf.CompileFlags.CDBSearch.Policy,
            Config::CDBSearchSpec::NoCDBSearch);

  Frag.CompileFlags.CompilationDatabase.emplace("Ancestors");
  EXPECT_TRUE(compileAndApply());
  EXPECT_EQ(Conf.CompileFlags.CDBSearch.Policy,
            Config::CDBSearchSpec::Ancestors);

  // Relative path not allowed without directory set.
  Frag.CompileFlags.CompilationDatabase.emplace("Something");
  EXPECT_TRUE(compileAndApply());
  EXPECT_EQ(Conf.CompileFlags.CDBSearch.Policy,
            Config::CDBSearchSpec::Ancestors)
      << "default value";
  EXPECT_THAT(Diags.Diagnostics,
              ElementsAre(diagMessage(
                  "CompilationDatabase must be an absolute path, because this "
                  "fragment is not associated with any directory.")));

  // Relative path allowed if directory is set.
  Frag.Source.Directory = testRoot();
  EXPECT_TRUE(compileAndApply());
  EXPECT_EQ(Conf.CompileFlags.CDBSearch.Policy,
            Config::CDBSearchSpec::FixedDir);
  EXPECT_EQ(Conf.CompileFlags.CDBSearch.FixedCDBPath, testPath("Something"));
  EXPECT_THAT(Diags.Diagnostics, IsEmpty());

  // Absolute path allowed.
  Frag.Source.Directory.clear();
  Frag.CompileFlags.CompilationDatabase.emplace(testPath("Something2"));
  EXPECT_TRUE(compileAndApply());
  EXPECT_EQ(Conf.CompileFlags.CDBSearch.Policy,
            Config::CDBSearchSpec::FixedDir);
  EXPECT_EQ(Conf.CompileFlags.CDBSearch.FixedCDBPath, testPath("Something2"));
  EXPECT_THAT(Diags.Diagnostics, IsEmpty());
}

TEST_F(ConfigCompileTests, Index) {
  Frag.Index.Background.emplace("Skip");
  EXPECT_TRUE(compileAndApply());
  EXPECT_EQ(Conf.Index.Background, Config::BackgroundPolicy::Skip);

  Frag = {};
  Frag.Index.Background.emplace("Foo");
  EXPECT_TRUE(compileAndApply());
  EXPECT_EQ(Conf.Index.Background, Config::BackgroundPolicy::Build)
      << "by default";
  EXPECT_THAT(
      Diags.Diagnostics,
      ElementsAre(diagMessage(
          "Invalid Background value 'Foo'. Valid values are Build, Skip.")));
}

TEST_F(ConfigCompileTests, PathSpecMatch) {
  auto BarPath = llvm::sys::path::convert_to_slash(testPath("foo/bar.h"));
  Parm.Path = BarPath;

  struct {
    std::string Directory;
    std::string PathSpec;
    bool ShouldMatch;
  } Cases[] = {
      {
          // Absolute path matches.
          "",
          llvm::sys::path::convert_to_slash(testPath("foo/bar.h")),
          true,
      },
      {
          // Absolute path fails.
          "",
          llvm::sys::path::convert_to_slash(testPath("bar/bar.h")),
          false,
      },
      {
          // Relative should fail to match as /foo/bar.h doesn't reside under
          // /baz/.
          testPath("baz"),
          "bar\\.h",
          false,
      },
      {
          // Relative should pass with /foo as directory.
          testPath("foo"),
          "bar\\.h",
          true,
      },
  };

  // PathMatch
  for (const auto &Case : Cases) {
    Frag = {};
    Frag.If.PathMatch.emplace_back(Case.PathSpec);
    Frag.Source.Directory = Case.Directory;
    EXPECT_EQ(compileAndApply(), Case.ShouldMatch);
    ASSERT_THAT(Diags.Diagnostics, IsEmpty());
  }

  // PathEclude
  for (const auto &Case : Cases) {
    SCOPED_TRACE(Case.Directory);
    SCOPED_TRACE(Case.PathSpec);
    Frag = {};
    Frag.If.PathExclude.emplace_back(Case.PathSpec);
    Frag.Source.Directory = Case.Directory;
    EXPECT_NE(compileAndApply(), Case.ShouldMatch);
    ASSERT_THAT(Diags.Diagnostics, IsEmpty());
  }
}

TEST_F(ConfigCompileTests, DiagnosticsIncludeCleaner) {
  // Defaults to Strict.
  EXPECT_TRUE(compileAndApply());
  EXPECT_EQ(Conf.Diagnostics.UnusedIncludes, Config::IncludesPolicy::Strict);

  Frag = {};
  Frag.Diagnostics.UnusedIncludes.emplace("None");
  EXPECT_TRUE(compileAndApply());
  EXPECT_EQ(Conf.Diagnostics.UnusedIncludes, Config::IncludesPolicy::None);

  Frag = {};
  Frag.Diagnostics.UnusedIncludes.emplace("Strict");
  EXPECT_TRUE(compileAndApply());
  EXPECT_EQ(Conf.Diagnostics.UnusedIncludes, Config::IncludesPolicy::Strict);

  Frag = {};
  EXPECT_TRUE(Conf.Diagnostics.Includes.IgnoreHeader.empty())
      << Conf.Diagnostics.Includes.IgnoreHeader.size();
  Frag.Diagnostics.Includes.IgnoreHeader.push_back(
      Located<std::string>("foo.h"));
  Frag.Diagnostics.Includes.IgnoreHeader.push_back(
      Located<std::string>(".*inc"));
  EXPECT_TRUE(compileAndApply());
  auto HeaderFilter = [this](llvm::StringRef Path) {
    for (auto &Filter : Conf.Diagnostics.Includes.IgnoreHeader) {
      if (Filter(Path))
        return true;
    }
    return false;
  };
  EXPECT_TRUE(HeaderFilter("foo.h"));
  EXPECT_FALSE(HeaderFilter("bar.h"));

  Frag = {};
  EXPECT_FALSE(Conf.Diagnostics.Includes.AnalyzeAngledIncludes);
  Frag.Diagnostics.Includes.AnalyzeAngledIncludes = true;
  EXPECT_TRUE(compileAndApply());
  EXPECT_TRUE(Conf.Diagnostics.Includes.AnalyzeAngledIncludes);
}

TEST_F(ConfigCompileTests, DiagnosticSuppression) {
  Frag.Diagnostics.Suppress.emplace_back("bugprone-use-after-move");
  Frag.Diagnostics.Suppress.emplace_back("unreachable-code");
  Frag.Diagnostics.Suppress.emplace_back("-Wunused-variable");
  Frag.Diagnostics.Suppress.emplace_back("typecheck_bool_condition");
  Frag.Diagnostics.Suppress.emplace_back("err_unexpected_friend");
  Frag.Diagnostics.Suppress.emplace_back("warn_alloca");
  EXPECT_TRUE(compileAndApply());
  EXPECT_THAT(Conf.Diagnostics.Suppress.keys(),
              UnorderedElementsAre("bugprone-use-after-move",
                                   "unreachable-code", "unused-variable",
                                   "typecheck_bool_condition",
                                   "unexpected_friend", "warn_alloca"));
  EXPECT_TRUE(isBuiltinDiagnosticSuppressed(
      diag::warn_unreachable, Conf.Diagnostics.Suppress, LangOptions()));
  // Subcategory not respected/suppressed.
  EXPECT_FALSE(isBuiltinDiagnosticSuppressed(
      diag::warn_unreachable_break, Conf.Diagnostics.Suppress, LangOptions()));
  EXPECT_TRUE(isBuiltinDiagnosticSuppressed(
      diag::warn_unused_variable, Conf.Diagnostics.Suppress, LangOptions()));
  EXPECT_TRUE(isBuiltinDiagnosticSuppressed(diag::err_typecheck_bool_condition,
                                            Conf.Diagnostics.Suppress,
                                            LangOptions()));
  EXPECT_TRUE(isBuiltinDiagnosticSuppressed(
      diag::err_unexpected_friend, Conf.Diagnostics.Suppress, LangOptions()));
  EXPECT_TRUE(isBuiltinDiagnosticSuppressed(
      diag::warn_alloca, Conf.Diagnostics.Suppress, LangOptions()));

  Frag.Diagnostics.Suppress.emplace_back("*");
  EXPECT_TRUE(compileAndApply());
  EXPECT_TRUE(Conf.Diagnostics.SuppressAll);
  EXPECT_THAT(Conf.Diagnostics.Suppress, IsEmpty());
}

TEST_F(ConfigCompileTests, Tidy) {
  auto &Tidy = Frag.Diagnostics.ClangTidy;
  Tidy.Add.emplace_back("bugprone-use-after-move");
  Tidy.Add.emplace_back("llvm-*");
  Tidy.Remove.emplace_back("llvm-include-order");
  Tidy.Remove.emplace_back("readability-*");
  Tidy.CheckOptions.emplace_back(
      std::make_pair(std::string("StrictMode"), std::string("true")));
  Tidy.CheckOptions.emplace_back(std::make_pair(
      std::string("example-check.ExampleOption"), std::string("0")));
  EXPECT_TRUE(compileAndApply());
  EXPECT_EQ(Conf.Diagnostics.ClangTidy.CheckOptions.size(), 2U);
  EXPECT_EQ(Conf.Diagnostics.ClangTidy.CheckOptions.lookup("StrictMode"),
            "true");
  EXPECT_EQ(Conf.Diagnostics.ClangTidy.CheckOptions.lookup(
                "example-check.ExampleOption"),
            "0");
#if CLANGD_TIDY_CHECKS
  EXPECT_EQ(
      Conf.Diagnostics.ClangTidy.Checks,
      "bugprone-use-after-move,llvm-*,-llvm-include-order,-readability-*");
  EXPECT_THAT(Diags.Diagnostics, IsEmpty());
#else // !CLANGD_TIDY_CHECKS
  EXPECT_EQ(Conf.Diagnostics.ClangTidy.Checks, "llvm-*,-readability-*");
  EXPECT_THAT(
      Diags.Diagnostics,
      ElementsAre(
          diagMessage(
              "clang-tidy check 'bugprone-use-after-move' was not found"),
          diagMessage("clang-tidy check 'llvm-include-order' was not found")));
#endif
}

TEST_F(ConfigCompileTests, TidyBadChecks) {
  auto &Tidy = Frag.Diagnostics.ClangTidy;
  Tidy.Add.emplace_back("unknown-check");
  Tidy.Remove.emplace_back("*");
  Tidy.Remove.emplace_back("llvm-includeorder");
  EXPECT_TRUE(compileAndApply());
  // Ensure bad checks are stripped from the glob.
  EXPECT_EQ(Conf.Diagnostics.ClangTidy.Checks, "-*");
  EXPECT_THAT(
      Diags.Diagnostics,
      ElementsAre(
          AllOf(diagMessage("clang-tidy check 'unknown-check' was not found"),
                diagKind(llvm::SourceMgr::DK_Warning)),
          AllOf(
              diagMessage("clang-tidy check 'llvm-includeorder' was not found"),
              diagKind(llvm::SourceMgr::DK_Warning))));
}

TEST_F(ConfigCompileTests, ExternalServerNeedsTrusted) {
  Fragment::IndexBlock::ExternalBlock External;
  External.Server.emplace("xxx");
  Frag.Index.External = std::move(External);
  compileAndApply();
  EXPECT_THAT(
      Diags.Diagnostics,
      ElementsAre(diagMessage(
          "Remote index may not be specified by untrusted configuration. "
          "Copy this into user config to use it.")));
  EXPECT_EQ(Conf.Index.External.Kind, Config::ExternalIndexSpec::None);
}

TEST_F(ConfigCompileTests, ExternalBlockWarnOnMultipleSource) {
  Frag.Source.Trusted = true;
  Fragment::IndexBlock::ExternalBlock External;
  External.File.emplace("");
  External.Server.emplace("");
  Frag.Index.External = std::move(External);
  compileAndApply();
#ifdef CLANGD_ENABLE_REMOTE
  EXPECT_THAT(
      Diags.Diagnostics,
      Contains(
          AllOf(diagMessage("Exactly one of File, Server or None must be set."),
                diagKind(llvm::SourceMgr::DK_Error))));
#else
  ASSERT_TRUE(Conf.Index.External.hasValue());
  EXPECT_EQ(Conf.Index.External->Kind, Config::ExternalIndexSpec::File);
#endif
}

TEST_F(ConfigCompileTests, ExternalBlockDisableWithNone) {
  compileAndApply();
  EXPECT_EQ(Conf.Index.External.Kind, Config::ExternalIndexSpec::None);

  Fragment::IndexBlock::ExternalBlock External;
  External.IsNone = true;
  Frag.Index.External = std::move(External);
  compileAndApply();
  EXPECT_EQ(Conf.Index.External.Kind, Config::ExternalIndexSpec::None);
}

TEST_F(ConfigCompileTests, ExternalBlockErrOnNoSource) {
  Frag.Index.External.emplace(Fragment::IndexBlock::ExternalBlock{});
  compileAndApply();
  EXPECT_THAT(
      Diags.Diagnostics,
      Contains(
          AllOf(diagMessage("Exactly one of File, Server or None must be set."),
                diagKind(llvm::SourceMgr::DK_Error))));
}

TEST_F(ConfigCompileTests, ExternalBlockDisablesBackgroundIndex) {
  auto BazPath = testPath("foo/bar/baz.h", llvm::sys::path::Style::posix);
  Parm.Path = BazPath;
  Frag.Index.Background.emplace("Build");
  Fragment::IndexBlock::ExternalBlock External;
  External.File.emplace(testPath("foo"));
  External.MountPoint.emplace(
      testPath("foo/bar", llvm::sys::path::Style::posix));
  Frag.Index.External = std::move(External);
  compileAndApply();
  EXPECT_EQ(Conf.Index.Background, Config::BackgroundPolicy::Skip);
}

TEST_F(ConfigCompileTests, ExternalBlockMountPoint) {
  auto GetFrag = [](llvm::StringRef Directory,
                    std::optional<const char *> MountPoint) {
    Fragment Frag;
    Frag.Source.Directory = Directory.str();
    Fragment::IndexBlock::ExternalBlock External;
    External.File.emplace(testPath("foo"));
    if (MountPoint)
      External.MountPoint.emplace(*MountPoint);
    Frag.Index.External = std::move(External);
    return Frag;
  };

  auto BarPath = testPath("foo/bar.h", llvm::sys::path::Style::posix);
  BarPath = llvm::sys::path::convert_to_slash(BarPath);
  Parm.Path = BarPath;
  // Non-absolute MountPoint without a directory raises an error.
  Frag = GetFrag("", "foo");
  compileAndApply();
  ASSERT_THAT(
      Diags.Diagnostics,
      ElementsAre(
          AllOf(diagMessage("MountPoint must be an absolute path, because this "
                            "fragment is not associated with any directory."),
                diagKind(llvm::SourceMgr::DK_Error))));
  EXPECT_EQ(Conf.Index.External.Kind, Config::ExternalIndexSpec::None);

  auto FooPath = testPath("foo/", llvm::sys::path::Style::posix);
  FooPath = llvm::sys::path::convert_to_slash(FooPath);
  // Ok when relative.
  Frag = GetFrag(testRoot(), "foo/");
  compileAndApply();
  ASSERT_THAT(Diags.Diagnostics, IsEmpty());
  ASSERT_EQ(Conf.Index.External.Kind, Config::ExternalIndexSpec::File);
  EXPECT_THAT(Conf.Index.External.MountPoint, FooPath);

  // None defaults to ".".
  Frag = GetFrag(FooPath, std::nullopt);
  compileAndApply();
  ASSERT_THAT(Diags.Diagnostics, IsEmpty());
  ASSERT_EQ(Conf.Index.External.Kind, Config::ExternalIndexSpec::File);
  EXPECT_THAT(Conf.Index.External.MountPoint, FooPath);

  // Without a file, external index is empty.
  Parm.Path = "";
  Frag = GetFrag("", FooPath.c_str());
  compileAndApply();
  ASSERT_THAT(Diags.Diagnostics, IsEmpty());
  ASSERT_EQ(Conf.Index.External.Kind, Config::ExternalIndexSpec::None);

  // File outside MountPoint, no index.
  auto BazPath = testPath("bar/baz.h", llvm::sys::path::Style::posix);
  BazPath = llvm::sys::path::convert_to_slash(BazPath);
  Parm.Path = BazPath;
  Frag = GetFrag("", FooPath.c_str());
  compileAndApply();
  ASSERT_THAT(Diags.Diagnostics, IsEmpty());
  ASSERT_EQ(Conf.Index.External.Kind, Config::ExternalIndexSpec::None);

  // File under MountPoint, index should be set.
  BazPath = testPath("foo/baz.h", llvm::sys::path::Style::posix);
  BazPath = llvm::sys::path::convert_to_slash(BazPath);
  Parm.Path = BazPath;
  Frag = GetFrag("", FooPath.c_str());
  compileAndApply();
  ASSERT_THAT(Diags.Diagnostics, IsEmpty());
  ASSERT_EQ(Conf.Index.External.Kind, Config::ExternalIndexSpec::File);
  EXPECT_THAT(Conf.Index.External.MountPoint, FooPath);

  // Only matches case-insensitively.
  BazPath = testPath("fOo/baz.h", llvm::sys::path::Style::posix);
  BazPath = llvm::sys::path::convert_to_slash(BazPath);
  Parm.Path = BazPath;

  FooPath = testPath("FOO/", llvm::sys::path::Style::posix);
  FooPath = llvm::sys::path::convert_to_slash(FooPath);
  Frag = GetFrag("", FooPath.c_str());
  compileAndApply();
  ASSERT_THAT(Diags.Diagnostics, IsEmpty());
#ifdef CLANGD_PATH_CASE_INSENSITIVE
  ASSERT_EQ(Conf.Index.External.Kind, Config::ExternalIndexSpec::File);
  EXPECT_THAT(Conf.Index.External.MountPoint, FooPath);
#else
  ASSERT_EQ(Conf.Index.External.Kind, Config::ExternalIndexSpec::None);
#endif
}

TEST_F(ConfigCompileTests, AllScopes) {
  // Defaults to true.
  EXPECT_TRUE(compileAndApply());
  EXPECT_TRUE(Conf.Completion.AllScopes);

  Frag = {};
  Frag.Completion.AllScopes = false;
  EXPECT_TRUE(compileAndApply());
  EXPECT_FALSE(Conf.Completion.AllScopes);

  Frag = {};
  Frag.Completion.AllScopes = true;
  EXPECT_TRUE(compileAndApply());
  EXPECT_TRUE(Conf.Completion.AllScopes);
}

TEST_F(ConfigCompileTests, Style) {
  Frag = {};
  Frag.Style.FullyQualifiedNamespaces.push_back(std::string("foo"));
  Frag.Style.FullyQualifiedNamespaces.push_back(std::string("bar"));
  EXPECT_TRUE(compileAndApply());
  EXPECT_THAT(Conf.Style.FullyQualifiedNamespaces, ElementsAre("foo", "bar"));
}
} // namespace
} // namespace config
} // namespace clangd
} // namespace clang
