/*
 * Copyright 2011 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.user.server.rpc;

import com.google.gwt.user.client.rpc.IncompatibleRemoteServiceException;
import com.google.gwt.user.client.rpc.RemoteService;
import com.google.gwt.user.client.rpc.SerializationException;
import com.google.gwt.user.client.rpc.SerializedTypeViolationException;

import junit.framework.TestCase;

import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;

/**
 * Array test cases for the type checking code found in server-side RPC
 * deserialization.
 */
@SuppressWarnings("rawtypes")
public class RPCTypeCheckArraysTest extends TestCase {

  /*
   * Test that primitive arrays cannot be replaced by non-array types
   *   - int for int[]: testPrimitiveSpoofingArray
   *   - String for String[]: testPrimitiveSpoofingArray
   *   - HashSet for int[]: testHashSetSpoofingArray
   *   - HashSet for String[]: testHashSetSpoofingArray
   *   - HashSet for AClass[]: testHashSetSpoofingClassArray
   * 
   * Test that non-array-type cannot be replaced by array types
   *   - int[] for int: testArraysSpoofingObjects
   *   - String[] for String: testArraytestArraysSpoofingObjects
   *   - String[] for AClass: testArraysSpoofingObjects
   *
   * Test generics parameterized by arrays:
   *   - HashMap<String, int[]>: testArrayAsGeneric
   *   - HashMap<String, Integer[]>: testArrayAsGeneric
   *   - HashMap<String, AClass[]>: testArrayAsGeneric
   *
   * Test arrays of generics:
   *   - List<Integer>[]: testArrayOfGeneric
   *   - List[]: testArrayOfGeneric
   *   - List<? extends HashSet<Integer>>[]: testArrayOfGeneric
   */  
  
  
  /**
   * A class for testing spoofing of arrays.
   */
  public static class ArraysParamTestClass implements RemoteService {
    @SuppressWarnings("unused")
    public static void testArrays(int[] intArg, String[] stringArg) { }

    @SuppressWarnings("unused")
    public static void testAClassArray(RPCTypeCheckTest.AClass[] arg1) { }
    
    @SuppressWarnings("unused")
    public static void testGenericsArrays(
        HashMap<String, int[]> arg1,
        HashMap<String, Integer[]> arg2,
        HashMap<String, RPCTypeCheckTest.AClass[]> arg3) { }
    
    @SuppressWarnings("unused")
    public static void testArrayOfGeneric(List<Integer>[] arg1) { }
    
    @SuppressWarnings("unused")
    public static void testArrayOfGenericRaw(List[] arg1) { }
    
    @SuppressWarnings("unused")
    public static void testArrayOfGenericWildcard(List<? extends HashSet<Integer>>[] arg1) { }
  }

  private static String generateArrayGenericsA() {
    try {
      RPCTypeCheckFactory strFactory =
          new RPCTypeCheckFactory(ArraysParamTestClass.class, "testGenericsArrays");
      
      HashMap<String, HashSet> arg1 = new HashMap<String, HashSet>();
      arg1.put("foo", RPCTypeCheckFactory.generateTestHashSet());
      strFactory.write(arg1);
      
      HashMap<String, Integer[]> arg2 = new HashMap<String, Integer[]>();
      Integer[] entry1 = new Integer[] {12345, 67890};
      arg2.put("bar", entry1);
      strFactory.write(arg2);

      HashMap<String, RPCTypeCheckTest.AClass[]> arg3 =
        new HashMap<String, RPCTypeCheckTest.AClass[]>();
      RPCTypeCheckTest.AClass[] entry2 = new RPCTypeCheckTest.AClass[2];
      entry2[0] = new RPCTypeCheckTest.AClass(234);
      entry2[1] = new RPCTypeCheckTest.AClass(567);
      arg3.put("baz", entry2);
      strFactory.write(arg3);

      return strFactory.toString();
      
    } catch (Exception e) {
      fail(e.getMessage());

      return null;
    }
  }

