// Copyright © 2012-2020 VLINGO LABS. All rights reserved.
//
// This Source Code Form is subject to the terms of the
// Mozilla Public License, v. 2.0. If a copy of the MPL
// was not distributed with this file, You can obtain
// one at https://mozilla.org/MPL/2.0/.

package io.vlingo.http.resource;

import static io.vlingo.http.Response.Status.Ok;
import static io.vlingo.http.Response.Status.PermanentRedirect;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;

import java.util.Random;
import java.util.concurrent.atomic.AtomicInteger;

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

import io.vlingo.actors.Definition;
import io.vlingo.actors.testkit.AccessSafely;
import io.vlingo.http.Response;
import io.vlingo.http.ResponseHeader;
import io.vlingo.http.resource.TestResponseChannelConsumer.Progress;
import io.vlingo.http.sample.user.model.User;
import io.vlingo.wire.channel.ResponseChannelConsumer;
import io.vlingo.wire.fdx.bidirectional.ClientRequestResponseChannel;
import io.vlingo.wire.fdx.bidirectional.netty.client.NettyClientRequestResponseChannel;
import io.vlingo.wire.node.Address;
import io.vlingo.wire.node.AddressType;
import io.vlingo.wire.node.Host;

public abstract class ServerTest extends ResourceTestFixtures {
  private static final int TOTAL_REQUESTS_RESPONSES = 200;

  private static final Random random = new Random();
  private static final AtomicInteger baseServerPort = new AtomicInteger(10_000 + random.nextInt(50_000));

  protected int serverPort;
  protected boolean skipTests;

  private ClientRequestResponseChannel client;
  private ResponseChannelConsumer consumer;
  private Progress progress;
  private Server server;

  @Test
  public void testThatServerHandlesThrowables() {
    System.out.println(">>>>>>>>>>>>>>>>>>>>> testThatServerHandlesThrowables");

    if (skipTests) {
      System.out.println(">>>>>>>>>>>>>>>>>>>>> skipped");
      return;
    }

    final String request = getExceptionRequest("1");
    client.requestWith(toByteBuffer(request));

    final AccessSafely consumeCalls = progress.expectConsumeTimes(1);
    while (consumeCalls.totalWrites() < 1) {
      client.probeChannel();
    }
    consumeCalls.readFrom("completed");

    final Response createdResponse = progress.responses.poll();

    assertEquals(1, progress.consumeCount.get());
    assertEquals(Response.Status.InternalServerError, createdResponse.status);
  }

  @Test
  public void testThatServerDispatchesRequests() throws Exception {
    System.out.println(">>>>>>>>>>>>>>>>>>>>> testThatServerDispatchesRequests");

    if (skipTests) {
      System.out.println(">>>>>>>>>>>>>>>>>>>>> skipped");
      return;
    }

    final String request = postRequest(uniqueJohnDoe());
    client.requestWith(toByteBuffer(request));

    final AccessSafely consumeCalls = progress.expectConsumeTimes(1);
    while (consumeCalls.totalWrites() < 1) {
      client.probeChannel();
    }
    consumeCalls.readFrom("completed");

    final Response createdResponse = progress.responses.poll();

    assertEquals(1, progress.consumeCount.get());
    assertNotNull(createdResponse.headers.headerOf(ResponseHeader.Location));

    final String getUserMessage = "GET " + createdResponse.headerOf(ResponseHeader.Location).value + " HTTP/1.1\nHost: vlingo.io\nConnection: keep-alive\n\n";

    client.requestWith(toByteBuffer(getUserMessage));

    final AccessSafely moreConsumeCalls = progress.expectConsumeTimes(1);
    while (moreConsumeCalls.totalWrites() < 1) {
      client.probeChannel();
    }
    moreConsumeCalls.readFrom("completed");

    final Response getResponse = progress.responses.poll();

    assertEquals(2, progress.consumeCount.get());
    assertEquals(Response.Status.Ok, getResponse.status);
    assertNotNull(getResponse.entity);
    assertNotNull(getResponse.entity.content());
    assertTrue(getResponse.entity.hasContent());
  }

