/*
 *  Copyright (c) 2011-2015 The original author or authors
 *  ------------------------------------------------------
 *  All rights reserved. This program and the accompanying materials
 *  are made available under the terms of the Eclipse Public License v1.0
 *  and Apache License v2.0 which accompanies this distribution.
 *
 *       The Eclipse Public License is available at
 *       http://www.eclipse.org/legal/epl-v10.html
 *
 *       The Apache License v2.0 is available at
 *       http://www.opensource.org/licenses/apache2.0.php
 *
 *  You may elect to redistribute this code under either of these licenses.
 */

package io.vertx.ext.stomp.impl;

import com.jayway.awaitility.Awaitility;
import io.vertx.core.Vertx;
import io.vertx.core.buffer.Buffer;
import io.vertx.core.eventbus.DeliveryOptions;
import io.vertx.core.eventbus.Message;
import io.vertx.core.eventbus.MessageConsumer;
import io.vertx.core.json.JsonObject;
import io.vertx.ext.bridge.PermittedOptions;
import io.vertx.ext.stomp.*;
import io.vertx.ext.stomp.utils.Headers;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;

import static org.assertj.core.api.Assertions.assertThat;

/**
 * @author <a href="http://escoffier.me">Clement Escoffier</a>
 */
public class EventBusBridgeTest {

  private StompServer server;
  private Vertx vertx;
  private List<StompClient> clients = new ArrayList<>();
  private List<MessageConsumer> consumers = new ArrayList<>();

  @Before
  public void setUp() {
    AsyncLock<StompServer> lock = new AsyncLock<>();

    vertx = Vertx.vertx();
    server = StompServer.create(vertx)
        .handler(StompServerHandler.create(vertx)
                .bridge(new BridgeOptions()
                    .addInboundPermitted(new PermittedOptions().setAddress("/bus"))
                    .addOutboundPermitted(new PermittedOptions().setAddress("/bus")))
        )
        .listen(lock.handler());

    lock.waitForSuccess();
  }

  @After
  public void tearDown() {
    clients.forEach(StompClient::close);
    clients.clear();
    consumers.forEach(MessageConsumer::unregister);
    consumers.clear();

    AsyncLock<Void> lock = new AsyncLock<>();
    server.close(lock.handler());
    lock.waitForSuccess();

    lock = new AsyncLock<>();
    vertx.close(lock.handler());
    lock.waitForSuccess();
  }

  @Test
  public void testThatStompMessagesAreTransferredToTheEventBus() {
    AtomicReference<Message> reference = new AtomicReference<>();
    consumers.add(vertx.eventBus().consumer("/bus", reference::set));

    clients.add(StompClient.create(vertx).connect(ar -> {
      final StompClientConnection connection = ar.result();
      connection.send("/bus", Headers.create("foo", "bar"), Buffer.buffer("Hello from STOMP"));
    }));

    Awaitility.await().atMost(10, TimeUnit.SECONDS).until(() -> reference.get() != null);
    assertThat(reference.get().headers().get("foo")).isEqualTo("bar");
    assertThat(reference.get().headers().get("destination")).isEqualTo("/bus");
    assertThat(reference.get().headers().get("content-length")).isEqualTo("16");
    assertThat(reference.get().address()).isEqualTo("/bus");
    assertThat(reference.get().replyAddress()).isNullOrEmpty();
    assertThat(reference.get().body().toString()).isEqualTo("Hello from STOMP");
  }

  @Test
  public void testThatEventBusMessagesAreTransferredToStomp() {
    AtomicReference<Frame> reference = new AtomicReference<>();

    clients.add(StompClient.create(vertx).connect(ar -> {
          final StompClientConnection connection = ar.result();
          connection.subscribe("/bus", reference::set,
              f -> {
                vertx.eventBus().publish("/bus", "Hello from Vert.x", new DeliveryOptions().addHeader("foo", "bar"));
              }
          );
        }
    ));

    Awaitility.await().atMost(10, TimeUnit.SECONDS).until(() -> reference.get() != null);

    assertThat(reference.get().getHeaders().get("foo")).isEqualTo("bar");
    assertThat(reference.get().getHeaders().get("destination")).isEqualTo("/bus");
    assertThat(reference.get().getHeaders().get("content-length")).isEqualTo("17");
    assertThat(reference.get().getBodyAsString()).isEqualTo("Hello from Vert.x");
  }

