/**
 * Copyright 2015 Google Inc. All Rights Reserved.
 * 
 * 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.apphosting.tests.usercode.testservlets;

import com.google.appengine.api.socket.SocketServicePb.AcceptReply;
import com.google.appengine.api.socket.SocketServicePb.AddressPort;
import com.google.appengine.api.socket.SocketServicePb.BindReply;
import com.google.appengine.api.socket.SocketServicePb.CloseReply;
import com.google.appengine.api.socket.SocketServicePb.ConnectReply;
import com.google.appengine.api.socket.SocketServicePb.CreateSocketReply;
import com.google.appengine.api.socket.SocketServicePb.GetSocketNameReply;
import com.google.appengine.api.socket.SocketServicePb.ListenReply;
import com.google.appengine.api.socket.SocketServicePb.ReceiveReply;
import com.google.appengine.api.socket.SocketServicePb.ReceiveRequest;
import com.google.appengine.api.socket.SocketServicePb.RemoteSocketServiceError;
import com.google.appengine.api.socket.SocketServicePb.SendReply;
import com.google.appengine.api.socket.SocketServicePb.SendRequest;
import com.google.appengine.api.socket.SocketServicePb.SetSocketOptionsReply;
import com.google.appengine.api.socket.SocketServicePb.ShutDownReply;
import com.google.apphosting.api.ApiProxy;

import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.DatagramSocketImpl;
import java.net.DatagramSocketImplFactory;
import java.net.InetAddress;
import java.net.NetworkInterface;
import java.net.SocketAddress;
import java.net.SocketException;
import java.util.List;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

/**
 */
public class TestDatagramSocketServlet extends HttpServletTest {
  private static final String SEND_STRING = "ssssssssss";
  private static final String READ_STRING = "rrrrrrrrrr";

  /**
   * Set up a mock delegate to socket resolve calls.
   */
  private ApiProxy.Delegate setUpMockDelegate() {
    ApiProxy.Delegate oldDelegate = ApiProxy.getDelegate();
    ApiProxy.setDelegate(new MockDelegate());
    return oldDelegate;
  }

  @Override
  protected void doGet(HttpServletRequest request, HttpServletResponse response)
      throws ServletException, IOException {
    ApiProxy.Delegate oldDelegate = setUpMockDelegate();

    response.setContentType("text/plain");
    try {
      testOpenAndClose(response);

      testConnectWriteAndRead(response);
      testSocketOpt(response);
      testSetDatagramSocketImpl(response);
      testSocketImplConstructor(response);
    } catch (AssertionFailedException e) {
      return;
      //return the error response
    } finally {
      ApiProxy.setDelegate(oldDelegate);
    }
    response.getWriter().print("Success!");
  }

  private void testOpenAndClose(HttpServletResponse response) throws IOException {
    DatagramSocket socket = new DatagramSocket(0);
    socket.close();
  }

  private void testSetDatagramSocketImpl(HttpServletResponse response)
      throws IOException, AssertionFailedException {
    DatagramSocketImplFactory mockFactory =
        new DatagramSocketImplFactory() {
          @Override
          public DatagramSocketImpl createDatagramSocketImpl() {
            return null;
          }
        };

    SocketException caught = null;
    try {
      DatagramSocket.setDatagramSocketImplFactory(mockFactory);
    } catch (SocketException e) {
      caught = e;
    }
    assertNotNull("caught", caught, response);
  }

  private void testSocketOpt(HttpServletResponse response) throws IOException {
    DatagramSocket socket = new DatagramSocket(0);
    socket.setSoTimeout(10);
  }

  private DatagramPacket readSomeData(DatagramSocket socket, int size) throws IOException {
    byte[] data = new byte[size];
    DatagramPacket packet = new DatagramPacket(data, data.length);
    socket.receive(packet);
    return packet;
  }

