// Copyright 2018 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

#include "google/cloud/internal/filesystem.h"
#include "google/cloud/internal/random.h"
#include <gmock/gmock.h>
#include <fstream>
#if GTEST_OS_LINUX
#include <sys/socket.h>
#include <sys/un.h>
#endif  // GTEST_OS_LINUX

namespace google {
namespace cloud {
GOOGLE_CLOUD_CPP_INLINE_NAMESPACE_BEGIN
namespace internal {
namespace {
using ::testing::HasSubstr;

// When running on the internal Google CI systems we cannot write to the local
// directory, but GTest has a good temporary directory in that case.
std::string CreateRandomFileName(
    std::string const& tmp = ::testing::TempDir()) {
  static DefaultPRNG generator = MakeDefaultPRNG();
  return PathAppend(
      tmp, Sample(generator, 8, "abcdefghijklmnopqrstuvwxyz0123456789"));
}

TEST(FilesystemTest, PermissionsOperatorBitand) {
  EXPECT_EQ(perms::none, perms::owner_all & perms::others_all);
  EXPECT_EQ(perms::owner_exec, perms::owner_all & perms::owner_exec);
}

TEST(FilesystemTest, PermissionsOperatorBitor) {
  EXPECT_EQ(0707, static_cast<unsigned>(perms::owner_all | perms::others_all));
  EXPECT_EQ(perms::owner_all, perms::owner_all | perms::owner_exec);
}

TEST(FilesystemTest, PermissionsOperatorBitxor) {
  EXPECT_EQ(0707, static_cast<unsigned>(perms::owner_all ^ perms::others_all));
  EXPECT_EQ(0600, static_cast<unsigned>(perms::owner_all ^ perms::owner_exec));
}

TEST(FilesystemTest, PermissionsNegate) {
  EXPECT_EQ(07077, static_cast<unsigned>(~perms::owner_all));
  EXPECT_EQ(07677, static_cast<unsigned>(~perms::owner_exec));
  EXPECT_EQ(07707, static_cast<unsigned>(~perms::group_all));
  EXPECT_EQ(07770, static_cast<unsigned>(~perms::others_all));
}

TEST(FilesystemTest, PermissionsOperatorBitandEquals) {
  perms lhs = perms::owner_all;
  lhs &= perms::others_all;
  EXPECT_EQ(0, static_cast<unsigned>(lhs));
}

TEST(FilesystemTest, PermissionsOperatorBitorEquals) {
  perms lhs = perms::owner_all;
  lhs |= perms::others_all;
  EXPECT_EQ(0707, static_cast<unsigned>(lhs));
}

TEST(FilesystemTest, PermissionsOperatorBitxorEquals) {
  perms lhs = perms::owner_all;
  lhs ^= perms::owner_exec;
  EXPECT_EQ(0600, static_cast<unsigned>(lhs));
}

TEST(FilesystemTest, Equality) {
  auto a = file_status(file_type::socket, perms::group_read);
  auto b = a;
  EXPECT_EQ(a, b);
  auto c = file_status(file_type::regular, perms::group_read);
  EXPECT_NE(c, a);
  c = file_status(file_type::socket, perms::owner_read);
  EXPECT_NE(c, a);
}

TEST(FilesystemTest, StatusDirectory) {
  std::error_code ec;
  auto file_status = status(".", ec);
  EXPECT_FALSE(static_cast<bool>(ec));
  EXPECT_TRUE(is_directory(file_status));
}

TEST(FilesystemTest, StatusBlock) {
#if GTEST_OS_LINUX
  std::error_code ec;
  auto file_status = status("/dev/loop0", ec);
  EXPECT_FALSE(static_cast<bool>(ec));
  if (!exists(file_status)) {
    // In some CI builds there is no /dev/loop0, and no other well-known
    // block device comes to mind, simply stop the test when that happens.
    return;
  }
  EXPECT_TRUE(is_block_file(file_status));
  EXPECT_TRUE(is_other(file_status));
#else
  std::error_code ec;
  auto file_status = status(".", ec);
  EXPECT_FALSE(static_cast<bool>(ec));
  EXPECT_FALSE(is_block_file(file_status));
  EXPECT_FALSE(is_other(file_status));
#endif  // GTEST_OS_LINUX
}

TEST(FilesystemTest, StatusCharacter) {
#if GTEST_OS_LINUX
  std::error_code ec;
  auto file_status = status("/dev/null", ec);
  EXPECT_FALSE(static_cast<bool>(ec));
  EXPECT_TRUE(is_character_file(file_status));
  EXPECT_TRUE(is_other(file_status));
#else
  std::error_code ec;
  auto file_status = status(".", ec);
  EXPECT_FALSE(static_cast<bool>(ec));
  EXPECT_FALSE(is_block_file(file_status));
  EXPECT_FALSE(is_other(file_status));
#endif  // GTEST_OS_LINUX
}

TEST(FilesystemTest, StatusFifo) {
#if GTEST_OS_LINUX
  auto file_name = CreateRandomFileName();
  ASSERT_NE(-1, mkfifo(file_name.c_str(), 0777));
  std::error_code ec;
  auto file_status = status(file_name, ec);
  EXPECT_FALSE(static_cast<bool>(ec));
  EXPECT_TRUE(is_fifo(file_status));
  EXPECT_TRUE(is_other(file_status));
  EXPECT_EQ(0, std::remove(file_name.c_str()));
#else
  std::error_code ec;
  auto file_status = status(".", ec);
  EXPECT_FALSE(static_cast<bool>(ec));
  EXPECT_FALSE(is_fifo(file_status));
  EXPECT_FALSE(is_other(file_status));
#endif  // GTEST_OS_LINUX
}

TEST(FilesystemTest, StatusRegular) {
  auto file_name = CreateRandomFileName();
  std::ofstream(file_name).close();
  std::error_code ec;
  auto file_status = status(file_name, ec);
  EXPECT_FALSE(static_cast<bool>(ec));
  EXPECT_TRUE(is_regular(file_status));
  EXPECT_EQ(0, std::remove(file_name.c_str()));
}

TEST(FilesystemTest, StatusSocket) {
#if GTEST_OS_LINUX
  auto const file_name = CreateRandomFileName();
  // With Bazel and gtest >= 1.11 the default temp directory is too long.
  sockaddr_un address{};
  if (file_name.size() >= sizeof(address.sun_path)) GTEST_SKIP();

  int fd = socket(AF_UNIX, SOCK_SEQPACKET, 0);
  ASSERT_NE(-1, fd);

  address.sun_family = AF_UNIX;
  strncpy(address.sun_path, file_name.c_str(), sizeof(address.sun_path) - 1);
  int r = bind(fd, reinterpret_cast<sockaddr*>(&address), sizeof(address));
  EXPECT_NE(-1, r);
  if (r == -1) {
    (void)close(fd);
    return;
  }

  std::error_code ec;
  auto file_status = status(file_name, ec);
  EXPECT_FALSE(static_cast<bool>(ec));
  EXPECT_TRUE(is_socket(file_status));
  EXPECT_TRUE(is_other(file_status));
  EXPECT_NE(-1, close(fd));
  EXPECT_EQ(0, std::remove(file_name.c_str()));
#else
  std::error_code ec;
  auto file_status = status(".", ec);
  EXPECT_FALSE(static_cast<bool>(ec));
  EXPECT_FALSE(is_socket(file_status));
  EXPECT_FALSE(is_other(file_status));
#endif  // GTEST_OS_LINUX
}

TEST(FilesystemTest, StatusSymlink) {
  // status() is supposed to follow symbolic links, there is a different
  // function in C++17 (which we are not implementing) that does not follow
  // symbolic links.
#if GTEST_OS_LINUX
  auto file_name = CreateRandomFileName();
  std::ofstream(file_name).close();

  auto symbolic_link = CreateRandomFileName();
  ASSERT_NE(-1, link(file_name.c_str(), symbolic_link.c_str()));

  std::error_code ec;
  auto file_status = status(symbolic_link, ec);
  EXPECT_FALSE(static_cast<bool>(ec));
  EXPECT_TRUE(is_regular(file_status));
  EXPECT_FALSE(is_symlink(file_status));

  EXPECT_EQ(0, std::remove(symbolic_link.c_str()));
  EXPECT_EQ(0, std::remove(file_name.c_str()));
#else
  std::error_code ec;
  auto file_status = status(".", ec);
  EXPECT_FALSE(static_cast<bool>(ec));
  EXPECT_FALSE(is_fifo(file_status));
  EXPECT_FALSE(is_other(file_status));
#endif  // GTEST_OS_LINUX
}

TEST(FilesystemTest, StatusNotFound) {
  auto file_name = CreateRandomFileName();
  std::error_code ec;
  auto file_status = status(file_name, ec);
  EXPECT_FALSE(static_cast<bool>(ec));
  EXPECT_FALSE(exists(file_status));
  EXPECT_EQ(file_type::not_found, file_status.type());
}

TEST(FilesystemTest, StatusNotFoundDoesNotThrow) {
  auto file_name = CreateRandomFileName();
  file_status fs;
#if GOOGLE_CLOUD_CPP_HAVE_EXCEPTIONS
  EXPECT_NO_THROW(fs = status(file_name));
#else
  fs = status(file_name);
  SUCCEED();
#endif  // GOOGLE_CLOUD_CPP_HAVE_EXCEPTIONS
  EXPECT_FALSE(exists(fs));
  EXPECT_EQ(file_type::not_found, fs.type());
}

TEST(FilesystemTest, StatusAccessDoesNotThrow) {
#if GTEST_OS_LINUX
  auto constexpr kFileName = "/proc/1/fd/0";
  file_status fs;
#if GOOGLE_CLOUD_CPP_HAVE_EXCEPTIONS
  EXPECT_NO_THROW(fs = status(kFileName));
#else
  fs = status(kFileName);
  SUCCEED();
#endif  // GOOGLE_CLOUD_CPP_HAVE_EXCEPTIONS
  // Do not check the returned file type, in some CI builds the tests run
  // as root, and therefore have full access to all the files and directories.
  // EXPECT_EQ(file_type::unknown, fs.type());
#endif
}

TEST(FilesystemTest, StatusErrorDoesThrow) {
#if GTEST_OS_LINUX
  auto file_name = CreateRandomFileName();
  std::ofstream(file_name).close();
  auto path = file_name + "/files/cannot/be/directories";
#if GOOGLE_CLOUD_CPP_HAVE_EXCEPTIONS
  EXPECT_THROW(
      try { status(path); } catch (std::system_error const& ex) {
        EXPECT_EQ(static_cast<int>(std::errc::not_a_directory),
                  ex.code().value());
        EXPECT_THAT(ex.what(), HasSubstr(path));
        throw;
      },
      std::system_error);
#else
  EXPECT_DEATH_IF_SUPPORTED(status(path), "exceptions are disabled");
#endif  // GOOGLE_CLOUD_CPP_HAVE_EXCEPTIONS
  EXPECT_EQ(0, std::remove(file_name.c_str()));
#else
  // I (coryan) do not know of a way to generate an error on Windows. One would
  // need to pass a null string (not empty, null) or something similar, and the
  // function signatures do not allow for that type of mistake.
#endif  // GTEST_OS_LINUX
}

TEST(FilesystemTest, FileSize) {
  auto file_name = CreateRandomFileName();
  std::ofstream os(file_name);
  os << std::string(1000, ' ');
  os.close();
  std::error_code ec;
  auto size = file_size(file_name, ec);
  EXPECT_FALSE(static_cast<bool>(ec));
  EXPECT_EQ(1000, size);
  EXPECT_EQ(0, std::remove(file_name.c_str()));
}

TEST(FilesystemTest, FileSizeEmpty) {
  auto file_name = CreateRandomFileName();
  std::ofstream(file_name).close();
  std::error_code ec;
  auto size = file_size(file_name, ec);
  EXPECT_FALSE(static_cast<bool>(ec));
  EXPECT_EQ(0, size);
  EXPECT_EQ(0, std::remove(file_name.c_str()));
}

TEST(FilesystemTest, FileSizeNotFound) {
  auto file_name = CreateRandomFileName();
  std::error_code ec;
  auto size = file_size(file_name, ec);
  EXPECT_TRUE(static_cast<bool>(ec));
  EXPECT_EQ(std::uintmax_t(-1), size);
}

TEST(FilesystemTest, FileSizeNotFoundDoesThrow) {
  auto path = CreateRandomFileName();
#if GOOGLE_CLOUD_CPP_HAVE_EXCEPTIONS
  EXPECT_THROW(
      try { file_size(path); } catch (std::system_error const& ex) {
        EXPECT_EQ(static_cast<int>(std::errc::no_such_file_or_directory),
                  ex.code().value());
        EXPECT_THAT(ex.what(), HasSubstr(path));
        throw;
      },
      std::system_error);
#else
  EXPECT_DEATH_IF_SUPPORTED(file_size(path), "exceptions are disabled");
#endif  // GOOGLE_CLOUD_CPP_HAVE_EXCEPTIONS
}

TEST(FilesystemTest, PathAppend) {
  struct TestCase {
    std::string dir;
    std::string path;
    std::string expected;
  } cases[] = {
#if _WIN32
      {"\\tmp", "path", "\\tmp\\path"},
      {"\\tmp", "\\path", "\\tmp\\path"},
      {"\\tmp\\", "path", "\\tmp\\path"},
      {"\\tmp\\", "\\path", "\\tmp\\path"},
      {"", "path", "path"},
      {"\\", "path", "\\path"},
      {"\\tmp", "", "\\tmp"},
      {"\\tmp\\", "", "\\tmp\\"},
#else
      {"/tmp", "path", "/tmp/path"},  {"/tmp", "/path", "/tmp/path"},
      {"/tmp/", "path", "/tmp/path"}, {"/tmp/", "/path", "/tmp/path"},
      {"", "path", "path"},           {"/", "path", "/path"},
      {"/tmp", "", "/tmp"},           {"/tmp/", "", "/tmp/"},
#endif  // _WIN32
  };
  for (auto const& test : cases) {
    SCOPED_TRACE("Testing dir=<" + test.dir + "> path=<" + test.path + ">");
    auto const actual = PathAppend(test.dir, test.path);
    EXPECT_EQ(test.expected, actual);
  }
}

#ifndef _WIN32
TEST(FilesystemTest, GetFileNames) {
  auto full_path1 = CreateRandomFileName();
  std::ofstream(full_path1).close();
  size_t separator_pos1 = full_path1.find_last_of('/');
  std::string directory = full_path1.substr(0, separator_pos1);
  std::string filename1 = full_path1.substr(separator_pos1 + 1);

  auto full_path2 = CreateRandomFileName();
  std::ofstream(full_path2).close();
  size_t separator_pos2 = full_path2.find_last_of('/');
  std::string filename2 = full_path2.substr(separator_pos2 + 1);

  auto filenames = GetFileNames(directory);
  EXPECT_THAT(filenames, ::testing::Contains(filename1));
  EXPECT_THAT(filenames, ::testing::Contains(filename2));
}
#endif

}  // namespace
}  // namespace internal
GOOGLE_CLOUD_CPP_INLINE_NAMESPACE_END
}  // namespace cloud
}  // namespace google
