///////////////////////////////////////////////////////////////////////////////
// Copyright (c) Lewis Baker
// Licenced under MIT license. See LICENSE.txt for details.
///////////////////////////////////////////////////////////////////////////////

#include <cppcoro/io_service.hpp>
#include <cppcoro/read_only_file.hpp>
#include <cppcoro/write_only_file.hpp>
#include <cppcoro/read_write_file.hpp>
#include <cppcoro/task.hpp>
#include <cppcoro/sync_wait.hpp>
#include <cppcoro/when_all.hpp>
#include <cppcoro/cancellation_source.hpp>
#include <cppcoro/on_scope_exit.hpp>

#include <random>
#include <thread>
#include <cassert>
#include <string>

#include "io_service_fixture.hpp"

#include <ostream>
#include "doctest/cppcoro_doctest.h"

TEST_SUITE_BEGIN("file");

namespace fs = cppcoro::filesystem;

namespace
{
	class temp_dir_fixture
	{
	public:

		temp_dir_fixture()
		{
			auto tempDir = fs::temp_directory_path();

			std::random_device random;
			for (int attempt = 1;; ++attempt)
			{
				m_path = tempDir / std::to_string(random());
				try
				{
					fs::create_directories(m_path);
					return;
				}
				catch (const fs::filesystem_error&)
				{
					if (attempt == 10)
					{
						throw;
					}
				}
			}
		}

		~temp_dir_fixture()
		{
			fs::remove_all(m_path);
		}

		const cppcoro::filesystem::path& temp_dir()
		{
			return m_path;
		}

	private:

		cppcoro::filesystem::path m_path;

	};

	class temp_dir_with_io_service_fixture :
		public io_service_fixture,
		public temp_dir_fixture
	{
	};
}

TEST_CASE_FIXTURE(temp_dir_fixture, "write a file")
{
	auto filePath = temp_dir() / "foo";

	cppcoro::io_service ioService;

	auto write = [&](cppcoro::io_service& ioService) -> cppcoro::task<>
	{
		std::printf(" starting write\n"); std::fflush(stdout);

		auto f = cppcoro::write_only_file::open(ioService, filePath);

		CHECK(f.size() == 0);

		char buffer[1024];
		char c = 'a';
		for (int i = 0; i < sizeof(buffer); ++i, c = (c == 'z' ? 'a' : c + 1))
		{
			buffer[i] = c;
		}

		for (int chunk = 0; chunk < 10; ++chunk)
		{
			co_await f.write(chunk * sizeof(buffer), buffer, sizeof(buffer));
		}
	};

	auto read = [&](cppcoro::io_service& io) -> cppcoro::task<>
	{
		std::printf(" starting read\n"); std::fflush(stdout);

		auto f = cppcoro::read_only_file::open(io, filePath);

		const auto fileSize = f.size();

		CHECK(fileSize == 10240);

		char buffer[20];

		for (std::uint64_t i = 0; i < fileSize;)
		{
			auto bytesRead = co_await f.read(i, buffer, 20);
			for (size_t j = 0; j < bytesRead; ++j, ++i)
			{
				CHECK(buffer[j] == ('a' + ((i % 1024) % 26)));
			}
		}
	};

	cppcoro::sync_wait(cppcoro::when_all(
		[&]() -> cppcoro::task<int>
		{
			auto stopOnExit = cppcoro::on_scope_exit([&] { ioService.stop(); });
			co_await write(ioService);
			co_await read(ioService);
			co_return 0;
		}(),
		[&]() -> cppcoro::task<int>
		{
			ioService.process_events();
			co_return 0;
		}()));
}

TEST_CASE_FIXTURE(temp_dir_with_io_service_fixture, "read write file")
{
	auto run = [&]() -> cppcoro::task<>
	{
		cppcoro::io_work_scope ioScope{ io_service() };
		auto f = cppcoro::read_write_file::open(io_service(), temp_dir() / "foo.txt");

		char buffer1[100];
		std::memset(buffer1, 0xAB, sizeof(buffer1));

		co_await f.write(0, buffer1, sizeof(buffer1));

		char buffer2[50];
		std::memset(buffer2, 0xCC, sizeof(buffer2));

		co_await f.read(0, buffer2, 50);
		CHECK(std::memcmp(buffer1, buffer2, 50) == 0);

		co_await f.read(50, buffer2, 50);
		CHECK(std::memcmp(buffer1 + 50, buffer2, 50) == 0);
	};

	cppcoro::sync_wait(run());
}

TEST_CASE_FIXTURE(temp_dir_with_io_service_fixture, "cancel read")
{
	cppcoro::sync_wait([&]() -> cppcoro::task<>
	{
		cppcoro::io_work_scope ioScope{ io_service() };
		auto f = cppcoro::read_write_file::open(io_service(), temp_dir() / "foo.txt");

		f.set_size(20 * 1024 * 1024);

		cppcoro::cancellation_source canceller;

		try
		{
			(void)co_await cppcoro::when_all(
				[&]() -> cppcoro::task<int>
			{
				const auto fileSize = f.size();
				const std::size_t bufferSize = 64 * 1024;
				auto buffer = std::make_unique<std::uint8_t[]>(bufferSize);
				std::uint64_t offset = 0;
				while (offset < fileSize)
				{
					auto bytesRead = co_await f.read(offset, buffer.get(), bufferSize, canceller.token());
					offset += bytesRead;
				}
				WARN("should have been cancelled");
				co_return 0;
			}(),
				[&]() -> cppcoro::task<int>
			{
				using namespace std::chrono_literals;

				co_await io_service().schedule_after(1ms);
				canceller.request_cancellation();
				co_return 0;
			}());
			WARN("Expected exception to be thrown");
		}
		catch (const cppcoro::operation_cancelled&)
		{
		}
	}());
}

TEST_SUITE_END();
