package org.jooby;

import static org.easymock.EasyMock.eq;
import static org.easymock.EasyMock.expect;
import static org.easymock.EasyMock.expectLastCall;
import static org.easymock.EasyMock.isA;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;

import java.io.IOException;
import java.nio.channels.ClosedChannelException;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Optional;
import java.util.UUID;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.Executors;

import org.jooby.internal.SseRenderer;
import org.jooby.test.MockUnit;
import org.jooby.test.MockUnit.Block;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.powermock.core.classloader.annotations.PrepareForTest;
import org.powermock.modules.junit4.PowerMockRunner;

import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Sets;
import com.google.common.util.concurrent.MoreExecutors;
import com.google.inject.Injector;
import com.google.inject.Key;
import com.google.inject.TypeLiteral;
import com.google.inject.name.Names;

import javaslang.concurrent.Promise;

@RunWith(PowerMockRunner.class)
@PrepareForTest({Sse.class, Deferred.class, Executors.class, SseRenderer.class })
public class SseTest {

  private Block handshake = unit -> {
    Request request = unit.get(Request.class);
    Injector injector = unit.get(Injector.class);
    Route route = unit.get(Route.class);
    Mutant lastEventId = unit.mock(Mutant.class);

    expect(route.produces()).andReturn(MediaType.ALL);

    expect(request.require(Injector.class)).andReturn(injector);
    expect(request.route()).andReturn(route);
    expect(request.attributes()).andReturn(ImmutableMap.of());
    expect(request.header("Last-Event-ID")).andReturn(lastEventId);

    expect(injector.getInstance(Renderer.KEY)).andReturn(Sets.newHashSet());
  };

  private Block locale = unit -> {
    Request req = unit.get(Request.class);
    expect(req.locale()).andReturn(Locale.CANADA);
  };

  @Test
  public void sseId() throws Exception {
    Sse sse = new Sse() {

      @Override
      protected void closeInternal() {
      }

      @Override
      protected Promise<Optional<Object>> send(final Optional<Object> id, final byte[] data) {
        return null;
      }

      @Override
      protected void handshake(final Runnable handler) throws Exception {
      }
    };
    assertNotNull(sse.id());
    UUID.fromString(sse.id());
    sse.close();
  }

  @Test
  public void handshake() throws Exception {
    new MockUnit(Request.class, Injector.class, Runnable.class, Route.class)
        .expect(handshake)
        .expect(locale)
        .expect(unit -> {
          Injector injector = unit.get(Injector.class);
          expect(injector.getInstance(Key.get(Object.class))).andReturn(null).times(2);
          expect(injector.getInstance(Key.get(TypeLiteral.get(Object.class)))).andReturn(null);
          expect(injector.getInstance(Key.get(Object.class, Names.named("n")))).andReturn(null);
        })
        .run(unit -> {
          Sse sse = new Sse() {

            @Override
            protected void closeInternal() {
            }

            @Override
            protected Promise<Optional<Object>> send(final Optional<Object> id, final byte[] data) {
              return null;
            }

            @Override
            protected void handshake(final Runnable handler) throws Exception {
            }
          };
          sse.handshake(unit.get(Request.class), unit.get(Runnable.class));
          sse.require(Object.class);
          sse.require(Key.get(Object.class));
          sse.require(TypeLiteral.get(Object.class));
          sse.require("n", Object.class);
          sse.close();
        });
  }

  @Test
  public void ifCloseClosedChannel() throws Exception {
    CountDownLatch latch = new CountDownLatch(1);
    new MockUnit()
        .run(unit -> {
          Sse sse = new Sse() {

            @Override
            protected void closeInternal() {
              latch.countDown();
            }

            @Override
            protected Promise<Optional<Object>> send(final Optional<Object> id, final byte[] data) {
              return null;
            }

            @Override
            protected void handshake(final Runnable handler) throws Exception {
            }
          };
          sse.onClose(() -> sse.close());
          sse.ifClose(new ClosedChannelException());
          latch.await();
        });
  }

  @Test
  public void ifCloseBrokenPipe() throws Exception {
    CountDownLatch latch = new CountDownLatch(1);
    new MockUnit()
        .run(unit -> {
          Sse sse = new Sse() {

            @Override
            protected void closeInternal() {
              latch.countDown();
            }

            @Override
            protected Promise<Optional<Object>> send(final Optional<Object> id, final byte[] data) {
              return null;
            }

            @Override
            protected void handshake(final Runnable handler) throws Exception {
            }
          };
          sse.onClose(() -> sse.close());
          sse.ifClose(new IOException("Broken pipe"));
          latch.await();
        });
  }

