// 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;

import static org.junit.Assert.assertArrayEquals;
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.net.URI;
import java.net.URISyntaxException;
import java.nio.charset.StandardCharsets;
import java.security.NoSuchAlgorithmException;
import java.time.Duration;
import java.util.Base64;
import java.util.Collection;
import java.util.Properties;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;

import javax.net.ssl.SSLContext;

import org.junit.Test;

import io.nats.client.ConnectionListener.Events;
import io.nats.client.impl.DataPort;
import io.nats.client.utils.CloseOnUpgradeAttempt;

public class OptionsTests {
    @Test
    public void testDefaultOptions() {
        Options o = new Options.Builder().build();

        assertEquals("default one server", 1, o.getServers().size());
        assertEquals("default url", Options.DEFAULT_URL, o.getServers().toArray()[0].toString());

        assertEquals("default data port type", Options.DEFAULT_DATA_PORT_TYPE, o.getDataPortType());

        assertEquals("default verbose", false, o.isVerbose());
        assertEquals("default pedantic", false, o.isPedantic());
        assertEquals("default norandomize", false, o.isNoRandomize());
        assertEquals("default oldstyle", false, o.isOldRequestStyle());
        assertEquals("default noEcho", false, o.isNoEcho());
        assertEquals("default UTF8 Support", false, o.supportUTF8Subjects());
        assertEquals("default discard messages when outgoing queue full", Options.DEFAULT_DISCARD_MESSAGES_WHEN_OUTGOING_QUEUE_FULL,
                o.isDiscardMessagesWhenOutgoingQueueFull());

        assertNull("default username", o.getUsernameChars());
        assertNull("default password", o.getPasswordChars());
        assertNull("default token", o.getTokenChars());
        assertNull("default connection name", o.getConnectionName());

        assertNull("default ssl context", o.getSslContext());

        assertEquals("default max reconnect", Options.DEFAULT_MAX_RECONNECT, o.getMaxReconnect());
        assertEquals("default ping max", Options.DEFAULT_MAX_PINGS_OUT, o.getMaxPingsOut());
        assertEquals("default reconnect buffer size", Options.DEFAULT_RECONNECT_BUF_SIZE, o.getReconnectBufferSize());
        assertEquals("default max messages in outgoing queue", Options.DEFAULT_MAX_MESSAGES_IN_OUTGOING_QUEUE,
                o.getMaxMessagesInOutgoingQueue());

        assertEquals("default reconnect wait", Options.DEFAULT_RECONNECT_WAIT, o.getReconnectWait());
        assertEquals("default connection timeout", Options.DEFAULT_CONNECTION_TIMEOUT, o.getConnectionTimeout());
        assertEquals("default ping interval", Options.DEFAULT_PING_INTERVAL, o.getPingInterval());
        assertEquals("default cleanup interval", Options.DEFAULT_REQUEST_CLEANUP_INTERVAL,
                o.getRequestCleanupInterval());

        assertNull("error handler", o.getErrorListener());
        assertNull("disconnect handler", o.getConnectionListener());
    }

    @Test
    public void testChainedBooleanOptions() throws NoSuchAlgorithmException {
        Options o = new Options.Builder().verbose().pedantic().noRandomize().supportUTF8Subjects().noEcho().oldRequestStyle()
                .discardMessagesWhenOutgoingQueueFull()
                .build();
        assertNull("default username", o.getUsernameChars());
        assertEquals("chained verbose", true, o.isVerbose());
        assertEquals("chained pedantic", true, o.isPedantic());
        assertEquals("chained norandomize", true, o.isNoRandomize());
        assertEquals("chained oldstyle", true, o.isOldRequestStyle());
        assertEquals("chained noecho", true, o.isNoEcho());
        assertEquals("chained utf8", true, o.supportUTF8Subjects());
        assertEquals("chained discard messages when outgoing queue full", true, o.isDiscardMessagesWhenOutgoingQueueFull());
    }

    @Test
    public void testChainedStringOptions() throws NoSuchAlgorithmException {
        Options o = new Options.Builder().userInfo("hello".toCharArray(), "world".toCharArray()).connectionName("name").build();
        assertEquals("default verbose", false, o.isVerbose()); // One from a different type
        assertArrayEquals("chained username", "hello".toCharArray(), o.getUsernameChars());
        assertArrayEquals("chained password", "world".toCharArray(), o.getPasswordChars());
        assertEquals("chained connection name", "name", o.getConnectionName());
    }

