/*
 * Copyright 2008 Google Inc.
 *
 * 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 com.google.gwt.dev.shell.remoteui;

import com.google.gwt.dev.shell.remoteui.MessageTransport.RequestException;
import com.google.gwt.dev.shell.remoteui.RemoteMessageProto.Message;
import com.google.gwt.dev.shell.remoteui.RemoteMessageProto.Message.Failure;
import com.google.gwt.dev.shell.remoteui.RemoteMessageProto.Message.Request;
import com.google.gwt.dev.shell.remoteui.RemoteMessageProto.Message.Request.DevModeRequest;
import com.google.gwt.dev.shell.remoteui.RemoteMessageProto.Message.Request.DevModeRequest.RequestType;
import com.google.gwt.dev.shell.remoteui.RemoteMessageProto.Message.Response;
import com.google.gwt.dev.shell.remoteui.RemoteMessageProto.Message.Response.DevModeResponse;

import junit.framework.TestCase;

import java.io.IOException;
import java.net.InetAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;

/**
 * Tests for {@link MessageTransport}.
 */
public class MessageTransportTest extends TestCase {

  private static class MockNetwork {
    private final Socket clientSocket;
    private final Socket serverSocket;

    private final ServerSocket listenSocket;

    public MockNetwork(Socket clientSocket, Socket serverSocket,
        ServerSocket listenSocket) {
      this.clientSocket = clientSocket;
      this.serverSocket = serverSocket;
      this.listenSocket = listenSocket;
    }

    public Socket getClientSocket() {
      return clientSocket;
    }

    public Socket getServerSocket() {
      return serverSocket;
    }

    public void shutdown() {
      try {
        clientSocket.close();
      } catch (IOException e) {
        // Ignore
      }

      try {
        serverSocket.close();
      } catch (IOException e) {
        // Ignore
      }

      try {
        listenSocket.close();
      } catch (IOException e) {
        // Ignore
      }
    }
  }

  private static MockNetwork createMockNetwork() throws IOException {
    InetAddress localHost = InetAddress.getLocalHost();
    ServerSocket listenSocket = new ServerSocket(0, 1, localHost);
    Socket clientSocket = new Socket(localHost, listenSocket.getLocalPort());
    Socket serverSocket = listenSocket.accept();
    return new MockNetwork(clientSocket, serverSocket, listenSocket);
  }

  /**
   * Tests that sending an async request to a server when the server's socket is
   * closed with result in an ExecutionException on a call to future.get().
   *
   * @throws ExecutionException
   * @throws InterruptedException
   * @throws IOException
   */
  public void testExecuteAsyncRequestWithClosedServerSocket()
      throws IOException, InterruptedException {
    MockNetwork network = createMockNetwork();

    /*
     * Define a dummy request processor. The message transport is being set up
     * on the client side, which means that it should not be receiving any
     * requests (any responses).
     */
    RequestProcessor requestProcessor = new RequestProcessor() {
      @Override
      public Response execute(Request request) throws Exception {
        fail("Should not reach here.");
        return null;
      }
    };

    // Set up a transport on the client side
    MessageTransport messageTransport = new MessageTransport(
        network.getClientSocket().getInputStream(),
        network.getClientSocket().getOutputStream(), requestProcessor,
        new MessageTransport.ErrorCallback() {
          @Override
          public void onResponseException(Exception e) {
          }
          @Override
          public void onTermination(Exception e) {
          }
        });
    messageTransport.start();

    Message.Request.Builder requestMessageBuilder = Message.Request.newBuilder();
    requestMessageBuilder.setServiceType(Message.Request.ServiceType.DEV_MODE);
    Message.Request request = requestMessageBuilder.build();

    // Close the server's socket; that will close the client's output
    // stream
    network.getServerSocket().close();

    int sleepCycles = 0;
    while (!network.getServerSocket().isClosed() && sleepCycles < 8) {
      // Wait until the stream is closed before attempting to execute the
      // request.
      Thread.sleep(250);
      sleepCycles++;
    }

    assertTrue("Unable to close socket; cannot proceed with the test.",
        network.getServerSocket().isClosed());

    Future<Response> responseFuture = null;
    responseFuture = messageTransport.executeRequestAsync(request);
    assertNotNull(responseFuture);

    try {
      responseFuture.get(2, TimeUnit.SECONDS);
      fail("Should have thrown an exception");
    } catch (TimeoutException te) {
      fail("Should not have timed out");
    } catch (ExecutionException e) {
      /*
       * An IOException can happen if the request gets in the queue before the
       * message processing thread terminates. If the request gets in the queue
       * after the message processing thread terminates, then the result will be
       * an IllegalStateException.
       */
      assertTrue("Expected: IllegalStateException or IOException, actual:"
          + e.getCause(), e.getCause() instanceof IllegalStateException
          || e.getCause() instanceof IOException);
    } catch (Exception e) {
      fail("Should not have thrown any other exception");
    }

    network.shutdown();
  }