  private void testConnectWriteAndRead(HttpServletResponse response)
      throws IOException, AssertionFailedException {
    DatagramSocket socket = new DatagramSocket(9999);
    DatagramPacket packet =
        new DatagramPacket(SEND_STRING.getBytes(), 0, 10, InetAddress.getByName("10.1.1.1"), 9999);
    socket.send(packet);
    packet = readSomeData(socket, 10);
    assertEquals("socket packet read data", READ_STRING, new String(packet.getData()), response);
    assertEquals("socket packet port", 9999, packet.getPort(), response);
    assertEquals(
        "socket packet address", InetAddress.getByName("10.1.1.1"), packet.getAddress(), response);
  }

  private void testSocketImplConstructor(HttpServletResponse response)
      throws IOException, AssertionFailedException {
    MockSocketImpl mockImpl = new MockSocketImpl();
    DatagramSocket socket = new DatagramSocket(mockImpl) {}; // Accessing protected constructor.

    try {
      socket.connect(InetAddress.getByName("10.1.1.1"), 9999);
      assertTrue(
          "Expected SecurityException when not using AppEngineDatagramSocketImpl.",
          false,
          response);
    } catch (SecurityException e) {
      // OK
    }
  }

  /**
   * A mock ApiProxy.Delegate specifically for handling Socket calls.
   */
  private static class MockDelegate implements ApiProxy.Delegate<ApiProxy.Environment> {
    private static final String DESCRIPTOR_PREFIX = "mock-descriptor:";

    interface Responder {
      byte[] makeResponse(Method method, byte[] request);
    }

    public enum Method {
      Bind(
          new Responder() {
            @Override
            public byte[] makeResponse(Method method, byte[] request) {
              return new BindReply().toByteArray();
            }
          }),
      CreateSocket(
          new Responder() {
            int descriptorCount = 0;

            @Override
            public byte[] makeResponse(Method method, byte[] request) {
              CreateSocketReply response = new CreateSocketReply();
              response.setSocketDescriptor(DESCRIPTOR_PREFIX + ++descriptorCount);
              return response.toByteArray();
            }
          }),
      Connect(
          new Responder() {
            @Override
            public byte[] makeResponse(Method method, byte[] request) {
              return new ConnectReply().toByteArray();
            }
          }),
      Listen(
          new Responder() {
            @Override
            public byte[] makeResponse(Method method, byte[] request) {
              return new ListenReply().toByteArray();
            }
          }),
      Accept(
          new Responder() {
            @Override
            public byte[] makeResponse(Method method, byte[] request) {
              return new AcceptReply().toByteArray();
            }
          }),
      Send(
          new Responder() {
            @Override
            public byte[] makeResponse(Method method, byte[] request) {
              SendRequest requestPb = new SendRequest();
              requestPb.parseFrom(request);
              if (!requestPb.getSocketDescriptor().startsWith(DESCRIPTOR_PREFIX)) {
                throw new ApiProxy.ApplicationException(
                    RemoteSocketServiceError.ErrorCode.SYSTEM_ERROR.getValue(),
                    "Descriptor not set correctly.");
              }
              if (!new String(requestPb.getDataAsBytes()).equals(SEND_STRING)) {
                throw new ApiProxy.ApplicationException(
                    RemoteSocketServiceError.ErrorCode.SYSTEM_ERROR.getValue(),
                    "Incorrect data being sent.");
              }
              return new SendReply().toByteArray();
            }
          }),
      Receive(
          new Responder() {
            @Override
            public byte[] makeResponse(Method method, byte[] request) {
              ReceiveRequest requestProto = new ReceiveRequest();
              requestProto.parseFrom(request);
              ReceiveReply response = new ReceiveReply();
              response.setDataAsBytes(READ_STRING.getBytes());
              AddressPort addressPort = new AddressPort();
              addressPort.setPort(9999);
              addressPort.setPackedAddressAsBytes(
                  new byte[] {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, -1, 10, 1, 1, 1});
              response.setReceivedFrom(addressPort);
              return response.toByteArray();
            }
          }),
      Close(
          new Responder() {
            @Override
            public byte[] makeResponse(Method method, byte[] request) {
              return new CloseReply().toByteArray();
            }
          }),
      ShutDown(
          new Responder() {
            @Override
            public byte[] makeResponse(Method method, byte[] request) {
              return new ShutDownReply().toByteArray();
            }
          }),
      SetSocketOptions(
          new Responder() {
            @Override
            public byte[] makeResponse(Method method, byte[] request) {
              return new SetSocketOptionsReply().toByteArray();
            }
          }),
      GetSocketName(
          new Responder() {
            @Override
            public byte[] makeResponse(Method method, byte[] request) {
              GetSocketNameReply reply = new GetSocketNameReply();
              AddressPort externalIp = reply.getMutableProxyExternalIp();
              externalIp.setPort(551212);
              externalIp.setPackedAddressAsBytes(new byte[] {127, 77, 88, 99});
              return reply.toByteArray();
            }
          });

