package com.hosopy.actioncable;

import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.hosopy.actioncable.annotation.Data;
import com.hosopy.actioncable.annotation.Perform;
import com.squareup.okhttp.Response;
import com.squareup.okhttp.mockwebserver.MockResponse;
import com.squareup.okhttp.mockwebserver.MockWebServer;
import com.squareup.okhttp.ws.WebSocket;
import com.squareup.okhttp.ws.WebSocketListener;
import okio.Buffer;
import okio.BufferedSource;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;

import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;

import static org.hamcrest.CoreMatchers.*;
import static org.junit.Assert.assertThat;

@RunWith(JUnit4.class)
public class SubscriptionTest {

    private static final int TIMEOUT = 10000;

    @Test
    public void getIdentifierByDefaultInterface() throws URISyntaxException {
        final Consumer consumer = new Consumer(new URI("ws://example.com:28080"));
        final Channel channel = new Channel("CommentsChannel");
        final Subscription subscription = consumer.getSubscriptions().create(channel);

        assertThat(subscription.getIdentifier(), is(channel.toIdentifier()));
    }

    @Test
    public void getIdentifierByCustomInterface() throws URISyntaxException {
        final Consumer consumer = new Consumer(new URI("ws://example.com:28080"));
        final Channel channel = new Channel("CommentsChannel");
        final Subscription subscription = consumer.getSubscriptions().create(channel, CustomSubscription.class);

        assertThat(subscription.getIdentifier(), is(channel.toIdentifier()));
    }

    @Test
    public void onConnectedByDefaultInterface() throws URISyntaxException, InterruptedException {
        final BlockingQueue<String> events = new LinkedBlockingQueue<String>();

        final Consumer consumer = new Consumer(new URI("ws://example.com:28080"));
        final Channel channel = new Channel("CommentsChannel");
        final Subscription subscription = consumer.getSubscriptions().create(channel);

        final Subscription returned = subscription.onConnected(new Subscription.ConnectedCallback() {
            @Override
            public void call() {
                events.offer("onConnected");
            }
        });
        assertThat(returned, is(theInstance(subscription)));

        consumer.getSubscriptions().notifyConnected(subscription.getIdentifier());

        assertThat(events.take(), is("onConnected"));
    }

    @Test
    public void onConnectedByCustomInterface() throws URISyntaxException, InterruptedException {
        final BlockingQueue<String> events = new LinkedBlockingQueue<String>();

        final Consumer consumer = new Consumer(new URI("ws://example.com:28080"));
        final Channel channel = new Channel("CommentsChannel");
        final Subscription subscription = consumer.getSubscriptions().create(channel, CustomSubscription.class);

        final Subscription returned = subscription.onConnected(new Subscription.ConnectedCallback() {
            @Override
            public void call() {
                events.offer("onConnected");
            }
        });
        assertThat(returned, is(theInstance(subscription)));

        consumer.getSubscriptions().notifyConnected(subscription.getIdentifier());

        assertThat(events.take(), is("onConnected"));
    }

    @Test
    public void onDisconnectedByDefaultInterface() throws URISyntaxException, InterruptedException {
        final BlockingQueue<String> events = new LinkedBlockingQueue<String>();

        final Consumer consumer = new Consumer(new URI("ws://example.com:28080"));
        final Channel channel = new Channel("CommentsChannel");
        final Subscription subscription = consumer.getSubscriptions().create(channel);

        final Subscription returned = subscription.onDisconnected(new Subscription.DisconnectedCallback() {
            @Override
            public void call() {
                events.offer("onDisconnected");
            }
        });
        assertThat(returned, is(theInstance(subscription)));

        consumer.getSubscriptions().notifyDisconnected();

        assertThat(events.take(), is("onDisconnected"));
    }

