//== unittests/Serialization/LoadSpecLazily.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 "clang/Frontend/CompilerInstance.h"
#include "clang/Frontend/FrontendAction.h"
#include "clang/Frontend/FrontendActions.h"
#include "clang/Parse/ParseAST.h"
#include "clang/Serialization/ASTDeserializationListener.h"
#include "clang/Tooling/Tooling.h"
#include "gtest/gtest.h"

using namespace llvm;
using namespace clang;
using namespace clang::tooling;

namespace {

class LoadSpecLazilyTest : public ::testing::Test {
  void SetUp() override {
    ASSERT_FALSE(
        sys::fs::createUniqueDirectory("load-spec-lazily-test", TestDir));
  }

  void TearDown() override { sys::fs::remove_directories(TestDir); }

public:
  SmallString<256> TestDir;

  void addFile(StringRef Path, StringRef Contents) {
    ASSERT_FALSE(sys::path::is_absolute(Path));

    SmallString<256> AbsPath(TestDir);
    sys::path::append(AbsPath, Path);

    ASSERT_FALSE(
        sys::fs::create_directories(llvm::sys::path::parent_path(AbsPath)));

    std::error_code EC;
    llvm::raw_fd_ostream OS(AbsPath, EC);
    ASSERT_FALSE(EC);
    OS << Contents;
  }

  std::string GenerateModuleInterface(StringRef ModuleName,
                                      StringRef Contents) {
    std::string FileName = llvm::Twine(ModuleName + ".cppm").str();
    addFile(FileName, Contents);

    IntrusiveRefCntPtr<llvm::vfs::FileSystem> VFS =
        llvm::vfs::createPhysicalFileSystem();
    IntrusiveRefCntPtr<DiagnosticsEngine> Diags =
        CompilerInstance::createDiagnostics(*VFS, new DiagnosticOptions());
    CreateInvocationOptions CIOpts;
    CIOpts.Diags = Diags;
    CIOpts.VFS = VFS;

    std::string CacheBMIPath =
        llvm::Twine(TestDir + "/" + ModuleName + ".pcm").str();
    std::string PrebuiltModulePath =
        "-fprebuilt-module-path=" + TestDir.str().str();
    const char *Args[] = {"clang++",
                          "-std=c++20",
                          "--precompile",
                          PrebuiltModulePath.c_str(),
                          "-working-directory",
                          TestDir.c_str(),
                          "-I",
                          TestDir.c_str(),
                          FileName.c_str(),
                          "-o",
                          CacheBMIPath.c_str()};
    std::shared_ptr<CompilerInvocation> Invocation =
        createInvocation(Args, CIOpts);
    EXPECT_TRUE(Invocation);

    CompilerInstance Instance;
    Instance.setDiagnostics(Diags.get());
    Instance.setInvocation(Invocation);
    Instance.getFrontendOpts().OutputFile = CacheBMIPath;
    GenerateModuleInterfaceAction Action;
    EXPECT_TRUE(Instance.ExecuteAction(Action));
    EXPECT_FALSE(Diags->hasErrorOccurred());

    return CacheBMIPath;
  }
};

enum class CheckingMode { Forbidden, Required };

class DeclsReaderListener : public ASTDeserializationListener {
  StringRef SpeficiedName;
  CheckingMode Mode;

  bool ReadedSpecifiedName = false;

public:
  void DeclRead(GlobalDeclID ID, const Decl *D) override {
    auto *ND = dyn_cast<NamedDecl>(D);
    if (!ND)
      return;

    ReadedSpecifiedName |= ND->getName().contains(SpeficiedName);
    if (Mode == CheckingMode::Forbidden) {
      EXPECT_FALSE(ReadedSpecifiedName);
    }
  }

  DeclsReaderListener(StringRef SpeficiedName, CheckingMode Mode)
      : SpeficiedName(SpeficiedName), Mode(Mode) {}

