package de.ozgcloud.xta.client;

import static de.ozgcloud.xta.client.XtaClient.*;
import static de.ozgcloud.xta.client.extension.XtaServerSetupExtensionTestUtil.*;
import static java.util.Collections.*;
import static org.assertj.core.api.Assertions.*;

import java.util.ArrayList;
import java.util.List;
import java.util.function.Consumer;
import java.util.function.Predicate;

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;

import de.ozgcloud.xta.client.exception.XtaClientException;
import de.ozgcloud.xta.client.extension.StaticStringListAppender;
import de.ozgcloud.xta.client.extension.XtaMessageExampleLoader;
import de.ozgcloud.xta.client.extension.XtaTestServerSetupExtension;
import de.ozgcloud.xta.client.model.XtaIdentifier;
import de.ozgcloud.xta.client.model.XtaMessage;
import de.ozgcloud.xta.client.model.XtaMessageMetaData;
import de.ozgcloud.xta.client.model.XtaMessageStatus;
import de.ozgcloud.xta.client.model.XtaTransportReport;
import lombok.SneakyThrows;

class XtaClientITCase {

	@RegisterExtension
	private static final XtaTestServerSetupExtension XTA_TEST_SERVER_SETUP_EXTENSION = new XtaTestServerSetupExtension();
	static final int TWO_MAX_LIST_ITEMS = 2;

	private XtaClient silentTestClient;
	private XtaClient testClient;

	private List<XtaMessageMetaData> supportCheckedMetadataItems;
	private List<XtaMessage> processedMessages;
	private Consumer<XtaMessage> processMessageDummy;
	private Predicate<XtaMessageMetaData> isSupportedDummy;

	@BeforeEach
	@SneakyThrows
	void setup() {
		processMessageDummy = (message) -> {
		};
		supportCheckedMetadataItems = new ArrayList<>();
		isSupportedDummy = (metaData) -> true;
		processedMessages = new ArrayList<>();
		silentTestClient = XTA_TEST_SERVER_SETUP_EXTENSION.getSilentTestClient();

		StaticStringListAppender.clearLogLines();
		closeMessagesForAllReaders();
	}

	private void closeMessagesForAllReaders() {
		var silentTestClientConfig = XTA_TEST_SERVER_SETUP_EXTENSION.getSilentTestClientConfig();
		closeAllMessages(silentTestClientConfig, READER_CLIENT_IDENTIFIER1);
		closeAllMessages(silentTestClientConfig, READER_CLIENT_IDENTIFIER2);
		closeAllMessages(silentTestClientConfig, READER_CLIENT_IDENTIFIER3);
	}

	@DisplayName("fetch messages")
	@Nested
	class TestFetchMessages {

		private List<XtaMessage> sendMessages;
		private List<String> sendMessageIds;

		@BeforeEach
		void setup() {
			sendMessages = List.of(
					createMessage("dfoerdermittel", AUTHOR_CLIENT_IDENTIFIER, READER_CLIENT_IDENTIFIER1),
					createMessage("dfoerdermittel", AUTHOR_CLIENT_IDENTIFIER, READER_CLIENT_IDENTIFIER2),
					createMessage("abgabe0401-kleiner-waffenschein", AUTHOR_CLIENT_IDENTIFIER, READER_CLIENT_IDENTIFIER2),
					createMessage("versammlungsanzeige", AUTHOR_CLIENT_IDENTIFIER2, READER_CLIENT_IDENTIFIER2),
					createMessage("versammlungsanzeige", AUTHOR_CLIENT_IDENTIFIER2, READER_CLIENT_IDENTIFIER3),
					createMessage("versammlungsanzeige", AUTHOR_CLIENT_IDENTIFIER3, READER_CLIENT_IDENTIFIER3)
			);
			sendMessageIds = sendMessages.stream()
					.map(message -> sendTestMessage(silentTestClient, message))
					.toList();
		}

		private XtaMessage createMessage(String messageLabel, XtaIdentifier author, XtaIdentifier reader) {
			return XtaMessageExampleLoader.load(
					XtaMessageExampleLoader.MessageExampleConfig.builder()
							.messageLabel(messageLabel)
							.reader(reader)
							.author(author)
							.build());
		}