    @Test
    public void onDisconnectedByCustomInterface() throws URISyntaxException, InterruptedException {
        final BlockingQueue<String> events = new LinkedBlockingQueue<String>();

        final Consumer consumer = new Consumer(new URI("ws://example.com:28080"));
        final Channel channel = new Channel("CommentsChannel");
        final Subscription subscription = consumer.getSubscriptions().create(channel, CustomSubscription.class);

        final Subscription returned = subscription.onDisconnected(new Subscription.DisconnectedCallback() {
            @Override
            public void call() {
                events.offer("onDisconnected");
            }
        });
        assertThat(returned, is(theInstance(subscription)));

        consumer.getSubscriptions().notifyDisconnected();

        assertThat(events.take(), is("onDisconnected"));
    }

    @Test
    public void onRejectedByDefaultInterface() throws URISyntaxException, InterruptedException {
        final BlockingQueue<String> events = new LinkedBlockingQueue<String>();

        final Consumer consumer = new Consumer(new URI("ws://example.com:28080"));
        final Channel channel = new Channel("CommentsChannel");
        final Subscription subscription = consumer.getSubscriptions().create(channel);

        final Subscription returned = subscription.onRejected(new Subscription.RejectedCallback() {
            @Override
            public void call() {
                events.offer("onRejected");
            }
        });
        assertThat(returned, is(theInstance(subscription)));

        consumer.getSubscriptions().reject(subscription.getIdentifier());

        assertThat(events.take(), is("onRejected"));
    }

    @Test
    public void onRejectedByCustomInterface() throws URISyntaxException, InterruptedException {
        final BlockingQueue<String> events = new LinkedBlockingQueue<String>();

        final Consumer consumer = new Consumer(new URI("ws://example.com:28080"));
        final Channel channel = new Channel("CommentsChannel");
        final Subscription subscription = consumer.getSubscriptions().create(channel, CustomSubscription.class);

        final Subscription returned = subscription.onRejected(new Subscription.RejectedCallback() {
            @Override
            public void call() {
                events.offer("onRejected");
            }
        });
        assertThat(returned, is(theInstance(subscription)));

        consumer.getSubscriptions().reject(subscription.getIdentifier());

        assertThat(events.take(), is("onRejected"));
    }

    @Test
    public void onReceivedByDefaultInterface() throws URISyntaxException, InterruptedException {
        final BlockingQueue<String> events = new LinkedBlockingQueue<String>();

        final Consumer consumer = new Consumer(new URI("ws://example.com:28080"));
        final Channel channel = new Channel("CommentsChannel");
        final Subscription subscription = consumer.getSubscriptions().create(channel);

        final Subscription returned = subscription.onReceived(new Subscription.ReceivedCallback() {
            @Override
            public void call(JsonElement data) {
                events.offer("onReceived:" + data.toString());
            }
        });
        assertThat(returned, is(theInstance(subscription)));

        final JsonObject data = new JsonObject();
        data.addProperty("foo", "bar");
        consumer.getSubscriptions().notifyReceived(subscription.getIdentifier(), data);

        assertThat(events.take(), is("onReceived:" + data.toString()));
    }

    @Test
    public void onReceivedByCustomInterface() throws URISyntaxException, InterruptedException {
        final BlockingQueue<String> events = new LinkedBlockingQueue<String>();

        final Consumer consumer = new Consumer(new URI("ws://example.com:28080"));
        final Channel channel = new Channel("CommentsChannel");
        final Subscription subscription = consumer.getSubscriptions().create(channel, CustomSubscription.class);

        final Subscription returned = subscription.onReceived(new Subscription.ReceivedCallback() {
            @Override
            public void call(JsonElement data) {
                events.offer("onReceived:" + data.toString());
            }
        });
        assertThat(returned, is(theInstance(subscription)));

        final JsonObject data = new JsonObject();
        data.addProperty("foo", "bar");
        consumer.getSubscriptions().notifyReceived(subscription.getIdentifier(), data);

        assertThat(events.take(), is("onReceived:" + data.toString()));
    }