  ~DeclsReaderListener() {
    if (Mode == CheckingMode::Required) {
      EXPECT_TRUE(ReadedSpecifiedName);
    }
  }
};

class LoadSpecLazilyConsumer : public ASTConsumer {
  DeclsReaderListener Listener;

public:
  LoadSpecLazilyConsumer(StringRef SpecifiedName, CheckingMode Mode)
      : Listener(SpecifiedName, Mode) {}

  ASTDeserializationListener *GetASTDeserializationListener() override {
    return &Listener;
  }
};

class CheckLoadSpecLazilyAction : public ASTFrontendAction {
  StringRef SpecifiedName;
  CheckingMode Mode;

public:
  std::unique_ptr<ASTConsumer>
  CreateASTConsumer(CompilerInstance &CI, StringRef /*Unused*/) override {
    return std::make_unique<LoadSpecLazilyConsumer>(SpecifiedName, Mode);
  }

  CheckLoadSpecLazilyAction(StringRef SpecifiedName, CheckingMode Mode)
      : SpecifiedName(SpecifiedName), Mode(Mode) {}
};

TEST_F(LoadSpecLazilyTest, BasicTest) {
  GenerateModuleInterface("M", R"cpp(
export module M;
export template <class T>
class A {};
export class ShouldNotBeLoaded {};
export class Temp {
   A<ShouldNotBeLoaded> AS;
};
  )cpp");

  const char *test_file_contents = R"cpp(
import M;
A<int> a;
  )cpp";
  std::string DepArg = "-fprebuilt-module-path=" + TestDir.str().str();
  EXPECT_TRUE(
      runToolOnCodeWithArgs(std::make_unique<CheckLoadSpecLazilyAction>(
                                "ShouldNotBeLoaded", CheckingMode::Forbidden),
                            test_file_contents,
                            {
                                "-std=c++20",
                                DepArg.c_str(),
                                "-I",
                                TestDir.c_str(),
                            },
                            "test.cpp"));
}

TEST_F(LoadSpecLazilyTest, ChainedTest) {
  GenerateModuleInterface("M", R"cpp(
export module M;
export template <class T>
class A {};
  )cpp");

  GenerateModuleInterface("N", R"cpp(
export module N;
export import M;
export class ShouldNotBeLoaded {};
export class Temp {
   A<ShouldNotBeLoaded> AS;
};
  )cpp");

  const char *test_file_contents = R"cpp(
import N;
A<int> a;
  )cpp";
  std::string DepArg = "-fprebuilt-module-path=" + TestDir.str().str();
  EXPECT_TRUE(
      runToolOnCodeWithArgs(std::make_unique<CheckLoadSpecLazilyAction>(
                                "ShouldNotBeLoaded", CheckingMode::Forbidden),
                            test_file_contents,
                            {
                                "-std=c++20",
                                DepArg.c_str(),
                                "-I",
                                TestDir.c_str(),
                            },
                            "test.cpp"));
}

/// Test that we won't crash due to we may invalidate the lazy specialization
/// lookup table during the loading process.
TEST_F(LoadSpecLazilyTest, ChainedTest2) {
  GenerateModuleInterface("M", R"cpp(
export module M;
export template <class T>
class A {};

export class B {};

export class C {
  A<B> D;
};
  )cpp");

  GenerateModuleInterface("N", R"cpp(
export module N;
export import M;
export class MayBeLoaded {};

export class Temp {
   A<MayBeLoaded> AS;
};

export class ExportedClass {};

export template<> class A<ExportedClass> {
   A<MayBeLoaded> AS;
   A<B>           AB;
};
  )cpp");

  const char *test_file_contents = R"cpp(
import N;
Temp T;
A<ExportedClass> a;
  )cpp";
  std::string DepArg = "-fprebuilt-module-path=" + TestDir.str().str();
  EXPECT_TRUE(runToolOnCodeWithArgs(std::make_unique<CheckLoadSpecLazilyAction>(
                                        "MayBeLoaded", CheckingMode::Required),
                                    test_file_contents,
                                    {
                                        "-std=c++20",
                                        DepArg.c_str(),
                                        "-I",
                                        TestDir.c_str(),
                                    },
                                    "test.cpp"));
}

} // namespace