  @Test
  public void testBidirectionalPingPong() {
    server.stompHandler().bridge(new BridgeOptions()
            .addInboundPermitted(new PermittedOptions().setAddressRegex("/toBu."))
            .addOutboundPermitted(new PermittedOptions().setAddressRegex("/to.tomp"))
    );
    List<Frame> stomp = new ArrayList<>();
    List<Message> bus = new ArrayList<>();

    consumers.add(vertx.eventBus().consumer("/toBus", msg -> {
      bus.add(msg);
      if (bus.size() < 5) {
        vertx.eventBus().send("/toStomp", "pong");
      }
    }));

    clients.add(StompClient.create(vertx).connect(ar -> {
      final StompClientConnection connection = ar.result();
      connection.subscribe("/toStomp", frame -> {
        stomp.add(frame);
        if (stomp.size() < 4) {
          connection.send("/toBus", Buffer.buffer("ping"));
        }
      }, receipt -> {
        connection.send("/toBus", Buffer.buffer("ping"));
      });
    }));

    Awaitility.await().atMost(10, TimeUnit.SECONDS).until(() -> bus.size() == 4 && stomp.size() == 4);

    for (Frame frame : stomp) {
      assertThat(frame.getBodyAsString()).isEqualTo("pong");
    }
    for (Message message : bus) {
      assertThat(message.body().toString()).isEqualTo("ping");
    }
  }

  @Test
  public void testThatEventBusMessagesContainingJsonObjectAreTransferredToStomp() {
    AtomicReference<Frame> reference = new AtomicReference<>();

    clients.add(StompClient.create(vertx).connect(ar -> {
          final StompClientConnection connection = ar.result();
          connection.subscribe("/bus", reference::set,
              f -> {
                vertx.eventBus().publish("/bus", new JsonObject()
                        .put("name", "vert.x")
                        .put("count", 1)
                        .put("bool", true),
                    new DeliveryOptions().addHeader("foo", "bar"));
              }
          );
        }
    ));

    Awaitility.await().atMost(10, TimeUnit.SECONDS).until(() -> reference.get() != null);

    assertThat(reference.get().getHeaders().get("foo")).isEqualTo("bar");
    assertThat(reference.get().getHeaders().get("destination")).isEqualTo("/bus");
    JsonObject object = new JsonObject(reference.get().getBodyAsString());
    assertThat(object.getString("name")).isEqualTo("vert.x");
    assertThat(object.getInteger("count")).isEqualTo(1);
    assertThat(object.getBoolean("bool")).isTrue();
  }

  @Test
  public void testThatEventBusMessagesContainingBufferAreTransferredToStomp() {
    AtomicReference<Frame> reference = new AtomicReference<>();

    byte[] bytes = new byte[]{0, 1, 2, 3, 4, 5, 6};
    clients.add(StompClient.create(vertx).connect(ar -> {
          final StompClientConnection connection = ar.result();
          connection.subscribe("/bus", reference::set,
              f -> {
                vertx.eventBus().publish("/bus", Buffer.buffer(bytes),
                    new DeliveryOptions().addHeader("foo", "bar"));
              }
          );
        }
    ));

    Awaitility.await().atMost(10, TimeUnit.SECONDS).until(() -> reference.get() != null);

    assertThat(reference.get().getHeaders().get("foo")).isEqualTo("bar");
    assertThat(reference.get().getHeaders().get("destination")).isEqualTo("/bus");
    byte[] body = reference.get().getBody().getBytes();
    assertThat(body).containsExactly(bytes);
  }

  @Test
  public void testThatEventBusMessagesContainingNoBodyAreTransferredToStomp() {
    AtomicReference<Frame> reference = new AtomicReference<>();

    clients.add(StompClient.create(vertx).connect(ar -> {
          final StompClientConnection connection = ar.result();
          connection.subscribe("/bus", reference::set,
              f -> {
                vertx.eventBus().publish("/bus", null,
                    new DeliveryOptions().addHeader("foo", "bar"));
              }
          );
        }
    ));

    Awaitility.await().atMost(10, TimeUnit.SECONDS).until(() -> reference.get() != null);

    assertThat(reference.get().getHeaders().get("foo")).isEqualTo("bar");
    assertThat(reference.get().getHeaders().get("destination")).isEqualTo("/bus");
    byte[] body = reference.get().getBody().getBytes();
    assertThat(body).hasSize(0);
  }