    @Test
    public void onFailedByDefaultInterface() throws URISyntaxException, InterruptedException {
        final BlockingQueue<String> events = new LinkedBlockingQueue<String>();

        final Consumer consumer = new Consumer(new URI("ws://example.com:28080"));
        final Channel channel = new Channel("CommentsChannel");
        final Subscription subscription = consumer.getSubscriptions().create(channel);

        final Subscription returned = subscription.onFailed(new Subscription.FailedCallback() {
            @Override
            public void call(ActionCableException e) {
                events.offer("onFailed:" + e.getMessage());
            }
        });
        assertThat(returned, is(theInstance(subscription)));

        final ActionCableException e = new ActionCableException(new Exception("error"));
        consumer.getSubscriptions().notifyFailed(e);

        assertThat(events.take(), is("onFailed:" + e.getMessage()));
    }

    @Test
    public void onFailedByCustomInterface() throws URISyntaxException, InterruptedException {
        final BlockingQueue<String> events = new LinkedBlockingQueue<String>();

        final Consumer consumer = new Consumer(new URI("ws://example.com:28080"));
        final Channel channel = new Channel("CommentsChannel");
        final Subscription subscription = consumer.getSubscriptions().create(channel);

        final Subscription returned = subscription.onFailed(new Subscription.FailedCallback() {
            @Override
            public void call(ActionCableException e) {
                events.offer("onFailed:" + e.getMessage());
            }
        });
        assertThat(returned, is(theInstance(subscription)));

        final ActionCableException e = new ActionCableException(new Exception("error"));
        consumer.getSubscriptions().notifyFailed(e);

        assertThat(events.take(), is("onFailed:" + e.getMessage()));
    }

    @Test(timeout = TIMEOUT)
    public void performWithDataByDefaultInterface() throws URISyntaxException, InterruptedException, IOException {
        final BlockingQueue<String> events = new LinkedBlockingQueue<String>();

        final MockWebServer mockWebServer = new MockWebServer();
        final MockResponse response = new MockResponse();
        response.withWebSocketUpgrade(new DefaultWebSocketListener() {
            @Override
            public void onMessage(BufferedSource payload, WebSocket.PayloadType type) throws IOException {
                events.offer(payload.readUtf8());
                payload.close();
            }
        });
        mockWebServer.enqueue(response);
        mockWebServer.start();

        final Consumer consumer = new Consumer(mockWebServer.url("/").uri());
        final Subscription subscription = consumer.getSubscriptions().create(new Channel("CommentsChannel"));
        consumer.connect();

        events.take(); // { command: subscribe }

        final JsonObject data = new JsonObject();
        data.addProperty("foo", "bar");
        subscription.perform("follow", data);

        final JsonObject expected = new JsonObject();
        expected.addProperty("command", "message");
        expected.addProperty("identifier", subscription.getIdentifier());
        expected.addProperty("data", data.toString());
        assertThat(events.take(), is(expected.toString()));

        mockWebServer.shutdown();
    }

    @Test(timeout = TIMEOUT)
    public void performWithDataByCustomInterface() throws URISyntaxException, InterruptedException, IOException {
        final BlockingQueue<String> events = new LinkedBlockingQueue<String>();

        final MockWebServer mockWebServer = new MockWebServer();
        final MockResponse response = new MockResponse();
        response.withWebSocketUpgrade(new DefaultWebSocketListener() {
            @Override
            public void onMessage(BufferedSource payload, WebSocket.PayloadType type) throws IOException {
                events.offer(payload.readUtf8());
                payload.close();
            }
        });
        mockWebServer.enqueue(response);
        mockWebServer.start();

        final Consumer consumer = new Consumer(mockWebServer.url("/").uri());
        final Subscription subscription = consumer.getSubscriptions().create(new Channel("CommentsChannel"), CustomSubscription.class);
        consumer.connect();

        events.take(); // { command: subscribe }

        final JsonObject data = new JsonObject();
        data.addProperty("foo", "bar");
        subscription.perform("follow", data);

        final JsonObject expected = new JsonObject();
        expected.addProperty("command", "message");
        expected.addProperty("identifier", subscription.getIdentifier());
        expected.addProperty("data", data.toString());
        assertThat(events.take(), is(expected.toString()));

        mockWebServer.shutdown();
    }

