//===-- ConfigProviderTests.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 "ConfigProvider.h"
#include "ConfigTesting.h"
#include "TestFS.h"
#include "llvm/Support/Path.h"
#include "llvm/Support/SourceMgr.h"
#include "gmock/gmock.h"
#include "gtest/gtest.h"
#include <atomic>
#include <chrono>

namespace clang {
namespace clangd {
namespace config {
namespace {
using ::testing::ElementsAre;
using ::testing::IsEmpty;

// Provider that appends an arg to compile flags.
// The arg is prefix<N>, where N is the times getFragments() was called.
// It also yields a diagnostic each time it's called.
class FakeProvider : public Provider {
  std::string Prefix;
  mutable std::atomic<unsigned> Index = {0};

  std::vector<CompiledFragment>
  getFragments(const Params &, DiagnosticCallback DC) const override {
    DC(llvm::SMDiagnostic("", llvm::SourceMgr::DK_Error, Prefix));
    CompiledFragment F =
        [Arg(Prefix + std::to_string(++Index))](const Params &P, Config &C) {
          C.CompileFlags.Edits.push_back(
              [Arg](std::vector<std::string> &Argv) { Argv.push_back(Arg); });
          return true;
        };
    return {F};
  }

public:
  FakeProvider(llvm::StringRef Prefix) : Prefix(Prefix) {}
};

std::vector<std::string> getAddedArgs(Config &C) {
  std::vector<std::string> Argv;
  for (auto &Edit : C.CompileFlags.Edits)
    Edit(Argv);
  return Argv;
}

// The provider from combine() should invoke its providers in order, and not
// cache their results.
TEST(ProviderTest, Combine) {
  CapturedDiags Diags;
  FakeProvider Foo("foo");
  FakeProvider Bar("bar");
  auto Combined = Provider::combine({&Foo, &Bar});
  Config Cfg = Combined->getConfig(Params(), Diags.callback());
  EXPECT_THAT(Diags.Diagnostics,
              ElementsAre(diagMessage("foo"), diagMessage("bar")));
  EXPECT_THAT(getAddedArgs(Cfg), ElementsAre("foo1", "bar1"));
  Diags.Diagnostics.clear();

  Cfg = Combined->getConfig(Params(), Diags.callback());
  EXPECT_THAT(Diags.Diagnostics,
              ElementsAre(diagMessage("foo"), diagMessage("bar")));
  EXPECT_THAT(getAddedArgs(Cfg), ElementsAre("foo2", "bar2"));
}

const char *AddFooWithErr = R"yaml(
CompileFlags:
  Add: foo
  Unknown: 42
)yaml";

const char *AddFooWithTypoErr = R"yaml(
CompileFlags:
  Add: foo
  Removr: 42
)yaml";

const char *AddBarBaz = R"yaml(
CompileFlags:
  Add: bar
---
CompileFlags:
  Add: baz
)yaml";

TEST(ProviderTest, FromYAMLFile) {
  MockFS FS;
  FS.Files["foo.yaml"] = AddFooWithErr;

  CapturedDiags Diags;
  auto P = Provider::fromYAMLFile(testPath("foo.yaml"), /*Directory=*/"", FS);
  auto Cfg = P->getConfig(Params(), Diags.callback());
  EXPECT_THAT(Diags.Diagnostics,
              ElementsAre(diagMessage("Unknown CompileFlags key 'Unknown'")));
  EXPECT_THAT(Diags.Files, ElementsAre(testPath("foo.yaml")));
  EXPECT_THAT(getAddedArgs(Cfg), ElementsAre("foo"));
  Diags.clear();

  Cfg = P->getConfig(Params(), Diags.callback());
  EXPECT_THAT(Diags.Diagnostics, IsEmpty()) << "Cached, not re-parsed";
  EXPECT_THAT(Diags.Files, IsEmpty());
  EXPECT_THAT(getAddedArgs(Cfg), ElementsAre("foo"));

  FS.Files["foo.yaml"] = AddFooWithTypoErr;
  Cfg = P->getConfig(Params(), Diags.callback());
  EXPECT_THAT(
      Diags.Diagnostics,
      ElementsAre(diagMessage(
          "Unknown CompileFlags key 'Removr'; did you mean 'Remove'?")));
  EXPECT_THAT(Diags.Files, ElementsAre(testPath("foo.yaml")));
  EXPECT_THAT(getAddedArgs(Cfg), ElementsAre("foo"));
  Diags.clear();

  FS.Files["foo.yaml"] = AddBarBaz;
  Cfg = P->getConfig(Params(), Diags.callback());
  EXPECT_THAT(Diags.Diagnostics, IsEmpty()) << "New config, no errors";
  EXPECT_THAT(Diags.Files, ElementsAre(testPath("foo.yaml")));
  EXPECT_THAT(getAddedArgs(Cfg), ElementsAre("bar", "baz"));
  Diags.clear();

  FS.Files.erase("foo.yaml");
  Cfg = P->getConfig(Params(), Diags.callback());
  EXPECT_THAT(Diags.Diagnostics, IsEmpty()) << "Missing file is not an error";
  EXPECT_THAT(Diags.Files, IsEmpty());
  EXPECT_THAT(getAddedArgs(Cfg), IsEmpty());
}

TEST(ProviderTest, FromAncestorRelativeYAMLFiles) {
  MockFS FS;
  FS.Files["a/b/c/foo.yaml"] = AddBarBaz;
  FS.Files["a/foo.yaml"] = AddFooWithErr;

  std::string ABCPath =
      testPath("a/b/c/d/test.cc", llvm::sys::path::Style::posix);
  Params ABCParams;
  ABCParams.Path = ABCPath;
  std::string APath =
      testPath("a/b/e/f/test.cc", llvm::sys::path::Style::posix);
  Params AParams;
  AParams.Path = APath;

  CapturedDiags Diags;
  auto P = Provider::fromAncestorRelativeYAMLFiles("foo.yaml", FS);

  auto Cfg = P->getConfig(Params(), Diags.callback());
  EXPECT_THAT(Diags.Diagnostics, IsEmpty());
  EXPECT_THAT(Diags.Files, IsEmpty());
  EXPECT_THAT(getAddedArgs(Cfg), IsEmpty());

  Cfg = P->getConfig(ABCParams, Diags.callback());
  EXPECT_THAT(Diags.Diagnostics,
              ElementsAre(diagMessage("Unknown CompileFlags key 'Unknown'")));
  // FIXME: fails on windows: paths have mixed slashes like C:\a/b\c.yaml
  EXPECT_THAT(Diags.Files,
              ElementsAre(testPath("a/foo.yaml"), testPath("a/b/c/foo.yaml")));
  EXPECT_THAT(getAddedArgs(Cfg), ElementsAre("foo", "bar", "baz"));
  Diags.clear();

  Cfg = P->getConfig(AParams, Diags.callback());
  EXPECT_THAT(Diags.Diagnostics, IsEmpty()) << "Cached config";
  EXPECT_THAT(Diags.Files, IsEmpty());
  EXPECT_THAT(getAddedArgs(Cfg), ElementsAre("foo"));

  FS.Files.erase("a/foo.yaml");
  Cfg = P->getConfig(ABCParams, Diags.callback());
  EXPECT_THAT(Diags.Diagnostics, IsEmpty());
  EXPECT_THAT(Diags.Files, IsEmpty());
  EXPECT_THAT(getAddedArgs(Cfg), ElementsAre("bar", "baz"));
}

TEST(ProviderTest, SourceInfo) {
  MockFS FS;

  FS.Files["baz/foo.yaml"] = R"yaml(
If:
  PathMatch: .*
  PathExclude: bar.h
CompileFlags:
  Add: bar
)yaml";
  const auto BarPath = testPath("baz/bar.h", llvm::sys::path::Style::posix);
  CapturedDiags Diags;
  Params Bar;
  Bar.Path = BarPath;

  // This should be an absolute match/exclude hence baz/bar.h should not be
  // excluded.
  auto P =
      Provider::fromYAMLFile(testPath("baz/foo.yaml"), /*Directory=*/"", FS);
  auto Cfg = P->getConfig(Bar, Diags.callback());
  ASSERT_THAT(Diags.Diagnostics, IsEmpty());
  EXPECT_THAT(getAddedArgs(Cfg), ElementsAre("bar"));
  Diags.clear();

  // This should be a relative match/exclude hence baz/bar.h should be excluded.
  P = Provider::fromAncestorRelativeYAMLFiles("foo.yaml", FS);
  Cfg = P->getConfig(Bar, Diags.callback());
  ASSERT_THAT(Diags.Diagnostics, IsEmpty());
  EXPECT_THAT(getAddedArgs(Cfg), IsEmpty());
  Diags.clear();
}
} // namespace
} // namespace config
} // namespace clangd
} // namespace clang