  @Test
  public void testThatTwoEventBusConsumersReceiveAStompMessage() {
    List<Message> messages = new CopyOnWriteArrayList<>();
    consumers.add(vertx.eventBus().consumer("/bus", messages::add));
    consumers.add(vertx.eventBus().consumer("/bus", messages::add));

    clients.add(StompClient.create(vertx).connect(ar -> {
      final StompClientConnection connection = ar.result();
      connection.send("/bus", Headers.create("foo", "bar"), Buffer.buffer("Hello from STOMP"));
    }));

    Awaitility.await().atMost(10, TimeUnit.SECONDS).until(() -> messages.size() == 2);
  }

  @Test
  public void testThatOnlyOnEventBusConsumersReceiveAStompMessageInP2P() throws InterruptedException {
    server.stompHandler().bridge(new BridgeOptions()
            .addInboundPermitted(new PermittedOptions().setAddress("/toBus"))
            .setPointToPoint(true)
    );
    List<Message> messages = new CopyOnWriteArrayList<>();
    consumers.add(vertx.eventBus().consumer("/toBus", messages::add));
    consumers.add(vertx.eventBus().consumer("/toBus", messages::add));

    clients.add(StompClient.create(vertx).connect(ar -> {
      final StompClientConnection connection = ar.result();
      connection.send("/toBus", Headers.create("foo", "bar"), Buffer.buffer("Hello from STOMP"));
    }));

    Thread.sleep(500);
    assertThat(messages).hasSize(1);
  }

  @Test
  public void testThatEventBusMessagesAreTransferredToSeveralStompClients() {
    List<Frame> frames = new CopyOnWriteArrayList<>();

    clients.add(StompClient.create(vertx).connect(ar -> {
      final StompClientConnection connection = ar.result();
      connection.subscribe("/bus", frames::add,
          f -> {
            clients.add(StompClient.create(vertx).connect(ar2 -> {
              final StompClientConnection connection2 = ar2.result();
              connection2.subscribe("/bus", frames::add, receipt -> {
                vertx.eventBus().publish("/bus", "Hello from Vert.x", new DeliveryOptions().addHeader("foo", "bar"));
              });
            }));
          });
    }));

    Awaitility.await().atMost(10, TimeUnit.SECONDS).until(() -> frames.size() == 2);
  }

  @Test
  public void testThatEventBusMessagesAreOnlyTransferredToOneStompClientsInP2P() throws InterruptedException {
    List<Frame> frames = new CopyOnWriteArrayList<>();
    server.stompHandler().bridge(new BridgeOptions()
            .addOutboundPermitted(new PermittedOptions().setAddress("/toStomp"))
            .setPointToPoint(true)
    );

    clients.add(StompClient.create(vertx).connect(ar -> {
      final StompClientConnection connection = ar.result();
      connection.subscribe("/toStomp", frames::add,
          f -> {
            clients.add(StompClient.create(vertx).connect(ar2 -> {
              final StompClientConnection connection2 = ar2.result();
              connection2.subscribe("/toStomp", frames::add, receipt -> {
                vertx.eventBus().publish("/toStomp", "Hello from Vert.x", new DeliveryOptions().addHeader("foo", "bar"));
              });
            }));
          });
    }));

    Thread.sleep(500);
    assertThat(frames).hasSize(1);
  }

  @Test
  public void testThatEventBusConsumerCanReplyToStompMessages() {
    server.stompHandler().bridge(new BridgeOptions()
            .addOutboundPermitted(new PermittedOptions().setAddress("/replyTo"))
            .addInboundPermitted(new PermittedOptions().setAddress("/request"))
            .setPointToPoint(true)
    );

    AtomicReference<Frame> response = new AtomicReference<>();

    consumers.add(vertx.eventBus().consumer("/request", msg -> {
      msg.reply("pong");
    }));

    clients.add(StompClient.create(vertx).connect(ar -> {
      final StompClientConnection connection = ar.result();
      connection.subscribe("/replyTo", response::set, r1 -> {
        connection.send("/request", Headers.create("reply-address", "/replyTo"), Buffer.buffer("ping"));
      });
    }));

    Awaitility.await().atMost(10, TimeUnit.SECONDS).until(() -> response.get() != null);
  }