  private static String generateArrayGenericsB() {
    try {
      RPCTypeCheckFactory strFactory =
          new RPCTypeCheckFactory(ArraysParamTestClass.class, "testGenericsArrays");
      
      HashMap<String, int[]> arg1 = new HashMap<String, int[]>();
      int[] entry1 = new int[] { 234, 567 };
      arg1.put("foo", entry1);
      strFactory.write(arg1);
      
      HashMap<String, HashSet[]> arg2 = new HashMap<String, HashSet[]>();
      HashSet[] entry2 = new HashSet[] { RPCTypeCheckFactory.generateTestHashSet() };
      arg2.put("bar", entry2);
      strFactory.write(arg2);

      HashMap<String, RPCTypeCheckTest.AClass[]> arg3 =
        new HashMap<String, RPCTypeCheckTest.AClass[]>();
      RPCTypeCheckTest.AClass[] entry3 = new RPCTypeCheckTest.AClass[2];
      entry3[0] = new RPCTypeCheckTest.AClass(234);
      entry3[1] = new RPCTypeCheckTest.AClass(567);
      arg3.put("baz", entry3);
      strFactory.write(arg3);

      return strFactory.toString();
      
    } catch (Exception e) {
      fail(e.getMessage());

      return null;
    }
  }

  private static String generateArrayGenericsC() {
    try {
      RPCTypeCheckFactory strFactory =
          new RPCTypeCheckFactory(ArraysParamTestClass.class, "testGenericsArrays");
      
      HashMap<String, int[]> arg1 = new HashMap<String, int[]>();
      int[] entry1 = new int[] { 234, 567 };
      arg1.put("foo", entry1);
      strFactory.write(arg1);
      
      HashMap<String, Integer[]> arg2 = new HashMap<String, Integer[]>();
      Integer[] entry2 = new Integer[] {12345, 67890};
      arg2.put("bar", entry2);
      strFactory.write(arg2);

      HashMap<String, HashSet[]> arg3 = new HashMap<String, HashSet[]>();
      HashSet[] entry3 = new HashSet[] { RPCTypeCheckFactory.generateTestHashSet() };
      arg3.put("baz", entry3);
      strFactory.write(arg3);

      return strFactory.toString();
      
    } catch (Exception e) {
      fail(e.getMessage());

      return null;
    }
  }

  private static String generateArrayOfGenericsA(String methodName) {
    try {
      RPCTypeCheckFactory strFactory =
          new RPCTypeCheckFactory(ArraysParamTestClass.class, methodName);
      
      HashMap<String, HashSet> arg1 = new HashMap<String, HashSet>();
      arg1.put("foo", RPCTypeCheckFactory.generateTestHashSet());
      strFactory.write(arg1);
      
      return strFactory.toString();
      
    } catch (Exception e) {
      fail(e.getMessage());

      return null;
    }
  }

  private static String generateArrayOfGenericsB(String methodName) {
    try {
      RPCTypeCheckFactory strFactory =
          new RPCTypeCheckFactory(ArraysParamTestClass.class, methodName);
      
      LinkedList[] arg1 = new LinkedList[1];
      LinkedList<TestLinkedHashSet> list = new LinkedList<TestLinkedHashSet>();
      list.add(RPCTypeCheckFactory.generateTestLinkedHashSet());
      arg1[0] = list;
      strFactory.write(arg1);
      
      return strFactory.toString();
      
    } catch (Exception e) {
      fail(e.getMessage());

      return null;
    }
  }

  @SuppressWarnings("unchecked")
  private static String generateArrayOfGenericsC(String methodName) {
    try {
      RPCTypeCheckFactory strFactory =
          new RPCTypeCheckFactory(ArraysParamTestClass.class, methodName);
      
      LinkedList<HashSet<Integer>>[] arg1 = new LinkedList[1];
      LinkedList<HashSet<Integer>> list = new LinkedList<HashSet<Integer>>();
      HashSet<Integer> listElmt = new HashSet<Integer>();
      listElmt.add(12345);
      listElmt.add(67890);
      list.add(listElmt);
      arg1[0] = list;
      strFactory.write(arg1);
      
      return strFactory.toString();
      
    } catch (Exception e) {
      fail(e.getMessage());

      return null;
    }
  }

