// Copyright 2015-2018 The NATS Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at:
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package io.nats.client.impl;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;

import java.io.IOException;
import java.time.Duration;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;

import org.junit.Test;

import io.nats.client.Connection;
import io.nats.client.Dispatcher;
import io.nats.client.Message;
import io.nats.client.MessageHandler;
import io.nats.client.Nats;
import io.nats.client.NatsTestServer;
import io.nats.client.Options;
import io.nats.client.Subscription;



// Some tests are a bit tricky, and depend on the fact that the dispatcher
// uses a single queue, so the "subject" messages go through before
// the done message (or should) - wanted to note that somewhere

public class DispatcherTests {
    @Test
    public void testSingleMessage() throws IOException, InterruptedException, ExecutionException, TimeoutException {
        try (NatsTestServer ts = new NatsTestServer(false);
                Connection nc = Nats.connect(ts.getURI())) {
            assertTrue("Connected Status", Connection.Status.CONNECTED == nc.getStatus());

            final CompletableFuture<Message> msgFuture = new CompletableFuture<>();
            Dispatcher d = nc.createDispatcher((msg) -> {
                msgFuture.complete(msg);
            });

            d.subscribe("subject");
            nc.flush(Duration.ofMillis(500));// Get them all to the server

            nc.publish("subject", new byte[16]);

            Message msg = msgFuture.get(500, TimeUnit.MILLISECONDS);

            assertTrue(d.isActive());
            assertEquals("subject", msg.getSubject());
            assertNotNull(msg.getSubscription());
            assertNull(msg.getReplyTo());
            assertEquals(16, msg.getData().length);
        }
    }

    @Test
    public void testDispatcherMessageContainsConnection() throws IOException, InterruptedException, ExecutionException, TimeoutException {
        try (NatsTestServer ts = new NatsTestServer(false);
                Connection nc = Nats.connect(ts.getURI())) {
            assertTrue("Connected Status", Connection.Status.CONNECTED == nc.getStatus());

            final CompletableFuture<Message> msgFuture = new CompletableFuture<>();
            final CompletableFuture<Connection> connFuture = new CompletableFuture<>();
            Dispatcher d = nc.createDispatcher((msg) -> {
                msgFuture.complete(msg);
                connFuture.complete(msg.getConnection());
            });

            d.subscribe("subject");
            nc.flush(Duration.ofMillis(5000));// Get them all to the server

            nc.publish("subject", new byte[16]);

            Message msg = msgFuture.get(5000, TimeUnit.MILLISECONDS);
            Connection conn = connFuture.get(5000, TimeUnit.MILLISECONDS);

            assertTrue(d.isActive());
            assertEquals("subject", msg.getSubject());
            assertNotNull(msg.getSubscription());
            assertNull(msg.getReplyTo());
            assertEquals(16, msg.getData().length);
            assertTrue(conn == nc);
        }
    }

    @Test
    public void testMultiSubject() throws IOException, InterruptedException, ExecutionException, TimeoutException {
        try (NatsTestServer ts = new NatsTestServer(false);
                Connection nc = Nats.connect(new Options.Builder().server(ts.getURI()).maxReconnects(0).build())) {
            assertTrue("Connected Status", Connection.Status.CONNECTED == nc.getStatus());

            final CompletableFuture<Message> one = new CompletableFuture<>();
            final CompletableFuture<Message> two = new CompletableFuture<>();
            Dispatcher d = nc.createDispatcher((msg) -> {
                if (msg.getSubject().equals("one")) {
                    one.complete(msg);
                } else if (msg.getSubject().equals("two")) {
                    two.complete(msg);
                }
            });

            d.subscribe("one");
            d.subscribe("two");
            nc.flush(Duration.ofMillis(500));// Get them all to the server

            nc.publish("one", new byte[16]);
            nc.publish("two", new byte[16]);

            Message msg = one.get(500, TimeUnit.MILLISECONDS);
            assertEquals("one", msg.getSubject());
            msg = two.get(500, TimeUnit.MILLISECONDS);
            assertEquals("two", msg.getSubject());
        }
    }