  @SuppressWarnings("resource")
  @Test
  public void ifCloseErrorOnFireClose() throws Exception {
    CountDownLatch latch = new CountDownLatch(1);
    new MockUnit()
        .run(unit -> {
          Sse sse = new Sse() {

            @Override
            protected void closeInternal() {
              latch.countDown();
            }

            @Override
            protected Promise<Optional<Object>> send(final Optional<Object> id, final byte[] data) {
              return null;
            }

            @Override
            protected void handshake(final Runnable handler) throws Exception {
            }
          };
          sse.onClose(() -> {
            throw new IllegalStateException("intentional err");
          });
          sse.ifClose(new IOException("Broken pipe"));
          latch.await();
        });
  }

  @Test
  public void ifCloseFailure() throws Exception {
    CountDownLatch latch = new CountDownLatch(1);
    new MockUnit()
        .run(unit -> {
          Sse sse = new Sse() {

            @Override
            protected void closeInternal() {
              latch.countDown();
            }

            @Override
            protected Promise<Optional<Object>> send(final Optional<Object> id, final byte[] data) {
              return null;
            }

            @Override
            protected void handshake(final Runnable handler) throws Exception {
            }
          };
          sse.onClose(() -> sse.close());
          sse.ifClose(new IOException("Broken pipe"));
          latch.await();
        });
  }

  @Test(expected = IllegalStateException.class)
  public void closeFailure() throws Exception {
    new MockUnit()
        .run(unit -> {
          Sse sse = new Sse() {

            @Override
            protected void closeInternal() {
              throw new IllegalStateException("intentional err");
            }

            @Override
            protected Promise<Optional<Object>> send(final Optional<Object> id, final byte[] data) {
              return null;
            }

            @Override
            protected void handshake(final Runnable handler) throws Exception {
            }
          };
          sse.close();
        });
  }

  @Test
  public void ifCloseIgnoreIO() throws Exception {
    CountDownLatch latch = new CountDownLatch(1);
    new MockUnit()
        .run(unit -> {
          Sse sse = new Sse() {

            @Override
            protected void closeInternal() {
              latch.countDown();
            }

            @Override
            protected Promise<Optional<Object>> send(final Optional<Object> id, final byte[] data) {
              return null;
            }

            @Override
            protected void handshake(final Runnable handler) throws Exception {
            }
          };
          sse.onClose(() -> sse.close());
          sse.ifClose(new IOException("Ignored"));
          assertEquals(1, latch.getCount());
        });
  }

  @Test
  public void ifCloseIgnoreEx() throws Exception {
    CountDownLatch latch = new CountDownLatch(1);
    new MockUnit()
        .run(unit -> {
          Sse sse = new Sse() {

            @Override
            protected void closeInternal() {
              latch.countDown();
            }

            @Override
            protected Promise<Optional<Object>> send(final Optional<Object> id, final byte[] data) {
              return null;
            }

            @Override
            protected void handshake(final Runnable handler) throws Exception {
            }
          };
          sse.onClose(() -> sse.close());
          sse.ifClose(new IllegalArgumentException("Ignored"));
          assertEquals(1, latch.getCount());
        });
  }

  @Test
  public void sseHandlerSuccess() throws Exception {
    CountDownLatch latch = new CountDownLatch(1);
    new MockUnit(Request.class, Response.class, Route.Chain.class, Sse.class)
        .expect(unit -> {
          Request req = unit.get(Request.class);
          Sse sse = unit.get(Sse.class);

          sse.handshake(eq(unit.get(Request.class)), unit.capture(Runnable.class));

          expect(req.require(Sse.class)).andReturn(sse);
          expect(req.path()).andReturn("/sse");
        })
        .expect(unit -> {
          Response rsp = unit.get(Response.class);
          rsp.send(unit.capture(Deferred.class));
        })
        .run(unit -> {
          Sse.Handler handler = (req, sse) -> {
            latch.countDown();
          };
          handler.handle(unit.get(Request.class), unit.get(Response.class),
              unit.get(Route.Chain.class));
        }, unit -> {
          Deferred deferred = unit.captured(Deferred.class).iterator().next();
          deferred.handler(null, (value, ex) -> {
          });

          unit.captured(Runnable.class).iterator().next().run();

          latch.await();
        });
  }

  @Test
  public void sseHandlerFailure() throws Exception {
    new MockUnit(Request.class, Response.class, Sse.class, Route.Chain.class)
        .expect(unit -> {
          Request req = unit.get(Request.class);
          Sse sse = unit.get(Sse.class);

          sse.handshake(eq(unit.get(Request.class)), unit.capture(Runnable.class));

          expect(req.require(Sse.class)).andReturn(sse);
          expect(req.path()).andReturn("/sse");
        })
        .expect(unit -> {
          Response rsp = unit.get(Response.class);
          rsp.send(unit.capture(Deferred.class));
        })
        .run(unit -> {
          Sse.Handler handler = (req, sse) -> {
            throw new IllegalStateException("intentional err");
          };
          handler.handle(unit.get(Request.class), unit.get(Response.class),
              unit.get(Route.Chain.class));
        }, unit -> {
          Deferred deferred = unit.captured(Deferred.class).iterator().next();
          deferred.handler(null, (value, ex) -> {
          });

          unit.captured(Runnable.class).iterator().next().run();
        });
  }