  /**
   * Tests that an async request to a remote server is successfully sent, and
   * the server's response is successfully received.
   */
  public void testExecuteRequestAsync() throws InterruptedException,
      ExecutionException, IOException, TimeoutException {

    MockNetwork network = createMockNetwork();

    /*
     * Define a dummy request processor. The message transport is being set up
     * on the client side, which means that it should not be receiving any
     * requests (any responses).
     */
    RequestProcessor requestProcessor = new RequestProcessor() {
      @Override
      public Response execute(Request request) throws Exception {
        fail("Should not reach here.");
        return null;
      }
    };

    // Set up a transport on the client side
    MessageTransport messageTransport = new MessageTransport(
        network.getClientSocket().getInputStream(),
        network.getClientSocket().getOutputStream(), requestProcessor, null);
    messageTransport.start();

    // Generate a new request
    DevModeRequest.Builder devModeRequestBuilder = DevModeRequest.newBuilder();
    devModeRequestBuilder.setRequestType(RequestType.CAPABILITY_EXCHANGE);
    Message.Request.Builder requestMessageBuilder = Message.Request.newBuilder();
    requestMessageBuilder.setServiceType(Message.Request.ServiceType.DEV_MODE);
    requestMessageBuilder.setDevModeRequest(devModeRequestBuilder);
    Message.Request request = requestMessageBuilder.build();

    // Execute the request on the remote server
    Future<Response> responseFuture = messageTransport.executeRequestAsync(request);
    assertNotNull(responseFuture);

    // Get the request on the server side
    Message receivedRequest = Message.parseDelimitedFrom(network.getServerSocket().getInputStream());
    assertEquals(receivedRequest.getRequest(), request);

    // Generate a response on the server
    DevModeResponse.CapabilityExchange.Capability.Builder capabilityBuilder = DevModeResponse.CapabilityExchange.Capability.newBuilder();
    capabilityBuilder.setCapability(DevModeRequest.RequestType.RESTART_WEB_SERVER);
    DevModeResponse.CapabilityExchange.Builder capabilityExchangeResponseBuilder = DevModeResponse.CapabilityExchange.newBuilder();
    capabilityExchangeResponseBuilder.addCapabilities(capabilityBuilder);
    DevModeResponse.Builder devModeResponseBuilder = DevModeResponse.newBuilder();
    devModeResponseBuilder.setResponseType(DevModeResponse.ResponseType.CAPABILITY_EXCHANGE);
    devModeResponseBuilder.setCapabilityExchange(capabilityExchangeResponseBuilder);
    Response.Builder responseBuilder = Response.newBuilder();
    responseBuilder.setDevModeResponse(devModeResponseBuilder);
    Response response = responseBuilder.build();
    Message.Builder responseMsgBuilder = Message.newBuilder();
    responseMsgBuilder.setMessageType(Message.MessageType.RESPONSE);
    // Make sure we set the right message id
    responseMsgBuilder.setMessageId(receivedRequest.getMessageId());
    responseMsgBuilder.setResponse(response);
    Message responseMsg = responseMsgBuilder.build();

    // Send the response back to the client
    responseMsg.writeDelimitedTo(network.getServerSocket().getOutputStream());

    // Make sure that the response received on the client is identical to
    // the response sent by the server
    assertEquals(responseFuture.get(2, TimeUnit.SECONDS), response);

    network.shutdown();
  }

