/* Distributed under the OSI-approved BSD 3-Clause License.  See accompanying
   file Copyright.txt or https://cmake.org/licensing for details.  */

#include <cmConfigure.h> // IWYU pragma: keep

#include <cerrno>
#include <map>
#include <string>
#include <utility>

#ifdef _WIN32
#  include <cctype>
#endif

#include <cmsys/Status.hxx>

#include "cmPathResolver.h"

#if defined(_WIN32) || defined(__APPLE__)
#  include "cmSystemTools.h"
#endif

#include "testCommon.h"

// IWYU pragma: no_forward_declare cm::PathResolver::Policies::LogicalPath
// IWYU pragma: no_forward_declare cm::PathResolver::Policies::NaivePath
// IWYU pragma: no_forward_declare cm::PathResolver::Policies::RealPath

namespace {

class MockSystem : public cm::PathResolver::System
{
public:
  ~MockSystem() override = default;

  struct Path
  {
    std::string Name;
    std::string Link;
  };

  std::map<std::string, Path> Paths;

  void SetPaths(std::map<std::string, Path> paths)
  {
    this->Paths = std::move(paths);
  }

  static std::string AdjustCase(std::string const& path)
  {
#if defined(_WIN32) || defined(__APPLE__)
    return cmSystemTools::LowerCase(path);
#else
    return path;
#endif
  }

  cmsys::Status ReadSymlink(std::string const& path,
                            std::string& link) override
  {
    auto i = this->Paths.find(AdjustCase(path));
    if (i == this->Paths.end()) {
      return cmsys::Status::POSIX(ENOENT);
    }
    if (i->second.Link.empty()) {
      return cmsys::Status::POSIX(EINVAL);
    }
    link = i->second.Link;
    return cmsys::Status::Success();
  }

  bool PathExists(std::string const& path) override
  {
    return this->Paths.find(AdjustCase(path)) != this->Paths.end();
  }

  std::string WorkDir;

  void SetWorkDir(std::string wd) { this->WorkDir = std::move(wd); }

  std::string GetWorkingDirectory() override { return this->WorkDir; }

#ifdef _WIN32
  std::map<char, std::string> WorkDirOnDrive;

  void SetWorkDirOnDrive(std::map<char, std::string> wd)
  {
    this->WorkDirOnDrive = std::move(wd);
  }