    @Test
    public void testMultiMessage() throws IOException, InterruptedException, ExecutionException, TimeoutException {
        try (NatsTestServer ts = new NatsTestServer(false);
                Connection nc = Nats.connect(ts.getURI())) {
            final CompletableFuture<Boolean> done = new CompletableFuture<>();
            int msgCount = 100;
            assertTrue("Connected Status", Connection.Status.CONNECTED == nc.getStatus());

            final ConcurrentLinkedQueue<Message> q = new ConcurrentLinkedQueue<>();
            Dispatcher d = nc.createDispatcher((msg) -> {
                if (msg.getSubject().equals("done")) {
                    done.complete(Boolean.TRUE);
                } else {
                    q.add(msg);
                }
            });

            d.subscribe("subject");
            d.subscribe("done");
            nc.flush(Duration.ofMillis(1000)); // wait for them to go through

            for (int i = 0; i < msgCount; i++) {
                nc.publish("subject", new byte[16]);
            }
            nc.publish("done", new byte[16]);
            nc.flush(Duration.ofMillis(1000)); // wait for them to go through

            done.get(500, TimeUnit.MILLISECONDS);

            assertEquals(msgCount, q.size());
        }
    }

    @Test(expected=TimeoutException.class)
    public void testClose() throws IOException, InterruptedException, ExecutionException, TimeoutException {
        try (NatsTestServer ts = new NatsTestServer(false);
                Connection nc = Nats.connect(ts.getURI())) {
            final CompletableFuture<Boolean> phase1 = new CompletableFuture<>();
            final CompletableFuture<Boolean> phase2 = new CompletableFuture<>();
            assertTrue("Connected Status", Connection.Status.CONNECTED == nc.getStatus());

            final ConcurrentLinkedQueue<Message> q = new ConcurrentLinkedQueue<>();
            Dispatcher d = nc.createDispatcher((msg) -> {
                if (msg.getSubject().equals("phase1")) {
                    phase1.complete(Boolean.TRUE);
                } else if (msg.getSubject().equals("phase1")) {
                    phase2.complete(Boolean.TRUE);
                } else {
                    q.add(msg);
                }
            });

            d.subscribe("subject");
            d.subscribe("phase1");
            d.subscribe("phase2");
            nc.flush(Duration.ofMillis(500));// Get them all to the server

            nc.publish("subject", new byte[16]);
            nc.publish("phase1", null);

            nc.flush(Duration.ofMillis(1000)); // wait for them to go through
            phase1.get(200, TimeUnit.MILLISECONDS);

            assertEquals(1, q.size());

            nc.closeDispatcher(d);

            assertFalse(d.isActive());

            // This won't arrive
            nc.publish("phase2", new byte[16]);

            nc.flush(Duration.ofMillis(1000)); // wait for them to go through
            phase2.get(200, TimeUnit.MILLISECONDS);
        }
    }


    @Test
    public void testQueueSubscribers() throws IOException, InterruptedException, ExecutionException, TimeoutException {
        try (NatsTestServer ts = new NatsTestServer(false);
                 Connection nc = Nats.connect(ts.getURI())) {
            int msgs = 100;
            AtomicInteger received = new AtomicInteger();
            AtomicInteger sub1Count = new AtomicInteger();
            AtomicInteger sub2Count = new AtomicInteger();

            final CompletableFuture<Boolean> done1 = new CompletableFuture<>();
            final CompletableFuture<Boolean> done2 = new CompletableFuture<>();

            assertTrue("Connected Status", Connection.Status.CONNECTED == nc.getStatus());

            Dispatcher d1 = nc.createDispatcher((msg) -> {
                if (msg.getSubject().equals("done")) {
                    done1.complete(Boolean.TRUE);
                } else {
                    sub1Count.incrementAndGet();
                    received.incrementAndGet();
                }
            });

            Dispatcher d2 = nc.createDispatcher((msg) -> {
                if (msg.getSubject().equals("done")) {
                    done2.complete(Boolean.TRUE);
                } else {
                    sub2Count.incrementAndGet();
                    received.incrementAndGet();
                }
            });

            d1.subscribe("subject", "queue");
            d2.subscribe("subject", "queue");
            d1.subscribe("done");
            d2.subscribe("done");
            nc.flush(Duration.ofMillis(500));

            for (int i = 0; i < msgs; i++) {
                nc.publish("subject", new byte[16]);
            }

            nc.publish("done", null);

            nc.flush(Duration.ofMillis(500));
            done1.get(500, TimeUnit.MILLISECONDS);
            done2.get(500, TimeUnit.MILLISECONDS);

            assertEquals(msgs, received.get());
            assertEquals(msgs, sub1Count.get() + sub2Count.get());

            // They won't be equal but print to make sure they are close (human testing)
            System.out.println("### Sub 1 " + sub1Count.get());
            System.out.println("### Sub 2 " + sub2Count.get());
        }
    }

