diff --git a/test-harness/src/main/java/cz/moneta/test/harness/support/messaging/ImqRequest.java b/test-harness/src/main/java/cz/moneta/test/harness/support/messaging/ImqRequest.java
new file mode 100644
index 0000000..8dcef81
--- /dev/null
+++ b/test-harness/src/main/java/cz/moneta/test/harness/support/messaging/ImqRequest.java
@@ -0,0 +1,516 @@
+package cz.moneta.test.harness.support.messaging;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import cz.moneta.test.harness.endpoints.imq.ImqFirstVisionEndpoint;
+import cz.moneta.test.harness.endpoints.imq.ImqFirstVisionQueue;
+import cz.moneta.test.harness.exception.HarnessException;
+import cz.moneta.test.harness.messaging.MessageContentType;
+import cz.moneta.test.harness.messaging.MqMessageFormat;
+import cz.moneta.test.harness.messaging.ReceivedMessage;
+import cz.moneta.test.harness.messaging.exception.MessagingTimeoutException;
+import cz.moneta.test.harness.support.util.FileReader;
+import cz.moneta.test.harness.support.util.Template;
+import org.apache.commons.lang3.StringUtils;
+
+import java.time.Duration;
+import java.util.*;
+import java.util.function.Predicate;
+
+/**
+ * Fluent builder for IBM MQ requests.
+ *
+ * Usage:
+ *
{@code
+ * ImqRequest.toQueue(endpoint, queue)
+ * .asJson()
+ * .withPayload("{\"field\": \"value\"}")
+ * .send();
+ *
+ * ImqRequest.fromQueue(endpoint, queue)
+ * .receiveWhere(msg -> msg.extract("field").equals("value"))
+ * .withTimeout(10, TimeUnit.SECONDS)
+ * .andAssertFieldValue("result", "OK");
+ * }
+ *
+ */
+@SuppressWarnings("unchecked")
+public final class ImqRequest {
+
+ private static final ObjectMapper JSON_MAPPER = new ObjectMapper();
+
+ private ImqRequest() {
+ }
+
+ /**
+ * Start building a message to send to a queue.
+ */
+ public static QueuePhase toQueue(ImqFirstVisionEndpoint endpoint, ImqFirstVisionQueue queue) {
+ return new QueueBuilder(endpoint, queue, null);
+ }
+
+ /**
+ * Start building a message to receive from a queue.
+ */
+ public static ReceivePhase fromQueue(ImqFirstVisionEndpoint endpoint, ImqFirstVisionQueue queue) {
+ return new QueueBuilder(endpoint, queue, null);
+ }
+
+ /**
+ * Start building a message to send to a queue by physical name.
+ */
+ public static QueuePhase toQueue(ImqFirstVisionEndpoint endpoint, String queueName) {
+ return new QueueBuilder(endpoint, null, queueName);
+ }
+
+ /**
+ * Start building a message to receive from a queue by physical name.
+ */
+ public static ReceivePhase fromQueue(ImqFirstVisionEndpoint endpoint, String queueName) {
+ return new QueueBuilder(endpoint, null, queueName);
+ }
+
+ /**
+ * Phase after specifying queue - can send or receive.
+ */
+ public interface QueuePhase extends PayloadPhase, ReceivePhase {
+ }
+
+ /**
+ * Phase for building message payload.
+ */
+ public interface PayloadPhase {
+ /**
+ * Set message format to JSON (default).
+ */
+ PayloadPhase asJson();
+
+ /**
+ * Set message format to XML.
+ */
+ PayloadPhase asXml();
+
+ /**
+ * Set message format to EBCDIC (IBM-870).
+ */
+ PayloadPhase asEbcdic();
+
+ /**
+ * Set message format to UTF-8 (CCSID 1208).
+ */
+ PayloadPhase asUtf8();
+
+ /**
+ * Set message payload as JSON string.
+ */
+ PayloadPhase withPayload(String payload);
+
+ /**
+ * Load payload from resource file.
+ */
+ PayloadPhase withPayloadFromFile(String path);
+
+ /**
+ * Render payload from template.
+ */
+ PayloadPhase withPayloadFromTemplate(Template template);
+
+ /**
+ * Add field to JSON payload at root level.
+ */
+ PayloadPhase addField(String fieldName, Object value);
+
+ /**
+ * Add field to JSON payload at specified path.
+ */
+ PayloadPhase addField(String path, String fieldName, Object value);
+
+ /**
+ * Append value to JSON array at specified path.
+ */
+ PayloadPhase appendToArray(String path, Object value);
+
+ /**
+ * Send the message.
+ */
+ void send();
+ }
+
+ /**
+ * Phase for receiving messages.
+ */
+ public interface ReceivePhase {
+ /**
+ * Set JMS message selector.
+ */
+ ReceivePhase withSelector(String selector);
+
+ /**
+ * Start receiving message matching predicate.
+ */
+ AwaitingPhase receiveWhere(Predicate filter);
+
+ /**
+ * Browse messages from queue (non-destructive).
+ */
+ List browse(int maxMessages);
+
+ /**
+ * Browse messages from queue with selector (non-destructive).
+ */
+ List browse(String selector, int maxMessages);
+ }
+
+ /**
+ * Phase after specifying filter - await message with timeout.
+ */
+ public interface AwaitingPhase {
+ /**
+ * Set timeout and get message response for assertions.
+ */
+ MessageResponse withTimeout(long duration, java.util.concurrent.TimeUnit unit);
+
+ /**
+ * Set timeout duration and get message response for assertions.
+ */
+ MessageResponse withTimeout(Duration timeout);
+ }
+
+ /**
+ * Response with fluent assertions.
+ */
+ public interface MessageResponse extends cz.moneta.test.harness.support.messaging.MessageResponse {
+ }
+
+ /**
+ * Builder for queue operations.
+ */
+ private static class QueueBuilder implements QueuePhase, AwaitingPhase {
+
+ private final ImqFirstVisionEndpoint endpoint;
+ private final ImqFirstVisionQueue logicalQueue;
+ private final String physicalQueue;
+ private String selector;
+ private MqMessageFormat format = MqMessageFormat.JSON;
+ private String payload;
+ private Map fields = new HashMap<>();
+ private List> arrayAppends = new ArrayList<>();
+ private Predicate filter;
+ private Duration timeout;
+
+ public QueueBuilder(ImqFirstVisionEndpoint endpoint, ImqFirstVisionQueue logicalQueue, String physicalQueue) {
+ this.endpoint = endpoint;
+ this.logicalQueue = logicalQueue;
+ this.physicalQueue = physicalQueue;
+ }
+
+ private String getQueueName() {
+ if (logicalQueue != null) {
+ return endpoint.resolveQueue(logicalQueue);
+ }
+ return physicalQueue;
+ }
+
+ @Override
+ public PayloadPhase asJson() {
+ this.format = MqMessageFormat.JSON;
+ return this;
+ }
+
+ @Override
+ public PayloadPhase asXml() {
+ this.format = MqMessageFormat.XML;
+ return this;
+ }
+
+ @Override
+ public PayloadPhase asEbcdic() {
+ this.format = MqMessageFormat.EBCDIC_870;
+ return this;
+ }
+
+ @Override
+ public PayloadPhase asUtf8() {
+ this.format = MqMessageFormat.UTF8_1208;
+ return this;
+ }
+
+ @Override
+ public PayloadPhase withPayload(String payload) {
+ this.payload = payload;
+ return this;
+ }
+
+ @Override
+ public PayloadPhase withPayloadFromFile(String path) {
+ this.payload = FileReader.readFileFromResources(path);
+ return this;
+ }
+
+ @Override
+ public PayloadPhase withPayloadFromTemplate(Template template) {
+ this.payload = template.render();
+ return this;
+ }
+
+ @Override
+ public PayloadPhase addField(String fieldName, Object value) {
+ return addField("", fieldName, value);
+ }
+
+ @Override
+ public PayloadPhase addField(String path, String fieldName, Object value) {
+ String key = StringUtils.isNotBlank(path) && StringUtils.isNotBlank(fieldName) ? path + "." + fieldName : fieldName;
+ this.fields.put(key, value);
+ return this;
+ }
+
+ @Override
+ public PayloadPhase appendToArray(String path, Object value) {
+ this.arrayAppends.add(Map.entry(path, value));
+ return this;
+ }
+
+ @Override
+ public void send() {
+ String finalPayload = buildPayload();
+ Map properties = new HashMap<>();
+
+ if (logicalQueue != null) {
+ properties.put("LogicalQueue", logicalQueue.name());
+ }
+ if (selector != null && !selector.isBlank()) {
+ properties.put("Selector", selector);
+ }
+
+ endpoint.send(getQueueName(), finalPayload, format, properties);
+ }
+
+ @Override
+ public ReceivePhase withSelector(String selector) {
+ this.selector = selector;
+ return this;
+ }
+
+ @Override
+ public AwaitingPhase receiveWhere(Predicate filter) {
+ this.filter = filter;
+ return this;
+ }
+
+ @Override
+ public List browse(int maxMessages) {
+ return browse(selector, maxMessages);
+ }
+
+ @Override
+ public List browse(String sel, int maxMessages) {
+ return endpoint.browse(getQueueName(), sel, format, maxMessages);
+ }
+
+ @Override
+ public MessageResponse withTimeout(long duration, java.util.concurrent.TimeUnit unit) {
+ return withTimeout(Duration.of(duration, java.time.temporal.ChronoUnit.MILLIS));
+ }
+
+ @Override
+ public MessageResponse withTimeout(Duration timeout) {
+ this.timeout = timeout;
+
+ if (filter == null) {
+ throw new IllegalStateException("Must specify receiveWhere filter before withTimeout");
+ }
+
+ ReceivedMessage message = receiveMessage();
+ return new ResponseImpl(message);
+ }
+
+ private ReceivedMessage receiveMessage() {
+ long startTime = System.currentTimeMillis();
+ long timeoutMs = timeout.toMillis();
+
+ while (System.currentTimeMillis() - startTime < timeoutMs) {
+ try {
+ ReceivedMessage message = endpoint.receive(getQueueName(), selector, format, Duration.ofSeconds(1));
+ if (filter.test(message)) {
+ return message;
+ }
+ } catch (MessagingTimeoutException e) {
+ // Continue polling
+ }
+ }
+
+ throw new MessagingTimeoutException(
+ "No message matching filter found on queue '" + getQueueName() +
+ "' within " + timeout.toMillis() + "ms");
+ }
+
+ private String buildPayload() {
+ if (payload == null) {
+ return "{}";
+ }
+
+ if (fields.isEmpty() && arrayAppends.isEmpty()) {
+ return payload;
+ }
+
+ try {
+ Map json = JSON_MAPPER.readValue(payload, Map.class);
+
+ for (Map.Entry entry : fields.entrySet()) {
+ setField(json, entry.getKey(), entry.getValue());
+ }
+
+ for (Map.Entry entry : arrayAppends) {
+ appendToArray(json, entry.getKey(), entry.getValue());
+ }
+
+ return JSON_MAPPER.writeValueAsString(json);
+ } catch (Exception e) {
+ throw new HarnessException("Failed to build payload", e);
+ }
+ }
+
+ private void setField(Map json, String path, Object value) {
+ if (StringUtils.isBlank(path)) {
+ json.put(path, value);
+ return;
+ }
+
+ String[] parts = path.split("\\.");
+ Map current = json;
+
+ for (int i = 0; i < parts.length - 1; i++) {
+ String part = parts[i];
+ if (!current.containsKey(part)) {
+ current.put(part, new HashMap());
+ }
+ current = (Map) current.get(part);
+ }
+
+ current.put(parts[parts.length - 1], value);
+ }
+
+ private void appendToArray(Map json, String path, Object value) {
+ String[] parts = path.split("\\.");
+ Map current = json;
+
+ for (int i = 0; i < parts.length - 1; i++) {
+ String part = parts[i];
+ if (!current.containsKey(part)) {
+ current.put(part, new ArrayList