  @Test
  public void testThatServerDispatchesManyRequests() throws Exception {
    System.out.println(">>>>>>>>>>>>>>>>>>>>> testThatServerDispatchesManyRequests");

    if (skipTests) {
      System.out.println(">>>>>>>>>>>>>>>>>>>>> skipped");
      return;
    }

    final long startTime = System.currentTimeMillis();

    final AccessSafely consumeCalls = progress.expectConsumeTimes(TOTAL_REQUESTS_RESPONSES);
    final int totalPairs = TOTAL_REQUESTS_RESPONSES / 2;
    int currentConsumeCount = 0;
    for (int idx = 0; idx < totalPairs; ++idx) {
      client.requestWith(toByteBuffer(postRequest(uniqueJohnDoe())));
      client.requestWith(toByteBuffer(postRequest(uniqueJaneDoe())));
      final int expected = currentConsumeCount + 2;
      while (consumeCalls.totalWrites() < expected) {
        client.probeChannel();
      }
      currentConsumeCount = expected;

      Thread.sleep(100);
    }

    while (consumeCalls.totalWrites() < TOTAL_REQUESTS_RESPONSES) {
      client.probeChannel();
    }

    consumeCalls.readFrom("completed");

    System.out.println("TOTAL REQUESTS-RESPONSES: " + TOTAL_REQUESTS_RESPONSES + " TIME: " + (System.currentTimeMillis() - startTime) + " ms");

    assertTrue(TOTAL_REQUESTS_RESPONSES <= progress.consumeCount.get());
    final Response createdResponse = progress.responses.peek();
    assertNotNull(createdResponse.headers.headerOf(ResponseHeader.Location));
  }

  @Test
  public void testThatServerRespondsPermanentRedirectWithNoContentLengthHeader() {
    System.out.println(">>>>>>>>>>>>>>>>>>>>> testThatServerRespondsPermanentRedirectWithNoContentLengthHeader");

    if (skipTests) {
      System.out.println(">>>>>>>>>>>>>>>>>>>>> skipped");
      return;
    }

    final String request = putRequest("u-123", uniqueJohnDoe());
    client.requestWith(toByteBuffer(request));

    final AccessSafely consumeCalls = progress.expectConsumeTimes(1);
    while (consumeCalls.totalWrites() < 1) {
      client.probeChannel();
    }
    consumeCalls.readFrom("completed");

    final Response response = progress.responses.poll();

    assertNotNull(response);
    assertEquals(PermanentRedirect.name(), response.status.name());
    assertEquals(1, progress.consumeCount.get());
  }

  @Test
  public void testThatServerRespondsOkWithNoContentLengthHeader() {
    System.out.println(">>>>>>>>>>>>>>>>>>>>> testThatServerRespondsOkWithNoContentLengthHeader");

    if (skipTests) {
      System.out.println(">>>>>>>>>>>>>>>>>>>>> skipped");
      return;
    }

    final String request = putRequest("u-456", uniqueJohnDoe());
    client.requestWith(toByteBuffer(request));

    final AccessSafely consumeCalls = progress.expectConsumeTimes(1);
    while (consumeCalls.totalWrites() < 1) {
      client.probeChannel();
    }
    consumeCalls.readFrom("completed");

    final Response response = progress.responses.poll();

    assertNotNull(response);
    assertEquals(Ok.name(), response.status.name());
    assertEquals(1, progress.consumeCount.get());
  }

  @Test
  public void testThatServerClosesChannelAfterSingleRequest() {
    System.out.println(">>>>>>>>>>>>>>>>>>>>> testThatServerClosesChannelAfterSingleRequest");

    if (skipTests) {
      System.out.println(">>>>>>>>>>>>>>>>>>>>> skipped");
      return;
    }

    int totalResponses = 0;
    final int maxRequests = 10;

    for (int count = 0; count < maxRequests; ++count) {
      final AccessSafely consumeCalls = progress.expectConsumeTimes(1);
      if (count % 2 == 0) {
        client.requestWith(toByteBuffer(postRequestCloseFollowing(uniqueJohnDoe())));
      } else {
        client.requestWith(toByteBuffer(postRequestCloseFollowing(uniqueJaneDoe())));
      }
      System.out.println("1");
      while (consumeCalls.totalWrites() < 1) {
        client.probeChannel();
      }
      totalResponses += (int) consumeCalls.readFrom("completed");
      System.out.println("2: " + totalResponses);

      client.close();

      client = new NettyClientRequestResponseChannel(Address.from(Host.of("localhost"), serverPort, AddressType.NONE), consumer, 100, 10240);
    }

    assertEquals(maxRequests, totalResponses);
  }

  @Override
  @Before
  public void setUp() throws Exception {
    super.setUp();

    User.resetId();

    skipTests = false;
    serverPort = baseServerPort.getAndIncrement();
    server = startServer();
    assertTrue(server.startUp().await(500L));

    progress = new Progress();

    consumer = world.actorFor(ResponseChannelConsumer.class, Definition.has(TestResponseChannelConsumer.class, Definition.parameters(progress)));

    client = new NettyClientRequestResponseChannel(Address.from(Host.of("localhost"), serverPort, AddressType.NONE), consumer, 100, 10240);
  }

  protected abstract Server startServer();

  @Override
  @After
  public void tearDown() throws InterruptedException {
    client.close();

    server.shutDown();

    super.tearDown();
  }
}