  /**
   * Tests that an async request to a remote server which ends up throwing an
   * exception on the server side ends up throwing the proper exception via the
   * future that the client is waiting on.
   */
  public void testExecuteRequestAsyncServerThrowsException() throws IOException {
    MockNetwork network = createMockNetwork();

    /*
     * Define a dummy request processor. The message transport is being set up
     * on the client side, which means that it should not be receiving any
     * requests (any responses).
     */
    RequestProcessor requestProcessor = new RequestProcessor() {
      @Override
      public Response execute(Request request) throws Exception {
        fail("Should not reach here.");
        return null;
      }
    };

    // Set up a message transport on the client side
    MessageTransport messageTransport = new MessageTransport(
        network.getClientSocket().getInputStream(),
        network.getClientSocket().getOutputStream(), requestProcessor,
        new MessageTransport.ErrorCallback() {
          @Override
          public void onResponseException(Exception e) {
          }
          @Override
          public void onTermination(Exception e) {
          }
        });
    messageTransport.start();

    // Generate a new request
    Message.Request.Builder requestMessageBuilder = Message.Request.newBuilder();
    requestMessageBuilder.setServiceType(Message.Request.ServiceType.DEV_MODE);
    Message.Request request = requestMessageBuilder.build();

    // Execute the request on the remote server
    Future<Response> responseFuture = messageTransport.executeRequestAsync(request);
    assertNotNull(responseFuture);

    // Get the request on the server side
    Message receivedRequest = Message.parseDelimitedFrom(network.getServerSocket().getInputStream());
    assertEquals(receivedRequest.getRequest(), request);

    // Generate a failure response on the server
    Failure.Builder failureBuilder = Failure.newBuilder();
    failureBuilder.setMessage("Unable to process the request.");
    Message.Builder messageBuilder = Message.newBuilder();
    // Make sure that we set the matching message id
    messageBuilder.setMessageId(receivedRequest.getMessageId());
    messageBuilder.setMessageType(Message.MessageType.FAILURE);
    messageBuilder.setFailure(failureBuilder);
    Message failureMsg = messageBuilder.build();

    // Send the failure message back to the client
    failureMsg.writeDelimitedTo(network.getServerSocket().getOutputStream());

    // Wait for the response on the client. This should result in a
    // RequestException being thrown.
    try {
      responseFuture.get(2, TimeUnit.SECONDS);
      fail("Should have thrown an exception");
    } catch (TimeoutException te) {
      fail("Should not have timed out");
    } catch (ExecutionException e) {
      // This is where we should hit
      assertTrue("Expected: MessageTransport.RequestException, actual:"
          + e.getCause(), e.getCause() instanceof RequestException);
      RequestException re = (RequestException) e.getCause();
      assertEquals(re.getMessage(), "Unable to process the request.");
    } catch (Exception e) {
      fail("Should not have thrown any other exception");
    }

    network.shutdown();
  }