    @Test(timeout = TIMEOUT)
    public void performByDefaultInterface() throws URISyntaxException, InterruptedException, IOException {
        final BlockingQueue<String> events = new LinkedBlockingQueue<String>();

        final MockWebServer mockWebServer = new MockWebServer();
        final MockResponse response = new MockResponse();
        response.withWebSocketUpgrade(new DefaultWebSocketListener() {
            @Override
            public void onMessage(BufferedSource payload, WebSocket.PayloadType type) throws IOException {
                events.offer(payload.readUtf8());
                payload.close();
            }
        });
        mockWebServer.enqueue(response);
        mockWebServer.start();

        final Consumer consumer = new Consumer(mockWebServer.url("/").uri());
        final Subscription subscription = consumer.getSubscriptions().create(new Channel("CommentsChannel"));
        consumer.connect();

        events.take(); // { command: subscribe }

        subscription.perform("follow");

        final JsonObject expected = new JsonObject();
        expected.addProperty("command", "message");
        expected.addProperty("identifier", subscription.getIdentifier());
        expected.addProperty("data", "{\"action\":\"follow\"}");
        assertThat(events.take(), is(expected.toString()));

        mockWebServer.shutdown();
    }

    @Test(timeout = TIMEOUT)
    public void performByCustomInterface() throws URISyntaxException, InterruptedException, IOException {
        final BlockingQueue<String> events = new LinkedBlockingQueue<String>();

        final MockWebServer mockWebServer = new MockWebServer();
        final MockResponse response = new MockResponse();
        response.withWebSocketUpgrade(new DefaultWebSocketListener() {
            @Override
            public void onMessage(BufferedSource payload, WebSocket.PayloadType type) throws IOException {
                events.offer(payload.readUtf8());
                payload.close();
            }
        });
        mockWebServer.enqueue(response);
        mockWebServer.start();

        final Consumer consumer = new Consumer(mockWebServer.url("/").uri());
        final Subscription subscription = consumer.getSubscriptions().create(new Channel("CommentsChannel"), CustomSubscription.class);
        consumer.connect();

        events.take(); // { command: subscribe }

        subscription.perform("follow");

        final JsonObject expected = new JsonObject();
        expected.addProperty("command", "message");
        expected.addProperty("identifier", subscription.getIdentifier());
        expected.addProperty("data", "{\"action\":\"follow\"}");
        assertThat(events.take(), is(expected.toString()));

        mockWebServer.shutdown();
    }