    @Test
    public void testChainedSecure() throws Exception {
        SSLContext ctx = TestSSLUtils.createTestSSLContext();
        SSLContext.setDefault(ctx);
        Options o = new Options.Builder().secure().build();
        assertEquals("chained context", ctx, o.getSslContext());
    }

    @Test
    public void testChainedSSLOptions() throws Exception {
        SSLContext ctx = TestSSLUtils.createTestSSLContext();
        Options o = new Options.Builder().sslContext(ctx).build();
        assertEquals("default verbose", false, o.isVerbose()); // One from a different type
        assertEquals("chained context", ctx, o.getSslContext());
    }

    @Test
    public void testChainedIntOptions() {
        Options o = new Options.Builder().maxReconnects(100).maxPingsOut(200).reconnectBufferSize(300)
                .maxControlLine(400)
                .maxMessagesInOutgoingQueue(500)
                .build();
        assertEquals("default verbose", false, o.isVerbose()); // One from a different type
        assertEquals("chained max reconnect", 100, o.getMaxReconnect());
        assertEquals("chained ping max", 200, o.getMaxPingsOut());
        assertEquals("chained reconnect buffer size", 300, o.getReconnectBufferSize());
        assertEquals("chained max control line", 400, o.getMaxControlLine());
        assertEquals("chained max messages in outgoing queue", 500, o.getMaxMessagesInOutgoingQueue());
    }

    @Test
    public void testChainedDurationOptions() {
        Options o = new Options.Builder().reconnectWait(Duration.ofMillis(101))
                .connectionTimeout(Duration.ofMillis(202)).pingInterval(Duration.ofMillis(303))
                .requestCleanupInterval(Duration.ofMillis(404)).build();
        assertEquals("default verbose", false, o.isVerbose()); // One from a different type
        assertEquals("chained reconnect wait", Duration.ofMillis(101), o.getReconnectWait());
        assertEquals("chained connection timeout", Duration.ofMillis(202), o.getConnectionTimeout());
        assertEquals("chained ping interval", Duration.ofMillis(303), o.getPingInterval());
        assertEquals("chained cleanup interval", Duration.ofMillis(404), o.getRequestCleanupInterval());
    }

    @Test
    public void testChainedErrorHandler() {
        TestHandler handler = new TestHandler();
        Options o = new Options.Builder().errorListener(handler).build();
        assertEquals("default verbose", false, o.isVerbose()); // One from a different type
        assertEquals("chained error handler", handler, o.getErrorListener());
    }

    @Test
    public void testChainedConnectionListener() {
        ConnectionListener cHandler = (c, e) -> System.out.println("connection event" + e);
        Options o = new Options.Builder().connectionListener(cHandler).build();
        assertEquals("default verbose", false, o.isVerbose()); // One from a different type
        assertNull("error handler", o.getErrorListener());
        assertTrue("chained connection handler", cHandler == o.getConnectionListener());
    }

    @Test
    public void testPropertiesBooleanBuilder() {
        Properties props = new Properties();
        props.setProperty(Options.PROP_VERBOSE, "true");
        props.setProperty(Options.PROP_PEDANTIC, "true");
        props.setProperty(Options.PROP_NORANDOMIZE, "true");
        props.setProperty(Options.PROP_USE_OLD_REQUEST_STYLE, "true");
        props.setProperty(Options.PROP_OPENTLS, "true");
        props.setProperty(Options.PROP_NO_ECHO, "true");
        props.setProperty(Options.PROP_UTF8_SUBJECTS, "true");
        props.setProperty(Options.PROP_DISCARD_MESSAGES_WHEN_OUTGOING_QUEUE_FULL, "true");

        Options o = new Options.Builder(props).build();
        assertNull("default username chars", o.getUsernameChars());
        assertEquals("property verbose", true, o.isVerbose());
        assertEquals("property pedantic", true, o.isPedantic());
        assertEquals("property norandomize", true, o.isNoRandomize());
        assertEquals("property oldstyle", true, o.isOldRequestStyle());
        assertEquals("property noecho", true, o.isNoEcho());
        assertEquals("property utf8", true, o.supportUTF8Subjects());
        assertEquals("property discard messages when outgoing queue full", true, o.isDiscardMessagesWhenOutgoingQueueFull());
        assertNotNull("property opentls", o.getSslContext());
    }

