//===-- StdLibTests.cpp -----------------------------------------*- C++ -*-===//
//
// 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 "Annotations.h"
#include "ClangdServer.h"
#include "CodeComplete.h"
#include "Compiler.h"
#include "Config.h"
#include "SyncAPI.h"
#include "TestFS.h"
#include "index/Index.h"
#include "index/StdLib.h"
#include "clang/Basic/LangOptions.h"
#include "clang/Basic/SourceManager.h"
#include "llvm/Testing/Support/Error.h"
#include "gmock/gmock.h"
#include "gtest/gtest.h"
#include <memory>

using namespace testing;

namespace clang {
namespace clangd {
namespace {

// Check the generated header sources contains usual standard library headers.
TEST(StdLibTests, getStdlibUmbrellaHeader) {
  LangOptions LO;
  LO.CPlusPlus = true;

  auto CXX = getStdlibUmbrellaHeader(LO).str();
  EXPECT_THAT(CXX, HasSubstr("#include <string>"));
  EXPECT_THAT(CXX, HasSubstr("#include <cstdio>"));
  EXPECT_THAT(CXX, Not(HasSubstr("#include <ios646.h>")));

  LO.CPlusPlus = false;
  auto C = getStdlibUmbrellaHeader(LO).str();
  EXPECT_THAT(C, Not(HasSubstr("#include <string>")));
  EXPECT_THAT(C, Not(HasSubstr("#include <cstdio>")));
  EXPECT_THAT(C, HasSubstr("#include <stdio.h>"));
}

MATCHER_P(Named, Name, "") { return arg.Name == Name; }

// Build an index, and check if it contains the right symbols.
TEST(StdLibTests, indexStandardLibrary) {
  MockFS FS;
  FS.Files["std/foo.h"] = R"cpp(
  #include <platform_stuff.h>
  #if __cplusplus >= 201703L
    int foo17();
  #elif __cplusplus >= 201402L
    int foo14();
  #else
    bool foo98();
  #endif
  )cpp";
  FS.Files["nonstd/platform_stuff.h"] = "int magic = 42;";

  ParseInputs OriginalInputs;
  OriginalInputs.TFS = &FS;
  OriginalInputs.CompileCommand.Filename = testPath("main.cc");
  OriginalInputs.CompileCommand.CommandLine = {"clang++", testPath("main.cc"),
                                               "-isystemstd/",
                                               "-isystemnonstd/", "-std=c++14"};
  OriginalInputs.CompileCommand.Directory = testRoot();
  IgnoreDiagnostics Diags;
  auto CI = buildCompilerInvocation(OriginalInputs, Diags);
  ASSERT_TRUE(CI);

  StdLibLocation Loc;
  Loc.Paths.push_back(testPath("std/"));

