/*
 * Copyright 2002-2017 the original author or authors.
 *
 * 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 org.springframework.web.client

import com.nhaarman.mockito_kotlin.mock
import org.junit.Assert
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.Answers
import org.mockito.Mock
import org.mockito.Mockito.*
import org.mockito.junit.MockitoJUnitRunner
import org.springframework.core.ParameterizedTypeReference
import org.springframework.http.HttpEntity
import org.springframework.http.HttpMethod
import org.springframework.http.RequestEntity
import org.springframework.util.ReflectionUtils
import java.net.URI
import kotlin.reflect.full.createType
import kotlin.reflect.jvm.kotlinFunction

/**
 * Mock object based tests for [RestOperations] Kotlin extensions.
 *
 * @author Sebastien Deleuze
 */
@RunWith(MockitoJUnitRunner::class)
class RestOperationsExtensionsTests {

	@Mock(answer = Answers.RETURNS_MOCKS)
	lateinit var template: RestOperations

	@Test
	fun `getForObject with reified type parameters, String and varargs`() {
		val url = "https://spring.io"
		val var1 = "var1"
		val var2 = "var2"
		template.getForObject<Foo>(url, var1, var2)
		verify(template, times(1)).getForObject(url, Foo::class.java, var1, var2)
	}

	@Test
	fun `getForObject with reified type parameters, String and Map`() {
		val url = "https://spring.io"
		val vars = mapOf(Pair("key1", "value1"), Pair("key2", "value2"))
		template.getForObject<Foo>(url, vars)
		verify(template, times(1)).getForObject(url, Foo::class.java, vars)
	}

	@Test
	fun `getForObject with reified type parameters and URI`() {
		val url = URI("https://spring.io")
		template.getForObject<Foo>(url)
		verify(template, times(1)).getForObject(url, Foo::class.java)
	}

	@Test
	fun `getForEntity with reified type parameters, String and URI`() {
		val url = URI("https://spring.io")
		template.getForEntity<Foo>(url)
		verify(template, times(1)).getForEntity(url, Foo::class.java)
	}

	@Test
	fun `getForEntity with reified type parameters, String and varargs`() {
		val url = "https://spring.io"
		val var1 = "var1"
		val var2 = "var2"
		template.getForEntity<Foo>(url, var1, var2)
		verify(template, times(1)).getForEntity(url, Foo::class.java, var1, var2)
	}

	@Test
	fun `getForEntity with reified type parameters and Map`() {
		val url = "https://spring.io"
		val vars = mapOf(Pair("key1", "value1"), Pair("key2", "value2"))
		template.getForEntity<Foo>(url, vars)
		verify(template, times(1)).getForEntity(url, Foo::class.java, vars)
	}

	@Test
	fun `patchForObject with reified type parameters, String and varargs`() {
		val url = "https://spring.io"
		val body: Any = "body"
		val var1 = "var1"
		val var2 = "var2"
		template.patchForObject<Foo>(url, body, var1, var2)
		verify(template, times(1)).patchForObject(url, body, Foo::class.java, var1, var2)
	}

	@Test
	fun `patchForObject with reified type parameters, String and Map`() {
		val url = "https://spring.io"
		val body: Any = "body"
		val vars = mapOf(Pair("key1", "value1"), Pair("key2", "value2"))
		template.patchForObject<Foo>(url, body, vars)
		verify(template, times(1)).patchForObject(url, body, Foo::class.java, vars)
	}

	@Test
	fun `patchForObject with reified type parameters and String`() {
		val url = "https://spring.io"
		val body: Any = "body"
		template.patchForObject<Foo>(url, body)
		verify(template, times(1)).patchForObject(url, body, Foo::class.java)
	}

	@Test
	fun `patchForObject with reified type parameters`() {
		val url = "https://spring.io"
		template.patchForObject<Foo>(url)
		verify(template, times(1)).patchForObject(url, null, Foo::class.java)
	}

	@Test
	fun `postForObject with reified type parameters, String and varargs`() {
		val url = "https://spring.io"
		val body: Any = "body"
		val var1 = "var1"
		val var2 = "var2"
		template.postForObject<Foo>(url, body, var1, var2)
		verify(template, times(1)).postForObject(url, body, Foo::class.java, var1, var2)
	}

	@Test
	fun `postForObject with reified type parameters, String and Map`() {
		val url = "https://spring.io"
		val body: Any = "body"
		val vars = mapOf(Pair("key1", "value1"), Pair("key2", "value2"))
		template.postForObject<Foo>(url, body, vars)
		verify(template, times(1)).postForObject(url, body, Foo::class.java, vars)
	}

