/*
 * Copyright 2012-present 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
 *
 *      https://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.boot.restclient.autoconfigure.service;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;
import java.net.http.HttpClient;
import java.net.http.HttpClient.Redirect;
import java.time.Duration;
import java.util.HashMap;
import java.util.Map;

import org.apache.hc.client5.http.HttpRoute;
import org.apache.hc.client5.http.config.ConnectionConfig;
import org.apache.hc.client5.http.impl.classic.CloseableHttpClient;
import org.apache.hc.core5.function.Resolver;
import org.apache.hc.core5.util.Timeout;
import org.assertj.core.extractor.Extractors;
import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor;

import org.springframework.aop.Advisor;
import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.http.client.ClientHttpRequestFactoryBuilder;
import org.springframework.boot.http.client.HttpClientSettings;
import org.springframework.boot.http.client.HttpRedirects;
import org.springframework.boot.http.client.autoconfigure.HttpClientAutoConfiguration;
import org.springframework.boot.http.client.autoconfigure.imperative.ImperativeHttpClientAutoConfiguration;
import org.springframework.boot.http.client.autoconfigure.service.HttpServiceClientPropertiesAutoConfiguration;
import org.springframework.boot.restclient.RestClientCustomizer;
import org.springframework.boot.restclient.autoconfigure.RestClientAutoConfiguration;
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.client.ClientHttpRequestFactory;
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
import org.springframework.test.util.ReflectionTestUtils;
import org.springframework.test.web.client.MockRestServiceServer;
import org.springframework.web.client.RestClient;
import org.springframework.web.client.RestClient.Builder;
import org.springframework.web.client.support.RestClientHttpServiceGroupConfigurer;
import org.springframework.web.service.annotation.GetExchange;
import org.springframework.web.service.registry.HttpServiceGroup;
import org.springframework.web.service.registry.HttpServiceGroupConfigurer.ClientCallback;
import org.springframework.web.service.registry.HttpServiceGroupConfigurer.Groups;
import org.springframework.web.service.registry.HttpServiceProxyRegistry;
import org.springframework.web.service.registry.ImportHttpServices;

import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.BDDMockito.given;
import static org.mockito.BDDMockito.then;
import static org.mockito.Mockito.mock;
import static org.springframework.test.web.client.match.MockRestRequestMatchers.header;
import static org.springframework.test.web.client.match.MockRestRequestMatchers.requestTo;
import static org.springframework.test.web.client.response.MockRestResponseCreators.withSuccess;

/**
 * Tests for {@link HttpServiceClientPropertiesAutoConfiguration},
 * {@link PropertiesRestClientHttpServiceGroupConfigurer} and
 * {@link RestClientCustomizerHttpServiceGroupConfigurer}.
 *
 * @author Phillip Webb
 */
class HttpServiceClientAutoConfigurationTests {

	private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
		.withConfiguration(AutoConfigurations.of(HttpServiceClientPropertiesAutoConfiguration.class,
				HttpServiceClientAutoConfiguration.class, ImperativeHttpClientAutoConfiguration.class,
				RestClientAutoConfiguration.class));

	@Test
	void configuresClientFromProperties() {
		this.contextRunner
			.withPropertyValues("spring.http.serviceclient.one.default-header.test=true",
					"spring.http.serviceclient.one.base-url=https://example.com/one",
					"spring.http.serviceclient.two.default-header.test=true",
					"spring.http.serviceclient.two.default-header.two=iam2")
			.withUserConfiguration(HttpClientConfiguration.class, MockRestServiceServerConfiguration.class)
			.run((context) -> {
				HttpServiceProxyRegistry serviceProxyRegistry = context.getBean(HttpServiceProxyRegistry.class);
				assertThat(serviceProxyRegistry.getGroupNames()).containsOnly("one", "two");
				MockRestServiceServerConfiguration mockServers = context
					.getBean(MockRestServiceServerConfiguration.class);
				MockRestServiceServer serverOne = mockServers.getMock("one");
				serverOne.expect(requestTo("https://example.com/one/hello"))
					.andExpect(header("test", "true"))
					.andRespond(withSuccess().body("world!"));
				TestClientOne clientOne = context.getBean(TestClientOne.class);
				assertThat(clientOne.hello()).isEqualTo("world!");
				MockRestServiceServer serverTwo = mockServers.getMock("two");
				serverTwo.expect((request) -> request.getURI().toString().equals("https://example.com/"))
					.andExpect(header("test", "true"))
					.andExpect(header("two", "iam2"))
					.andRespond(withSuccess().body("boot!"));
				TestClientTwo clientTwo = context.getBean(TestClientTwo.class);
				assertThat(clientTwo.there()).isEqualTo("boot!");
			});
	}