    @Test
    public void testPropertiesStringOptions() throws NoSuchAlgorithmException {
        Properties props = new Properties();
        props.setProperty(Options.PROP_USERNAME, "hello");
        props.setProperty(Options.PROP_PASSWORD, "world");
        props.setProperty(Options.PROP_CONNECTION_NAME, "name");

        Options o = new Options.Builder(props).build();
        assertEquals("default verbose", false, o.isVerbose()); // One from a different type
        assertArrayEquals("property username", "hello".toCharArray(), o.getUsernameChars());
        assertArrayEquals("property password", "world".toCharArray(), o.getPasswordChars());
        assertEquals("property connection name", "name", o.getConnectionName());
    }

    @Test
    public void testPropertiesSSLOptions() throws Exception {
        // don't use default for tests, issues with forcing algorithm exception in other tests break it
        SSLContext.setDefault(TestSSLUtils.createTestSSLContext());
        Properties props = new Properties();
        props.setProperty(Options.PROP_SECURE, "true");

        Options o = new Options.Builder(props).build();
        assertEquals("default verbose", false, o.isVerbose()); // One from a different type
        assertNotNull("property context", o.getSslContext());
    }

    @Test
    public void testPropertyIntOptions() {
        Properties props = new Properties();
        props.setProperty(Options.PROP_MAX_RECONNECT, "100");
        props.setProperty(Options.PROP_MAX_PINGS, "200");
        props.setProperty(Options.PROP_RECONNECT_BUF_SIZE, "300");
        props.setProperty(Options.PROP_MAX_CONTROL_LINE, "400");
        props.setProperty(Options.PROP_MAX_MESSAGES_IN_OUTGOING_QUEUE, "500");

        Options o = new Options.Builder(props).build();
        assertEquals("default verbose", false, o.isVerbose()); // One from a different type
        assertEquals("property max reconnect", 100, o.getMaxReconnect());
        assertEquals("property ping max", 200, o.getMaxPingsOut());
        assertEquals("property reconnect buffer size", 300, o.getReconnectBufferSize());
        assertEquals("property max control line", 400, o.getMaxControlLine());
        assertEquals("property max messages in outgoing queue", 500, o.getMaxMessagesInOutgoingQueue());
    }

    @Test
    public void testDefaultPropertyIntOptions() {
        Properties props = new Properties();
        props.setProperty(Options.PROP_RECONNECT_WAIT, "-1");
        props.setProperty(Options.PROP_CONNECTION_TIMEOUT, "-1");
        props.setProperty(Options.PROP_PING_INTERVAL, "-1");
        props.setProperty(Options.PROP_CLEANUP_INTERVAL, "-1");
        props.setProperty(Options.PROP_MAX_CONTROL_LINE, "-1");
        props.setProperty(Options.PROP_MAX_MESSAGES_IN_OUTGOING_QUEUE, "-1");

        Options o = new Options.Builder(props).build();
        assertEquals("default max control line", Options.DEFAULT_MAX_CONTROL_LINE, o.getMaxControlLine());
        assertEquals("default reconnect wait", Options.DEFAULT_RECONNECT_WAIT, o.getReconnectWait());
        assertEquals("default connection timeout", Options.DEFAULT_CONNECTION_TIMEOUT, o.getConnectionTimeout());
        assertEquals("default ping interval", Options.DEFAULT_PING_INTERVAL, o.getPingInterval());
        assertEquals("default cleanup interval", Options.DEFAULT_REQUEST_CLEANUP_INTERVAL,
                o.getRequestCleanupInterval());
        assertEquals("default max messages in outgoing queue", Options.DEFAULT_MAX_MESSAGES_IN_OUTGOING_QUEUE,
                o.getMaxMessagesInOutgoingQueue());
    }

