//===- LibraryResolverTest.cpp - Unit tests for LibraryResolver -===//
//
// 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 "llvm/ExecutionEngine/Orc/TargetProcess/LibraryResolver.h"
#include "llvm/ExecutionEngine/Orc/JITTargetMachineBuilder.h"
#include "llvm/ExecutionEngine/Orc/TargetProcess/LibraryScanner.h"
#include "llvm/ObjectYAML/MachOYAML.h"
#include "llvm/ObjectYAML/yaml2obj.h"
#include "llvm/Support/FileSystem.h"
#include "llvm/Support/MemoryBuffer.h"
#include "llvm/Support/Path.h"
#include "llvm/Support/YAMLParser.h"
#include "llvm/Support/YAMLTraits.h"
#include "llvm/Support/raw_ostream.h"

#include "llvm/Testing/Support/SupportHelpers.h"

#include "gtest/gtest.h"

#include <algorithm>
#include <string>
#include <vector>

using namespace llvm;
using namespace llvm::orc;

#if defined(__APPLE__) || defined(__linux__)
// TODO: Add COFF (Windows) support for these tests.
// this facility also works correctly on Windows (COFF),
// so we should eventually enable and run these tests for that platform as well.
namespace {

#if defined(__APPLE__)
constexpr const char *ext = ".dylib";
#elif defined(_WIN32)
constexpr const char *ext = ".dll";
#else
constexpr const char *ext = ".so";
#endif

bool EnvReady = false;

Triple getTargetTriple() {
  auto JTMB = JITTargetMachineBuilder::detectHost();
  if (!JTMB) {
    consumeError(JTMB.takeError());
    return Triple();
  }
  return JTMB->getTargetTriple();
}

static bool CheckHostSupport() {
  auto Triple = getTargetTriple();
  // TODO: Extend support to COFF (Windows) once test setup and YAML conversion
  // are verified.
  if (!Triple.isOSBinFormatMachO() &&
      !(Triple.isOSBinFormatELF() && Triple.getArch() == Triple::x86_64))
    return false;

  return true;
}

std::string getYamlFilePlatformExt() {
  auto Triple = getTargetTriple();
  if (Triple.isOSBinFormatMachO())
    return "_macho";
  else if (Triple.isOSBinFormatELF())
    return "_linux";

  return "";
}

unsigned getYamlDocNum() {
  // auto Triple = getTargetTriple();
  // if (Triple.isOSBinFormatELF())
  //   return 1;

  return 1;
}

class LibraryTestEnvironment : public ::testing::Environment {
  std::vector<std::string> CreatedDylibsDir;
  std::vector<std::string> CreatedDylibs;
  SmallVector<char, 128> DirPath;

public:
  void SetUp() override {
    if (!CheckHostSupport()) {
      EnvReady = false;
      return;
    }

    StringRef ThisFile = __FILE__;
    SmallVector<char, 128> InputDirPath(ThisFile.begin(), ThisFile.end());
    sys::path::remove_filename(InputDirPath);
    sys::path::append(InputDirPath, "Inputs");
    if (!sys::fs::exists(InputDirPath))
      return;

    SmallString<512> ExecPath(sys::fs::getMainExecutable(nullptr, nullptr));
    sys::path::remove_filename(ExecPath);
    SmallString<128> UniqueDir(ExecPath);
    std::error_code EC = sys::fs::createUniqueDirectory(UniqueDir, DirPath);

    if (EC)
      return;

    // given yamlPath + DylibPath, validate + convert
    auto processYamlToDylib = [&](const SmallVector<char, 128> &YamlPath,
                                  const SmallVector<char, 128> &DylibPath,
                                  unsigned DocNum) -> bool {
      if (!sys::fs::exists(YamlPath)) {
        errs() << "YAML file missing: "
               << StringRef(YamlPath.data(), YamlPath.size()) << "\n";
        EnvReady = false;
        return false;
      }

      auto BufOrErr = MemoryBuffer::getFile(YamlPath);
      if (!BufOrErr) {
        errs() << "Failed to read "
               << StringRef(YamlPath.data(), YamlPath.size()) << ": "
               << BufOrErr.getError().message() << "\n";
        EnvReady = false;
        return false;
      }

      yaml::Input yin(BufOrErr->get()->getBuffer());
      std::error_code EC;
      raw_fd_ostream outFile(StringRef(DylibPath.data(), DylibPath.size()), EC,
                             sys::fs::OF_None);

      if (EC) {
        errs() << "Failed to open "
               << StringRef(DylibPath.data(), DylibPath.size())
               << " for writing: " << EC.message() << "\n";
        EnvReady = false;
        return false;
      }

      if (!yaml::convertYAML(
              yin, outFile,
              [](const Twine &M) {
                // Handle or ignore errors here
                errs() << "Yaml Error :" << M << "\n";
              },
              DocNum)) {
        errs() << "Failed to convert "
               << StringRef(YamlPath.data(), YamlPath.size()) << " to "
               << StringRef(DylibPath.data(), DylibPath.size()) << "\n";
        EnvReady = false;
        return false;
      }

      CreatedDylibsDir.push_back(std::string(sys::path::parent_path(
          StringRef(DylibPath.data(), DylibPath.size()))));
      CreatedDylibs.push_back(std::string(DylibPath.begin(), DylibPath.end()));
      return true;
    };

    std::vector<const char *> LibDirs = {"Z", "A", "B", "C"};

    unsigned DocNum = getYamlDocNum();
    std::string YamlPltExt = getYamlFilePlatformExt();
    for (const auto &LibdirName : LibDirs) {
      // YAML path
      SmallVector<char, 128> YamlPath(InputDirPath.begin(), InputDirPath.end());
      SmallVector<char, 128> YamlFileName;
      YamlFileName.append(LibdirName, LibdirName + strlen(LibdirName));
      YamlFileName.append(YamlPltExt.begin(), YamlPltExt.end());
      sys::path::append(YamlPath, LibdirName, YamlFileName);
      sys::path::replace_extension(YamlPath, ".yaml");

      // dylib path
      SmallVector<char, 128> DylibPath(DirPath.begin(), DirPath.end());
      SmallVector<char, 128> DylibFileName;
      StringRef prefix("lib");
      DylibFileName.append(prefix.begin(), prefix.end());
      DylibFileName.append(LibdirName, LibdirName + strlen(LibdirName));

      sys::path::append(DylibPath, LibdirName);
      if (!sys::fs::exists(DylibPath)) {
        auto EC = sys::fs::create_directory(DylibPath);
        if (EC)
          return;
      }
      sys::path::append(DylibPath, DylibFileName);
      sys::path::replace_extension(DylibPath, ext);
      if (!processYamlToDylib(YamlPath, DylibPath, DocNum))
        return;
    }

    EnvReady = true;
  }

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

