// SPDX-License-Identifier: LGPL-3.0-or-later
// Copyright © 2020-2021 Florian Fischer
#include <arpa/inet.h>								// for inet_addr
#include <bits/types/struct_iovec.h>	// for iovec
#include <fcntl.h>										// for open, O_RDONLY
#include <netinet/in.h>								// for sockaddr_in, htons, in_addr
#include <sys/socket.h>								// for bind, listen, setsockopt
#include <unistd.h>										// for close

#include <cassert>	// for assert
#include <cstdlib>	// for mkstemp, exit, EXIT_SUCCESS
#include <cstring>	// for memcmp, memset
#include <memory>		// for allocator, unique_ptr
#include <string>		// for string
#include <vector>		// for vector

#include "Common.hpp"										 // for DIE_MSG_ERRNO, DIE_MSG
#include "CountingPrivateSemaphore.hpp"	 // for CPS
#include "emper.hpp"										 // for spawn
#include "fixtures/network.hpp"					 // for echo_client
#include "io.hpp"												 // for readFile, accept, recv, send
#include "io/Future.hpp"								 // for Future

#define PORT 4243
#define MAX 1024
static void server_func(int sockfd) {
	struct sockaddr_in clientaddr;
	socklen_t clientaddr_len = sizeof(clientaddr);
	auto client_fd =
			emper::io::accept(sockfd, reinterpret_cast<struct sockaddr*>(&clientaddr), &clientaddr_len)
					->wait();

	if (client_fd < 0) {
		DIE_MSG_ERRNO("accept failed");
	}

	// NOLINTNEXTLINE(modernize-avoid-c-arrays)
	char recv_buf[MAX];
	// NOLINTNEXTLINE(modernize-avoid-c-arrays)
	char read_buf[MAX];

	for (;;) {
		int received = emper::io::recv(client_fd, recv_buf, sizeof(recv_buf), 0)->wait();
		if (received == 0) {
			exit(EXIT_SUCCESS);
		}

		if (received == -1) {
			DIE_MSG_ERRNO("recv failed");
			break;
		}

		// NOLINTNEXTLINE(modernize-avoid-c-arrays)
		char file_name[] = "/tmp/emper-SimpleDiskAndNetworkTestFile-XXXXXX";
		int file_fd = mkstemp(file_name);
		if (file_fd == -1) {
			DIE_MSG_ERRNO("mkstemp failed");
		}

		int written = emper::io::writeFile(file_fd, recv_buf, received)->wait();
		if (written < 0) {
			DIE_MSG_ERRNO("write failed");
		}
		close(file_fd);

		file_fd = emper::io::openAndWait(file_name, O_RDONLY);
		if (file_fd == -1) {
			DIE_MSG_ERRNO("open failed");
		}

		int bytes_read = emper::io::readFile(file_fd, read_buf, written)->wait();
		if (bytes_read == 0) {
			DIE_MSG("nothing to read");
		}

		if (bytes_read < 0) {
			DIE_MSG_ERRNO("read failed");
		}
		close(file_fd);

		int sent = emper::io::send(client_fd, read_buf, bytes_read, 0)->wait();
		if (sent == 0) {
			DIE_MSG("client socket unexpected shutdown");
		}

		if (sent == -1) {
			DIE_MSG_ERRNO("send failed");
		}

		// NOLINTNEXTLINE(modernize-avoid-c-arrays)
		char file2_name[] = "/tmp/emper-SimpleDiskAndNetworkTestFile-XXXXXX";
		file_fd = mkstemp(file2_name);
		if (file_fd == -1) {
			DIE_MSG_ERRNO("mkstemp failed");
		}

		const int iovcnt = 2;
		// NOLINTNEXTLINE(modernize-avoid-c-arrays)
		struct iovec iov[iovcnt];

		std::string s1 = "foo";
		std::string s2 = "bar";

		iov[0].iov_base = (void*)s1.c_str();
		iov[0].iov_len = s1.length();
		iov[1].iov_base = (void*)s2.c_str();
		iov[1].iov_len = s2.length();

		auto writevFuture = emper::io::writev(file_fd, &iov[0], iovcnt);
		written = writevFuture->wait();
		if (written < 0) {
			DIE_MSG_ERRNO("wrtev failed");
		}
		close(file_fd);

		file_fd = emper::io::openAndWait(file2_name, O_RDONLY);
		if (file_fd == -1) {
			DIE_MSG_ERRNO("open failed");
		}

		auto readFuture = emper::io::readFile(file_fd, read_buf, written, 0, true);
		bytes_read = readFuture->wait();
		if (bytes_read == 0) {
			DIE_MSG("nothing to read");
		}

		if (bytes_read < 0) {
			DIE_MSG_ERRNO("read failed");
		}

		assert(written == bytes_read);

		assert(memcmp(read_buf, iov[0].iov_base, iov[0].iov_len) == 0);
		assert(memcmp((char*)read_buf + iov[0].iov_len, iov[1].iov_base, iov[1].iov_len) == 0);

		close(file_fd);
	}
}

void emperTest() {
	int sockfd;
	struct sockaddr_in servaddr;

	// socket creation and verification
	sockfd = socket(AF_INET, SOCK_STREAM, 0);
	if (sockfd == -1) {
		DIE_MSG_ERRNO("socket creation failed");
	}
	memset(&servaddr, 0, sizeof(servaddr));

	// assign IP, PORT
	servaddr.sin_family = AF_INET;
	servaddr.sin_addr.s_addr = inet_addr("127.0.0.1");
	servaddr.sin_port = htons(PORT);

	int reuseaddr = 1;
	if (setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &reuseaddr, sizeof(reuseaddr)) == -1) {
		DIE_MSG_ERRNO("setsockopt failed");
	}
	if (bind(sockfd, (sockaddr*)&servaddr, sizeof(servaddr)) == -1) {
		DIE_MSG_ERRNO("bind failed");
	}
	if (listen(sockfd, 1) != 0) {
		DIE_MSG_ERRNO("listen failed");
	}

	CPS cps;
	spawn([=] { server_func(sockfd); }, cps);

	spawn(
			[] {
				const std::vector<std::string> msgs{"foo", "bar"};
				std::string port = std::to_string(PORT);
				std::string host("127.0.0.1");
				echo_client(host, port, msgs);
			},
			cps);

	cps.wait();
}