    @Test
    public void testPropertyDurationOptions() {
        Properties props = new Properties();
        props.setProperty(Options.PROP_RECONNECT_WAIT, "101");
        props.setProperty(Options.PROP_CONNECTION_TIMEOUT, "202");
        props.setProperty(Options.PROP_PING_INTERVAL, "303");
        props.setProperty(Options.PROP_CLEANUP_INTERVAL, "404");

        Options o = new Options.Builder(props).build();
        assertEquals("default verbose", false, o.isVerbose()); // One from a different type
        assertEquals("property reconnect wait", Duration.ofMillis(101), o.getReconnectWait());
        assertEquals("property connection timeout", Duration.ofMillis(202), o.getConnectionTimeout());
        assertEquals("property ping interval", Duration.ofMillis(303), o.getPingInterval());
        assertEquals("property cleanup interval", Duration.ofMillis(404), o.getRequestCleanupInterval());
    }

    @Test
    public void testPropertyErrorHandler() {
        Properties props = new Properties();
        props.setProperty(Options.PROP_ERROR_LISTENER, TestHandler.class.getCanonicalName());

        Options o = new Options.Builder(props).build();
        assertEquals("default verbose", false, o.isVerbose()); // One from a different type
        assertNotNull("property error handler", o.getErrorListener());

        o.getErrorListener().errorOccurred(null, "bad subject");
        assertEquals("property error handler class", ((TestHandler) o.getErrorListener()).getCount(), 1);
    }

    @Test
    public void testPropertyConnectionListeners() {
        Properties props = new Properties();
        props.setProperty(Options.PROP_CONNECTION_CB, TestHandler.class.getCanonicalName());

        Options o = new Options.Builder(props).build();
        assertEquals("default verbose", false, o.isVerbose()); // One from a different type
        assertNotNull("property connection handler", o.getConnectionListener());

        o.getConnectionListener().connectionEvent(null, Events.DISCONNECTED);
        o.getConnectionListener().connectionEvent(null, Events.RECONNECTED);
        o.getConnectionListener().connectionEvent(null, Events.CLOSED);

        assertEquals("property connect handler class", ((TestHandler) o.getConnectionListener()).getCount(), 3);
    }

    @Test
    public void testChainOverridesProperties() throws NoSuchAlgorithmException {
        Properties props = new Properties();
        props.setProperty(Options.PROP_TOKEN, "token");
        props.setProperty(Options.PROP_CONNECTION_NAME, "name");

        Options o = new Options.Builder(props).connectionName("newname").build();
        assertEquals("default verbose", false, o.isVerbose()); // One from a different type
        assertArrayEquals("property token", "token".toCharArray(), o.getTokenChars());
        assertEquals("property connection name", "newname", o.getConnectionName());
    }

    @Test
    public void testDefaultConnectOptions() {
        Options o = new Options.Builder().build();
        String expected = "{\"lang\":\"java\",\"version\":\"" + Nats.CLIENT_VERSION + "\""
                + ",\"protocol\":1,\"verbose\":false,\"pedantic\":false,\"tls_required\":false,\"echo\":true}";
        assertEquals("default connect options", expected, o.buildProtocolConnectOptionsString("nats://localhost:4222", false, null).toString());
    }

    @Test
    public void testConnectOptionsWithNameAndContext() throws Exception {
        SSLContext ctx = TestSSLUtils.createTestSSLContext();
        Options o = new Options.Builder().sslContext(ctx).connectionName("c1").build();
        String expected = "{\"lang\":\"java\",\"version\":\"" + Nats.CLIENT_VERSION + "\",\"name\":\"c1\""
                + ",\"protocol\":1,\"verbose\":false,\"pedantic\":false,\"tls_required\":true,\"echo\":true}";
        assertEquals("default connect options", expected, o.buildProtocolConnectOptionsString("nats://localhost:4222", false, null).toString());
    }