  /**
   * Tests that a client request is successfully received by the
   * RequestProcessor, and the response generated by the RequestProcessor is
   * successfully received by the client.
   *
   * @throws IOException
   * @throws ExecutionException
   * @throws InterruptedException
   */
  public void testRequestProcessor() throws IOException {
    MockNetwork network = createMockNetwork();

    // Create the request that will be sent to the server
    DevModeRequest.Builder devModeRequestBuilder = DevModeRequest.newBuilder();
    devModeRequestBuilder.setRequestType(DevModeRequest.RequestType.CAPABILITY_EXCHANGE);
    Message.Request.Builder clientRequestBuilder = Message.Request.newBuilder();
    clientRequestBuilder.setDevModeRequest(devModeRequestBuilder);
    clientRequestBuilder.setServiceType(Message.Request.ServiceType.DEV_MODE);
    final Message.Request clientRequest = clientRequestBuilder.build();

    // Create the response that will be sent back from the server
    DevModeResponse.Builder devModeResponseBuilder = DevModeResponse.newBuilder();
    devModeResponseBuilder.setResponseType(DevModeResponse.ResponseType.CAPABILITY_EXCHANGE);
    Message.Response.Builder clientResponseBuilder = Message.Response.newBuilder();
    clientResponseBuilder.setDevModeResponse(devModeResponseBuilder);
    final Message.Response clientResponse = clientResponseBuilder.build();

    /*
     * Define a request processor, which will expect to receive the request that
     * we've defined, and then return the response that we've defined.
     */
    RequestProcessor requestProcessor = new RequestProcessor() {
      @Override
      public Response execute(Request request) throws Exception {
        assertEquals(clientRequest, request);
        return clientResponse;
      }
    };

    // Start up the message transport on the server side
    MessageTransport messageTransport = new MessageTransport(
        network.getClientSocket().getInputStream(),
        network.getClientSocket().getOutputStream(), requestProcessor,
        new MessageTransport.ErrorCallback() {
          @Override
          public void onResponseException(Exception e) {
          }
          @Override
          public void onTermination(Exception e) {
          }
        });
    messageTransport.start();

    // Send the request from the client to the server
    Message.Builder clientRequestMsgBuilder = Message.newBuilder();
    clientRequestMsgBuilder.setMessageType(Message.MessageType.REQUEST);
    clientRequestMsgBuilder.setMessageId(25);
    clientRequestMsgBuilder.setRequest(clientRequest);
    Message clientRequestMsg = clientRequestMsgBuilder.build();
    clientRequestMsg.writeDelimitedTo(network.getServerSocket().getOutputStream());

    // Receive the response on the client (which was returned by the
    // RequestProcessor)
    Message receivedResponseMsg = Message.parseDelimitedFrom(network.getServerSocket().getInputStream());

    // Make sure the message ids match
    assertEquals(receivedResponseMsg.getMessageId(), 25);

    // Make sure that the response matches the one that was returned by the
    // RequestProcessor
    assertEquals(receivedResponseMsg.getResponse(), clientResponse);

    network.shutdown();
  }

  /**
   * Tests that a client request is successfully received by the
   * RequestProcessor, and the exception thrown by the RequestProcessor is
   * passed back in the form of an error response to the client.
   *
   * @throws IOException
   * @throws ExecutionException
   * @throws InterruptedException
   */
  public void testRequestProcessorThrowsException() throws IOException {
    MockNetwork network = createMockNetwork();

    /*
     * Define a request processor that throws an exception when it receives the
     * request. We'll expect to receive this exception as a failure message on
     * the client side.
     */
    RequestProcessor requestProcessor = new RequestProcessor() {
      @Override
      public Response execute(Request request) throws Exception {
        throw new Exception("There was an exception processing this request.");
      }
    };

    // Start up the message transport on the server side
    MessageTransport messageTransport = new MessageTransport(
        network.getClientSocket().getInputStream(),
        network.getClientSocket().getOutputStream(), requestProcessor,
        new MessageTransport.ErrorCallback() {
          @Override
          public void onResponseException(Exception e) {
          }
          @Override
          public void onTermination(Exception e) {
          }
        });
    messageTransport.start();

    // Send a request to the server
    Message.Request.Builder clientRequestBuilder = Message.Request.newBuilder();
    clientRequestBuilder.setServiceType(Message.Request.ServiceType.DEV_MODE);
    final Message.Request clientRequest = clientRequestBuilder.build();
    Message.Builder clientRequestMsgBuilder = Message.newBuilder();
    clientRequestMsgBuilder.setMessageType(Message.MessageType.REQUEST);
    clientRequestMsgBuilder.setMessageId(25);
    clientRequestMsgBuilder.setRequest(clientRequest);
    Message clientRequestMsg = clientRequestMsgBuilder.build();
    clientRequestMsg.writeDelimitedTo(network.getServerSocket().getOutputStream());

    // Receive the response on the client (which was returned by the
    // RequestProcessor)
    Message receivedResponseMsg = Message.parseDelimitedFrom(network.getServerSocket().getInputStream());

    // Make sure the message ids match
    assertEquals(receivedResponseMsg.getMessageId(), 25);

    // Verify that the message is of type FAILURE
    assertEquals(receivedResponseMsg.getMessageType(),
        Message.MessageType.FAILURE);

    // Verify that the failure message field is set
    assertNotNull(receivedResponseMsg.getFailure());

    // Verify that the actual failure message is equal to the message
    // set for the Exception in the RequestProcessor
    assertEquals(receivedResponseMsg.getFailure().getMessage(),
        "There was an exception processing this request.");

    network.shutdown();
  }
}
