package com.hubspot.smtp.client;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.assertj.core.api.Assertions.catchThrowable;
import static org.mockito.Mockito.*;

import java.time.Duration;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.CompletableFuture;
import java.util.function.Consumer;
import java.util.function.Supplier;

import org.junit.Before;
import org.junit.Test;

import com.google.common.collect.Lists;

import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.smtp.DefaultSmtpResponse;
import io.netty.handler.codec.smtp.SmtpResponse;

public class ResponseHandlerTest {
  private static final DefaultSmtpResponse SMTP_RESPONSE = new DefaultSmtpResponse(250);
  private static final Supplier<String> DEBUG_STRING = () -> "debug";
  private static final String CONNECTION_ID = "connection#1";
  private static final String CONNECTION_ID_PREFIX = "[" + CONNECTION_ID + "] ";

  private ResponseHandler responseHandler;
  private ChannelHandlerContext context;

  @Before
  public void setup() {
    responseHandler = new ResponseHandler(CONNECTION_ID, Optional.empty(), Optional.empty());
    context = mock(ChannelHandlerContext.class);
  }

  @Test
  public void itCompletesExceptionallyIfAnExceptionIsCaught() throws Exception {
    CompletableFuture<List<SmtpResponse>> f = responseHandler.createResponseFuture(1, DEBUG_STRING);
    Exception testException = new Exception("test");

    responseHandler.exceptionCaught(context, testException);

    assertThat(f.isCompletedExceptionally()).isTrue();

    assertThat(catchThrowable(f::get).getCause())
        .isInstanceOf(ResponseException.class)
        .hasMessage("Received an exception while waiting for a response to [debug]; responses so far: <none>")
        .hasCause(testException);
  }

  @Test
  public void itCompletesWithAResponseWhenHandled() throws Exception {
    CompletableFuture<List<SmtpResponse>> f = responseHandler.createResponseFuture(1, DEBUG_STRING);

    responseHandler.channelRead(context, SMTP_RESPONSE);

    assertThat(f.isCompletedExceptionally()).isFalse();
    assertThat(f.get()).isEqualTo(Lists.newArrayList(SMTP_RESPONSE));
  }

  @Test
  public void itDoesNotCompleteWhenSomeOtherObjectIsRead() throws Exception {
    CompletableFuture<List<SmtpResponse>> f = responseHandler.createResponseFuture(1, DEBUG_STRING);

    responseHandler.channelRead(context, "unexpected");

    assertThat(f.isDone()).isFalse();
  }

  @Test
  public void itOnlyCreatesOneResponseFutureAtATime() {
    assertThat(responseHandler.createResponseFuture(1, () -> "old")).isNotNull();

    assertThatThrownBy(() -> responseHandler.createResponseFuture(1, () -> "new"))
        .isInstanceOf(IllegalStateException.class)
        .hasMessage(CONNECTION_ID_PREFIX + "Cannot wait for a response to [new] because we're still waiting for a response to [old]");
  }

  @Test
  public void itOnlyCreatesOneResponseFutureAtATimeForMultipleResponses() {
    assertThat(responseHandler.createResponseFuture(2, () -> "old")).isNotNull();

    assertThatThrownBy(() -> responseHandler.createResponseFuture(1, () -> "new"))
        .isInstanceOf(IllegalStateException.class)
        .hasMessage(CONNECTION_ID_PREFIX + "Cannot wait for a response to [new] because we're still waiting for a response to [old]");
  }

  @Test
  public void itCanCreateAFutureThatWaitsForMultipleReponses() throws Exception {
    CompletableFuture<List<SmtpResponse>> f = responseHandler.createResponseFuture(3, DEBUG_STRING);
    SmtpResponse response1 = new DefaultSmtpResponse(250, "1");
    SmtpResponse response2 = new DefaultSmtpResponse(250, "2");
    SmtpResponse response3 = new DefaultSmtpResponse(250, "3");

    responseHandler.channelRead(context, response1);

    assertThat(f.isDone()).isFalse();

    responseHandler.channelRead(context, response2);
    responseHandler.channelRead(context, response3);

    assertThat(f.isDone()).isTrue();

    assertThat(f.isCompletedExceptionally()).isFalse();
    assertThat(f.get().get(0)).isEqualTo(response1);
    assertThat(f.get().get(1)).isEqualTo(response2);
    assertThat(f.get().get(2)).isEqualTo(response3);
  }

  @Test
  public void itCanCreateAFutureInTheCallbackForAPreviousFuture() throws Exception {
    CompletableFuture<List<SmtpResponse>> future = responseHandler.createResponseFuture(1, DEBUG_STRING);

    CompletableFuture<Void> assertion = future.thenRun(() -> assertThat(responseHandler.createResponseFuture(1, DEBUG_STRING)).isNotNull());

    responseHandler.channelRead(context, SMTP_RESPONSE);

    assertion.get();
  }