  std::string getBaseDir() const {
    return std::string(DirPath.begin(), DirPath.end());
  }

  std::vector<std::string> getDylibPaths() const { return CreatedDylibs; }
};

static LibraryTestEnvironment *GlobalEnv =
    static_cast<LibraryTestEnvironment *>(
        ::testing::AddGlobalTestEnvironment(new LibraryTestEnvironment()));

inline std::string libPath(const std::string &BaseDir,
                           const std::string &name) {
#if defined(__APPLE__)
  return BaseDir + "/" + name + ".dylib";
#elif defined(_WIN32)
  return BaseDir + "/" + name + ".dll";
#else
  return BaseDir + "/" + name + ".so";
#endif
}

inline std::string withext(const std::string &lib) {
  SmallString<128> P(lib);
  sys::path::replace_extension(P, ext);
  return P.str().str();
}

inline std::string platformSymbolName(const std::string &name) {
#if defined(__APPLE__)
  return "_" + name; // macOS prepends underscore
#else
  return name;
#endif
}

struct TestLibrary {
  std::string path;
  std::vector<std::string> Syms;
};

class LibraryResolverIT : public ::testing::Test {
protected:
  std::string BaseDir;
  std::unordered_map<std::string, TestLibrary> libs;

  void addLib(const std::string &name) {
    SmallString<512> path;
    std::error_code EC =
        sys::fs::real_path(libPath(BaseDir, name + "/lib" + name), path);
    if (EC || path.empty() || !sys::fs::exists(path))
      GTEST_SKIP();
    libs[name] = {path.str().str(), {platformSymbolName("say" + name)}};
  }