		@DisplayName("should throw exception on connection failure")
		@Test
		@SneakyThrows
		void shouldThrowExceptionOnConnectionFailure() {
			setupClientWithoutTrustStore();

			assertThatThrownBy(() -> testClient.fetchMessages((message) -> fail("Should not process any message!")))
					.isInstanceOf(XtaClientException.class);
		}

		@DisplayName("should fetch no messages if no client identifier is configured")
		@Test
		void shouldFetchNoMessagesIfNoClientIdentifierIsConfigured() {
			setupClientWithIdentifiers(emptyList());

			var messages = fetchMessages();

			assertThat(supportCheckedMetadataItems).isEmpty();
			assertThat(processedMessages).isEmpty();
			assertThat(messages).isEmpty();
		}

		@DisplayName("should fetch no messages if client identifier has no messages pending")
		@Test
		void shouldFetchNoMessagesIfClientIdentifierHasNoMessagesPending() {
			setupClientWithIdentifiers(List.of(AUTHOR_CLIENT_IDENTIFIER));

			var messages = fetchMessages();

			assertThat(supportCheckedMetadataItems).isEmpty();
			assertThat(processedMessages).isEmpty();
			assertThat(messages).isEmpty();
		}

		@DisplayName("should fetch messages from first reader")
		@Test
		@SneakyThrows
		void shouldFetchMessagesFromFirstReader() {
			setupClientWithIdentifiers(List.of(READER_CLIENT_IDENTIFIER1));

			var transportReports = fetchMessages();

			assertThat(supportCheckedMetadataItems).hasSize(1);
			assertThatMessages(processedMessages).containExactlyInAnyOrder(sendMessages.getFirst());
			assertThatTransportReports(transportReports)
					.reportExactlyFor(processedMessages)
					.haveExactlyClosedStatusFor(messageIdBySendIndex(0));
		}

		@DisplayName("should fetch messages from second reader")
		@Test
		void shouldFetchMessagesFromSecondReader() {
			setupClientWithIdentifiers(List.of(READER_CLIENT_IDENTIFIER2));

			var transportReports = fetchMessages();

			assertThat(supportCheckedMetadataItems).hasSize(3);
			assertThatMessages(processedMessages).containExactlyInAnyOrder(sendMessages.get(1), sendMessages.get(2), sendMessages.get(3));
			assertThatTransportReports(transportReports)
					.reportExactlyFor(processedMessages)
					.haveExactlyClosedStatusFor(messageIdBySendIndex(1), messageIdBySendIndex(2), messageIdBySendIndex(3));
		}

		@DisplayName("should fetch messages from first and second reader")
		@Test
		void shouldFetchMessagesFromFirstAndSecondReader() {
			setupClientWithIdentifiers(List.of(READER_CLIENT_IDENTIFIER1, READER_CLIENT_IDENTIFIER2));

			var transportReports = fetchMessages();

			assertThat(supportCheckedMetadataItems).hasSize(1 + 3);
			assertThatMessages(processedMessages).containExactlyInAnyOrder(
					sendMessages.get(0),
					sendMessages.get(1), sendMessages.get(2), sendMessages.get(3)
			);
			assertThatTransportReports(transportReports)
					.reportExactlyFor(processedMessages)
					.haveExactlyClosedStatusFor(
							messageIdBySendIndex(0),
							messageIdBySendIndex(1), messageIdBySendIndex(2), messageIdBySendIndex(3)
					);
		}

		@DisplayName("should fetch messages from first, second and third reader")
		@Test
		void shouldFetchMessagesFromFirstSecondAndThirdReader() {
			setupClientWithIdentifiers(List.of(READER_CLIENT_IDENTIFIER1, READER_CLIENT_IDENTIFIER2, READER_CLIENT_IDENTIFIER3));

			var transportReports = fetchMessages();

			assertThat(supportCheckedMetadataItems).hasSize(1 + 3 + 2);
			assertThatMessages(processedMessages).containExactlyInAnyOrder(
					sendMessages.get(0),
					sendMessages.get(1), sendMessages.get(2), sendMessages.get(3),
					sendMessages.get(4), sendMessages.get(5)
			);
			assertThatTransportReports(transportReports)
					.reportExactlyFor(processedMessages)
					.haveExactlyClosedStatusFor(
							messageIdBySendIndex(0),
							messageIdBySendIndex(1), messageIdBySendIndex(2), messageIdBySendIndex(3),
							messageIdBySendIndex(4), messageIdBySendIndex(5)
					);
		}

