package cz.moneta.test.harness.support.messaging;

import cz.moneta.test.harness.endpoints.messaging.MessagingEndpoint;
import cz.moneta.test.harness.messaging.model.MqMessageFormat;
import cz.moneta.test.harness.messaging.model.ReceivedMessage;
import cz.moneta.test.harness.support.util.FileReader;
import cz.moneta.test.harness.support.util.Template;
import org.junit.jupiter.api.Assertions;

import java.time.Duration;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.TimeUnit;
import java.util.function.Predicate;

public final class MessagingRequest {

    private static final Duration DEFAULT_TIMEOUT = Duration.ofSeconds(30);

    private final MessagingEndpoint endpoint;
    private String destinationName;
    private String key;
    private String payload;
    private Duration timeout = DEFAULT_TIMEOUT;
    private final Map<String, String> headers = new LinkedHashMap<>();
    private MqMessageFormat formatOverride;
    private Predicate<ReceivedMessage> receiveFilter;
    private ReceivedMessage receivedMessage;
    private boolean receivePending;
    private Mode mode = Mode.UNSET;

    private MessagingRequest(MessagingEndpoint endpoint) {
        this.endpoint = endpoint;
    }

    public static MessagingRequest builder(MessagingEndpoint endpoint) {
        return new MessagingRequest(endpoint);
    }

    public MessagingRequest to(String destinationName) {
        this.destinationName = destinationName;
        this.mode = Mode.SEND;
        resetReceiveState();
        return this;
    }

    public MessagingRequest from(String destinationName) {
        this.destinationName = destinationName;
        this.mode = Mode.RECEIVE;
        resetReceiveState();
        return this;
    }

    public MessagingRequest withKey(String key) {
        this.key = key;
        return this;
    }

    public MessagingRequest withPayload(String payload) {
        this.payload = payload;
        return this;
    }

    public MessagingRequest withPayloadFromTemplate(Template template) {
        this.payload = template.render();
        return this;
    }

    public MessagingRequest withPayloadFromFile(String path) {
        this.payload = FileReader.readFileFromResources(path);
        return this;
    }

    public MessagingRequest withHeader(String key, String value) {
        this.headers.put(key, value);
        return this;
    }

    public MessagingRequest withTraceparent(String value) {
        return withHeader("traceparent", value);
    }

    public MessagingRequest withRequestID(String value) {
        return withHeader("requestID", value);
    }

    public MessagingRequest withActivityID(String value) {
        return withHeader("activityID", value);
    }

    public MessagingRequest withSourceCodebookId(String value) {
        return withHeader("sourceCodebookId", value);
    }

    public MessagingRequest asJson() {
        this.formatOverride = MqMessageFormat.JSON;
        return this;
    }

    public MessagingRequest asXml() {
        this.formatOverride = MqMessageFormat.XML;
        return this;
    }

    public MessagingRequest asEbcdic() {
        this.formatOverride = MqMessageFormat.EBCDIC_870;
        return this;
    }

    public MessagingRequest asUtf8() {
        this.formatOverride = MqMessageFormat.UTF8_1208;
        return this;
    }

    public MessagingRequest withTimeout(long value, TimeUnit unit) {
        this.timeout = Duration.ofMillis(unit.toMillis(value));
        if (receivePending && receivedMessage == null) {
            doReceive();
        }
        return this;
    }

    public MessagingRequest send() {
        ensureMode(Mode.SEND);
        if (payload == null) {
            throw new IllegalStateException("Message payload must be provided before send()");
        }
        endpoint.send(destinationName, key, payload, formatOverride, headers);
        return this;
    }

    public MessagingRequest receiveWhere(Predicate<ReceivedMessage> filter) {
        ensureMode(Mode.RECEIVE);
        this.receiveFilter = filter;
        this.receivePending = true;
        this.receivedMessage = null;
        return this;
    }

    public MessagingRequest andAssertFieldValue(String expression, String expectedValue) {
        ReceivedMessage message = getReceivedMessage();
        Assertions.assertEquals(expectedValue,
                message.extract(expression),
                String.format("Unexpected message field value for '%s'. Message body: %s", expression, message.getBody()));
        return this;
    }

    public String extract(String expression) {
        return getReceivedMessage().extract(expression);
    }

    public ReceivedMessage getReceivedMessage() {
        ensureMode(Mode.RECEIVE);
        if (receivedMessage == null) {
            doReceive();
        }
        return receivedMessage;
    }

    private void doReceive() {
        if (!receivePending) {
            receiveFilter = msg -> true;
            receivePending = true;
        }

        receivedMessage = endpoint.receive(
                destinationName,
                Optional.ofNullable(receiveFilter).orElse(msg -> true),
                timeout,
                formatOverride
        );
        receivePending = false;
    }

    private void ensureMode(Mode requiredMode) {
        if (this.mode != requiredMode) {
            throw new IllegalStateException("Messaging request is not in " + requiredMode + " mode");
        }
    }

    private void resetReceiveState() {
        this.receiveFilter = null;
        this.receivedMessage = null;
        this.receivePending = false;
    }

    private enum Mode {
        UNSET,
        SEND,
        RECEIVE
    }
}