    @Test(expected = IllegalStateException.class)
    public void testCantUnsubSubFromDispatcher()
            throws IOException, InterruptedException, ExecutionException, TimeoutException {
                try (NatsTestServer ts = new NatsTestServer(false);
                            Connection nc = Nats.connect(ts.getURI())) {
            assertTrue("Connected Status", Connection.Status.CONNECTED == nc.getStatus());

            final CompletableFuture<Message> msgFuture = new CompletableFuture<>();
            Dispatcher d = nc.createDispatcher((msg) -> {
                msgFuture.complete(msg);
            });

            d.subscribe("subject");
            nc.flush(Duration.ofMillis(500));// Get them all to the server

            nc.publish("subject", new byte[16]);

            Message msg = msgFuture.get(500, TimeUnit.MILLISECONDS);

            msg.getSubscription().unsubscribe(); // Should throw
            assertFalse(true);
        }
    }

    @Test(expected = IllegalStateException.class)
    public void testCantAutoUnsubSubFromDispatcher()
            throws IOException, InterruptedException, ExecutionException, TimeoutException {
                try (NatsTestServer ts = new NatsTestServer(false);
                            Connection nc = Nats.connect(ts.getURI())) {
            assertTrue("Connected Status", Connection.Status.CONNECTED == nc.getStatus());

            final CompletableFuture<Message> msgFuture = new CompletableFuture<>();
            Dispatcher d = nc.createDispatcher((msg) -> {
                msgFuture.complete(msg);
            });

            d.subscribe("subject");
            nc.flush(Duration.ofMillis(500));// Get them all to the server

            nc.publish("subject", new byte[16]);

            Message msg = msgFuture.get(500, TimeUnit.MILLISECONDS);

            msg.getSubscription().unsubscribe(1); // Should throw
            assertFalse(true);
        }
    }

    @Test
    public void testPublishAndFlushFromCallback()
            throws IOException, InterruptedException, ExecutionException, TimeoutException {
        try (NatsTestServer ts = new NatsTestServer(false);
                    Connection nc = Nats.connect(ts.getURI())) {
            assertTrue("Connected Status", Connection.Status.CONNECTED == nc.getStatus());

            final CompletableFuture<Message> msgFuture = new CompletableFuture<>();
            Dispatcher d = nc.createDispatcher((msg) -> {
                try {
                    nc.flush(Duration.ofMillis(1000));
                } catch (Exception ex) {
                    System.out.println("!!! Exception in callback");
                    ex.printStackTrace();
                }
                msgFuture.complete(msg);
            });

            d.subscribe("subject");
            nc.flush(Duration.ofMillis(500));// Get them all to the server

            nc.publish("subject", new byte[16]); // publish one to kick it off

            Message msg = msgFuture.get(500, TimeUnit.MILLISECONDS);
            assertNotNull(msg);

            assertEquals(2, ((NatsStatistics)(nc.getStatistics())).getFlushCounter());
        }
    }

    @Test
    public void testUnsub() throws IOException, InterruptedException, ExecutionException, TimeoutException {
        try (NatsTestServer ts = new NatsTestServer(false);
                Connection nc = Nats.connect(ts.getURI())) {
            final CompletableFuture<Boolean> phase1 = new CompletableFuture<>();
            final CompletableFuture<Boolean> phase2 = new CompletableFuture<>();
            int msgCount = 10;
            assertTrue("Connected Status", Connection.Status.CONNECTED == nc.getStatus());

            final ConcurrentLinkedQueue<Message> q = new ConcurrentLinkedQueue<>();
            Dispatcher d = nc.createDispatcher((msg) -> {
                if (msg.getSubject().equals("phase1")) {
                    phase1.complete(Boolean.TRUE);
                } else if (msg.getSubject().equals("phase2")) {
                    phase2.complete(Boolean.TRUE);
                } else {
                    q.add(msg);
                }
            });

            d.subscribe("subject");
            d.subscribe("phase1");
            d.subscribe("phase2");
            nc.flush(Duration.ofMillis(1000));// Get them all to the server

            for (int i = 0; i < msgCount; i++) {
                nc.publish("subject", new byte[16]);
            }
            nc.publish("phase1", new byte[16]);
            nc.flush(Duration.ofMillis(1000)); // wait for them to go through

            phase1.get(5000, TimeUnit.MILLISECONDS);

            d.unsubscribe("subject");
            nc.flush(Duration.ofMillis(1000));// Get them all to the server

            for (int i = 0; i < msgCount; i++) {
                nc.publish("subject", new byte[16]);
            }
            nc.publish("phase2", new byte[16]);
            nc.flush(Duration.ofMillis(1000)); // wait for them to go through

            phase2.get(1000, TimeUnit.MILLISECONDS); // make sure we got them

            assertEquals(msgCount, q.size());
        }
    }