		@DisplayName("should close messages only if no exception occurs during processing, with no exception for author1")
		@Test
		void shouldCloseMessagesOnlyIfNoExceptionOccursDuringProcessingWithNoExceptionForAuthor1() {
			setupClientWithIdentifiers(List.of(READER_CLIENT_IDENTIFIER1, READER_CLIENT_IDENTIFIER2, READER_CLIENT_IDENTIFIER3));
			processMessageDummy = message -> throwRuntimeExceptionExceptForAuthorIdentifier(message, AUTHOR_CLIENT_IDENTIFIER);

			var transportReports = fetchMessages();

			assertThat(supportCheckedMetadataItems).hasSize(1 + 3 + 2);
			assertThat(hasLogLineContaining(NO_MESSAGE_CLOSED_WARNING)).isFalse();
			assertThatMessages(processedMessages).containExactlyInAnyOrder(
					sendMessages.get(0),
					sendMessages.get(1), sendMessages.get(2), sendMessages.get(3),
					sendMessages.get(4), sendMessages.get(5)
			);
			assertThatTransportReports(transportReports)
					.reportExactlyFor(processedMessages)
					.haveExactlyClosedStatusFor(
							messageIdBySendIndex(0),
							messageIdBySendIndex(1), messageIdBySendIndex(2)
					);
		}

		@DisplayName("should close messages only if no exception occurs during processing, with no exception for author3")
		@Test
		void shouldCloseMessagesOnlyIfNoExceptionOccursDuringProcessingWithNoExceptionForAuthor3() {
			setupClientWithIdentifiers(List.of(READER_CLIENT_IDENTIFIER1, READER_CLIENT_IDENTIFIER2, READER_CLIENT_IDENTIFIER3));
			processMessageDummy = message -> throwRuntimeExceptionExceptForAuthorIdentifier(message, AUTHOR_CLIENT_IDENTIFIER3);

			var transportReports = fetchMessages();

			assertThat(supportCheckedMetadataItems).hasSize(1 + 2 + 2);
			assertThat(hasLogLineContaining(NO_MESSAGE_CLOSED_WARNING)).isTrue();
			assertThatMessages(processedMessages).containMetaDataExactlyInAnyOrder(
					sendMessages.get(0).metaData(),
					supportCheckedMetadataItems.get(1), supportCheckedMetadataItems.get(2),
					sendMessages.get(4).metaData(), sendMessages.get(5).metaData()
			);
			assertThatTransportReports(transportReports)
					.reportExactlyFor(processedMessages)
					.haveExactlyClosedStatusFor(
							messageIdBySendIndex(5)
					);
		}

		private void throwRuntimeExceptionExceptForAuthorIdentifier(XtaMessage message, XtaIdentifier authorIdentifier) {
			var authorId = message.metaData().authorIdentifier().value();
			var readerId = message.metaData().readerIdentifier().value();
			if (!authorId.equals(authorIdentifier.value())) {
				throw new RuntimeException("Test exception for message with author '%s' and reader '%s'!".formatted(authorId, readerId));
			}
		}