	@Test
	fun `postForObject with reified type parameters and String`() {
		val url = "https://spring.io"
		val body: Any = "body"
		template.postForObject<Foo>(url, body)
		verify(template, times(1)).postForObject(url, body, Foo::class.java)
	}

	@Test
	fun `postForObject with reified type parameters`() {
		val url = "https://spring.io"
		template.postForObject<Foo>(url)
		verify(template, times(1)).postForObject(url, null, Foo::class.java)
	}

	@Test
	fun `postForEntity with reified type parameters, String and varargs`() {
		val url = "https://spring.io"
		val body: Any = "body"
		val var1 = "var1"
		val var2 = "var2"
		template.postForEntity<Foo>(url, body, var1, var2)
		verify(template, times(1)).postForEntity(url, body, Foo::class.java, var1, var2)
	}

	@Test
	fun `postForEntity with reified type parameters, String and Map`() {
		val url = "https://spring.io"
		val body: Any = "body"
		val vars = mapOf(Pair("key1", "value1"), Pair("key2", "value2"))
		template.postForEntity<Foo>(url, body, vars)
		verify(template, times(1)).postForEntity(url, body, Foo::class.java, vars)
	}

	@Test
	fun `postForEntity with reified type parameters and String`() {
		val url = "https://spring.io"
		val body: Any = "body"
		template.postForEntity<Foo>(url, body)
		verify(template, times(1)).postForEntity(url, body, Foo::class.java)
	}

	@Test
	fun `postForEntity with reified type parameters`() {
		val url = "https://spring.io"
		template.postForEntity<Foo>(url)
		verify(template, times(1)).postForEntity(url, null, Foo::class.java)
	}

	@Test
	fun `exchange with reified type parameters, String, HttpMethod, HttpEntity and varargs`() {
		val url = "https://spring.io"
		val method = HttpMethod.GET
		val entity = mock<HttpEntity<Foo>>()
		val var1 = "var1"
		val var2 = "var2"
		template.exchange<List<Foo>>(url, method, entity, var1, var2)
		verify(template, times(1)).exchange(url, method, entity,
				object : ParameterizedTypeReference<List<Foo>>() {}, var1, var2)
	}

	@Test
	fun `exchange with reified type parameters, String, HttpMethod, HttpEntity and Map`() {
		val url = "https://spring.io"
		val method = HttpMethod.GET
		val entity = mock<HttpEntity<Foo>>()
		val vars = mapOf(Pair("key1", "value1"), Pair("key2", "value2"))
		template.exchange<List<Foo>>(url, method, entity, vars)
		verify(template, times(1)).exchange(url, method, entity,
				object : ParameterizedTypeReference<List<Foo>>() {}, vars)
	}

	@Test
	fun `exchange with reified type parameters, String, HttpMethod and HttpEntity`() {
		val url = "https://spring.io"
		val method = HttpMethod.GET
		val entity = mock<HttpEntity<Foo>>()
		template.exchange<List<Foo>>(url, method, entity)
		verify(template, times(1)).exchange(url, method, entity,
				object : ParameterizedTypeReference<List<Foo>>() {})
	}

	@Test
	fun `exchange with reified type parameters, String and HttpMethod`() {
		val url = "https://spring.io"
		val method = HttpMethod.GET
		template.exchange<List<Foo>>(url, method)
		verify(template, times(1)).exchange(url, method, null,
				object : ParameterizedTypeReference<List<Foo>>() {})
	}

	@Test
	fun `exchange with reified type parameters, String and HttpEntity`() {
		val entity = mock<RequestEntity<Foo>>()
		template.exchange<List<Foo>>(entity)
		verify(template, times(1)).exchange(entity,
				object : ParameterizedTypeReference<List<Foo>>() {})
	}

	@Test
	fun `RestOperations are available`() {
		val extensions = Class.forName("org.springframework.web.client.RestOperationsExtensionsKt")
		ReflectionUtils.doWithMethods(RestOperations::class.java) { method ->
			arrayOf(ParameterizedTypeReference::class, Class::class).forEach { kClass ->
				if (method.parameterTypes.contains(kClass.java)) {
					val parameters = mutableListOf<Class<*>>(RestOperations::class.java).apply { addAll(method.parameterTypes.filter { it !=  kClass.java }) }
					val f = extensions.getDeclaredMethod(method.name, *parameters.toTypedArray()).kotlinFunction!!
					Assert.assertEquals(1, f.typeParameters.size)
					Assert.assertEquals(listOf(Any::class.createType()), f.typeParameters[0].upperBounds)
				}
			}
		}
	}

	class Foo

}