  @Test
  public void itCanFailMultipleResponseFuturesAtAnyTime() throws Exception {
    CompletableFuture<List<SmtpResponse>> f = responseHandler.createResponseFuture(3, DEBUG_STRING);
    Exception testException = new Exception("test");

    responseHandler.exceptionCaught(context, testException);

    assertThat(f.isCompletedExceptionally()).isTrue();

    assertThat(catchThrowable(f::get).getCause())
        .isInstanceOf(ResponseException.class)
        .hasMessage("Received an exception while waiting for a response to [debug]; responses so far: <none>")
        .hasCause(testException);
  }

  @Test
  public void itCanCreateNewFuturesOnceAResponseHasArrived() throws Exception {
    responseHandler.createResponseFuture(1, DEBUG_STRING);
    responseHandler.channelRead(context, SMTP_RESPONSE);

    responseHandler.createResponseFuture(1, DEBUG_STRING);
  }

  @Test
  public void itCanCreateNewFuturesOnceATheExpectedResponsesHaveArrived() throws Exception {
    responseHandler.createResponseFuture(2, DEBUG_STRING);
    responseHandler.channelRead(context, SMTP_RESPONSE);
    responseHandler.channelRead(context, SMTP_RESPONSE);

    responseHandler.createResponseFuture(1, DEBUG_STRING);
  }

  @Test
  public void itCanCreateNewFuturesOnceAnExceptionIsHandled() throws Exception {
    responseHandler.createResponseFuture(1, DEBUG_STRING);
    responseHandler.exceptionCaught(context, new Exception("test"));

    responseHandler.createResponseFuture(1, DEBUG_STRING);
  }

  @Test
  public void itCanTellWhenAResponseIsPending() {
    assertThat(responseHandler.getPendingResponseDebugString()).isEmpty();

    responseHandler.createResponseFuture(1, DEBUG_STRING);

    assertThat(responseHandler.getPendingResponseDebugString()).contains(DEBUG_STRING.get());
  }

  @Test
  public void itCompletesExceptionallyIfTheChannelIsClosed() throws Exception {
    CompletableFuture<List<SmtpResponse>> f = responseHandler.createResponseFuture(1, DEBUG_STRING);

    responseHandler.channelInactive(context);

    assertThat(f.isCompletedExceptionally()).isTrue();

    assertThat(catchThrowable(f::get).getCause())
        .isInstanceOf(ResponseException.class)
        .hasMessage("Received an exception while waiting for a response to [debug]; responses so far: <none>")
        .hasCauseInstanceOf(ChannelClosedException.class);
  }

  @Test
  public void itCompletesExceptionallyIfTheDefaultResponseTimeoutIsExceeded() throws Exception {
    ResponseHandler impatientHandler = new ResponseHandler(CONNECTION_ID, Optional.of(Duration.ofMillis(200)), Optional.empty());

    CompletableFuture<List<SmtpResponse>> responseFuture = impatientHandler.createResponseFuture(1, DEBUG_STRING);
    assertThat(responseFuture.isCompletedExceptionally()).isFalse();

    Thread.sleep(400);
    assertThat(responseFuture.isCompletedExceptionally()).isTrue();
  }

  @Test
  public void itCompletesExceptionallyIfTheResponseTimeoutIsExceeded() throws Exception {
    ResponseHandler impatientHandler = new ResponseHandler(CONNECTION_ID, Optional.of(Duration.ofDays(365)), Optional.empty());

    CompletableFuture<List<SmtpResponse>> responseFuture = impatientHandler.createResponseFuture(1, Optional.of(Duration.ofMillis(200)), DEBUG_STRING);
    assertThat(responseFuture.isCompletedExceptionally()).isFalse();

    Thread.sleep(400);
    assertThat(responseFuture.isCompletedExceptionally()).isTrue();
  }

  @Test
  public void itPassesExceptionsToTheProvidedHandlerIfPresent() throws Exception {
    Consumer<Throwable> exceptionHandler = (Consumer<Throwable>) mock(Consumer.class);
    ResponseHandler responseHandler = new ResponseHandler(CONNECTION_ID, Optional.empty(), Optional.of(exceptionHandler));

    Exception testException = new Exception("oh no");
    responseHandler.exceptionCaught(null, testException);

    verify(exceptionHandler).accept(testException);
  }

  @Test
  public void itDoesNotPasExceptionsToTheProvidedHandlerIfThereIsAPendingFuture() throws Exception {
    Consumer<Throwable> exceptionHandler = (Consumer<Throwable>) mock(Consumer.class);
    ResponseHandler responseHandler = new ResponseHandler(CONNECTION_ID, Optional.empty(), Optional.of(exceptionHandler));

    responseHandler.createResponseFuture(1, DEBUG_STRING);

    Exception testException = new Exception("oh no");
    responseHandler.exceptionCaught(null, testException);

    verify(exceptionHandler, never()).accept(testException);
  }

  @Test
  public void itPassesExceptionsToTheSuperclassIfTheHandlerIsNotProvided() throws Exception {
    ResponseHandler responseHandler = new ResponseHandler(CONNECTION_ID, Optional.empty(), Optional.empty());

    Exception testException = new Exception("oh no");
    ChannelHandlerContext ctx = mock(ChannelHandlerContext.class);

    responseHandler.exceptionCaught(ctx, testException);

    verify(ctx).fireExceptionCaught(testException);
  }
}