		@DisplayName("should process messages only if supported, with support for author1")
		@Test
		void shouldProcessMessagesOnlyIfSupportedWithSupportForAuthor1() {
			setupClientWithIdentifiers(List.of(READER_CLIENT_IDENTIFIER1, READER_CLIENT_IDENTIFIER2, READER_CLIENT_IDENTIFIER3));
			isSupportedDummy = metaData -> metaData.authorIdentifier().value().equals(AUTHOR_CLIENT_IDENTIFIER.value());

			var transportReports = fetchMessages();

			assertThat(supportCheckedMetadataItems).hasSize(1 + 3 + 2);
			assertThat(hasLogLineContaining(NO_MESSAGE_CLOSED_WARNING)).isFalse();
			assertThatMessages(processedMessages).containExactlyInAnyOrder(sendMessages.get(0), sendMessages.get(1), sendMessages.get(2));
			assertThatTransportReports(transportReports)
					.reportExactlyFor(processedMessages)
					.haveExactlyClosedStatusFor(messageIdBySendIndex(0), messageIdBySendIndex(1), messageIdBySendIndex(2));
		}

		@DisplayName("should process messages only if supported, with support for author3")
		@Test
		void shouldProcessMessagesOnlyIfSupportedWithSupportForAuthor3() {
			setupClientWithIdentifiers(List.of(READER_CLIENT_IDENTIFIER1, READER_CLIENT_IDENTIFIER2, READER_CLIENT_IDENTIFIER3));
			isSupportedDummy = metaData -> metaData.authorIdentifier().value().equals(AUTHOR_CLIENT_IDENTIFIER3.value());

			var transportReports = fetchMessages();

			assertThat(supportCheckedMetadataItems).hasSize(1 + 2 + 2);
			assertThat(hasLogLineContaining(NO_MESSAGE_CLOSED_WARNING)).isTrue();
			assertThatMessages(processedMessages).containExactlyInAnyOrder(sendMessages.get(5));
			assertThatTransportReports(transportReports)
					.reportExactlyFor(processedMessages)
					.haveExactlyClosedStatusFor(messageIdBySendIndex(5));
		}

		@SneakyThrows
		private void setupClientWithIdentifiers(List<XtaIdentifier> identifiers) {
			testClient = XtaClient.from(
					XTA_TEST_SERVER_SETUP_EXTENSION.createSpecificClientConfigBuilder()
							.clientIdentifiers(identifiers)
							.maxListItems(TWO_MAX_LIST_ITEMS)
							.isMessageSupported(metaData -> {
								supportCheckedMetadataItems.add(metaData);
								return isSupportedDummy.test(metaData);
							})
							.build()
			);
		}

		private String messageIdBySendIndex(int sendIndex) {
			return sendMessageIds.get(sendIndex);
		}

		@SneakyThrows
		private List<XtaTransportReport> fetchMessages() {
			return testClient.fetchMessages((message) -> {
				processedMessages.add(message);
				processMessageDummy.accept(message);
			});
		}
	}

	@DisplayName("send message")
	@Nested
	class TestSendMessage {

		private XtaMessage message;

		@BeforeEach
		void beforeEach() {
			var messageConfig = XtaMessageExampleLoader.MessageExampleConfig.builder()
					.messageLabel("dfoerdermittel")
					.reader(READER_CLIENT_IDENTIFIER1)
					.author(AUTHOR_CLIENT_IDENTIFIER)
					.build();
			message = XtaMessageExampleLoader.load(messageConfig);
		}

		@DisplayName("should throw exception on connection failure")
		@Test
		@SneakyThrows
		void shouldThrowExceptionOnConnectionFailure() {
			setupClientWithoutTrustStore();

			assertThatThrownBy(() -> testClient.sendMessage(message))
					.isInstanceOf(XtaClientException.class);
		}

		@DisplayName("should return transport report with open status")
		@Test
		@SneakyThrows
		void shouldReturnTransportReportWithOpenStatus() {
			var transportReport = silentTestClient.sendMessage(message);

			assertThat(transportReport.status()).isEqualTo(XtaMessageStatus.OPEN);
		}
	}

	@SneakyThrows
	private void setupClientWithoutTrustStore() {
		testClient = XtaClient.from(
				XTA_TEST_SERVER_SETUP_EXTENSION.createSpecificClientConfigBuilder()
						.trustStore(null)
						.clientIdentifiers(List.of(READER_CLIENT_IDENTIFIER1))
						.maxListItems(TWO_MAX_LIST_ITEMS)
						.isMessageSupported(metaData -> fail("Should not process any message!"))
						.build()
		);
	}
}