  auto Symbols =
      indexStandardLibrary("#include <foo.h>", std::move(CI), Loc, FS);
  EXPECT_THAT(Symbols, ElementsAre(Named("foo14")));
}

TEST(StdLibTests, StdLibSet) {
  StdLibSet Set;
  MockFS FS;
  FS.Files["std/_"] = "";
  FS.Files["libc/_"] = "";

  HeaderSearchOptions HSOpts;
  auto Add = [&](const LangOptions &LO,
                 std::vector<llvm::StringRef> SearchPath) {
    SourceManagerForFile SM("scratch", "");
    SM.get().getFileManager().setVirtualFileSystem(FS.view(std::nullopt));
    HeaderSearch HS(HSOpts, SM.get(), SM.get().getDiagnostics(), LO,
                    /*Target=*/nullptr);
    for (auto P : SearchPath)
      HS.AddSearchPath(
          DirectoryLookup(
              cantFail(SM.get().getFileManager().getDirectoryRef(testPath(P))),
              SrcMgr::C_System, /*isFramework=*/false),
          true);
    return Set.add(LO, HS);
  };

  Config Cfg;
  Cfg.Index.StandardLibrary = false;
  WithContextValue Disabled(Config::Key, std::move(Cfg));

  LangOptions LO;
  LO.CPlusPlus = true;
  EXPECT_FALSE(Add(LO, {"std"})) << "Disabled in config";

  Cfg = Config();
  Cfg.Index.StandardLibrary = true;
  WithContextValue Enabled(Config::Key, std::move(Cfg));

  EXPECT_FALSE(Add(LO, {"std"})) << "No <vector> found";
  FS.Files["std/vector"] = "class vector;";
  EXPECT_TRUE(Add(LO, {"std"})) << "Indexing as C++98";
  EXPECT_FALSE(Add(LO, {"std"})) << "Don't reindex";
  LO.CPlusPlus11 = true;
  EXPECT_TRUE(Add(LO, {"std"})) << "Indexing as C++11";
  LO.CPlusPlus = false;
  EXPECT_FALSE(Add(LO, {"libc"})) << "No <stdio.h>";
  FS.Files["libc/stdio.h"] = true;
  EXPECT_TRUE(Add(LO, {"libc"})) << "Indexing as C";
}

MATCHER_P(StdlibSymbol, Name, "") {
  return arg.Name == Name && arg.Includes.size() == 1 &&
         llvm::StringRef(arg.Includes.front().Header).starts_with("<");
}

TEST(StdLibTests, EndToEnd) {
  Config Cfg;
  Cfg.Index.StandardLibrary = true;
  WithContextValue Enabled(Config::Key, std::move(Cfg));

  MockFS FS;
  FS.Files["stdlib/vector"] =
      "namespace std { template <class> class vector; }";
  FS.Files["stdlib/list"] =
      " namespace std { template <typename T> class list; }";
  MockCompilationDatabase CDB;
  CDB.ExtraClangFlags.push_back("-isystem" + testPath("stdlib"));
  ClangdServer::Options Opts = ClangdServer::optsForTest();
  Opts.BuildDynamicSymbolIndex = true; // also used for stdlib index
  ClangdServer Server(CDB, FS, Opts);

  Annotations A("std::^");

  Server.addDocument(testPath("foo.cc"), A.code());
  ASSERT_TRUE(Server.blockUntilIdleForTest());
  clangd::CodeCompleteOptions CCOpts;
  auto Completions =
      cantFail(runCodeComplete(Server, testPath("foo.cc"), A.point(), CCOpts));
  EXPECT_THAT(
      Completions.Completions,
      UnorderedElementsAre(StdlibSymbol("list"), StdlibSymbol("vector")));
}

TEST(StdLibTests, StdLibDocComments) {
  Config Cfg;
  Cfg.Index.StandardLibrary = true;
  WithContextValue Enabled(Config::Key, std::move(Cfg));

  MockFS FS;
  FS.Files["stdlib/vector"] = R"cpp(
    namespace std {
      struct vector {
        /**doc comment*/
        struct inner {};
      };
    }
  )cpp";
  MockCompilationDatabase CDB;
  CDB.ExtraClangFlags.push_back("-isystem" + testPath("stdlib"));
  ClangdServer::Options Opts = ClangdServer::optsForTest();
  Opts.BuildDynamicSymbolIndex = true; // also used for stdlib index
  ClangdServer Server(CDB, FS, Opts);

  // Open an empty file. <vector> will not be part of the preamble index,
  // but it will be part of the stdlib index.
  Server.addDocument(testPath("foo.cc"), "");

  // Wait for the stdlib index to be built.
  ASSERT_TRUE(Server.blockUntilIdleForTest());

  // Check that the index contains the doc comment.
  SymbolSlab Result;
  EXPECT_THAT_ERROR(runCustomAction(Server, testPath("foo.cc"),
                                    [&](InputsAndAST Inputs) {
                                      FuzzyFindRequest Req;
                                      Req.Query = "inner";
                                      Req.Scopes = {"std::vector::"};
                                      SymbolSlab::Builder Builder;
                                      Inputs.Inputs.Index->fuzzyFind(
                                          Req, [&](const Symbol &Sym) {
                                            Builder.insert(Sym);
                                          });
                                      Result = std::move(Builder).build();
                                    }),
                    llvm::Succeeded());
  ASSERT_EQ(Result.size(), 1u);
  EXPECT_EQ(Result.begin()->Documentation, "doc comment");
}

} // namespace
} // namespace clangd
} // namespace clang
