//===--- LocateSymbolTest.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 "AnalysisInternal.h"
#include "TypesInternal.h"
#include "clang-include-cleaner/Types.h"
#include "clang/AST/Decl.h"
#include "clang/AST/DeclBase.h"
#include "clang/AST/RecursiveASTVisitor.h"
#include "clang/Basic/SourceLocation.h"
#include "clang/Lex/Preprocessor.h"
#include "clang/Testing/TestAST.h"
#include "clang/Tooling/Inclusions/StandardLibrary.h"
#include "llvm/ADT/StringRef.h"
#include "llvm/Support/Casting.h"
#include "llvm/Testing/Annotations/Annotations.h"
#include "gmock/gmock.h"
#include "gtest/gtest.h"
#include <tuple>
#include <vector>

namespace clang::include_cleaner {
namespace {
using testing::Each;
using testing::ElementsAre;
using testing::ElementsAreArray;
using testing::Eq;
using testing::Field;
using testing::Pair;
using testing::UnorderedElementsAre;

// A helper for building ASTs and getting decls out of it by name. Example usage
// looks like:
//   LocateExample X("void ^foo();");
//   Decl &Foo = X.findDecl("foo");
//   X.points(); // returns all the points in annotated test input.
struct LocateExample {
private:
  llvm::Annotations Target;
  TestAST AST;

public:
  LocateExample(llvm::StringRef AnnotatedCode)
      : Target(AnnotatedCode), AST([this] {
          TestInputs Inputs(Target.code());
          Inputs.ExtraArgs.push_back("-std=c++17");
          return Inputs;
        }()) {}

  const Decl &findDecl(llvm::StringRef SymbolName) {
    struct Visitor : RecursiveASTVisitor<Visitor> {
      llvm::StringRef NameToFind;
      const NamedDecl *Out = nullptr;
      bool VisitNamedDecl(const NamedDecl *ND) {
        // Skip the templated decls, as they have the same name and matches in
        // this file care about the outer template name.
        if (auto *TD = ND->getDescribedTemplate())
          ND = TD;
        if (ND->getName() == NameToFind) {
          EXPECT_TRUE(Out == nullptr || Out == ND->getCanonicalDecl())
              << "Found multiple matches for " << NameToFind.str();
          Out = llvm::cast<NamedDecl>(ND->getCanonicalDecl());
        }
        return true;
      }
    };
    Visitor V;
    V.NameToFind = SymbolName;
    V.TraverseDecl(AST.context().getTranslationUnitDecl());
    if (!V.Out)
      ADD_FAILURE() << "Couldn't find any decls with name: " << SymbolName;
    assert(V.Out);
    return *V.Out;
  }

  Macro findMacro(llvm::StringRef Name) {
    auto &PP = AST.preprocessor();
    auto *II = PP.getIdentifierInfo(Name);
    if (!II || !II->hasMacroDefinition()) {
      ADD_FAILURE() << "Couldn't find any macros with name: " << Name;
      return {};
    }
    auto MD = PP.getMacroDefinition(II);
    assert(MD.getMacroInfo());
    return {II, MD.getMacroInfo()->getDefinitionLoc()};
  }

  std::vector<SymbolLocation> points() {
    auto &SM = AST.sourceManager();
    auto FID = SM.getMainFileID();
    auto Offsets = Target.points();
    std::vector<SymbolLocation> Results;
    for (auto &Offset : Offsets)
      Results.emplace_back(SM.getComposedLoc(FID, Offset));
    return Results;
  }
};

TEST(LocateSymbol, Decl) {
  // Looks for decl with name 'foo' and performs locateSymbol on it.
  // Expects all the locations in the case to be returned as a location.
  const llvm::StringLiteral Cases[] = {
      "struct ^foo; struct ^foo {};",
      "namespace ns { void ^foo(); void ^foo() {} }",
      "enum class ^foo; enum class ^foo {};",
  };

  for (auto &Case : Cases) {
    SCOPED_TRACE(Case);
    LocateExample Test(Case);
    EXPECT_THAT(locateSymbol(Test.findDecl("foo")),
                ElementsAreArray(Test.points()));
  }
}

TEST(LocateSymbol, Stdlib) {
  {
    LocateExample Test("namespace std { struct vector; }");
    EXPECT_THAT(
        locateSymbol(Test.findDecl("vector")),
        ElementsAre(*tooling::stdlib::Symbol::named("std::", "vector")));
  }
  {
    LocateExample Test("#define assert(x)\nvoid foo() { assert(true); }");
    EXPECT_THAT(locateSymbol(Test.findMacro("assert")),
                ElementsAre(*tooling::stdlib::Symbol::named("", "assert")));
  }
}

TEST(LocateSymbol, Macros) {
  // Make sure we preserve the last one.
  LocateExample Test("#define FOO\n#undef FOO\n#define ^FOO");
  EXPECT_THAT(locateSymbol(Test.findMacro("FOO")),
              ElementsAreArray(Test.points()));
}

MATCHER_P2(HintedSymbol, Symbol, Hint, "") {
  return std::tie(arg.Hint, arg) == std::tie(Hint, Symbol);
}
TEST(LocateSymbol, CompleteSymbolHint) {
  {
    // stdlib symbols are always complete.
    LocateExample Test("namespace std { struct vector; }");
    EXPECT_THAT(locateSymbol(Test.findDecl("vector")),
                ElementsAre(HintedSymbol(
                    *tooling::stdlib::Symbol::named("std::", "vector"),
                    Hints::CompleteSymbol)));
  }
  {
    // macros are always complete.
    LocateExample Test("#define ^FOO");
    EXPECT_THAT(locateSymbol(Test.findMacro("FOO")),
                ElementsAre(HintedSymbol(Test.points().front(),
                                         Hints::CompleteSymbol)));
  }
  {
    // Completeness is only absent in cases that matters.
    const llvm::StringLiteral Cases[] = {
        "struct ^foo; struct ^foo {};",
        "template <typename> struct ^foo; template <typename> struct ^foo {};",
        "template <typename> void ^foo(); template <typename> void ^foo() {};",
    };
    for (auto &Case : Cases) {
      SCOPED_TRACE(Case);
      LocateExample Test(Case);
      EXPECT_THAT(locateSymbol(Test.findDecl("foo")),
                  ElementsAre(HintedSymbol(Test.points().front(), Hints::None),
                              HintedSymbol(Test.points().back(),
                                           Hints::CompleteSymbol)));
    }
  }
  {
    // All declarations should be marked as complete in cases that a definition
    // is not usually needed.
    const llvm::StringLiteral Cases[] = {
        "void foo(); void foo() {}",
        "extern int foo; int foo;",
    };
    for (auto &Case : Cases) {
      SCOPED_TRACE(Case);
      LocateExample Test(Case);
      EXPECT_THAT(locateSymbol(Test.findDecl("foo")),
                  Each(Field(&Hinted<SymbolLocation>::Hint,
                             Eq(Hints::CompleteSymbol))));
    }
  }
}

} // namespace
} // namespace clang::include_cleaner
