#include "TestingSupport.h"
#include "clang/AST/ASTContext.h"
#include "clang/ASTMatchers/ASTMatchFinder.h"
#include "clang/ASTMatchers/ASTMatchers.h"
#include "clang/Analysis/FlowSensitive/NoopAnalysis.h"
#include "clang/Tooling/Tooling.h"
#include "llvm/Testing/ADT/StringMapEntry.h"
#include "llvm/Testing/Support/Error.h"
#include "gmock/gmock.h"
#include "gtest/gtest.h"

using namespace clang;
using namespace dataflow;

namespace {

using ::clang::ast_matchers::functionDecl;
using ::clang::ast_matchers::hasAnyName;
using ::clang::ast_matchers::hasName;
using ::clang::ast_matchers::isDefinition;
using ::clang::dataflow::test::AnalysisInputs;
using ::clang::dataflow::test::AnalysisOutputs;
using ::clang::dataflow::test::checkDataflow;
using ::llvm::IsStringMapEntry;
using ::testing::_;
using ::testing::IsEmpty;
using ::testing::UnorderedElementsAre;

template <typename T>
const FunctionDecl *findTargetFunc(ASTContext &Context, T FunctionMatcher) {
  auto TargetMatcher =
      functionDecl(FunctionMatcher, isDefinition()).bind("target");
  for (const auto &Node : ast_matchers::match(TargetMatcher, Context)) {
    const auto *Func = Node.template getNodeAs<FunctionDecl>("target");
    if (Func == nullptr)
      continue;
    if (Func->isTemplated())
      continue;
    return Func;
  }
  return nullptr;
}

void runTest(
    llvm::StringRef Code, llvm::StringRef TargetName,
    std::function<void(const llvm::DenseMap<const Stmt *, std::string> &)>
        RunChecks) {
  llvm::Annotations AnnotatedCode(Code);
  auto Unit = tooling::buildASTFromCodeWithArgs(
      AnnotatedCode.code(), {"-fsyntax-only", "-std=c++17"});
  auto &Context = Unit->getASTContext();
  const FunctionDecl *Func = findTargetFunc(Context, hasName(TargetName));
  ASSERT_NE(Func, nullptr);

  llvm::Expected<llvm::DenseMap<const Stmt *, std::string>> Mapping =
      test::buildStatementToAnnotationMapping(Func, AnnotatedCode);
  ASSERT_TRUE(static_cast<bool>(Mapping));

  RunChecks(Mapping.get());
}

TEST(BuildStatementToAnnotationMappingTest, ReturnStmt) {
  runTest(R"(
    int target() {
      return 42;
      /*[[ok]]*/
    }
  )",
          "target",
          [](const llvm::DenseMap<const Stmt *, std::string> &Annotations) {
            ASSERT_EQ(Annotations.size(), static_cast<unsigned int>(1));
            EXPECT_TRUE(isa<ReturnStmt>(Annotations.begin()->first));
            EXPECT_EQ(Annotations.begin()->second, "ok");
          });
}

void checkDataflow(
    llvm::StringRef Code,
    ast_matchers::internal::Matcher<FunctionDecl> TargetFuncMatcher,
    std::function<
        void(const llvm::StringMap<DataflowAnalysisState<NoopLattice>> &,
             const AnalysisOutputs &)>
        Expectations) {
  ASSERT_THAT_ERROR(checkDataflow<NoopAnalysis>(
                        AnalysisInputs<NoopAnalysis>(
                            Code, std::move(TargetFuncMatcher),
                            [](ASTContext &Context, Environment &) {
                              return NoopAnalysis(
                                  Context,
                                  // Don't apply builtin transfer function.
                                  DataflowAnalysisOptions{std::nullopt});
                            })
                            .withASTBuildArgs({"-fsyntax-only", "-std=c++17"}),
                        /*VerifyResults=*/std::move(Expectations)),
                    llvm::Succeeded());
}

TEST(ProgramPointAnnotations, NoAnnotations) {
  ::testing::MockFunction<void(
      const llvm::StringMap<DataflowAnalysisState<NoopLattice>> &,
      const AnalysisOutputs &)>
      Expectations;

  EXPECT_CALL(Expectations, Call(IsEmpty(), _)).Times(1);

  checkDataflow("void target() {}", hasName("target"),
                Expectations.AsStdFunction());
}

TEST(ProgramPointAnnotations, NoAnnotationsDifferentTarget) {
  ::testing::MockFunction<void(
      const llvm::StringMap<DataflowAnalysisState<NoopLattice>> &,
      const AnalysisOutputs &)>
      Expectations;

  EXPECT_CALL(Expectations, Call(IsEmpty(), _)).Times(1);

  checkDataflow("void target() {}", hasName("target"),
                Expectations.AsStdFunction());
}

TEST(ProgramPointAnnotations, WithProgramPoint) {
  ::testing::MockFunction<void(
      const llvm::StringMap<DataflowAnalysisState<NoopLattice>> &,
      const AnalysisOutputs &)>
      Expectations;

  EXPECT_CALL(
      Expectations,
      Call(UnorderedElementsAre(IsStringMapEntry("program-point", _)), _))
      .Times(1);

  checkDataflow(R"cc(void target() {
                       int n;
                       // [[program-point]]
                     })cc",
                hasName("target"), Expectations.AsStdFunction());
}

TEST(ProgramPointAnnotations, MultipleProgramPoints) {
  ::testing::MockFunction<void(
      const llvm::StringMap<DataflowAnalysisState<NoopLattice>> &,
      const AnalysisOutputs &)>
      Expectations;

  EXPECT_CALL(Expectations,
              Call(UnorderedElementsAre(IsStringMapEntry("program-point-1", _),
                                        IsStringMapEntry("program-point-2", _)),
                   _))
      .Times(1);

  checkDataflow(R"cc(void target(bool b) {
                       if (b) {
                         int n;
                         // [[program-point-1]]
                       } else {
                         int m;
                         // [[program-point-2]]
                       }
                     })cc",
                hasName("target"), Expectations.AsStdFunction());
}

TEST(ProgramPointAnnotations, MultipleFunctionsMultipleProgramPoints) {
  ::testing::MockFunction<void(
      const llvm::StringMap<DataflowAnalysisState<NoopLattice>> &,
      const AnalysisOutputs &)>
      Expectations;

  EXPECT_CALL(Expectations, Call(UnorderedElementsAre(
                                     IsStringMapEntry("program-point-1a", _),
                                     IsStringMapEntry("program-point-1b", _)),
                                 _))
      .Times(1);

  EXPECT_CALL(Expectations, Call(UnorderedElementsAre(
                                     IsStringMapEntry("program-point-2a", _),
                                     IsStringMapEntry("program-point-2b", _)),
                                 _))
      .Times(1);

  checkDataflow(
      R"cc(
        void target1(bool b) {
          if (b) {
            int n;
            // [[program-point-1a]]
          } else {
            int m;
            // [[program-point-1b]]
          }
        }

        void target2(bool b) {
          if (b) {
            int n;
            // [[program-point-2a]]
          } else {
            int m;
            // [[program-point-2b]]
          }
        }
      )cc",
      functionDecl(hasAnyName("target1", "target2")),
      Expectations.AsStdFunction());
}

} // namespace