  @Test
  public void sseHandlerHandshakeFailure() throws Exception {
    new MockUnit(Request.class, Response.class, Sse.class, Route.Chain.class)
        .expect(unit -> {
          Request req = unit.get(Request.class);
          Sse sse = unit.get(Sse.class);

          sse.handshake(eq(unit.get(Request.class)), unit.capture(Runnable.class));
          expectLastCall().andThrow(new IllegalStateException("intentional error"));

          expect(req.require(Sse.class)).andReturn(sse);
          expect(req.path()).andReturn("/sse");
        })
        .expect(unit -> {
          Response rsp = unit.get(Response.class);
          rsp.send(unit.capture(Deferred.class));
        })
        .run(unit -> {
          Sse.Handler handler = (req, sse) -> {
          };
          handler.handle(unit.get(Request.class), unit.get(Response.class),
              unit.get(Route.Chain.class));
        }, unit -> {
          Deferred deferred = unit.captured(Deferred.class).iterator().next();
          deferred.handler(null, (value, ex) -> {
          });
        });
  }

  @Test
  public void sseKeepAlive() throws Exception {
    CountDownLatch latch = new CountDownLatch(1);
    new MockUnit()
        .run(unit -> {
          Sse sse = new Sse() {

            @Override
            protected void closeInternal() {
            }

            @Override
            protected Promise<Optional<Object>> send(final Optional<Object> id, final byte[] data) {
              Promise<Optional<Object>> promise = Promise
                  .make(MoreExecutors.newDirectExecutorService());
              promise.success(id);
              return promise;
            }

            @Override
            public Sse keepAlive(final long millis) {
              assertEquals(100, millis);
              latch.countDown();
              return this;
            }

            @Override
            protected void handshake(final Runnable handler) throws Exception {
            }
          };

          new Sse.KeepAlive(sse, 100).run();
          latch.await();
        });
  }

  @SuppressWarnings("resource")
  @Test
  public void renderFailure() throws Exception {
    CountDownLatch latch = new CountDownLatch(1);
    Object data = new Object();
    new MockUnit(Request.class, Route.class, Injector.class, Runnable.class)
        .expect(handshake)
        .expect(locale)
        .expect(unit -> {
          SseRenderer renderer = unit.constructor(SseRenderer.class)
              .args(List.class, List.class, Charset.class, Locale.class, Map.class)
              .build(isA(List.class), isA(List.class), eq(StandardCharsets.UTF_8),
                  eq(Locale.CANADA), isA(Map.class));

          expect(renderer.format(isA(Sse.Event.class))).andThrow(new IOException("failure"));
        })
        .run(unit -> {
          Sse sse = new Sse() {

            @Override
            protected void closeInternal() {
            }

            @Override
            protected void fireCloseEvent() {
            }

            @Override
            protected Promise<Optional<Object>> send(final Optional<Object> id, final byte[] data) {
              Promise<Optional<Object>> promise = Promise
                  .make(MoreExecutors.newDirectExecutorService());
              promise.failure(new IOException("intentional err"));
              return promise;
            }

            @Override
            public Sse keepAlive(final long millis) {
              return this;
            }

            @Override
            protected void handshake(final Runnable handler) throws Exception {
            }
          };
          sse.handshake(unit.get(Request.class), unit.get(Runnable.class));
          sse.event(data).type(MediaType.all).send().onFailure(cause -> latch.countDown());
          latch.await();
        });
  }

  @Test
  public void sseKeepAliveFailure() throws Exception {
    CountDownLatch latch = new CountDownLatch(2);
    new MockUnit()
        .run(unit -> {
          Sse sse = new Sse() {

            @Override
            protected void closeInternal() {
              latch.countDown();
            }

            @Override
            protected void fireCloseEvent() {
              latch.countDown();
            }

            @Override
            protected Promise<Optional<Object>> send(final Optional<Object> id, final byte[] data) {
              Promise<Optional<Object>> promise = Promise
                  .make(MoreExecutors.newDirectExecutorService());
              promise.failure(new IOException("intentional err"));
              return promise;
            }

            @Override
            public Sse keepAlive(final long millis) {
              return this;
            }

            @Override
            protected void handshake(final Runnable handler) throws Exception {
            }
          };

          new Sse.KeepAlive(sse, 100).run();
          latch.await();
        });
  }

}
