/***
 * Copyright (c) 2009 Caelum - www.caelum.com.br/opensource 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 br.com.caelum.vraptor.serialization.gson;

import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.instanceOf;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.nullValue;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertThat;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;

import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.lang.reflect.Method;
import java.lang.reflect.Type;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Date;
import java.util.GregorianCalendar;
import java.util.List;
import java.util.TimeZone;

import javax.enterprise.inject.Instance;
import javax.servlet.http.HttpServletRequest;

import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;

import com.google.gson.JsonDeserializationContext;
import com.google.gson.JsonDeserializer;
import com.google.gson.JsonElement;
import com.google.gson.JsonParseException;
import com.google.gson.JsonSerializer;

import br.com.caelum.vraptor.Consumes;
import br.com.caelum.vraptor.controller.BeanClass;
import br.com.caelum.vraptor.controller.ControllerMethod;
import br.com.caelum.vraptor.controller.DefaultBeanClass;
import br.com.caelum.vraptor.controller.DefaultControllerMethod;
import br.com.caelum.vraptor.core.DefaultReflectionProvider;
import br.com.caelum.vraptor.http.ParameterNameProvider;
import br.com.caelum.vraptor.http.ParanamerNameProvider;
import br.com.caelum.vraptor.ioc.Container;
import br.com.caelum.vraptor.serialization.Deserializee;
import br.com.caelum.vraptor.serialization.Serializee;
import br.com.caelum.vraptor.util.test.MockInstanceImpl;
import br.com.caelum.vraptor.view.GenericController;
import net.vidageek.mirror.dsl.Mirror;

public class GsonDeserializerTest {

	@Rule
	public ExpectedException exception = ExpectedException.none();

	private GsonDeserializerBuilder builder;
	private GsonDeserialization deserializer;
	private ParameterNameProvider provider;
	private HttpServletRequest request;

	private ControllerMethod dogParameter;
	private ControllerMethod dogAndIntegerParameter;
	private ControllerMethod noParameter;
	private ControllerMethod listDog;
	private ControllerMethod integerAndDogParameter;
	private ControllerMethod dateParameter;
	private ControllerMethod dogParameterWithoutRoot;
	private ControllerMethod dogParameterNameEqualsJsonPropertyWithoutRoot;

	private @Mock Container container;

	private @Mock Instance<Deserializee> deserializeeInstance;

	@Before
	public void setUp() throws Exception {
		MockitoAnnotations.initMocks(this);
		
		TimeZone.setDefault(TimeZone.getTimeZone("GMT-0300"));
		provider = new ParanamerNameProvider();
		request = mock(HttpServletRequest.class);
		List<JsonDeserializer<?>> jsonDeserializers = new ArrayList<>();
		List<JsonSerializer<?>> jsonSerializers = new ArrayList<>();
		jsonDeserializers.add(new CalendarGsonConverter());
		jsonDeserializers.add(new DateGsonConverter());

		builder = new GsonBuilderWrapper(new MockInstanceImpl<>(jsonSerializers), new MockInstanceImpl<>(jsonDeserializers), 
				new Serializee(new DefaultReflectionProvider()), new DefaultReflectionProvider());
		deserializer = new GsonDeserialization(builder, provider, request, container, deserializeeInstance);
		BeanClass controllerClass = new DefaultBeanClass(DogController.class);

		noParameter = new DefaultControllerMethod(controllerClass, DogController.class.getDeclaredMethod("noParameter"));
		dogParameter = new DefaultControllerMethod(controllerClass, DogController.class.getDeclaredMethod("dogParameter", Dog.class));
		dateParameter = new DefaultControllerMethod(controllerClass, DogController.class.getDeclaredMethod("dateParameter", Date.class));
		dogAndIntegerParameter = new DefaultControllerMethod(controllerClass, DogController.class.getDeclaredMethod("dogAndIntegerParameter", Dog.class,
				Integer.class));
		integerAndDogParameter = new DefaultControllerMethod(controllerClass, DogController.class.getDeclaredMethod("integerAndDogParameter",
				Integer.class, Dog.class));
		listDog = new DefaultControllerMethod(controllerClass, DogController.class.getDeclaredMethod("list", List.class));
		dogParameterWithoutRoot = new DefaultControllerMethod(controllerClass, DogController.class.getDeclaredMethod("dogParameterWithoutRoot", Dog.class));
		dogParameterNameEqualsJsonPropertyWithoutRoot = new DefaultControllerMethod(controllerClass, DogController.class.getDeclaredMethod("dogParameterNameEqualsJsonPropertyWithoutRoot", Dog.class));

		when(deserializeeInstance.get()).thenReturn(new Deserializee());
		when(container.instanceFor(WithRoot.class)).thenReturn(new WithRoot());
		when(container.instanceFor(WithoutRoot.class)).thenReturn(new WithoutRoot());
	}

	static class Dog {
		private String name;
		private Integer age;
		private Calendar birthday;
	}
	
	static class DogController {

		public void noParameter() {}

		@Consumes(options=WithRoot.class)
		public void dogParameter(Dog dog) {}

		@Consumes
		public void dogParameterWithoutRoot(Dog dog) {}

		@Consumes(options=WithoutRoot.class)
		public void dogParameterNameEqualsJsonPropertyWithoutRoot(Dog name) {}

		@Consumes
		public void dogAndIntegerParameter(Dog dog, Integer times) {}

		@Consumes
		public void integerAndDogParameter(Integer times, Dog dog) {}

		@Consumes
		public void dateParameter(Date date) {}

		@Consumes
		public void list(List<Dog> dogs) {}
	}

	private class DogDeserializer implements JsonDeserializer<Dog> {

		@Override
		public Dog deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context)
				throws JsonParseException {
			Dog dog = new Dog();
			dog.name = "Renan";
			dog.age = 25;

			return dog;
		}
	}

	static class DogGenericController extends GenericController<Dog> {

	}

	@Test
	public void shouldDeserializerParseArraysWithoutRoot(){
		InputStream stream = asStream(
			"[" + 
				"{'name':'name1','age':1}," + 
				"{'name':'name2','age':2}," + 
				"{'name':'name3','age':3}" + 
			"]");
 
		Object[] deserialized = deserializer.deserialize(stream, listDog);
		List<Dog>  dogs = (List<Dog>) deserialized[0]; 
		assertThat(dogs.size(), is(3));
		assertThat(dogs.get(0), is(instanceOf(Dog.class)));
		Dog dog = (Dog) dogs.get(0);
		assertThat(dog.name, is("name1"));
		assertThat(dog.age, is(1));
	}

	@Test
	public void shouldDeserializerParseArraysWithRoot(){
		InputStream stream = asStream(
			"{dogs : [" + 
				"{'name':'name1','age':1}," + 
				"{'name':'name2','age':2}," + 
				"{'name':'name3','age':3}" + 
			"]}");
 
		Object[] deserialized = deserializer.deserialize(stream, listDog);
		List<Dog>  dogs = (List<Dog>) deserialized[0]; 
		assertThat(dogs.size(), is(3));
		assertThat(dogs.get(0), is(instanceOf(Dog.class)));
		Dog dog = (Dog) dogs.get(0);
		assertThat(dog.name, is("name1"));
		assertThat(dog.age, is(1));
	}

	@Test
	public void shouldNotAcceptMethodsWithoutArguments() throws Exception {
		exception.expect(IllegalArgumentException.class);
		exception.expectMessage("Methods that consumes representations must receive just one argument");

		deserializer.deserialize(emptyStream(), noParameter);
	}

	@Test
	public void shouldBeAbleToDeserializeADog() throws Exception {
		InputStream stream = asStream("{'dog':{'name':'Brutus','age':7}}");

		Object[] deserialized = deserializer.deserialize(stream, dogParameter);

		assertThat(deserialized.length, is(1));
		assertThat(deserialized[0], is(instanceOf(Dog.class)));
		Dog dog = (Dog) deserialized[0];
		assertThat(dog.name, is("Brutus"));
		assertThat(dog.age, is(7));
	}

	@Test
	public void shouldBeAbleToDeserializeADogWithoutRootAndParameterNameEqualsJsonProperty() throws Exception {
		InputStream stream = asStream("{'name':'Brutus','age':7}");

		Object[] deserialized = deserializer.deserialize(stream, dogParameterNameEqualsJsonPropertyWithoutRoot);

		assertThat(deserialized.length, is(1));
		assertThat(deserialized[0], is(instanceOf(Dog.class)));
		Dog dog = (Dog) deserialized[0];
		assertThat(dog.name, is("Brutus"));
		assertThat(dog.age, is(7));
	}

	@Test
	public void shouldBeAbleToDeserializeADogWithoutRoot() throws Exception {
		InputStream stream = asStream("{'name':'Brutus','age':7}");
		
		Object[] deserialized = deserializer.deserialize(stream, dogParameterWithoutRoot);
		
		assertThat(deserialized.length, is(1));
		assertThat(deserialized[0], is(instanceOf(Dog.class)));
		Dog dog = (Dog) deserialized[0];
		assertThat(dog.name, is("Brutus"));
		assertThat(dog.age, is(7));
	}

	@Test
	public void shouldBeAbleToDeserializeADogWithDeserializerAdapter() throws Exception {
		List<JsonDeserializer<?>> deserializers = new ArrayList<>();
		List<JsonSerializer<?>> serializers = new ArrayList<>();
		deserializers.add(new DogDeserializer());

		builder = new GsonBuilderWrapper(new MockInstanceImpl<>(serializers), new MockInstanceImpl<>(deserializers), 
				new Serializee(new DefaultReflectionProvider()), new DefaultReflectionProvider());
		deserializer = new GsonDeserialization(builder, provider, request, container, deserializeeInstance);

		InputStream stream = asStream("{'dog':{'name':'Renan Reis','age':'0'}}");

		Object[] deserialized = deserializer.deserialize(stream, dogParameter);

		assertThat(deserialized.length, is(1));
		assertThat(deserialized[0], is(instanceOf(Dog.class)));
		Dog dog = (Dog) deserialized[0];
		assertThat(dog.name, is("Renan"));
		assertThat(dog.age, is(25));
	}

	@Test
	public void shouldBeAbleToDeserializeADogWhenMethodHasMoreThanOneArgument() throws Exception {
		InputStream stream = asStream("{'dog':{'name':'Brutus','age':7}}");

		Object[] deserialized = deserializer.deserialize(stream, dogAndIntegerParameter);

		assertThat(deserialized.length, is(2));
		assertThat(deserialized[0], is(instanceOf(Dog.class)));
		Dog dog = (Dog) deserialized[0];
		assertThat(dog.name, is("Brutus"));
		assertThat(dog.age, is(7));
	}

	@Test
	public void shouldBeAbleToDeserializeADogWhenMethodHasMoreThanOneArgumentAndJsonIsTheLastOne() throws Exception {
		InputStream stream = asStream("{'dog':{'name':'Brutus','age':7}}");

		Object[] deserialized = deserializer.deserialize(stream, integerAndDogParameter);

		assertThat(deserialized.length, is(2));
		assertThat(deserialized[1], is(instanceOf(Dog.class)));
		Dog dog = (Dog) deserialized[1];
		assertThat(dog.name, is("Brutus"));
		assertThat(dog.age, is(7));
	}

	@Test
	public void shouldBeAbleToDeserializeADogNamedDifferently() throws Exception {
		InputStream stream = asStream("{'dog':{'name':'Brutus','age':7}}");

		Object[] deserialized = deserializer.deserialize(stream, dogParameter);

		assertThat(deserialized.length, is(1));
		assertThat(deserialized[0], is(instanceOf(Dog.class)));
		Dog dog = (Dog) deserialized[0];
		assertThat(dog.name, is("Brutus"));
		assertThat(dog.age, is(7));
	}

	@Test
	public void shouldHonorRequestHeaderAcceptCharset() throws Exception {
		InputStream stream = asStream("{'dog':{'name':'ç'}}", StandardCharsets.ISO_8859_1);
		
		when(request.getHeader("Accept-Charset")).thenReturn("UTF-8,*;q=0.5");
		
		Object[] deserialized = deserializer.deserialize(stream, dogParameter);

		assertThat(deserialized.length, is(1));
		assertThat(deserialized[0], is(instanceOf(Dog.class)));

		Dog dog = (Dog) deserialized[0];

		assertThat(dog.name, is("ç"));
	}

	@Test
	public void whenNoCharsetHeaderIsFoundThanAssumeItIsUTF8() throws Exception {
		InputStream stream = asStream("{'dog':{'name':'ç'}}",  StandardCharsets.ISO_8859_1);

		when(request.getHeader("Accept-Charset")).thenReturn(null);
		
		Object[] deserialized = deserializer.deserialize(stream, dogParameter);

		assertThat(deserialized.length, is(1));
		assertThat(deserialized[0], is(instanceOf(Dog.class)));

		Dog dog = (Dog) deserialized[0];

		assertThat(dog.name, is("ç"));
	}

	@Test
	public void shouldByPassDeserializationWhenHasNoContent() {
		Object[] deserialized = deserializer.deserialize(emptyStream(), dogParameter);

		assertThat(deserialized.length, is(1));
		assertThat(deserialized[0], is(nullValue()));
	}

	@Test
	public void shouldBeAbleToDeserializeADogWhenMethodHasMoreThanOneArgumentAndHasNotRoot() {
		InputStream stream = asStream("{'name':'Brutus','age':7}");

		Object[] deserialized = deserializer.deserialize(stream, dogAndIntegerParameter);

		assertThat(deserialized[0], is(instanceOf(Dog.class)));
		Dog dog = (Dog) deserialized[0];
		assertThat(dog.name, is("Brutus"));
		assertThat(dog.age, is(7));
	}

	@Test
	public void shouldDeserializeFromGenericTypeOneParam() {
		InputStream stream = asStream("{'entity':{'name':'Brutus','age':7,'birthday':'2013-07-23T17:14:14-03:00'}}");
		BeanClass resourceClass = new DefaultBeanClass(DogGenericController.class);
		Method method = new Mirror().on(DogGenericController.class).reflect().method("method").withAnyArgs();
		ControllerMethod resource = new DefaultControllerMethod(resourceClass, method);
		
		Object[] deserialized = deserializer.deserialize(stream, resource);

		Dog dog = (Dog) deserialized[0];

		assertThat(dog.name, equalTo("Brutus"));
	}

	@Test
	public void shouldDeserializeFromGenericTypeWithoutRoot() {
		InputStream stream = asStream("{'name':'Brutus','age':7,'birthday':'2013-07-23T17:14:14-03:00'}");
		BeanClass resourceClass = new DefaultBeanClass(DogGenericController.class);
		Method method = new Mirror().on(DogGenericController.class).reflect().method("method").withAnyArgs();
		ControllerMethod resource = new DefaultControllerMethod(resourceClass, method);

		Object[] deserialized = deserializer.deserialize(stream, resource);

		Dog dog = (Dog) deserialized[0];

		assertThat(dog.name, equalTo("Brutus"));
		assertThat(dog.age, equalTo(7));
	}

	@Test
	public void shouldDeserializeFromGenericTypeTwoParams() {
		InputStream stream = asStream("{'entity':{'name':'Brutus','age':7,'birthday':'2013-07-23T17:14:14-03:00'}, 'param': 'test', 'over': 'value'}");
		BeanClass resourceClass = new DefaultBeanClass(DogGenericController.class);
		Method method = new Mirror().on(DogGenericController.class).reflect().method("anotherMethod").withAnyArgs();
		ControllerMethod resource = new DefaultControllerMethod(resourceClass, method);
		
		Object[] deserialized = deserializer.deserialize(stream, resource);

		Dog dog = (Dog) deserialized[0];
		String param = (String) deserialized[1];

		assertThat(dog.name, equalTo("Brutus"));
		assertThat(param, equalTo("test"));
		assertThat(deserialized.length, equalTo(2));
	}

	@Test
	public void shouldDeserializeADogWithCalendarWithISO8601() {
		InputStream stream = asStream("{'dog':{'name':'Otto','age':2,'birthday':'2013-07-23T17:14:14-03:00'}}");

		Object[] deserialized = deserializer.deserialize(stream, dogParameter);

		assertThat(deserialized.length, is(1));
		assertThat(deserialized[0], is(instanceOf(Dog.class)));
		Dog dog = (Dog) deserialized[0];
		assertThat(dog.name, is("Otto"));
		assertThat(dog.age, is(2));

		Calendar birthday = new GregorianCalendar(2013, 6, 23, 17, 14, 14);
		birthday.setTimeZone(TimeZone.getTimeZone("GMT-0300"));

		// calendar.equals is too bad :)
		assertThat(dog.birthday.compareTo(birthday), is(0));
	}

	@Test
	public void shouldDeserializeADateWithISO8601() {
		InputStream stream = asStream("{\"date\":\"1988-02-25T02:30:15 -0300\"}");
		
		Object[] deserialized = deserializer.deserialize(stream, dateParameter);
		assertThat(deserialized.length, is(1));
		assertThat(deserialized[0], is(instanceOf(Date.class)));
		Date deserializedDate = (Date) deserialized[0];
		Date date = new GregorianCalendar(1988, 1, 25, 2, 30, 15).getTime();
		assertEquals(date, deserializedDate);
	}
	
	private InputStream asStream(String str, Charset charset) {
		return new ByteArrayInputStream(str.getBytes(charset));
	}

	private InputStream asStream(String str) {
		return asStream(str, Charset.defaultCharset());
	}

	private InputStream emptyStream() {
		return new ByteArrayInputStream(new byte[0]);
	}
}