    @Test
    public void testAutoUnsub() throws IOException, InterruptedException, ExecutionException, TimeoutException {
        try (NatsTestServer ts = new NatsTestServer(false);
                    Connection nc = Nats.connect(ts.getURI())) {
            final CompletableFuture<Boolean> phase1 = new CompletableFuture<>();
            final CompletableFuture<Boolean> phase2 = new CompletableFuture<>();
            int msgCount = 100;
            assertTrue("Connected Status", Connection.Status.CONNECTED == nc.getStatus());

            final ConcurrentLinkedQueue<Message> q = new ConcurrentLinkedQueue<>();
            NatsDispatcher d = (NatsDispatcher) nc.createDispatcher((msg) -> {
                if (msg.getSubject().equals("phase1")) {
                    phase1.complete(Boolean.TRUE);
                }else if (msg.getSubject().equals("phase2")) {
                    phase2.complete(Boolean.TRUE);
                } else {
                    q.add(msg);
                }
            });

            d.subscribe("subject");
            d.subscribe("phase1");
            d.subscribe("phase2");
            nc.flush(Duration.ofMillis(500));// Get them all to the server

            for (int i = 0; i < msgCount; i++) {
                nc.publish("subject", new byte[16]);
            }
            nc.publish("phase1", new byte[16]);

            nc.flush(Duration.ofMillis(1000)); // wait for them to go through
            phase1.get(1000, TimeUnit.MILLISECONDS); // make sure we got them

            assertEquals(msgCount, q.size());

            d.unsubscribe("subject", msgCount + 1);

            for (int i = 0; i < msgCount; i++) {
                nc.publish("subject", new byte[16]);
            }
            nc.publish("phase2", new byte[16]);

            nc.flush(Duration.ofMillis(1000)); // Wait for it all to get processed
            phase2.get(1000, TimeUnit.MILLISECONDS); // make sure we got them

            assertEquals(msgCount + 1, q.size());
        }
    }

    @Test
    public void testUnsubFromCallback() throws IOException, InterruptedException, ExecutionException, TimeoutException {
        try (NatsTestServer ts = new NatsTestServer(false);
                Connection nc = Nats.connect(ts.getURI())) {
            final CompletableFuture<Boolean> done = new CompletableFuture<>();
            assertTrue("Connected Status", Connection.Status.CONNECTED == nc.getStatus());

            final AtomicReference<Dispatcher> dispatcher = new AtomicReference<>();
            final ConcurrentLinkedQueue<Message> q = new ConcurrentLinkedQueue<>();
            final Dispatcher d = nc.createDispatcher((msg) -> {
                if (msg.getSubject().equals("done")) {
                    done.complete(Boolean.TRUE);
                } else {
                    q.add(msg);
                    dispatcher.get().unsubscribe("subject");
                }
            });

            dispatcher.set(d);

            d.subscribe("subject");
            d.subscribe("done");
            nc.flush(Duration.ofMillis(500));// Get them all to the server

            nc.publish("subject", new byte[16]);
            nc.publish("subject", new byte[16]);
            nc.publish("done", new byte[16]); // when we get this we know the others are dispatched
            nc.flush(Duration.ofMillis(1000)); // Wait for the publish, or we will get multiples for sure
            done.get(200, TimeUnit.MILLISECONDS); // make sure we got them

            assertEquals(1, q.size());
        }
    }