	@Test // gh-46915
	void configuresClientFromPropertiesWhenHasHttpClientAutoConfiguration() {
		this.contextRunner.withConfiguration(AutoConfigurations.of(HttpClientAutoConfiguration.class))
			.withPropertyValues("spring.http.serviceclient.one.connect-timeout=5s",
					"spring.http.serviceclient.two.connect-timeout=10s")
			.withUserConfiguration(HttpClientConfiguration.class, MockRestServiceServerConfiguration.class)
			.run((context) -> {
				PropertiesRestClientHttpServiceGroupConfigurer configurer = context
					.getBean(PropertiesRestClientHttpServiceGroupConfigurer.class);
				Groups<RestClient.Builder> groups = mock();
				configurer.configureGroups(groups);
				ArgumentCaptor<ClientCallback<RestClient.Builder>> callbackCaptor = ArgumentCaptor.captor();
				then(groups).should().forEachClient(callbackCaptor.capture());
				ClientCallback<RestClient.Builder> callback = callbackCaptor.getValue();
				assertConnectTimeout(callback, "one", 5000);
				assertConnectTimeout(callback, "two", 10000);
			});
	}

	private void assertConnectTimeout(ClientCallback<RestClient.Builder> callback, String name,
			long expectedReadTimeout) {
		HttpServiceGroup group = mock();
		given(group.name()).willReturn(name);
		RestClient.Builder builder = mock();
		callback.withClient(group, builder);
		ArgumentCaptor<ClientHttpRequestFactory> requestFactoryCaptor = ArgumentCaptor.captor();
		then(builder).should().requestFactory(requestFactoryCaptor.capture());
		HttpComponentsClientHttpRequestFactory client = (HttpComponentsClientHttpRequestFactory) requestFactoryCaptor
			.getValue();
		assertThat(getConnectorConfig(client).getConnectTimeout())
			.isEqualTo(Timeout.of(Duration.ofMillis(expectedReadTimeout)));
	}

	@SuppressWarnings("unchecked")
	private ConnectionConfig getConnectorConfig(HttpComponentsClientHttpRequestFactory requestFactory) {
		CloseableHttpClient httpClient = (CloseableHttpClient) ReflectionTestUtils.getField(requestFactory,
				"httpClient");
		assertThat(httpClient).isNotNull();
		Object manager = ReflectionTestUtils.getField(httpClient, "connManager");
		assertThat(manager).isNotNull();
		Resolver<HttpRoute, ConnectionConfig> resolver = (Resolver<HttpRoute, ConnectionConfig>) ReflectionTestUtils
			.getField(manager, "connectionConfigResolver");
		assertThat(resolver).isNotNull();
		return resolver.resolve(null);
	}

	@Test
	void whenHasUserDefinedRequestFactoryBuilder() {
		this.contextRunner.withPropertyValues("spring.http.client.service.base-url=https://example.com")
			.withUserConfiguration(HttpClientConfiguration.class, RequestFactoryBuilderConfiguration.class)
			.run((context) -> {
				TestClientOne clientOne = context.getBean(TestClientOne.class);
				assertThat(getJdkHttpClient(clientOne).followRedirects()).isEqualTo(Redirect.NEVER);
			});
	}

	@Test
	void whenHasUserDefinedRequestFactorySettings() {
		this.contextRunner
			.withPropertyValues("spring.http.serviceclient.one.base-url=https://example.com",
					"spring.http.clients.imperative.factory=jdk")
			.withUserConfiguration(HttpClientConfiguration.class, RequestFactorySettingsConfiguration.class)
			.run((context) -> {
				TestClientOne clientOne = context.getBean(TestClientOne.class);
				assertThat(getJdkHttpClient(clientOne).followRedirects()).isEqualTo(Redirect.NEVER);
			});
	}

	@Test
	void whenHasUserDefinedRestClientCustomizer() {
		this.contextRunner.withPropertyValues("spring.http.serviceclient.one.base-url=https://example.com")
			.withUserConfiguration(HttpClientConfiguration.class, MockRestServiceServerConfiguration.class,
					RestClientCustomizerConfiguration.class)
			.run((context) -> {
				MockRestServiceServerConfiguration mockServers = context
					.getBean(MockRestServiceServerConfiguration.class);
				MockRestServiceServer serverOne = mockServers.getMock("one");
				serverOne.expect(requestTo("https://example.com/hello"))
					.andExpect(header("customized", "true"))
					.andRespond(withSuccess().body("world!"));
				TestClientOne clientOne = context.getBean(TestClientOne.class);
				assertThat(clientOne.hello()).isEqualTo("world!");
			});
	}