    @Test
    public void testAuthConnectOptions() {
        Options o = new Options.Builder().userInfo("hello".toCharArray(), "world".toCharArray()).build();
        String expectedNoAuth = "{\"lang\":\"java\",\"version\":\"" + Nats.CLIENT_VERSION + "\""
                + ",\"protocol\":1,\"verbose\":false,\"pedantic\":false,\"tls_required\":false,\"echo\":true}";
        String expectedWithAuth = "{\"lang\":\"java\",\"version\":\"" + Nats.CLIENT_VERSION + "\""
                + ",\"protocol\":1,\"verbose\":false,\"pedantic\":false,\"tls_required\":false,\"echo\":true"
                + ",\"user\":\"hello\",\"pass\":\"world\"}";
        assertEquals("no auth connect options", expectedNoAuth, o.buildProtocolConnectOptionsString("nats://localhost:4222", false, null).toString());
        assertEquals("auth connect options", expectedWithAuth, o.buildProtocolConnectOptionsString("nats://localhost:4222", true, null).toString());
    }

    @Test
    public void testNKeyConnectOptions() throws Exception {
        TestAuthHandler th = new TestAuthHandler();
        byte[] nonce = "abcdefg".getBytes(StandardCharsets.UTF_8);
        String sig = Base64.getUrlEncoder().withoutPadding().encodeToString(th.sign(nonce));

        Options o = new Options.Builder().authHandler(th).build();
        String expectedNoAuth = "{\"lang\":\"java\",\"version\":\"" + Nats.CLIENT_VERSION + "\""
                + ",\"protocol\":1,\"verbose\":false,\"pedantic\":false,\"tls_required\":false,\"echo\":true}";
        String expectedWithAuth = "{\"lang\":\"java\",\"version\":\"" + Nats.CLIENT_VERSION + "\""
                + ",\"protocol\":1,\"verbose\":false,\"pedantic\":false,\"tls_required\":false,\"echo\":true"
                + ",\"nkey\":\""+new String(th.getID())+"\",\"sig\":\""+sig+"\",\"jwt\":\"\"}";
        assertEquals("no auth connect options", expectedNoAuth, o.buildProtocolConnectOptionsString("nats://localhost:4222", false, nonce).toString());
        assertEquals("auth connect options", expectedWithAuth, o.buildProtocolConnectOptionsString("nats://localhost:4222", true, nonce).toString());
    }

    @Test
    public void testDefaultDataPort() {
        Options o = new Options.Builder().build();
        DataPort dataPort = o.buildDataPort();

        assertNotNull(dataPort);
        assertEquals("default dataPort", Options.DEFAULT_DATA_PORT_TYPE, dataPort.getClass().getCanonicalName());
    }

    @Test
    public void testPropertyDataPortType() {
        Properties props = new Properties();
        props.setProperty(Options.PROP_DATA_PORT_TYPE, CloseOnUpgradeAttempt.class.getCanonicalName());

        Options o = new Options.Builder(props).build();
        assertEquals("default verbose", false, o.isVerbose()); // One from a different type

        assertEquals("property data port class", CloseOnUpgradeAttempt.class.getCanonicalName(),
                o.buildDataPort().getClass().getCanonicalName());
    }

    @Test
    public void testUserPassInURL() {
        String serverURI = "nats://derek:[email protected]:2222";
        Options o = new Options.Builder().server(serverURI).build();

        String connectString = o.buildProtocolConnectOptionsString(serverURI, true, null).toString();
        assertTrue(connectString.contains("\"user\":\"derek\""));
        assertTrue(connectString.contains("\"pass\":\"password\""));
        assertFalse(connectString.contains("\"token\":"));
    }

    @Test
    public void testTokenInURL() {
        String serverURI = "nats://[email protected]:2222";
        Options o = new Options.Builder().server(serverURI).build();

        String connectString = o.buildProtocolConnectOptionsString(serverURI, true, null).toString();
        assertTrue(connectString.contains("\"auth_token\":\"alberto\""));
        assertFalse(connectString.contains("\"user\":"));
        assertFalse(connectString.contains("\"pass\":"));
    }

    @Test(expected=IllegalArgumentException.class)
    public void testThrowOnNoProps() throws NoSuchAlgorithmException {
        new Options.Builder(null);
        assertFalse(true);
    }

    @Test
    public void testServerInProperties() {
        Properties props = new Properties();
        String url = "nats://localhost:8080";
        props.setProperty(Options.PROP_URL, url);

        Options o = new Options.Builder(props).build();
        Collection<URI> servers = o.getServers();
        URI[] serverArray = servers.toArray(new URI[0]);
        assertEquals(1, serverArray.length);
        assertEquals("property server", url, serverArray[0].toString());
    }