  private static String generateHashSetSpoofingClassArray() {
    try {
      RPCTypeCheckFactory strFactory =
          new RPCTypeCheckFactory(ArraysParamTestClass.class, "testAClassArray");

      HashSet<Integer> hashSet = new HashSet<Integer>();
      hashSet.add(12345);
      hashSet.add(67890);
      strFactory.write(hashSet);

      return strFactory.toString();

    } catch (Exception e) {
      fail(e.getMessage());

      return null;
    }
  }

  private static String generateHashSetSpoofingIntArray() {
    try {
      RPCTypeCheckFactory strFactory =
          new RPCTypeCheckFactory(ArraysParamTestClass.class, "testArrays");

      HashSet<Integer> hashSet = new HashSet<Integer>();
      hashSet.add(12345);
      hashSet.add(67890);
      strFactory.write(hashSet);

      String[] stringArray = new String[]{"foo", "bar"};
      strFactory.write(stringArray);

      return strFactory.toString();

    } catch (Exception e) {
      fail(e.getMessage());

      return null;
    }
  }

  private static String generateHashSetSpoofingStringArray() {
    try {
      RPCTypeCheckFactory strFactory =
          new RPCTypeCheckFactory(ArraysParamTestClass.class, "testArrays");

      int[] intArray = new int[]{12345, 67890};
      strFactory.write(intArray);

      HashSet<Integer> hashSet = new HashSet<Integer>();
      hashSet.add(12345);
      hashSet.add(67890);
      strFactory.write(hashSet);

      return strFactory.toString();

    } catch (Exception e) {
      fail(e.getMessage());

      return null;
    }
  }

  private static String generateIntArraySpoofingInt() {
    try {
      RPCTypeCheckFactory strFactory =
          new RPCTypeCheckFactory(RPCTypeCheckTest.PrimitiveParamTestClass.class, "testIntString");

      int[] iArray = new int[]{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12};
      strFactory.write(iArray);

      strFactory.write("foo");

      return strFactory.toString();

    } catch (Exception e) {
      fail(e.getMessage());

      return null;
    }
  }

  private static String generateIntSpoofingIntArray() {
    try {
      RPCTypeCheckFactory strFactory =
          new RPCTypeCheckFactory(ArraysParamTestClass.class, "testArrays");

      int i = 12345;
      strFactory.write(i);

      String[] stringArray = new String[]{"foo", "bar"};
      strFactory.write(stringArray);

      return strFactory.toString();

    } catch (Exception e) {
      fail(e.getMessage());

      return null;
    }
  }
  
  private static String generateStringArraySpoofingClass() {
    try {
      RPCTypeCheckFactory strFactory =
          new RPCTypeCheckFactory(RPCTypeCheckTest.ClassesParamTestClass.class, "testAClass");

      String a = "a";
      String[] stringArray = new String[]{a, a, a, a, a, a, a, a, a, a, a, a, a, a};
      strFactory.write(stringArray);

      return strFactory.toString();

    } catch (Exception e) {
      fail(e.getMessage());

      return null;
    }
  }

  private static String generateStringArraySpoofingString() {
    try {
      RPCTypeCheckFactory strFactory =
          new RPCTypeCheckFactory(RPCTypeCheckTest.PrimitiveParamTestClass.class, "testIntString");

      int i = 12345;
      strFactory.write(i);

      String a = "a";
      String[] stringArray = new String[]{a, a, a, a, a, a, a, a, a, a, a, a, a, a};
      strFactory.write(stringArray);

      return strFactory.toString();

    } catch (Exception e) {
      fail(e.getMessage());

      return null;
    }
  }

  private static String generateStringSpoofingStringArray() {
    try {
      RPCTypeCheckFactory strFactory =
          new RPCTypeCheckFactory(ArraysParamTestClass.class, "testArrays");

      int[] arg1 = new int[]{12345, 67890};
      strFactory.write(arg1);

      String arg2 = "bar";
      strFactory.write(arg2);

      return strFactory.toString();

    } catch (Exception e) {
      fail(e.getMessage());

      return null;
    }
  }