	@Test
	void whenHasUserDefinedHttpServiceGroupConfigurer() {
		this.contextRunner.withPropertyValues("spring.http.serviceclient.one.base-url=https://example.com")
			.withUserConfiguration(HttpClientConfiguration.class, MockRestServiceServerConfiguration.class,
					HttpServiceGroupConfigurerConfiguration.class)
			.run((context) -> {
				MockRestServiceServerConfiguration mockServers = context
					.getBean(MockRestServiceServerConfiguration.class);
				MockRestServiceServer serverOne = mockServers.getMock("one");
				serverOne.expect(requestTo("https://example.com/hello"))
					.andExpect(header("customizedgroup", "true"))
					.andRespond(withSuccess().body("world!"));
				TestClientOne clientOne = context.getBean(TestClientOne.class);
				assertThat(clientOne.hello()).isEqualTo("world!");
			});
	}

	@Test
	void whenHasNoHttpServiceProxyRegistryBean() {
		this.contextRunner.withPropertyValues("spring.http.clients.service.base-url=https://example.com")
			.run((context) -> assertThat(context).doesNotHaveBean(HttpServiceProxyRegistry.class));
	}

	private HttpClient getJdkHttpClient(Object proxy) {
		return (HttpClient) Extractors.byName("clientRequestFactory.httpClient").apply(getRestClient(proxy));
	}

	private RestClient getRestClient(Object proxy) {
		InvocationHandler handler = Proxy.getInvocationHandler(proxy);
		Advisor[] advisors = (Advisor[]) Extractors.byName("advised.advisors").apply(handler);
		Map<?, ?> serviceMethods = (Map<?, ?>) Extractors.byName("advice.httpServiceMethods").apply(advisors[0]);
		Object serviceMethod = serviceMethods.values().iterator().next();
		return (RestClient) Extractors.byName("responseFunction.responseFunction.arg$1.restClient")
			.apply(serviceMethod);
	}

	@Configuration(proxyBeanMethods = false)
	static class MockRestServiceServerConfiguration {

		private Map<String, MockRestServiceServer> mocks = new HashMap<>();

		@Bean
		RestClientHttpServiceGroupConfigurer mockServerConfigurer() {
			return (groups) -> groups.forEachClient(this::addMock);
		}

		private MockRestServiceServer addMock(HttpServiceGroup group, Builder client) {
			return this.mocks.put(group.name(), MockRestServiceServer.bindTo(client).build());
		}

		MockRestServiceServer getMock(String name) {
			MockRestServiceServer mock = this.mocks.get(name);
			assertThat(mock).isNotNull();
			return mock;
		}

	}

	@Configuration(proxyBeanMethods = false)
	@ImportHttpServices(group = "one", types = TestClientOne.class)
	@ImportHttpServices(group = "two", types = TestClientTwo.class)
	static class HttpClientConfiguration {

	}

	@Configuration(proxyBeanMethods = false)
	static class RequestFactoryBuilderConfiguration {

		@Bean
		ClientHttpRequestFactoryBuilder<?> requestFactoryBuilder() {
			return ClientHttpRequestFactoryBuilder.jdk()
				.withHttpClientCustomizer((httpClient) -> httpClient.followRedirects(Redirect.NEVER));
		}

	}

	@Configuration(proxyBeanMethods = false)
	static class RequestFactorySettingsConfiguration {

		@Bean
		HttpClientSettings httpClientSettings() {
			return HttpClientSettings.defaults().withRedirects(HttpRedirects.DONT_FOLLOW);
		}

	}

	@Configuration(proxyBeanMethods = false)
	static class RestClientCustomizerConfiguration {

		@Bean
		RestClientCustomizer restClientCustomizer() {
			return (builder) -> builder.defaultHeader("customized", "true");
		}

	}

	@Configuration(proxyBeanMethods = false)
	static class HttpServiceGroupConfigurerConfiguration {

		@Bean
		RestClientHttpServiceGroupConfigurer restClientHttpServiceGroupConfigurer() {
			return (groups) -> groups.filterByName("one")
				.forEachClient((group, builder) -> builder.defaultHeader("customizedgroup", "true"));
		}

	}

	interface TestClientOne {

		@GetExchange("/hello")
		String hello();

	}

	interface TestClientTwo {

		@GetExchange("/there")
		String there();

	}

}