  @Test
  public void testThatStompClientCanUnsubscribe() throws InterruptedException {
    List<Frame> frames = new ArrayList<>();

    clients.add(StompClient.create(vertx).connect(ar -> {
          final StompClientConnection connection = ar.result();
          connection.subscribe("/bus", frame -> {
                frames.add(frame);
                connection.unsubscribe("/bus");
              },
              f -> {
                vertx.eventBus().publish("/bus", "Hello from Vert.x", new DeliveryOptions().addHeader("foo", "bar"));
              }
          );
        }
    ));

    Awaitility.await().atMost(10, TimeUnit.SECONDS).until(() -> frames.size() == 1);

    // Send another message
    vertx.eventBus().publish("/bus", "Hello from Vert.x", new DeliveryOptions().addHeader("foo", "bar"));

    Thread.sleep(500);
    assertThat(frames).hasSize(1);
  }

  @Test
  public void testThatStompClientCanCloseTheConnection() throws InterruptedException {
    List<Frame> frames = new ArrayList<>();

    clients.add(StompClient.create(vertx).connect(ar -> {
          final StompClientConnection connection = ar.result();
          connection.subscribe("/bus", frame -> {
                frames.add(frame);
                connection.close();
              },
              f -> {
                vertx.eventBus().publish("/bus", "Hello from Vert.x", new DeliveryOptions().addHeader("foo", "bar"));
              }
          );
        }
    ));

    Awaitility.await().atMost(10, TimeUnit.SECONDS).until(() -> frames.size() == 1);

    // Send another message
    vertx.eventBus().publish("/bus", "Hello from Vert.x", new DeliveryOptions().addHeader("foo", "bar"));

    Thread.sleep(500);
    assertThat(frames).hasSize(1);
  }

  @Test
  public void testThatStompClientCanDisconnect() throws InterruptedException {
    List<Frame> frames = new ArrayList<>();

    clients.add(StompClient.create(vertx).connect(ar -> {
          final StompClientConnection connection = ar.result();
          connection.subscribe("/bus", frame -> {
                frames.add(frame);
                connection.disconnect();
              },
              f -> {
                vertx.eventBus().publish("/bus", "Hello from Vert.x", new DeliveryOptions().addHeader("foo", "bar"));
              }
          );
        }
    ));

    Awaitility.await().atMost(10, TimeUnit.SECONDS).until(() -> frames.size() == 1);

    // Send another message
    vertx.eventBus().publish("/bus", "Hello from Vert.x", new DeliveryOptions().addHeader("foo", "bar"));

    Thread.sleep(500);
    assertThat(frames).hasSize(1);
  }

  @Test
  public void testThatStompFrameMatchingTheStructureAreTransferred() {
    tearDown();
    AsyncLock<StompServer> lock = new AsyncLock<>();

    vertx = Vertx.vertx();
    server = StompServer.create(vertx)
        .handler(StompServerHandler.create(vertx)
            .bridge(new BridgeOptions()
                .addInboundPermitted(new PermittedOptions().setAddress("/bus").setMatch(new JsonObject().put("id", 1)))
                .addOutboundPermitted(new PermittedOptions().setAddress("/bus")))
        )
        .listen(lock.handler());

    lock.waitForSuccess();

    AtomicReference<Message> reference = new AtomicReference<>();
    consumers.add(vertx.eventBus().consumer("/bus", reference::set));

    clients.add(StompClient.create(vertx).connect(ar -> {
      final StompClientConnection connection = ar.result();
      connection.send("/bus", Headers.create("foo", "bar"), Buffer.buffer(new JsonObject().put("id", 1).put("msg",
          "Hello from STOMP").toString()));
    }));

    Awaitility.await().atMost(10, TimeUnit.SECONDS).until(() -> reference.get() != null);
    assertThat(reference.get().headers().get("foo")).isEqualTo("bar");
    assertThat(reference.get().headers().get("destination")).isEqualTo("/bus");
    assertThat(reference.get().address()).isEqualTo("/bus");
    assertThat(reference.get().replyAddress()).isNullOrEmpty();
    JsonObject json = new JsonObject(reference.get().body().toString());
    assertThat(json.getString("msg")).isEqualTo("Hello from STOMP");
  }