  /**
   * This checks that arrays as generic types are checked correctly.
   */
  public void testArrayGenerics() {
    try {
      RPC.decodeRequest(generateArrayGenericsA());
      fail("Expected IncompatibleRemoteServiceException from testArrayGenerics (1)");
    } catch (IncompatibleRemoteServiceException e) {
      // Expected to get here
      assertEquals(SerializedTypeViolationException.class, e.getCause().getClass());
      assertTrue(e.getCause().getMessage().matches(".*HashSet.*int.*"));
    }
    try {
      RPC.decodeRequest(generateArrayGenericsB());
      fail("Expected IncompatibleRemoteServiceException from testArrayGenerics (2)");
    } catch (IncompatibleRemoteServiceException e) {
      // Expected to get here
      assertEquals(SerializedTypeViolationException.class, e.getCause().getClass());
      assertTrue(e.getCause().getMessage().matches(".*HashSet.*Integer.*"));
    }
    try {
      RPC.decodeRequest(generateArrayGenericsC());
      fail("Expected IncompatibleRemoteServiceException from testArrayGenerics (3)");
    } catch (IncompatibleRemoteServiceException e) {
      // Expected to get here
      assertEquals(SerializedTypeViolationException.class, e.getCause().getClass());
      assertTrue(e.getCause().getMessage().matches(".*HashSet.*AClass.*"));
    }
  }

  /**
   * This checks that arrays of generic types are checked correctly.
   */
  public void testArrayOfGenerics() {
    try {
      RPC.decodeRequest(generateArrayOfGenericsC("testArrayOfGenericRaw"));
      // Expect to pass
    } catch (Exception e) {
      fail("Unexpected exception from testArrayOfGenerics (0): " + e.getMessage());
    }
    try {
      RPC.decodeRequest(generateArrayOfGenericsA("testArrayOfGeneric"));
      fail("Expected IncompatibleRemoteServiceException from testArrayOfGenerics (1)");
    } catch (IncompatibleRemoteServiceException e) {
      // Expected to get here
      assertEquals(SerializedTypeViolationException.class, e.getCause().getClass());
      assertTrue(e.getCause().getMessage().matches(".*HashMap.*List.*"));
    }
    try {
      RPC.decodeRequest(generateArrayOfGenericsB("testArrayOfGeneric"));
      fail("Expected IncompatibleRemoteServiceException from testArrayOfGenerics (2)");
    } catch (IncompatibleRemoteServiceException e) {
      // Expected to get here
      assertEquals(SerializedTypeViolationException.class, e.getCause().getClass());
      assertTrue(e.getCause().getMessage().matches(".*HashSet.*Integer.*"));
    }
    try {
      RPC.decodeRequest(generateArrayOfGenericsA("testArrayOfGenericWildcard"));
      fail("Expected IncompatibleRemoteServiceException from testArrayOfGenerics (3)");
    } catch (IncompatibleRemoteServiceException e) {
      // Expected to get here
      assertEquals(SerializedTypeViolationException.class, e.getCause().getClass());
      assertTrue(e.getCause().getMessage().matches(".*HashMap.*List.*"));
    }
    try {
      RPC.decodeRequest(generateArrayOfGenericsC("testArrayOfGenericWildcard"));
    } catch (Exception e) {
      fail("Unexpected Exception from testArrayOfGenerics (4): " + e.getMessage());
    }
    try {
      RPC.decodeRequest(generateArrayOfGenericsB("testArrayOfGenericWildcard"));
      fail("Expected IncompatibleRemoteServiceException from testArrayOfGenerics (5)");
    } catch (IncompatibleRemoteServiceException e) {
      // Expected to get here
      assertEquals(SerializedTypeViolationException.class, e.getCause().getClass());
      assertTrue(e.getCause().getMessage().matches(".*HashSet.*Integer.*"));
    }
  }