    @Test
    public void testServersInProperties() {
        Properties props = new Properties();
        String url1 = "nats://localhost:8080";
        String url2 = "nats://localhost:8081";
        String urls = url1 + ", " + url2;
        props.setProperty(Options.PROP_SERVERS, urls);

        Options o = new Options.Builder(props).build();
        Collection<URI> servers = o.getServers();
        URI[] serverArray = servers.toArray(new URI[0]);
        assertEquals(2, serverArray.length);
        assertEquals("property server", url1, serverArray[0].toString());
        assertEquals("property server", url2, serverArray[1].toString());
    }

    @Test
    public void testServers() {
        String url1 = "nats://localhost:8080";
        String url2 = "nats://localhost:8081";
        String[] serverUrls = {url1, url2};
        Options o = new Options.Builder().servers(serverUrls).build();

        Collection<URI> servers = o.getServers();
        URI[] serverArray = servers.toArray(new URI[0]);
        assertEquals(2, serverArray.length);
        assertEquals("property server", url1, serverArray[0].toString());
        assertEquals("property server", url2, serverArray[1].toString());
    }

    @Test
    public void testServersWithCommas() {
        String url1 = "nats://localhost:8080";
        String url2 = "nats://localhost:8081";
        String serverURLs = url1 + "," + url2;
        Options o = new Options.Builder().server(serverURLs).build();

        Collection<URI> servers = o.getServers();
        URI[] serverArray = servers.toArray(new URI[0]);
        assertEquals(2, serverArray.length);
        assertEquals("property server", url1, serverArray[0].toString());
        assertEquals("property server", url2, serverArray[1].toString());
    }

    @Test
    public void testEmptyStringInServers() {
        String url1 = "nats://localhost:8080";
        String url2 = "";
        String[] serverUrls = {url1, url2};
        Options o = new Options.Builder().servers(serverUrls).build();

        Collection<URI> servers = o.getServers();
        URI[] serverArray = servers.toArray(new URI[0]);
        assertEquals(1, serverArray.length);
        assertEquals("property server", url1, serverArray[0].toString());
    }

    @Test(expected=IllegalArgumentException.class)
    public void testBadClassInPropertyConnectionListeners() {
        Properties props = new Properties();
        props.setProperty(Options.PROP_CONNECTION_CB, "foo");
        new Options.Builder(props);
        assertFalse(true);
    }

    @Test(expected=IllegalStateException.class)
    public void testTokenAndUserThrows() {
        new Options.Builder().token("foo".toCharArray()).userInfo("foo".toCharArray(), "bar".toCharArray()).build();
        assertFalse(true);
    }

    @Test(expected=IllegalArgumentException.class)
    public void testThrowOnBadServerURI() {
        new Options.Builder().server("foo:/bar\\:blammer").build();
        assertFalse(true);
    }

    @Test(expected=IllegalArgumentException.class)
    public void testThrowOnEmptyServersProp() {
        Properties props = new Properties();
        props.setProperty(Options.PROP_SERVERS, "");

        new Options.Builder(props).build();
        assertFalse(true);
    }

    @Test(expected=IllegalArgumentException.class)
    public void testThrowOnBadServersURI() {
        String url1 = "nats://localhost:8080";
        String url2 = "foo:/bar\\:blammer";
        String[] serverUrls = {url1, url2};
        new Options.Builder().servers(serverUrls).build();
        assertFalse(true);
    }
    
    @Test
    public void testSetExectuor() {
        ExecutorService exec = Executors.newCachedThreadPool();
        Options options = new Options.Builder().executor(exec).build();
        assertEquals(exec, options.getExecutor());
    }
    
    @Test
    public void testDefaultExecutor() throws Exception {
        Options options = new Options.Builder().connectionName("test").build();
        Future<String> future = options.getExecutor().submit(new Callable<String>(){
            public String call() {
                return Thread.currentThread().getName();
            }
        });
        String name = future.get(5, TimeUnit.SECONDS);
        assertTrue(name.startsWith("test"));

        options = new Options.Builder().build();
        future = options.getExecutor().submit(new Callable<String>(){
            public String call() {
                return Thread.currentThread().getName();
            }
        });
        name = future.get(5, TimeUnit.SECONDS);
        assertTrue(name.startsWith(Options.DEFAULT_THREAD_NAME_PREFIX));
    }