  void SetUp() override {
    if (!EnvReady || GlobalEnv == nullptr)
      GTEST_SKIP() << "Skipping test: environment setup failed.";

    {
      SmallString<512> path;
      std::error_code EC = sys::fs::real_path(GlobalEnv->getBaseDir(), path);
      if (path.empty() || EC)
        GTEST_SKIP() << "Base directory resolution failed: " << EC.message();
      BaseDir = path.str().str();
    }

    for (const auto &P : GlobalEnv->getDylibPaths()) {
      if (!sys::fs::exists(P))
        GTEST_SKIP() << "Missing dylib path: " << P;
    }

    const std::vector<std::string> libNames = {"A", "B", "C", "Z"};
    for (const auto &name : libNames)
      addLib(name);

    if (!EnvReady)
      GTEST_SKIP() << "Skipping test: environment setup failed.";
  }

  const std::vector<std::string> &sym(const std::string &key) {
    return libs[key].Syms;
  }
  const std::string &lib(const std::string &key) { return libs[key].path; }
  const std::string libdir(const std::string &key) {
    SmallString<512> P(libs[key].path);
    sys::path::remove_filename(P);
    return P.str().str();
  }
  const std::string libname(const std::string &key) {
    return sys::path::filename(libs[key].path).str();
  }
};

// Helper: allow either "sayA" or "_sayA" depending on how your
// SymbolEnumerator reports.
static bool matchesEitherUnderscore(const std::string &got,
                                    const std::string &bare) {
  return got == bare || got == ("_" + bare);
}

// Helper: normalize path ending check (we only care that it resolved to the
// right dylib)
static bool endsWith(const std::string &s, const std::string &suffix) {
  if (s.size() < suffix.size())
    return false;
  return std::equal(suffix.rbegin(), suffix.rend(), s.rbegin());
}

static llvm::SmallVector<llvm::StringRef, 16>
toRefs(const std::vector<std::string> &Syms) {
  llvm::SmallVector<llvm::StringRef, 16> R;
  for (auto &S : Syms)
    R.push_back(S);
  return R;
}

TEST_F(LibraryResolverIT, EnumerateSymbols_ExportsOnly_DefaultFlags) {
  const std::string libC = lib("C");
  SymbolEnumeratorOptions Opts = SymbolEnumeratorOptions::defaultOptions();

  std::vector<std::string> seen;
  auto onEach = [&](llvm::StringRef sym) -> EnumerateResult {
    seen.emplace_back(sym.str());
    return EnumerateResult::Continue;
  };

  ASSERT_TRUE(SymbolEnumerator::enumerateSymbols(libC, onEach, Opts));

  // sayC is exported, others are undefined → only sayC expected
  EXPECT_TRUE(any_of(seen, [&](const std::string &s) {
    return matchesEitherUnderscore(s, "sayC");
  }));
  EXPECT_FALSE(any_of(seen, [&](const std::string &s) {
    return matchesEitherUnderscore(s, "sayA");
  }));
  EXPECT_FALSE(any_of(seen, [&](const std::string &s) {
    return matchesEitherUnderscore(s, "sayB");
  }));
}

TEST_F(LibraryResolverIT, EnumerateSymbols_IncludesUndefineds) {
  const std::string libC = lib("C");

  SymbolEnumeratorOptions Opts;
  Opts.FilterFlags =
      SymbolEnumeratorOptions::IgnoreWeak |
      SymbolEnumeratorOptions::IgnoreIndirect; // no IgnoreUndefined

  std::vector<std::string> seen;
  auto onEach = [&](llvm::StringRef sym) -> EnumerateResult {
    seen.emplace_back(sym.str());
    return EnumerateResult::Continue;
  };

  ASSERT_TRUE(SymbolEnumerator::enumerateSymbols(libC, onEach, Opts));

  // Now we should see both sayC (export) and the undefined refs sayA, sayB,
  // sayZ
  EXPECT_TRUE(any_of(seen, [&](const std::string &s) {
    return matchesEitherUnderscore(s, "sayC");
  }));
  EXPECT_TRUE(any_of(seen, [&](const std::string &s) {
    return matchesEitherUnderscore(s, "sayA");
  }));
  EXPECT_TRUE(any_of(seen, [&](const std::string &s) {
    return matchesEitherUnderscore(s, "sayB");
  }));
}

// Full resolution via LibraryResolutionDriver/LibraryResolver ---
TEST_F(LibraryResolverIT, DriverResolvesSymbolsToCorrectLibraries) {
  // Create the resolver from real base paths (our fixtures dir)
  auto Stup = LibraryResolver::Setup::create({BaseDir});

  // Full system behavior: no mocks
  auto Driver = LibraryResolutionDriver::create(Stup);
  ASSERT_NE(Driver, nullptr);

  // Tell the Driver about the scan path kinds (User/System) as your
  // production code expects.
  Driver->addScanPath(libdir("A"), PathType::User);
  Driver->addScanPath(libdir("B"), PathType::User);
  Driver->addScanPath(libdir("Z"), PathType::User);

  // Symbols to resolve (bare names; class handles underscore differences
  // internally)
  std::vector<std::string> Syms = {platformSymbolName("sayA"),
                                   platformSymbolName("sayB"),
                                   platformSymbolName("sayZ")};

  bool CallbackRan = false;
  Driver->resolveSymbols(toRefs(Syms), [&](SymbolQuery &Q) {
    CallbackRan = true;

    // sayA should resolve to A.dylib
    {
      auto lib = Q.getResolvedLib(platformSymbolName("sayA"));
      ASSERT_TRUE(lib.has_value()) << "sayA should be resolved";
      EXPECT_TRUE(endsWith(lib->str(), libname("A")))
          << "sayA resolved to: " << lib->str();
    }

    // sayB should resolve to B.dylib
    {
      auto lib = Q.getResolvedLib(platformSymbolName("sayB"));
      ASSERT_TRUE(lib.has_value()) << "sayB should be resolved";
      EXPECT_TRUE(endsWith(lib->str(), libname("B")))
          << "sayB resolved to: " << lib->str();
    }

    // sayZ should resolve to B.dylib
    {
      auto lib = Q.getResolvedLib(platformSymbolName("sayZ"));
      ASSERT_TRUE(lib.has_value()) << "sayZ should be resolved";
      EXPECT_TRUE(endsWith(lib->str(), libname("Z")))
          << "sayZ resolved to: " << lib->str();
    }

    EXPECT_TRUE(Q.allResolved());
  });

  EXPECT_TRUE(CallbackRan);
}

// stress SymbolQuery with the real resolve flow
// And resolve libC dependency libA, libB, libZ ---
TEST_F(LibraryResolverIT, ResolveManySymbols) {
  auto Stup = LibraryResolver::Setup::create({BaseDir});
  auto Driver = LibraryResolutionDriver::create(Stup);
  ASSERT_NE(Driver, nullptr);
  Driver->addScanPath(libdir("C"), PathType::User);

  // Many duplicates to provoke concurrent updates inside SymbolQuery
  std::vector<std::string> Syms = {
      platformSymbolName("sayA"), platformSymbolName("sayB"),
      platformSymbolName("sayA"), platformSymbolName("sayB"),
      platformSymbolName("sayZ"), platformSymbolName("sayZ"),
      platformSymbolName("sayZ"), platformSymbolName("sayZ"),
      platformSymbolName("sayA"), platformSymbolName("sayB"),
      platformSymbolName("sayA"), platformSymbolName("sayB")};

  bool CallbackRan = false;
  Driver->resolveSymbols(toRefs(Syms), [&](SymbolQuery &Q) {
    CallbackRan = true;
    EXPECT_TRUE(Q.isResolved(platformSymbolName("sayA")));
    EXPECT_TRUE(Q.isResolved(platformSymbolName("sayB")));
    EXPECT_TRUE(Q.isResolved(platformSymbolName("sayZ")));

    auto A = Q.getResolvedLib(platformSymbolName("sayA"));
    auto B = Q.getResolvedLib(platformSymbolName("sayB"));
    auto Z = Q.getResolvedLib(platformSymbolName("sayZ"));
    ASSERT_TRUE(A.has_value());
    ASSERT_TRUE(B.has_value());
    ASSERT_TRUE(Z.has_value());
    EXPECT_TRUE(endsWith(A->str(), libname("A")));
    EXPECT_TRUE(endsWith(B->str(), libname("B")));
    EXPECT_TRUE(endsWith(Z->str(), libname("Z")));
    EXPECT_TRUE(Q.allResolved());
  });

  EXPECT_TRUE(CallbackRan);
}

TEST_F(LibraryResolverIT, ScanAndResolveDependencyGraph) {
  auto LibPathCache = std::make_shared<LibraryPathCache>();
  auto PResolver = std::make_shared<PathResolver>(LibPathCache);
  LibraryScanHelper ScanH({}, LibPathCache, PResolver);

  ScanH.addBasePath(libdir("C"), PathType::User);

  LibraryManager LibMgr;
  LibraryScanner Scanner(ScanH, LibMgr);

  Scanner.scanNext(PathType::User, 0);

  size_t numLibs = 0;
  LibMgr.forEachLibrary([&](const LibraryInfo &L) {
    numLibs++;
    return true;
  });

  EXPECT_GT(numLibs, 0u) << "Expected at least one library scanned";

  // Validate that each scanned library path is resolvable
  std::error_code EC;
  LibMgr.forEachLibrary([&](const LibraryInfo &L) {
    auto R = PResolver->resolve(L.getFullPath(), EC);
    EXPECT_TRUE(R.has_value());
    EXPECT_FALSE(EC);
    return true;
  });
}

TEST_F(LibraryResolverIT, ScanEmptyPath) {
  auto LibPathCache = std::make_shared<LibraryPathCache>();
  auto PResolver = std::make_shared<PathResolver>(LibPathCache);
  LibraryScanHelper ScanH({}, LibPathCache, PResolver);

  ScanH.addBasePath("/tmp/empty", PathType::User);

  LibraryManager LibMgr;
  LibraryScanner Scanner(ScanH, LibMgr);

  Scanner.scanNext(PathType::User, 0);

  size_t count = 0;
  LibMgr.forEachLibrary([&](const LibraryInfo &) {
    count++;
    return true;
  });
  EXPECT_EQ(count, 0u);
}

TEST_F(LibraryResolverIT, PathResolverResolvesKnownPaths) {
  auto LibPathCache = std::make_shared<LibraryPathCache>();
  auto PResolver = std::make_shared<PathResolver>(LibPathCache);

  std::error_code EC;
  auto Missing = PResolver->resolve("temp/foo/bar", EC);
  EXPECT_FALSE(Missing.has_value()) << "Unexpectedly resolved a bogus path";
  EXPECT_TRUE(EC) << "Expected error resolving path";

  auto DirPath = PResolver->resolve(BaseDir, EC);
  ASSERT_TRUE(DirPath.has_value());
  EXPECT_FALSE(EC) << "Expected no error resolving path";
  EXPECT_EQ(*DirPath, BaseDir);

  auto DylibPath = PResolver->resolve(lib("C"), EC);
  ASSERT_TRUE(DylibPath.has_value());
  EXPECT_FALSE(EC) << "Expected no error resolving path";
  EXPECT_EQ(*DylibPath, lib("C"));
}

TEST_F(LibraryResolverIT, PathResolverNormalizesDotAndDotDot) {
  auto LibPathCache = std::make_shared<LibraryPathCache>();
  auto PResolver = std::make_shared<PathResolver>(LibPathCache);

  std::error_code EC;

  // e.g. BaseDir + "/./C/../C/C.dylib" → BaseDir + "/C.dylib"
  std::string Messy = BaseDir + "/C/./../C/./libC" + ext;
  auto Resolved = PResolver->resolve(Messy, EC);
  ASSERT_TRUE(Resolved.has_value());
  EXPECT_FALSE(EC);
  EXPECT_EQ(*Resolved, lib("C")) << "Expected realpath to collapse . and ..";
}

#if !defined(_WIN32)
TEST_F(LibraryResolverIT, PathResolverFollowsSymlinks) {
  auto LibPathCache = std::make_shared<LibraryPathCache>();
  auto PResolver = std::make_shared<PathResolver>(LibPathCache);

  std::error_code EC;

  // Create a symlink temp -> BaseDir (only if filesystem allows it)
  std::string linkName = BaseDir + withext("/link_to_C");
  std::string target = lib("C");
  if (::symlink(target.c_str(), linkName.c_str()) != 0)
    GTEST_SKIP() << "Failed to create symlink: " << strerror(errno);

  auto resolved = PResolver->resolve(linkName, EC);
  ASSERT_TRUE(resolved.has_value());
  EXPECT_FALSE(EC);
  EXPECT_EQ(*resolved, target);

  (void)::unlink(linkName.c_str()); // cleanup
}

TEST_F(LibraryResolverIT, PathResolverCachesResults) {
  auto LibPathCache = std::make_shared<LibraryPathCache>();
  auto PResolver = std::make_shared<PathResolver>(LibPathCache);

  SmallString<128> TmpDylib;
  std::error_code EC;
  EC = sys::fs::createUniqueFile(withext("A-copy"), TmpDylib);
  if (EC)
    GTEST_SKIP() << "Failed to create temp dylib" << EC.message();

  EC = sys::fs::copy_file(lib("A"), TmpDylib);
  if (EC)
    GTEST_SKIP() << "Failed to copy libA: " << EC.message();
  EC.clear();

  // First resolve -> should populate LibPathCache
  auto first = PResolver->resolve(TmpDylib, EC);
  ASSERT_TRUE(first.has_value());

  // Forcefully remove the file from disk
  (void)::unlink(TmpDylib.c_str());

  // Second resolve -> should still succeed from LibPathCache
  auto second = PResolver->resolve(TmpDylib, EC);
  EXPECT_TRUE(second.has_value());
  EXPECT_EQ(*second, *first);
}
#endif

TEST_F(LibraryResolverIT, LoaderPathSubstitutionAndResolve) {
  auto LibPathCache = std::make_shared<LibraryPathCache>();
  auto PResolver = std::make_shared<PathResolver>(LibPathCache);

  DylibSubstitutor substitutor;
  substitutor.configure(libdir("C"));
#if defined(__APPLE__)
  // Substitute @loader_path with BaseDir
  std::string substituted =
      substitutor.substitute(withext("@loader_path/libC"));
#elif defined(__linux__)
  // Substitute $origin with BaseDir
  std::string substituted = substitutor.substitute(withext("$ORIGIN/libC"));
#endif
  ASSERT_FALSE(substituted.empty());
  EXPECT_EQ(substituted, lib("C"));

  // Now try resolving the substituted path
  std::error_code EC;
  auto resolved = PResolver->resolve(substituted, EC);
  ASSERT_TRUE(resolved.has_value()) << "Expected to resolve substituted dylib";
  EXPECT_EQ(*resolved, lib("C"));
  EXPECT_FALSE(EC) << "Expected no error resolving substituted dylib";
}

TEST_F(LibraryResolverIT, ResolveFromUsrOrSystemPaths) {
  auto LibPathCache = std::make_shared<LibraryPathCache>();
  auto PResolver = std::make_shared<PathResolver>(LibPathCache);

  DylibPathValidator validator(*PResolver, *LibPathCache);

  std::vector<std::string> Paths = {"/foo/bar/", "temp/foo",  libdir("C"),
                                    libdir("A"), libdir("B"), libdir("Z")};

  SmallVector<StringRef> P(Paths.begin(), Paths.end());

  DylibResolver Resolver(validator);
  Resolver.configure("", {{P, SearchPathType::UsrOrSys}});

  // Check "C"
  auto ValOptC = Resolver.resolve("libC", true);
  EXPECT_TRUE(ValOptC.has_value());
  EXPECT_EQ(*ValOptC, lib("C"));

  auto ValOptCdylib = Resolver.resolve(withext("libC"));
  EXPECT_TRUE(ValOptCdylib.has_value());
  EXPECT_EQ(*ValOptCdylib, lib("C"));

  // Check "A"
  auto ValOptA = Resolver.resolve("libA", true);
  EXPECT_TRUE(ValOptA.has_value());
  EXPECT_EQ(*ValOptA, lib("A"));

  auto ValOptAdylib = Resolver.resolve(withext("libA"));
  EXPECT_TRUE(ValOptAdylib.has_value());
  EXPECT_EQ(*ValOptAdylib, lib("A"));

  // Check "B"
  auto ValOptB = Resolver.resolve("libB", true);
  EXPECT_TRUE(ValOptB.has_value());
  EXPECT_EQ(*ValOptB, lib("B"));

  auto ValOptBdylib = Resolver.resolve(withext("libB"));
  EXPECT_TRUE(ValOptBdylib.has_value());
  EXPECT_EQ(*ValOptBdylib, lib("B"));

  // Check "Z"
  auto ValOptZ = Resolver.resolve("libZ", true);
  EXPECT_TRUE(ValOptZ.has_value());
  EXPECT_EQ(*ValOptZ, lib("Z"));

  auto ValOptZdylib = Resolver.resolve(withext("libZ"));
  EXPECT_TRUE(ValOptZdylib.has_value());
  EXPECT_EQ(*ValOptZdylib, lib("Z"));
}

#if defined(__APPLE__)
TEST_F(LibraryResolverIT, ResolveViaLoaderPathAndRPathSubstitution) {
  auto LibPathCache = std::make_shared<LibraryPathCache>();
  auto PResolver = std::make_shared<PathResolver>(LibPathCache);

  DylibPathValidator validator(*PResolver, *LibPathCache);

  std::vector<std::string> Paths = {"@loader_path/../A", "@loader_path/../B",
                                    "@loader_path/../C", "@loader_path/../Z"};

  SmallVector<StringRef> P(Paths.begin(), Paths.end());

  DylibResolver Resolver(validator);

  // Use only RPath config
  Resolver.configure(lib("C"), {{P, SearchPathType::RPath}});

  // --- Check A ---
  auto ValOptA = Resolver.resolve("@rpath/libA", true);
  EXPECT_TRUE(ValOptA.has_value());
  EXPECT_EQ(*ValOptA, lib("A"));

  auto ValOptAdylib = Resolver.resolve(withext("@rpath/libA"));
  EXPECT_TRUE(ValOptAdylib.has_value());
  EXPECT_EQ(*ValOptAdylib, lib("A"));

  // --- Check B ---
  auto ValOptB = Resolver.resolve("@rpath/libB", true);
  EXPECT_TRUE(ValOptB.has_value());
  EXPECT_EQ(*ValOptB, lib("B"));

  auto ValOptBdylib = Resolver.resolve(withext("@rpath/libB"));
  EXPECT_TRUE(ValOptBdylib.has_value());
  EXPECT_EQ(*ValOptBdylib, lib("B"));

  // --- Check Z ---
  auto ValOptZ = Resolver.resolve("@rpath/libZ", true);
  EXPECT_TRUE(ValOptZ.has_value());
  EXPECT_EQ(*ValOptZ, lib("Z"));

  auto ValOptZdylib = Resolver.resolve(withext("@rpath/libZ"));
  EXPECT_TRUE(ValOptZdylib.has_value());
  EXPECT_EQ(*ValOptZdylib, lib("Z"));
}
#endif

#if defined(__linux__)
TEST_F(LibraryResolverIT, ResolveViaOriginAndRPathSubstitution) {
  auto LibPathCache = std::make_shared<LibraryPathCache>();
  auto PResolver = std::make_shared<PathResolver>(LibPathCache);

  DylibPathValidator validator(*PResolver, *LibPathCache);

  // On Linux, $ORIGIN works like @loader_path
  std::vector<std::string> Paths = {"$ORIGIN/../A", "$ORIGIN/../B",
                                    "$ORIGIN/../C", "$ORIGIN/../Z"};

  SmallVector<StringRef> P(Paths.begin(), Paths.end());

  DylibResolver Resolver(validator);

  // Use only RPath config
  Resolver.configure(lib("C"), {{P, SearchPathType::RunPath}});

  // --- Check A ---
  auto ValOptA = Resolver.resolve("libA", true);
  EXPECT_TRUE(ValOptA.has_value());
  EXPECT_EQ(*ValOptA, lib("A"));

  auto valOptASO = Resolver.resolve(withext("libA"));
  EXPECT_TRUE(valOptASO.has_value());
  EXPECT_EQ(*valOptASO, lib("A"));

  // --- Check B ---
  auto ValOptB = Resolver.resolve("libB", true);
  EXPECT_TRUE(ValOptB.has_value());
  EXPECT_EQ(*ValOptB, lib("B"));

  auto valOptBSO = Resolver.resolve(withext("libB"));
  EXPECT_TRUE(valOptBSO.has_value());
  EXPECT_EQ(*valOptBSO, lib("B"));

  // --- Check Z ---
  auto ValOptZ = Resolver.resolve("libZ", true);
  EXPECT_TRUE(ValOptZ.has_value());
  EXPECT_EQ(*ValOptZ, lib("Z"));

  auto valOptZSO = Resolver.resolve(withext("libZ"));
  EXPECT_TRUE(valOptZSO.has_value());
  EXPECT_EQ(*valOptZSO, lib("Z"));
}
#endif
} // namespace
#endif // defined(__APPLE__)
