#include "plugin/windows_native_files.h"

#include <windows.h>

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

namespace {

using coreframe_flutter_windows::WindowsNativeFileSelection;
using coreframe_flutter_windows::WindowsNativeFileStager;
using coreframe_flutter_windows::WindowsNativeFileStagingFailure;
using coreframe_flutter_windows::WindowsPatternsForAccept;

std::filesystem::path TemporaryDirectory() {
  GUID guid = {};
  assert(SUCCEEDED(CoCreateGuid(&guid)));
  wchar_t value[40] = {};
  assert(StringFromGUID2(guid, value, static_cast<int>(std::size(value))) > 0);
  return std::filesystem::temp_directory_path() / value;
}

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

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

  const auto result = WindowsNativeFileStager(staging.wstring())
                          .Stage({{source.wstring(), L"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);
  std::ifstream copied(result.files.front().staged_path, std::ios::binary);
  assert(std::string(std::istreambuf_iterator<char>(copied), {}) == "hello");
  std::filesystem::remove_all(directory);
}

void TestRejectsMoreThanThirtyTwoFiles() {
  const auto directory = TemporaryDirectory();
  const auto source = directory / L"source.bin";
  std::vector<WindowsNativeFileSelection> selections(
      33, {source.wstring(), L"source.bin"});

  const auto result =
      WindowsNativeFileStager((directory / L"staging").wstring())
          .Stage(selections);

  assert(result.failure == WindowsNativeFileStagingFailure::kCapacityExceeded);
  std::filesystem::remove_all(directory);
}

void TestRejectsAnOversizedFile() {
  const auto directory = TemporaryDirectory();
  const auto source = directory / L"source.bin";
  WriteFile(source, "123456789");

  const auto staging = directory / L"staging";
  const auto result = WindowsNativeFileStager(staging.wstring(), 32, 8, 16)
                          .Stage({{source.wstring(), L"source.bin"}});

  assert(result.failure == WindowsNativeFileStagingFailure::kCapacityExceeded);
  assert(std::filesystem::is_empty(staging));
  std::filesystem::remove_all(directory);
}

void TestRollsBackWhenTotalCapacityIsExceeded() {
  const auto directory = TemporaryDirectory();
  const auto first = directory / L"first.bin";
  const auto second = directory / L"second.bin";
  const auto staging = directory / L"staging";
  WriteFile(first, "1234");
  WriteFile(second, "5678");

  const auto result = WindowsNativeFileStager(staging.wstring(), 32, 8, 6)
                          .Stage({{first.wstring(), L"first.bin"},
                                  {second.wstring(), L"second.bin"}});

  assert(result.failure == WindowsNativeFileStagingFailure::kCapacityExceeded);
  assert(std::filesystem::is_empty(staging));
  std::filesystem::remove_all(directory);
}

void TestRollsBackWhenALaterSelectionIsUnreadable() {
  const auto directory = TemporaryDirectory();
  const auto source = directory / L"source.txt";
  const auto missing = directory / L"missing.txt";
  const auto staging = directory / L"staging";
  WriteFile(source, "first");

  const auto result = WindowsNativeFileStager(staging.wstring())
                          .Stage({{source.wstring(), L"source.txt"},
                                  {missing.wstring(), L"missing.txt"}});

  assert(result.failure == WindowsNativeFileStagingFailure::kUnreadable);
  assert(std::filesystem::is_empty(staging));
  std::filesystem::remove_all(directory);
}

void TestSweepsOnlyStaleFiles() {
  const auto directory = TemporaryDirectory();
  const auto stale = directory / L"stale";
  const auto current = directory / L"current";
  WriteFile(stale, "old");
  WriteFile(current, "new");
  const HANDLE file =
      CreateFileW(stale.c_str(), FILE_WRITE_ATTRIBUTES, FILE_SHARE_READ,
                  nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr);
  assert(file != INVALID_HANDLE_VALUE);
  FILETIME old_time = {};
  ULARGE_INTEGER ticks = {};
  GetSystemTimeAsFileTime(&old_time);
  ticks.LowPart = old_time.dwLowDateTime;
  ticks.HighPart = old_time.dwHighDateTime;
  ticks.QuadPart -= 25ULL * 60 * 60 * 1000 * 10000;
  old_time.dwLowDateTime = ticks.LowPart;
  old_time.dwHighDateTime = ticks.HighPart;
  assert(SetFileTime(file, nullptr, nullptr, &old_time));
  CloseHandle(file);

  WindowsNativeFileStager::SweepStale(directory.wstring());

  assert(!std::filesystem::exists(stale));
  assert(std::filesystem::exists(current));
  std::filesystem::remove_all(directory);
}

void TestUsesBestEffortPatternsForUnsupportedFilters() {
  const auto unsupported = WindowsPatternsForAccept({"application/*"});
  assert(unsupported.empty());
  const auto mixed = WindowsPatternsForAccept({".pdf", "application/*"});
  assert(mixed == std::vector<std::wstring>{L"*.pdf"});
}

} // namespace

int main() {
  TestStagesMetadataAndContents();
  TestRejectsMoreThanThirtyTwoFiles();
  TestRejectsAnOversizedFile();
  TestRollsBackWhenTotalCapacityIsExceeded();
  TestRollsBackWhenALaterSelectionIsUnreadable();
  TestSweepsOnlyStaleFiles();
  TestUsesBestEffortPatternsForUnsupportedFilters();
  return 0;
}