    @Test(timeout = TIMEOUT)
    public void performByCustomInterfaceMethod() throws URISyntaxException, InterruptedException, IOException {
        final BlockingQueue<String> events = new LinkedBlockingQueue<String>();

        final MockWebServer mockWebServer = new MockWebServer();
        final MockResponse response = new MockResponse();
        response.withWebSocketUpgrade(new DefaultWebSocketListener() {
            @Override
            public void onMessage(BufferedSource payload, WebSocket.PayloadType type) throws IOException {
                events.offer(payload.readUtf8());
                payload.close();
            }
        });
        mockWebServer.enqueue(response);
        mockWebServer.start();

        final Consumer consumer = new Consumer(mockWebServer.url("/").uri());
        final CustomSubscription subscription = consumer.getSubscriptions().create(new Channel("CommentsChannel"), CustomSubscription.class);
        consumer.connect();

        events.take(); // { command: subscribe }

        subscription.touch();

        JsonObject expected = new JsonObject();
        expected.addProperty("command", "message");
        expected.addProperty("identifier", subscription.getIdentifier());
        expected.addProperty("data", "{\"action\":\"touch\"}");
        assertThat(events.take(), is(expected.toString()));

        subscription.follow(1);
        expected = new JsonObject();
        expected.addProperty("command", "message");
        expected.addProperty("identifier", subscription.getIdentifier());
        expected.addProperty("data", "{\"user_id\":1,\"action\":\"follow\"}");
        assertThat(events.take(), is(expected.toString()));

        subscription.send("My name is");
        expected = new JsonObject();
        expected.addProperty("command", "message");
        expected.addProperty("identifier", subscription.getIdentifier());
        expected.addProperty("data", "{\"body\":\"My name is\",\"action\":\"send\"}");
        assertThat(events.take(), is(expected.toString()));

        subscription.setPrivate(true);
        expected = new JsonObject();
        expected.addProperty("command", "message");
        expected.addProperty("identifier", subscription.getIdentifier());
        expected.addProperty("data", "{\"private\":true,\"action\":\"set_private\"}");
        assertThat(events.take(), is(expected.toString()));

        final JsonObject params = new JsonObject();
        params.addProperty("foo", "bar");
        subscription.save(1, params);
        expected = new JsonObject();
        expected.addProperty("command", "message");
        expected.addProperty("identifier", subscription.getIdentifier());
        expected.addProperty("data", "{\"item_id\":1,\"params\":{\"foo\":\"bar\"},\"action\":\"save\"}");
        assertThat(events.take(), is(expected.toString()));

        mockWebServer.shutdown();
    }

    @Test(expected = IllegalArgumentException.class)
    public void performParametersMustBeAnnotated() throws URISyntaxException, InterruptedException, IOException {
        final Consumer consumer = new Consumer(new URI("ws://example.com:28080"));
        final Channel channel = new Channel("CommentsChannel");
        final NotAnnotatedParameterSubscription subscription = consumer.getSubscriptions().create(
                channel, NotAnnotatedParameterSubscription.class);

        subscription.save(1, "title");
    }

    @Test(expected = IllegalArgumentException.class)
    public void performParametersMustBeSupportedType() throws URISyntaxException, InterruptedException, IOException {
        final Consumer consumer = new Consumer(new URI("ws://example.com:28080"));
        final Channel channel = new Channel("CommentsChannel");
        final InvalidParameterTypeSubscription subscription = consumer.getSubscriptions().create(
                channel, InvalidParameterTypeSubscription.class);

        final ActionCableException e = new ActionCableException(new Exception("error"));
        consumer.getSubscriptions().notifyFailed(e);

        subscription.save(1, new HashMap<String, String>());
    }

    private interface CustomSubscription extends Subscription {
        @Perform("touch")
        void touch();

        @Perform("follow")
        void follow(@Data("user_id") int userId);

        @Perform("send")
        void send(@Data("body") String body);

        @Perform("set_private")
        void setPrivate(@Data("private") boolean isPrivate);

        @Perform("save")
        void save(@Data("item_id") int itemId, @Data("params") JsonElement params);
    }

    private interface NotAnnotatedParameterSubscription extends Subscription {
        @Perform("save")
        void save(@Data("item_id") int itemId, String title);
    }

    private interface InvalidParameterTypeSubscription extends Subscription {
        @Perform("save")
        void save(@Data("item_id") int itemId, @Data("params") Map<String, String> params);
    }

    private static class DefaultWebSocketListener implements WebSocketListener {

        @Override
        public void onOpen(WebSocket webSocket, Response response) {
        }

        @Override
        public void onFailure(IOException e, Response response) {
        }

        @Override
        public void onMessage(BufferedSource payload, WebSocket.PayloadType type) throws IOException {
            payload.close();
        }

        @Override
        public void onPong(Buffer payload) {
        }

        @Override
        public void onClose(int code, String reason) {
        }
    }
}