  @Test
  public void testThatStompFrameNotMatchingTheStructureAreRejected() throws InterruptedException {
    tearDown();
    AsyncLock<StompServer> lock = new AsyncLock<>();

    vertx = Vertx.vertx();
    server = StompServer.create(vertx)
        .handler(StompServerHandler.create(vertx)
            .bridge(new BridgeOptions()
                .addInboundPermitted(new PermittedOptions().setAddress("/bus").setMatch(new JsonObject().put("id", 2)))
                .addOutboundPermitted(new PermittedOptions().setAddress("/bus")))
        )
        .listen(lock.handler());

    lock.waitForSuccess();

    AtomicReference<Message> reference = new AtomicReference<>();
    consumers.add(vertx.eventBus().consumer("/bus", reference::set));

    clients.add(StompClient.create(vertx).connect(ar -> {
      final StompClientConnection connection = ar.result();
      connection.send("/bus", Headers.create("foo", "bar"), Buffer.buffer(new JsonObject().put("msg",
          "Hello from STOMP").toString()));
    }));

    Thread.sleep(2000);
    assertThat(reference.get()).isNull();
  }

  @Test
  public void testThatEventBusMessagesMatchingTheStructureAreTransferredToStomp() {
    tearDown();
    AsyncLock<StompServer> lock = new AsyncLock<>();

    vertx = Vertx.vertx();
    server = StompServer.create(vertx)
        .handler(StompServerHandler.create(vertx)
            .bridge(new BridgeOptions()
                .addInboundPermitted(new PermittedOptions().setAddress("/bus"))
                .addOutboundPermitted(new PermittedOptions().setAddress("/bus").setMatch(new JsonObject().put("id", 2)
        ))))
        .listen(lock.handler());

    lock.waitForSuccess();


    AtomicReference<Frame> reference = new AtomicReference<>();

    clients.add(StompClient.create(vertx).connect(ar -> {
          final StompClientConnection connection = ar.result();
          connection.subscribe("/bus", reference::set,
              f -> {
                JsonObject payload = new JsonObject().put("id", 2).put("message", "Hello from Vert.x");
                vertx.eventBus().publish("/bus", payload, new DeliveryOptions().addHeader("foo", "bar"));
              }
          );
        }
    ));

    Awaitility.await().atMost(10, TimeUnit.SECONDS).until(() -> reference.get() != null);

    assertThat(reference.get().getHeaders().get("foo")).isEqualTo("bar");
    assertThat(reference.get().getHeaders().get("destination")).isEqualTo("/bus");
    JsonObject json = new JsonObject(reference.get().getBodyAsString());
    assertThat(json.getString("message")).isEqualTo("Hello from Vert.x");
  }

  @Test
  public void testThatEventBusMessagesNotMatchingTheStructureAreRejected() throws InterruptedException {
    tearDown();
    AsyncLock<StompServer> lock = new AsyncLock<>();

    vertx = Vertx.vertx();
    server = StompServer.create(vertx)
        .handler(StompServerHandler.create(vertx)
            .bridge(new BridgeOptions()
                .addInboundPermitted(new PermittedOptions().setAddress("/bus"))
                .addOutboundPermitted(new PermittedOptions().setAddress("/bus").setMatch(new JsonObject().put("id", 2)
                ))))
        .listen(lock.handler());

    lock.waitForSuccess();


    AtomicReference<Frame> reference = new AtomicReference<>();

    clients.add(StompClient.create(vertx).connect(ar -> {
          final StompClientConnection connection = ar.result();
          connection.subscribe("/bus", reference::set,
              f -> {
                JsonObject payload = new JsonObject().put("id", 1).put("message", "Hello from Vert.x");
                vertx.eventBus().publish("/bus", payload, new DeliveryOptions().addHeader("foo", "bar"));
              }
          );
        }
    ));
    Thread.sleep(2000);
    assertThat(reference.get()).isNull();
  }
}