#include "plugin/linux_native_files.h"

#include <fcntl.h>
#include <glib/gstdio.h>
#include <sys/stat.h>

#include <cassert>
#include <fstream>
#include <iterator>
#include <string>
#include <vector>

namespace {

std::string TemporaryDirectory() {
  GError *error = nullptr;
  gchar *directory = g_dir_make_tmp("coreframe-native-files-XXXXXX", &error);
  assert(error == nullptr);
  std::string result(directory);
  g_free(directory);
  return result;
}

std::string Child(const std::string &directory, const char *name) {
  gchar *path = g_build_filename(directory.c_str(), name, nullptr);
  std::string result(path);
  g_free(path);
  return result;
}

void WriteFile(const std::string &path, const std::string &contents) {
  std::ofstream output(path, std::ios::binary);
  output.write(contents.data(), static_cast<std::streamsize>(contents.size()));
  assert(output.good());
}

void RemoveTree(const std::string &directory) {
  GDir *entries = g_dir_open(directory.c_str(), 0, nullptr);
  if (entries != nullptr) {
    while (const gchar *name = g_dir_read_name(entries)) {
      const std::string path = Child(directory, name);
      if (g_file_test(path.c_str(), G_FILE_TEST_IS_DIR)) {
        RemoveTree(path);
      } else {
        g_unlink(path.c_str());
      }
    }
    g_dir_close(entries);
  }
  g_rmdir(directory.c_str());
}

void TestStagesMetadataAndContents() {
  const auto directory = TemporaryDirectory();
  const auto source = Child(directory, "report.txt");
  const auto staging = Child(directory, "staging");
  WriteFile(source, "hello");

  const auto result =
      LinuxNativeFileStager(staging).Stage({{source, "report.txt"}});

  assert(!result.failure.has_value());
  assert(result.files.size() == 1);
  assert(result.files.front().name == "report.txt");
  assert(result.files.front().size == 5);
  assert(result.files.front().media_type == "text/plain");
  struct stat directory_metadata = {};
  struct stat file_metadata = {};
  assert(stat(staging.c_str(), &directory_metadata) == 0);
  assert(stat(result.files.front().staged_path.c_str(), &file_metadata) == 0);
  assert((directory_metadata.st_mode & 0777) == 0700);
  assert((file_metadata.st_mode & 0777) == 0600);
  std::ifstream copied(result.files.front().staged_path, std::ios::binary);
  assert(std::string(std::istreambuf_iterator<char>(copied), {}) == "hello");
  RemoveTree(directory);
}

void TestRejectsMoreThanThirtyTwoFiles() {
  const auto directory = TemporaryDirectory();
  std::vector<LinuxNativeFileSelection> selections(
      33, {Child(directory, "source.bin"), "source.bin"});
  const auto result =
      LinuxNativeFileStager(Child(directory, "staging")).Stage(selections);
  assert(result.failure == LinuxNativeFileStagingFailure::kCapacityExceeded);
  RemoveTree(directory);
}

void TestRejectsAnOversizedFile() {
  const auto directory = TemporaryDirectory();
  const auto source = Child(directory, "source.bin");
  const auto staging = Child(directory, "staging");
  WriteFile(source, "123456789");
  const auto result =
      LinuxNativeFileStager(staging, 32, 8, 16).Stage({{source, "source.bin"}});
  assert(result.failure == LinuxNativeFileStagingFailure::kCapacityExceeded);
  GDir *entries = g_dir_open(staging.c_str(), 0, nullptr);
  assert(entries != nullptr && g_dir_read_name(entries) == nullptr);
  g_dir_close(entries);
  RemoveTree(directory);
}

void TestRollsBackLaterFailures() {
  const auto directory = TemporaryDirectory();
  const auto first = Child(directory, "first.bin");
  const auto second = Child(directory, "second.bin");
  const auto missing = Child(directory, "missing.bin");
  const auto staging = Child(directory, "staging");
  WriteFile(first, "1234");
  WriteFile(second, "5678");

  auto result = LinuxNativeFileStager(staging, 32, 8, 6)
                    .Stage({{first, "first.bin"}, {second, "second.bin"}});
  assert(result.failure == LinuxNativeFileStagingFailure::kCapacityExceeded);
  result = LinuxNativeFileStager(staging).Stage(
      {{first, "first.bin"}, {missing, "missing.bin"}});
  assert(result.failure == LinuxNativeFileStagingFailure::kUnreadable);
  GDir *entries = g_dir_open(staging.c_str(), 0, nullptr);
  assert(entries != nullptr && g_dir_read_name(entries) == nullptr);
  g_dir_close(entries);
  RemoveTree(directory);
}

void TestSweepsOnlyStaleFiles() {
  const auto directory = TemporaryDirectory();
  const auto stale = Child(directory, "stale");
  const auto current = Child(directory, "current");
  WriteFile(stale, "old");
  WriteFile(current, "new");
  struct stat times = {};
  assert(stat(stale.c_str(), &times) == 0);
  struct timespec stale_times[2] = {times.st_atim, times.st_mtim};
  stale_times[1].tv_sec -= 25 * 60 * 60;
  assert(utimensat(AT_FDCWD, stale.c_str(), stale_times, 0) == 0);

  LinuxNativeFileStager::SweepStale(directory);

  assert(!g_file_test(stale.c_str(), G_FILE_TEST_EXISTS));
  assert(g_file_test(current.c_str(), G_FILE_TEST_EXISTS));
  RemoveTree(directory);
}

} // namespace

int main() {
  TestStagesMetadataAndContents();
  TestRejectsMoreThanThirtyTwoFiles();
  TestRejectsAnOversizedFile();
  TestRollsBackLaterFailures();
  TestSweepsOnlyStaleFiles();
  return 0;
}
