//===-- CollectMacrosTests.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 "AST.h"
#include "Annotations.h"
#include "CollectMacros.h"
#include "Matchers.h"
#include "SourceCode.h"
#include "TestTU.h"
#include "clang/Basic/SourceLocation.h"
#include "llvm/Support/ScopedPrinter.h"
#include "gmock/gmock.h"
#include "gtest/gtest.h"
#include <vector>

namespace clang {
namespace clangd {
namespace {

using testing::UnorderedElementsAreArray;

MATCHER_P(rangeIs, R, "") {
  return arg.StartOffset == R.Begin && arg.EndOffset == R.End;
}
MATCHER(isDef, "") { return arg.IsDefinition; }
MATCHER(inConditionalDirective, "") { return arg.InConditionalDirective; }

TEST(CollectMainFileMacros, SelectedMacros) {
  // References of the same symbol must have the ranges with the same
  // name(integer). If there are N different symbols then they must be named
  // from 1 to N. Macros for which SymbolID cannot be computed must be named
  // "Unknown". The payload of the annotation describes the extra bit
  // information of the MacroOccurrence (e.g. $1(def) => IsDefinition).
  const char *Tests[] = {
      R"cpp(// Macros: Cursor on definition.
        #define $1(def)[[FOO]](x,y) (x + y)
        int main() { int x = $1[[FOO]]($1[[FOO]](3, 4), $1[[FOO]](5, 6)); }
      )cpp",
      R"cpp(
        #define $1(def)[[M]](X) X;
        #define $2(def)[[abc]] 123
        int s = $1[[M]]($2[[abc]]);
      )cpp",
      // FIXME: Locating macro in duplicate definitions doesn't work. Enable
      // this once LocateMacro is fixed.
      // R"cpp(// Multiple definitions.
      //   #define $1[[abc]] 1
      //   int func1() { int a = $1[[abc]];}
      //   #undef $1[[abc]]

      //   #define $2[[abc]] 2
      //   int func2() { int a = $2[[abc]];}
      //   #undef $2[[abc]]
      // )cpp",
      R"cpp(
        #ifdef $Unknown(condit)[[UNDEFINED]]
        #elifdef $Unknown(condit)[[UNDEFINED]]
        #endif

        #ifdef $Unknown(condit)[[UNDEFINED]]
        #elifndef $Unknown(condit)[[UNDEFINED]]
        #endif

        #ifndef $Unknown(condit)[[UNDEFINED]]
        #endif

        #if defined($Unknown(condit)[[UNDEFINED]])
        #endif
      )cpp",
      R"cpp(
        #ifndef $Unknown(condit)[[abc]]
        #define $1(def)[[abc]]
        #ifdef $1(condit)[[abc]]
        #endif
        #endif
      )cpp",
      R"cpp(
        // Macros from token concatenations not included.
        #define $1(def)[[CONCAT]](X) X##A()
        #define $2(def)[[PREPEND]](X) MACRO##X()
        #define $3(def)[[MACROA]]() 123
        int B = $1[[CONCAT]](MACRO);
        int D = $2[[PREPEND]](A);
      )cpp",
      R"cpp(
        #define $1(def)[[MACRO_ARGS2]](X, Y) X Y
        #define $3(def)[[BAR]] 1
        #define $2(def)[[FOO]] $3[[BAR]]
        int A = $2[[FOO]];
      )cpp"};
  auto ExpectedResults = [](const llvm::Annotations &T, StringRef Name) {
    std::vector<Matcher<MacroOccurrence>> ExpectedLocations;
    for (const auto &[R, Bits] : T.rangesWithPayload(Name)) {
      if (Bits == "def")
        ExpectedLocations.push_back(testing::AllOf(rangeIs(R), isDef()));
      else if (Bits == "condit")
        ExpectedLocations.push_back(
            testing::AllOf(rangeIs(R), inConditionalDirective()));
      else
        ExpectedLocations.push_back(testing::AllOf(rangeIs(R)));
    }
    return ExpectedLocations;
  };

  for (const char *Test : Tests) {
    llvm::Annotations T(Test);
    auto Inputs = TestTU::withCode(T.code());
    Inputs.ExtraArgs.push_back("-std=c++2b");
    auto AST = Inputs.build();
    auto ActualMacroRefs = AST.getMacros();
    auto &SM = AST.getSourceManager();
    auto &PP = AST.getPreprocessor();
    for (const auto &[Name, Ranges] : T.all_ranges()) {
      if (Name == "Unknown") {
        EXPECT_THAT(ActualMacroRefs.UnknownMacros,
                    UnorderedElementsAreArray(ExpectedResults(T, "Unknown")))
            << "Unknown macros doesn't match in " << Test;
        continue;
      }

      auto Loc = sourceLocationInMainFile(
          SM, offsetToPosition(T.code(), Ranges.front().Begin));
      ASSERT_TRUE(bool(Loc));
      const auto *Id = syntax::spelledIdentifierTouching(*Loc, AST.getTokens());
      ASSERT_TRUE(Id);
      auto Macro = locateMacroAt(*Id, PP);
      assert(Macro);
      auto SID = getSymbolID(Macro->Name, Macro->Info, SM);

      EXPECT_THAT(ActualMacroRefs.MacroRefs[SID],
                  UnorderedElementsAreArray(ExpectedResults(T, Name)))
          << "Annotation=" << Name << ", MacroName=" << Macro->Name
          << ", Test = " << Test;
    }
  }
}
} // namespace
} // namespace clangd
} // namespace clang