      Responder responder;

      Method(Responder responder) {
        this.responder = responder;
      }

      /**
       * Make a response for this method.
       */
      public byte[] makeResponse(byte[] request) {
        return responder.makeResponse(this, request);
      }
    }

    /**
     * Returns a response for a given method and request.
     */
    private byte[] makeResponse(String methodName, byte[] request) {
      Method method = Method.valueOf(Method.class, methodName);
      if (method == null) {
        throw new UnsupportedOperationException();
      }
      return method.makeResponse(request);
    }

    @Override
    public byte[] makeSyncCall(
        ApiProxy.Environment environment, String packageName, String methodName, byte[] request) {
      if (!"remote_socket".equals(packageName)) {
        throw new UnsupportedOperationException();
      }
      return makeResponse(methodName, request);
    }

    @Override
    public Future<byte[]> makeAsyncCall(
        ApiProxy.Environment environment,
        String packageName,
        String methodName,
        byte[] request,
        ApiProxy.ApiConfig apiConfig) {
      if ("remote_socket".equals(packageName)) {
        final byte[] response = makeResponse(methodName, request);
        return new Future<byte[]>() {
          @Override
          public boolean cancel(boolean mayInterruptIfRunning) {
            return false;
          }

          @Override
          public boolean isCancelled() {
            return false;
          }

          @Override
          public boolean isDone() {
            return true;
          }

          @Override
          public byte[] get() throws InterruptedException, ExecutionException {
            return response;
          }

          @Override
          public byte[] get(long timeout, TimeUnit unit)
              throws InterruptedException, ExecutionException, TimeoutException {
            return response;
          }
        };
      }
      throw new UnsupportedOperationException();
    }

    @Override
    public void log(ApiProxy.Environment environment, ApiProxy.LogRecord record) {}

    @Override
    public void flushLogs(ApiProxy.Environment environment) {}

    @Override
    public List<Thread> getRequestThreads(ApiProxy.Environment environment) {
      return null;
    }
  }

  private static class MockSocketImpl extends DatagramSocketImpl {
    @Override
    public void setOption(int optID, Object value) throws SocketException {}

    @Override
    public Object getOption(int optID) throws SocketException {
      return null;
    }

    @Override
    protected void create() throws SocketException {}

    @Override
    protected void bind(int lport, InetAddress laddr) throws SocketException {}

    @Override
    protected void send(DatagramPacket p) throws IOException {}

    @Override
    protected int peek(InetAddress i) throws IOException {
      return 0;
    }

    @Override
    protected int peekData(DatagramPacket p) throws IOException {
      return 0;
    }

    @Override
    protected void receive(DatagramPacket p) throws IOException {}

    @Override
    protected void setTTL(byte ttl) throws IOException {}

    @Override
    protected byte getTTL() throws IOException {
      return 0;
    }

    @Override
    protected void setTimeToLive(int ttl) throws IOException {}

    @Override
    protected int getTimeToLive() throws IOException {
      return 0;
    }

    @Override
    protected void join(InetAddress inetaddr) throws IOException {}

    @Override
    protected void leave(InetAddress inetaddr) throws IOException {}

    @Override
    protected void joinGroup(SocketAddress mcastaddr, NetworkInterface netIf) throws IOException {
    }

    @Override
    protected void leaveGroup(SocketAddress mcastaddr, NetworkInterface netIf) throws IOException {}

    @Override
    protected void close() {}
  }
}