    @Test
    public void testParseURIForServer() throws URISyntaxException {
        String[][] test = {
            {"nats://localhost:4222","nats://localhost:4222"},
            {"tls://localhost:4222","tls://localhost:4222"},
            {"opentls://localhost:4222","opentls://localhost:4222"},
            {"localhost:4222","nats://localhost:4222"},

            {"nats://localhost","nats://localhost:4222"},
            {"tls://localhost","tls://localhost:4222"},
            {"opentls://localhost","opentls://localhost:4222"},
            {"localhost","nats://localhost:4222"},

            {"nats://connect.nats.io:4222","nats://connect.nats.io:4222"},
            {"tls://connect.nats.io:4222","tls://connect.nats.io:4222"},
            {"opentls://connect.nats.io:4222","opentls://connect.nats.io:4222"},
            {"connect.nats.io:4222","nats://connect.nats.io:4222"},

            {"nats://connect.nats.io","nats://connect.nats.io:4222"},
            {"tls://connect.nats.io","tls://connect.nats.io:4222"},
            {"opentls://connect.nats.io","opentls://connect.nats.io:4222"},
            {"connect.nats.io","nats://connect.nats.io:4222"},
            
            {"nats://192.168.0.1:4222","nats://192.168.0.1:4222"},
            {"tls://192.168.0.1:4222","tls://192.168.0.1:4222"},
            {"opentls://192.168.0.1:4222","opentls://192.168.0.1:4222"},
            {"192.168.0.1:4222","nats://192.168.0.1:4222"},

            {"nats://192.168.0.1","nats://192.168.0.1:4222"},
            {"tls://192.168.0.1","tls://192.168.0.1:4222"},
            {"opentls://192.168.0.1","opentls://192.168.0.1:4222"},
            {"192.168.0.1","nats://192.168.0.1:4222"},
        };

        for (int i=0 ;i<test.length;i++) {
            URI actual = Options.parseURIForServer(test[i][0]);
            URI expected = new URI(test[i][1]);
            assertEquals(expected.toASCIIString(), actual.toASCIIString());
        }
    }

    @Test(expected = URISyntaxException.class)
    public void testParseBadURIForServer() throws URISyntaxException {
        Options.parseURIForServer("unk://123.1.1.1");
    }

/* These next three require that no default is set anywhere, if another test
    requires SSLContext.setDefault() and runs before these, they will fail. Commenting
    out for now, this can be run manually.

    @Test(expected=NoSuchAlgorithmException.class)
    public void testThrowOnBadContextForSecure() throws Exception {
        try {
            System.setProperty("javax.net.ssl.keyStore", "foo");
            System.setProperty("javax.net.ssl.trustStore", "bar");
            new Options.Builder().secure().build();
            assertFalse(true);
        }
        finally {
            System.clearProperty("javax.net.ssl.keyStore");
            System.clearProperty("javax.net.ssl.trustStore");
        }
    }

    @Test(expected=IllegalStateException.class)
    public void testThrowOnBadContextForTLSUrl() throws Exception {
        try {
            System.setProperty("javax.net.ssl.keyStore", "foo");
            System.setProperty("javax.net.ssl.trustStore", "bar");
            new Options.Builder().server("tls://localhost:4242").build();
            assertFalse(true);
        }
        finally {
            System.clearProperty("javax.net.ssl.keyStore");
            System.clearProperty("javax.net.ssl.trustStore");
        }
    }

    @Test(expected=IllegalArgumentException.class)
    public void testThrowOnBadContextSecureProp() {
        try {
            System.setProperty("javax.net.ssl.keyStore", "foo");
            System.setProperty("javax.net.ssl.trustStore", "bar");
            
            Properties props = new Properties();
            props.setProperty(Options.PROP_SECURE, "true");
            new Options.Builder(props).build();
            assertFalse(true);
        }
        finally {
            System.clearProperty("javax.net.ssl.keyStore");
            System.clearProperty("javax.net.ssl.trustStore");
        }
    }
    */
}