  std::string GetWorkingDirectoryOnDrive(char letter) override
  {
    std::string result;
    auto i = this->WorkDirOnDrive.find(std::tolower(letter));
    if (i != this->WorkDirOnDrive.end()) {
      result = i->second;
    }
    return result;
  }
#endif

#if defined(_WIN32) || defined(__APPLE__)
  cmsys::Status ReadName(std::string const& path, std::string& name) override
  {
    auto i = this->Paths.find(AdjustCase(path));
    if (i == this->Paths.end()) {
      return cmsys::Status::POSIX(ENOENT);
    }
    name = i->second.Name;
    return cmsys::Status::Success();
  }
#endif
};

#define EXPECT_RESOLVE(_in, _expect)                                          \
  do {                                                                        \
    std::string out;                                                          \
    ASSERT_TRUE(r.Resolve(_in, out));                                         \
    ASSERT_EQUAL(out, _expect);                                               \
  } while (false)

#define EXPECT_ENOENT(_in, _expect)                                           \
  do {                                                                        \
    std::string out;                                                          \
    ASSERT_EQUAL(r.Resolve(_in, out).GetPOSIX(), ENOENT);                     \
    ASSERT_EQUAL(out, _expect);                                               \
  } while (false)

using namespace cm::PathResolver;

bool posixRoot()
{
  std::cout << "posixRoot()\n";
  MockSystem os;
  os.SetPaths({
    { "/", { {}, {} } },
  });
  Resolver<Policies::RealPath> const r(os);
  EXPECT_RESOLVE("/", "/");
  EXPECT_RESOLVE("//", "/");
  EXPECT_RESOLVE("/.", "/");
  EXPECT_RESOLVE("/./", "/");
  EXPECT_RESOLVE("/..", "/");
  EXPECT_RESOLVE("/../", "/");
  return true;
}

bool posixAbsolutePath()
{
  std::cout << "posixAbsolutePath()\n";
  MockSystem os;
  os.SetPaths({
    { "/", { {}, {} } },
    { "/a", { {}, {} } },
  });
  Resolver<Policies::RealPath> const r(os);
  EXPECT_RESOLVE("/a", "/a");
  EXPECT_RESOLVE("/a/", "/a");
  EXPECT_RESOLVE("/a//", "/a");
  EXPECT_RESOLVE("/a/.", "/a");
  EXPECT_RESOLVE("/a/./", "/a");
  EXPECT_RESOLVE("/a/..", "/");
  EXPECT_RESOLVE("/a/../", "/");
  EXPECT_RESOLVE("/a/../..", "/");
#ifndef _WIN32
  EXPECT_RESOLVE("//a", "/a");
#endif
  return true;
}

bool posixWorkingDirectory()
{
  std::cout << "posixWorkingDirectory()\n";
  MockSystem os;
  os.SetPaths({
    { "/", { {}, {} } },
    { "/a", { {}, {} } },
    { "/cwd", { {}, {} } },
    { "/cwd/a", { {}, {} } },
  });
  Resolver<Policies::RealPath> const r(os);
  EXPECT_RESOLVE("", "/");
  EXPECT_RESOLVE(".", "/");
  EXPECT_RESOLVE("..", "/");
  EXPECT_RESOLVE("a", "/a");
  os.SetWorkDir("/cwd");
  EXPECT_RESOLVE("", "/cwd");
  EXPECT_RESOLVE(".", "/cwd");
  EXPECT_RESOLVE("..", "/");
  EXPECT_RESOLVE("a", "/cwd/a");
  return true;
}

bool posixSymlink()
{
  std::cout << "posixSymlink()\n";
  MockSystem os;
  os.SetPaths({
    { "/", { {}, {} } },
    { "/link-a", { {}, "a" } },
    { "/link-a-excess", { {}, "a//." } },
    { "/link-broken", { {}, "link-broken-dest" } },
    { "/a", { {}, {} } },
    { "/a/b", { {}, {} } },
    { "/a/link-b", { {}, "b" } },
    { "/a/b/link-c", { {}, "c" } },
    { "/a/b/c", { {}, {} } },
    { "/a/b/c/link-..|..", { {}, "../.." } },
    { "/a/link-|1|2", { {}, "/1/2" } },
    { "/1", { {}, {} } },
    { "/1/2", { {}, {} } },
    { "/1/2/3", { {}, {} } },
  });

  {
    Resolver<Policies::LogicalPath> const r(os);
    EXPECT_RESOLVE("/link-a", "/link-a");
    EXPECT_RESOLVE("/link-a-excess", "/link-a-excess");
    EXPECT_RESOLVE("/link-a-excess/b", "/link-a-excess/b");
    EXPECT_RESOLVE("/link-broken", "/link-broken");
    EXPECT_RESOLVE("/link-a/../missing", "/missing");
    EXPECT_RESOLVE("/a/b/link-c", "/a/b/link-c");
    EXPECT_RESOLVE("/a/link-b/c", "/a/link-b/c");
    EXPECT_RESOLVE("/a/link-b/link-c/..", "/a/link-b");
    EXPECT_RESOLVE("/a/b/c/link-..|..", "/a/b/c/link-..|..");
    EXPECT_RESOLVE("/a/b/c/link-..|../link-b", "/a/b/c/link-..|../link-b");
    EXPECT_RESOLVE("/a/link-|1|2/3", "/a/link-|1|2/3");
    EXPECT_RESOLVE("/a/link-|1|2/../2/3", "/1/2/3");
  }

  {
    Resolver<Policies::RealPath> const r(os);
    EXPECT_RESOLVE("/link-a", "/a");
    EXPECT_RESOLVE("/link-a-excess", "/a");
    EXPECT_RESOLVE("/link-a-excess/b", "/a/b");
    EXPECT_ENOENT("/link-broken", "/link-broken-dest");
    EXPECT_ENOENT("/link-a/../missing", "/missing");
    EXPECT_RESOLVE("/a/b/link-c", "/a/b/c");
    EXPECT_RESOLVE("/a/link-b/c", "/a/b/c");
    EXPECT_RESOLVE("/a/link-b/link-c/..", "/a/b");
    EXPECT_RESOLVE("/a/b/c/link-..|..", "/a");
    EXPECT_RESOLVE("/a/b/c/link-..|../link-b", "/a/b");
    EXPECT_RESOLVE("/a/link-|1|2/3", "/1/2/3");
  }

  return true;
}

#ifdef __APPLE__
bool macosActualCase()
{
  std::cout << "macosActualCase()\n";
  MockSystem os;
  os.SetPaths({
    { "/", { {}, {} } },
    { "/mixed", { "MiXeD", {} } },
    { "/mixed/link-mixed", { "LiNk-MiXeD", "mixed" } },
    { "/mixed/mixed", { "MiXeD", {} } },
    { "/mixed/link-c-mixed", { "LiNk-C-MiXeD", "/mIxEd" } },
    { "/upper", { "UPPER", {} } },
    { "/upper/link-upper", { "LINK-UPPER", "upper" } },
    { "/upper/upper", { "UPPER", {} } },
    { "/upper/link-c-upper", { "LINK-C-UPPER", "/upper" } },
  });

  {
    Resolver<Policies::LogicalPath> const r(os);
    EXPECT_RESOLVE("/mIxEd/MiSsInG", "/MiXeD/MiSsInG");
    EXPECT_RESOLVE("/mIxEd/link-MiXeD", "/MiXeD/LiNk-MiXeD");
    EXPECT_RESOLVE("/mIxEd/link-c-MiXeD", "/MiXeD/LiNk-C-MiXeD");
    EXPECT_RESOLVE("/upper/mIsSiNg", "/UPPER/mIsSiNg");
    EXPECT_RESOLVE("/upper/link-upper", "/UPPER/LINK-UPPER");
    EXPECT_RESOLVE("/upper/link-c-upper", "/UPPER/LINK-C-UPPER");
  }

  {
    Resolver<Policies::RealPath> const r(os);
    EXPECT_ENOENT("/mIxEd/MiSsInG", "/MiXeD/MiSsInG");
    EXPECT_RESOLVE("/mIxEd/link-MiXeD", "/MiXeD/MiXeD");
    EXPECT_RESOLVE("/mIxEd/link-c-MiXeD", "/MiXeD");
    EXPECT_ENOENT("/upper/mIsSiNg", "/UPPER/mIsSiNg");
    EXPECT_RESOLVE("/upper/link-upper", "/UPPER/UPPER");
    EXPECT_RESOLVE("/upper/link-c-upper", "/UPPER");
  }

  return true;
}
#endif

#ifdef _WIN32
bool windowsRoot()
{
  std::cout << "windowsRoot()\n";
  MockSystem os;
  {
    Resolver<Policies::NaivePath> const r(os);
    EXPECT_RESOLVE("c:/", "c:/");
    EXPECT_RESOLVE("C:/", "C:/");
    EXPECT_RESOLVE("c://", "c:/");
    EXPECT_RESOLVE("C:/.", "C:/");
    EXPECT_RESOLVE("c:/./", "c:/");
    EXPECT_RESOLVE("C:/..", "C:/");
    EXPECT_RESOLVE("c:/../", "c:/");
  }
  os.SetPaths({
    { "c:/", { {}, {} } },
    { "//host/", { {}, {} } },
  });
  {
    Resolver<Policies::RealPath> const r(os);
    EXPECT_RESOLVE("c:/", "C:/");
    EXPECT_RESOLVE("C:/", "C:/");
    EXPECT_RESOLVE("c://", "C:/");
    EXPECT_RESOLVE("C:/.", "C:/");
    EXPECT_RESOLVE("c:/./", "C:/");
    EXPECT_RESOLVE("C:/..", "C:/");
    EXPECT_RESOLVE("c:/../", "C:/");
    EXPECT_RESOLVE("//host", "//host/");
    EXPECT_RESOLVE("//host/.", "//host/");
    EXPECT_RESOLVE("//host/./", "//host/");
    EXPECT_RESOLVE("//host/..", "//host/");
    EXPECT_RESOLVE("//host/../", "//host/");
  }
  return true;
}

bool windowsAbsolutePath()
{
  std::cout << "windowsAbsolutePath()\n";
  MockSystem os;
  os.SetPaths({
    { "c:/", { {}, {} } },
    { "c:/a", { {}, {} } },
  });
  Resolver<Policies::RealPath> const r(os);
  EXPECT_RESOLVE("c:/a", "C:/a");
  EXPECT_RESOLVE("c:/a/", "C:/a");
  EXPECT_RESOLVE("c:/a//", "C:/a");
  EXPECT_RESOLVE("c:/a/.", "C:/a");
  EXPECT_RESOLVE("c:/a/./", "C:/a");
  EXPECT_RESOLVE("c:/a/..", "C:/");
  EXPECT_RESOLVE("c:/a/../", "C:/");
  EXPECT_RESOLVE("c:/a/../..", "C:/");
  return true;
}

bool windowsActualCase()
{
  std::cout << "windowsActualCase()\n";
  MockSystem os;
  os.SetPaths({
    { "c:/", { {}, {} } },
    { "c:/mixed", { "MiXeD", {} } },
    { "c:/mixed/link-mixed", { "LiNk-MiXeD", "mixed" } },
    { "c:/mixed/mixed", { "MiXeD", {} } },
    { "c:/mixed/link-c-mixed", { "LiNk-C-MiXeD", "C:/mIxEd" } },
    { "c:/upper", { "UPPER", {} } },
    { "c:/upper/link-upper", { "LINK-UPPER", "upper" } },
    { "c:/upper/upper", { "UPPER", {} } },
    { "c:/upper/link-c-upper", { "LINK-C-UPPER", "c:/upper" } },
  });

  {
    Resolver<Policies::LogicalPath> const r(os);
    EXPECT_RESOLVE("c:/mIxEd/MiSsInG", "C:/MiXeD/MiSsInG");
    EXPECT_RESOLVE("c:/mIxEd/link-MiXeD", "C:/MiXeD/LiNk-MiXeD");
    EXPECT_RESOLVE("c:/mIxEd/link-c-MiXeD", "C:/MiXeD/LiNk-C-MiXeD");
    EXPECT_RESOLVE("c:/upper/mIsSiNg", "C:/UPPER/mIsSiNg");
    EXPECT_RESOLVE("c:/upper/link-upper", "C:/UPPER/LINK-UPPER");
    EXPECT_RESOLVE("c:/upper/link-c-upper", "C:/UPPER/LINK-C-UPPER");
  }

  {
    Resolver<Policies::RealPath> const r(os);
    EXPECT_ENOENT("c:/mIxEd/MiSsInG", "C:/MiXeD/MiSsInG");
    EXPECT_RESOLVE("c:/mIxEd/link-MiXeD", "C:/MiXeD/MiXeD");
    EXPECT_RESOLVE("c:/mIxEd/link-c-MiXeD", "C:/MiXeD");
    EXPECT_ENOENT("c:/upper/mIsSiNg", "C:/UPPER/mIsSiNg");
    EXPECT_RESOLVE("c:/upper/link-upper", "C:/UPPER/UPPER");
    EXPECT_RESOLVE("c:/upper/link-c-upper", "C:/UPPER");
  }

  return true;
}

bool windowsWorkingDirectory()
{
  std::cout << "windowsWorkingDirectory()\n";
  MockSystem os;
  os.SetPaths({
    { "c:/", { {}, {} } },
    { "c:/a", { {}, {} } },
    { "c:/cwd", { {}, {} } },
    { "c:/cwd/a", { {}, {} } },
  });
  {
    Resolver<Policies::LogicalPath> const r(os);
    EXPECT_RESOLVE("", "/");
    EXPECT_RESOLVE(".", "/");
    EXPECT_RESOLVE("..", "/");
    EXPECT_RESOLVE("a", "/a");
  }
  {
    Resolver<Policies::RealPath> const r(os);
    os.SetWorkDir("c:/cwd");
    EXPECT_RESOLVE("", "C:/cwd");
    EXPECT_RESOLVE(".", "C:/cwd");
    EXPECT_RESOLVE("..", "C:/");
    EXPECT_RESOLVE("a", "C:/cwd/a");
    EXPECT_ENOENT("missing", "C:/cwd/missing");
  }
  return true;
}

bool windowsWorkingDirectoryOnDrive()
{
  std::cout << "windowsWorkingDirectoryOnDrive()\n";
  MockSystem os;
  os.SetWorkDir("c:/cwd");
  os.SetWorkDirOnDrive({
    { 'd', "d:/cwd-d" },
  });
  {
    Resolver<Policies::NaivePath> const r(os);
    EXPECT_RESOLVE("c:", "c:/cwd");
    EXPECT_RESOLVE("c:.", "c:/cwd");
    EXPECT_RESOLVE("c:..", "c:/");
    EXPECT_RESOLVE("C:", "C:/cwd");
    EXPECT_RESOLVE("C:.", "C:/cwd");
    EXPECT_RESOLVE("C:..", "C:/");
    EXPECT_RESOLVE("d:", "d:/cwd-d");
    EXPECT_RESOLVE("d:.", "d:/cwd-d");
    EXPECT_RESOLVE("d:..", "d:/");
    EXPECT_RESOLVE("D:", "D:/cwd-d");
    EXPECT_RESOLVE("D:.", "D:/cwd-d");
    EXPECT_RESOLVE("D:..", "D:/");
    EXPECT_RESOLVE("e:", "e:/");
    EXPECT_RESOLVE("e:.", "e:/");
    EXPECT_RESOLVE("e:..", "e:/");
    EXPECT_RESOLVE("E:", "E:/");
    EXPECT_RESOLVE("E:.", "E:/");
    EXPECT_RESOLVE("E:..", "E:/");
  }
  os.SetPaths({
    { "c:/", { {}, {} } },
    { "c:/cwd", { {}, {} } },
    { "c:/cwd/existing", { {}, {} } },
    { "d:/", { {}, {} } },
    { "d:/cwd-d", { {}, {} } },
    { "d:/cwd-d/existing", { {}, {} } },
    { "e:/", { {}, {} } },
  });
  {
    Resolver<Policies::RealPath> const r(os);
    EXPECT_RESOLVE("c:existing", "C:/cwd/existing");
    EXPECT_ENOENT("c:missing", "C:/cwd/missing");
    EXPECT_RESOLVE("C:existing", "C:/cwd/existing");
    EXPECT_ENOENT("C:missing", "C:/cwd/missing");
    EXPECT_RESOLVE("d:existing", "D:/cwd-d/existing");
    EXPECT_ENOENT("d:missing", "D:/cwd-d/missing");
    EXPECT_ENOENT("e:missing", "E:/missing");
    EXPECT_ENOENT("f:", "F:/");
  }
  return true;
}

bool windowsNetworkShare()
{
  std::cout << "windowsNetworkShare()\n";
  MockSystem os;
  os.SetPaths({
    { "c:/", { {}, {} } },
    { "c:/cwd", { {}, {} } },
    { "c:/cwd/link-to-host-share", { {}, "//host/share" } },
    { "//host/", { {}, {} } },
    { "//host/share", { {}, {} } },
  });
  os.SetWorkDir("c:/cwd");
  {
    Resolver<Policies::RealPath> const r(os);
    EXPECT_RESOLVE("//host/share", "//host/share");
    EXPECT_RESOLVE("//host/share/", "//host/share");
    EXPECT_RESOLVE("//host/share/.", "//host/share");
    EXPECT_RESOLVE("//host/share/./", "//host/share");
    EXPECT_RESOLVE("//host/share/..", "//host/");
    EXPECT_RESOLVE("//host/share/../", "//host/");
    EXPECT_RESOLVE("//host/share/../..", "//host/");
    EXPECT_RESOLVE("link-to-host-share", "//host/share");
    EXPECT_RESOLVE("link-to-host-share/..", "//host/");
    EXPECT_ENOENT("link-to-host-share/../missing", "//host/missing");
  }

  {
    Resolver<Policies::LogicalPath> const r(os);
    EXPECT_RESOLVE("link-to-host-share", "C:/cwd/link-to-host-share");
    EXPECT_RESOLVE("link-to-host-share/..", "//host/");
    EXPECT_RESOLVE("link-to-host-share/../missing", "//host/missing");
  }
  return true;
}
#endif

}

int testPathResolver(int /*unused*/, char* /*unused*/[])
{
  return runTests({
    posixRoot,
    posixAbsolutePath,
    posixWorkingDirectory,
    posixSymlink,
#ifdef __APPLE__
    macosActualCase,
#endif
#ifdef _WIN32
    windowsRoot,
    windowsAbsolutePath,
    windowsActualCase,
    windowsWorkingDirectory,
    windowsWorkingDirectoryOnDrive,
    windowsNetworkShare,
#endif
  });
}