  /**
   * This checks that arrays cannot be used in place of primitives.
   */
  public void testArraysSpoofingObjects() {
    try {
      RPC.decodeRequest(generateIntArraySpoofingInt());
      fail("Expected ArrayIndexOutOfBoundsException from testArraysSpoofingPrimitives (1)");
    } catch (Exception e) {
      // Expected to get here
      assertTrue(e instanceof ArrayIndexOutOfBoundsException);
    }
    try {
      RPCRequest result = RPC.decodeRequest(generateStringArraySpoofingString());
      assertTrue(result.getParameters()[1].toString().matches(".*\\[Ljava.lang.String.*"));
    } catch (Exception e) {
      fail("Unexpected Exception from testArraysSpoofingPrimitives (2)");
    }
    try {
      RPC.decodeRequest(generateStringArraySpoofingClass());
      fail("Expected IncompatibleRemoteServiceException from testArraysSpoofingPrimitives (3)");
    } catch (IncompatibleRemoteServiceException e) {
      // Expected to get here
      assertEquals(SerializedTypeViolationException.class, e.getCause().getClass());
      assertTrue(e.getCause().getMessage().matches(".*\\[Ljava.lang.String.*AClass.*"));
    }
  }

  /**
   * This checks situations in which an RPC message is modified to replace
   * arguments of a primitive type with another primitive type.
   */
  public void testHashSetSpoofingArray() {
    try {
      RPC.decodeRequest(generateHashSetSpoofingIntArray());
      fail("Expected IncompatibleRemoteServiceException from testHashSetSpoofingArray (1)");
    } catch (IncompatibleRemoteServiceException e) {
      // Expected to get here. When the int array is processed, it reads the
      // HashSet class and fails to know what it is, because it is not the
      // way an int would be serialized in this context.
      assertEquals(SerializedTypeViolationException.class, e.getCause().getClass());
      assertTrue(e.getCause().getMessage().matches(".*HashSet.*\\[I.*"));
    }
    try {
      RPC.decodeRequest(generateHashSetSpoofingStringArray());
      fail("Expected IncompatibleRemoteServiceException from testHashSetSpoofingArray (2)");
    } catch (IncompatibleRemoteServiceException e) {
      // Expected to get here
      assertEquals(SerializedTypeViolationException.class, e.getCause().getClass());
      assertTrue(e.getCause().getMessage().matches(".*HashSet.*String.*"));
    }
  }

  /**
   * This checks situations in which an RPC message is modified to replace an
   * array of objects with a HashSet (custom serialized container type).
   */
  public void testHashSetSpoofingClassArray() {
    try {
      RPC.decodeRequest(generateHashSetSpoofingClassArray());
      fail("Expected IncompatibleRemoteServiceException from testHashSetSpoofingClassArray");
    } catch (IncompatibleRemoteServiceException e) {
      // Expected to get here.
      assertEquals(SerializedTypeViolationException.class, e.getCause().getClass());
      assertTrue(e.getCause().getMessage().matches(".*HashSet.*AClass.*"));
    }
  }

  /**
   * This checks situations in which an RPC message is modified to replace
   * arguments of a primitive type with another primitive type.
   */
  public void testPrimitiveSpoofingArray() {
    try {
      RPC.decodeRequest(generateIntSpoofingIntArray());
      fail("Expected IncompatibleRemoteServiceException from testPrimitiveSpoofingArray (1)");
    } catch (Exception e) {
      // Expected to get here. When the int array value is processed, it reads
      // the
      // integer value and tries to look it up as a class string, only to run
      // out
      // of bounds in the string table.
      assertTrue(e instanceof ArrayIndexOutOfBoundsException);
    }
    try {
      RPC.decodeRequest(generateStringSpoofingStringArray());
      fail("Expected IncompatibleRemoteServiceException from testPrimitiveSpoofingArray (2)");
    } catch (IncompatibleRemoteServiceException e) {
      // Expected to get here
      assertEquals(SerializationException.class, e.getCause().getClass());
    }
  }
}