    @Test
    public void testAutoUnsubFromCallback()
            throws IOException, InterruptedException, ExecutionException, TimeoutException {
        try (NatsTestServer ts = new NatsTestServer(false);
                Connection nc = Nats.connect(ts.getURI())) {
            final CompletableFuture<Boolean> done = new CompletableFuture<>();
            assertTrue("Connected Status", Connection.Status.CONNECTED == nc.getStatus());

            final AtomicReference<Dispatcher> dispatcher = new AtomicReference<>();
            final ConcurrentLinkedQueue<Message> q = new ConcurrentLinkedQueue<>();
            final Dispatcher d = nc.createDispatcher((msg) -> {
                if (msg.getSubject().equals("done")) {
                    done.complete(Boolean.TRUE);
                } else {
                    q.add(msg);
                    dispatcher.get().unsubscribe("subject", 2); // get 1 more, for a total of 2
                }
            });

            dispatcher.set(d);

            d.subscribe("subject");
            d.subscribe("done");
            nc.flush(Duration.ofMillis(1000));// Get them all to the server

            nc.publish("subject", new byte[16]);
            nc.publish("subject", new byte[16]);
            nc.publish("subject", new byte[16]);
            nc.publish("done", new byte[16]); // when we get this we know the others are dispatched
            nc.flush(Duration.ofMillis(1000)); // Wait for the publish

            done.get(200, TimeUnit.MILLISECONDS); // make sure we got them

            assertEquals(2, q.size());
        }
    }

    @Test
    public void testCloseFromCallback() throws IOException, InterruptedException, ExecutionException, TimeoutException {
        try (NatsTestServer ts = new NatsTestServer(false);
                Connection nc = Nats.connect(ts.getURI())) {
            final CompletableFuture<Boolean> done = new CompletableFuture<>();
            assertTrue("Connected Status", Connection.Status.CONNECTED == nc.getStatus());

            final Dispatcher d = nc.createDispatcher((msg) -> {
                try {
                    if (msg.getSubject().equals("done")) {
                        nc.close();
                        done.complete(Boolean.TRUE);
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            });

            d.subscribe("done");
            nc.flush(Duration.ofMillis(5000));// Get them all to the server

            nc.publish("done", new byte[16]); // when we get this we know the others are dispatched
            nc.flush(Duration.ofMillis(5000)); // Wait for the publish

            done.get(5000, TimeUnit.MILLISECONDS); // make sure we got them
            assertTrue("Closed Status", Connection.Status.CLOSED == nc.getStatus());
        }
    }

    @Test
    public void testDispatchHandlesExceptionInHandler() throws IOException, InterruptedException, ExecutionException, TimeoutException {
        try (NatsTestServer ts = new NatsTestServer(false);
                Connection nc = Nats.connect(ts.getURI())) {
            final CompletableFuture<Boolean> done = new CompletableFuture<>();
            int msgCount = 100;
            assertTrue("Connected Status", Connection.Status.CONNECTED == nc.getStatus());

            final ConcurrentLinkedQueue<Message> q = new ConcurrentLinkedQueue<>();
            Dispatcher d = nc.createDispatcher((msg) -> {
                if (msg.getSubject().equals("done")) {
                    done.complete(Boolean.TRUE);
                } else {
                    q.add(msg);
                    throw new NumberFormatException();
                }
            });

            d.subscribe("subject");
            d.subscribe("done");
            nc.flush(Duration.ofMillis(500));// Get them all to the server

            for (int i = 0; i < msgCount; i++) {
                nc.publish("subject", new byte[16]);
            }
            nc.publish("done", new byte[16]);

            nc.flush(Duration.ofMillis(1000)); // wait for them to go through
            done.get(200, TimeUnit.MILLISECONDS);

            assertEquals(msgCount, q.size());
        }
    }

    @Test(expected=IllegalArgumentException.class)
    public void testThrowOnNullSubject() throws IOException, InterruptedException, TimeoutException {
        try (NatsTestServer ts = new NatsTestServer(false);
                    Connection nc = Nats.connect(ts.getURI())) {
            Dispatcher d = nc.createDispatcher((msg) -> {});
            d.subscribe(null);
            assertFalse(true);
        }
    }

    @Test(expected=IllegalArgumentException.class)
    public void testThrowOnEmptySubject() throws IOException, InterruptedException, TimeoutException {
        try (NatsTestServer ts = new NatsTestServer(false);
                    Connection nc = Nats.connect(ts.getURI())) {
            Dispatcher d = nc.createDispatcher((msg) -> {});

            d.subscribe("");
            assertFalse(true);
        }
    }

    @Test(expected=IllegalArgumentException.class)
    public void testThrowOnEmptyQueue() throws IOException, InterruptedException, TimeoutException {
        try (NatsTestServer ts = new NatsTestServer(false);
                    Connection nc = Nats.connect(ts.getURI())) {
            Dispatcher d = nc.createDispatcher((msg) -> {});
            d.subscribe("subject", "");
            assertFalse(true);
        }
    }

    @Test(expected=IllegalArgumentException.class)
    public void testThrowOnNullSubjectWithQueue() throws IOException, InterruptedException, TimeoutException {
        try (NatsTestServer ts = new NatsTestServer(false);
                    Connection nc = Nats.connect(ts.getURI())) {
            Dispatcher d = nc.createDispatcher((msg) -> {});
            d.subscribe(null, "quque");
            assertFalse(true);
        }
    }

    @Test(expected=IllegalArgumentException.class)
    public void testThrowOnEmptySubjectWithQueue() throws IOException, InterruptedException, TimeoutException {
        try (NatsTestServer ts = new NatsTestServer(false);
                    Connection nc = Nats.connect(ts.getURI())) {
            Dispatcher d = nc.createDispatcher((msg) -> {});
            d.subscribe("", "quque");
            assertFalse(true);
        }
    }

    @Test(expected = IllegalStateException.class)
    public void throwsOnCreateIfClosed() throws IOException, InterruptedException {
        try (NatsTestServer ts = new NatsTestServer(false);
                    Connection nc = Nats.connect(ts.getURI())) {
            nc.close();
            nc.createDispatcher((msg) -> {});
            assertFalse(true);
        }
    }

    @Test(expected = IllegalStateException.class)
    public void throwsOnSubscribeIfClosed() throws IOException, InterruptedException {
        try (NatsTestServer ts = new NatsTestServer(false);
                    Connection nc = Nats.connect(ts.getURI())) {
            Dispatcher d = nc.createDispatcher((msg) -> {});
            nc.close();
            d.subscribe("subject");
            assertFalse(true);
        }
    }

    @Test(expected=IllegalStateException.class)
    public void testThrowOnSubscribeWhenClosed() throws IOException, InterruptedException, TimeoutException {
        try (NatsTestServer ts = new NatsTestServer(false);
                    Connection nc = Nats.connect(ts.getURI())) {
            Dispatcher d = nc.createDispatcher((msg) -> {});
            nc.closeDispatcher(d);
            d.subscribe("foo");
            assertFalse(true);
        }
    }

    @Test(expected=IllegalStateException.class)
    public void testThrowOnUnsubscribeWhenClosed() throws IOException, InterruptedException, TimeoutException {
        try (NatsTestServer ts = new NatsTestServer(false);
                    Connection nc = Nats.connect(ts.getURI())) {
            Dispatcher d = nc.createDispatcher((msg) -> {});
            d.subscribe("foo");
            nc.closeDispatcher(d);
            d.unsubscribe("foo");
            assertFalse(true);
        }
    }

    @Test(expected=IllegalArgumentException.class)
    public void testThrowOnDoubleClose() throws IOException, InterruptedException, TimeoutException {
        try (NatsTestServer ts = new NatsTestServer(false);
                    Connection nc = Nats.connect(ts.getURI())) {
            Dispatcher d = nc.createDispatcher((msg) -> {});
            nc.closeDispatcher(d);
            nc.closeDispatcher(d);
            assertFalse(true);
        }
    }

    @Test(expected=IllegalStateException.class)
    public void testThrowOnConnClosed() throws IOException, InterruptedException, TimeoutException {
        try (NatsTestServer ts = new NatsTestServer(false);
                    Connection nc = Nats.connect(ts.getURI())) {
            Dispatcher d = nc.createDispatcher((msg) -> {});
            nc.close();
            nc.closeDispatcher(d);
            assertFalse(true);
        }
    }

    @Test
    public void testDoubleSubscribe() throws IOException, InterruptedException, ExecutionException, TimeoutException {
        try (NatsTestServer ts = new NatsTestServer(false);
                    Connection nc = Nats.connect(ts.getURI())) {
            final CompletableFuture<Boolean> done = new CompletableFuture<>();
            int msgCount = 100;
            assertTrue("Connected Status", Connection.Status.CONNECTED == nc.getStatus());

            final ConcurrentLinkedQueue<Message> q = new ConcurrentLinkedQueue<>();
            Dispatcher d = nc.createDispatcher((msg) -> {
                if (msg.getSubject().equals("done")) {
                    done.complete(Boolean.TRUE);
                } else {
                    q.add(msg);
                }
            });

            d.subscribe("subject").subscribe("subject").subscribe("subject").subscribe("done");
            nc.flush(Duration.ofSeconds(5)); // wait for them to go through

            for (int i = 0; i < msgCount; i++) {
                nc.publish("subject", new byte[16]);
            }
            nc.publish("done", new byte[16]);
            nc.flush(Duration.ofSeconds(5)); // wait for them to go through

            done.get(5, TimeUnit.SECONDS);

            assertEquals(msgCount, q.size()); // Shoudl only get one since all the extra subs do nothing??
        }
    }

    @Test
    public void testDoubleSubscribeWithCustomHandler() throws IOException, InterruptedException, ExecutionException, TimeoutException {
        try (NatsTestServer ts = new NatsTestServer(false);
                    Connection nc = Nats.connect(ts.getURI())) {
            final CompletableFuture<Boolean> done = new CompletableFuture<>();
            int msgCount = 100;
            assertTrue("Connected Status", Connection.Status.CONNECTED == nc.getStatus());

            final AtomicInteger count = new AtomicInteger(0);
            Dispatcher d = nc.createDispatcher((msg) -> {});

            d.subscribe("subject", (msg) -> { count.incrementAndGet(); });
            d.subscribe("subject", "queue", (msg) -> { count.incrementAndGet(); });
            d.subscribe("done", (msg) -> { done.complete(Boolean.TRUE); });

            nc.flush(Duration.ofSeconds(5)); // wait for them to go through

            for (int i = 0; i < msgCount; i++) {
                nc.publish("subject", new byte[16]);
            }
            nc.publish("done", new byte[16]);
            nc.flush(Duration.ofSeconds(5)); // wait for them to go through

            done.get(5, TimeUnit.SECONDS);

            assertEquals(msgCount * 2, count.get()); // We should get 2x the messages because we subscribed 2 times.
        }
    }

    @Test
    public void testDoubleSubscribeWithUnsubscribeAfterWithCustomHandler() throws IOException, InterruptedException, ExecutionException, TimeoutException {
        try (NatsTestServer ts = new NatsTestServer(false);
                    Connection nc = Nats.connect(ts.getURI())) {
            final CompletableFuture<Boolean> done1 = new CompletableFuture<>();
            final CompletableFuture<Boolean> done2 = new CompletableFuture<>();
            int msgCount = 100;
            assertTrue("Connected Status", Connection.Status.CONNECTED == nc.getStatus());

            final AtomicInteger count = new AtomicInteger(0);
            Dispatcher d = nc.createDispatcher((msg) -> {});
            Subscription s1 = d.subscribe("subject", (msg) -> { count.incrementAndGet(); });
            Subscription doneSub = d.subscribe("done", (msg) -> { done1.complete(Boolean.TRUE); });
            d.subscribe("subject", (msg) -> { count.incrementAndGet(); });

            nc.flush(Duration.ofSeconds(5)); // wait for the subs to go through

            for (int i = 0; i < msgCount; i++) {
                nc.publish("subject", new byte[16]);
            }
            nc.publish("done", new byte[16]);
            nc.flush(Duration.ofSeconds(5)); // wait for the messages to go through

            done1.get(5, TimeUnit.SECONDS);

            assertEquals(msgCount * 2, count.get()); // We should get 2x the messages because we subscribed 2 times.

            count.set(0);
            d.unsubscribe(s1);
            d.unsubscribe(doneSub);
            d.subscribe("done", (msg) -> { done2.complete(Boolean.TRUE); });
            nc.flush(Duration.ofSeconds(5)); // wait for the unsub to go through

            for (int i = 0; i < msgCount; i++) {
                nc.publish("subject", new byte[16]);
            }
            nc.publish("done", new byte[16]);
            nc.flush(Duration.ofSeconds(5)); // wait for the messages to go through

            done2.get(5, TimeUnit.SECONDS);

            assertEquals(msgCount, count.get()); // We only have 1 active subscription, so we should only get msgCount.
        }
    }

    @Test(expected=IllegalArgumentException.class)
    public void testThrowOnEmptySubjectWithMessageHandler() throws IOException, InterruptedException, TimeoutException {
        try (NatsTestServer ts = new NatsTestServer(false);
                    Connection nc = Nats.connect(ts.getURI())) {
            Dispatcher d = nc.createDispatcher((msg) -> {});
            d.subscribe("", (msg) -> {});
            assertFalse(true);
        }
    }

    @Test(expected=IllegalArgumentException.class)
    public void testThrowOnNullHandler() throws IOException, InterruptedException, TimeoutException {
        try (NatsTestServer ts = new NatsTestServer(false);
                    Connection nc = Nats.connect(ts.getURI())) {
            Dispatcher d = nc.createDispatcher((msg) -> {});
            d.subscribe("test", (MessageHandler)null);
            assertFalse(true);
        }
    }

    @Test(expected=IllegalArgumentException.class)
    public void testThrowOnNullHandlerWithQueue() throws IOException, InterruptedException, TimeoutException {
        try (NatsTestServer ts = new NatsTestServer(false);
                    Connection nc = Nats.connect(ts.getURI())) {
            Dispatcher d = nc.createDispatcher((msg) -> {});
            d.subscribe("test", "queue", (MessageHandler)null);
            assertFalse(true);
        }
    }

    @Test(expected=IllegalArgumentException.class)
    public void testThrowOnEmptyQueueWithMessageHandler() throws IOException, InterruptedException, TimeoutException {
        try (NatsTestServer ts = new NatsTestServer(false);
                    Connection nc = Nats.connect(ts.getURI())) {
            Dispatcher d = nc.createDispatcher((msg) -> {});
            d.subscribe("subject", "", (msg) -> {});
            assertFalse(true);
        }
    }

    @Test(expected=IllegalArgumentException.class)
    public void testThrowOnNullSubjectWithQueueWithMessageHandler() throws IOException, InterruptedException, TimeoutException {
        try (NatsTestServer ts = new NatsTestServer(false);
                    Connection nc = Nats.connect(ts.getURI())) {
            Dispatcher d = nc.createDispatcher((msg) -> {});
            d.subscribe(null, "quque", (msg) -> {});
            assertFalse(true);
        }
    }

    @Test(expected=IllegalArgumentException.class)
    public void testThrowOnEmptySubjectWithQueueWithMessageHandler() throws IOException, InterruptedException, TimeoutException {
        try (NatsTestServer ts = new NatsTestServer(false);
                    Connection nc = Nats.connect(ts.getURI())) {
            Dispatcher d = nc.createDispatcher((msg) -> {});
            d.subscribe("", "quque", (msg) -> {});
            assertFalse(true);
        }
    }

    @Test(expected=IllegalArgumentException.class)
    public void testThrowOnEmptySubjectInUnsub() throws IOException, InterruptedException, TimeoutException {
        try (NatsTestServer ts = new NatsTestServer(false);
                    Connection nc = Nats.connect(ts.getURI())) {
            Dispatcher d = nc.createDispatcher((msg) -> {});
            d.unsubscribe("");
            assertFalse(true);
        }
    }

    @Test(expected=IllegalStateException.class)
    public void testThrowOnUnsubWhenClosed() throws IOException, InterruptedException, TimeoutException {
        try (NatsTestServer ts = new NatsTestServer(false);
                    Connection nc = Nats.connect(ts.getURI())) {
            Dispatcher d = nc.createDispatcher((msg) -> {});
            Subscription sub = d.subscribe("subject", (msg) -> {});
            nc.closeDispatcher(d);
            d.unsubscribe(sub);
            assertFalse(true);
        }
    }

    @Test(expected=IllegalStateException.class)
    public void testThrowOnWrongSubscription() throws IOException, InterruptedException, TimeoutException {
        try (NatsTestServer ts = new NatsTestServer(false);
                    Connection nc = Nats.connect(ts.getURI())) {
            Dispatcher d = nc.createDispatcher((msg) -> {});
            Subscription sub2 = nc.subscribe("test");
            d.unsubscribe(sub2);
            assertFalse(true);
        }
    }
}