/*
 * Copyright (c) 2011-Present VMware, Inc. or its affiliates, 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
 *
 *       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 reactor.netty.http;

import io.netty.buffer.ByteBuf;
import io.netty.handler.ssl.SslContextBuilder;
import io.netty.handler.ssl.util.InsecureTrustManagerFactory;
import io.netty.handler.ssl.util.SelfSignedCertificate;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
import org.junit.runners.Parameterized.Parameter;
import org.junit.runners.Parameterized.Parameters;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import reactor.core.scheduler.Schedulers;
import reactor.netty.ByteBufFlux;
import reactor.netty.DisposableServer;
import reactor.netty.http.client.HttpClient;
import reactor.netty.http.client.HttpClientConfig;
import reactor.netty.http.server.HttpServer;
import reactor.netty.http.server.HttpServerConfig;
import reactor.test.StepVerifier;
import reactor.util.function.Tuple2;

import java.time.Duration;
import java.util.Arrays;
import java.util.List;

import static org.assertj.core.api.Assertions.assertThat;

/**
 * Test a combination of {@link HttpServer} + {@link HttpProtocol}
 * with a combination of {@link HttpClient} + {@link HttpProtocol}
 *
 * @author Violeta Georgieva
 * @since 1.0.0
 */
@RunWith(Parameterized.class)
public class HttpProtocolsTests {

	@Parameter
	public HttpServer server;

	@Parameter(1)
	public HttpClient client;

	@Parameters(name = "{index}: {0}, {1}")
	public static Object[][] data() throws Exception {
		SelfSignedCertificate cert = new SelfSignedCertificate();
		SslContextBuilder serverCtx = SslContextBuilder.forServer(cert.certificate(), cert.privateKey());
		SslContextBuilder clientCtx = SslContextBuilder.forClient()
		                                               .trustManager(InsecureTrustManagerFactory.INSTANCE);

		HttpServer _server = HttpServer.create()
		                               .wiretap(true)
		                               .httpRequestDecoder(spec -> spec.h2cMaxContentLength(256));
		HttpServer securedServer = _server.secure(spec -> spec.sslContext(serverCtx));

		HttpServer[] servers = new HttpServer[]{
				_server, // by default protocol is HTTP/1.1
				_server.protocol(HttpProtocol.H2C),
				_server.protocol(HttpProtocol.HTTP11, HttpProtocol.H2C),
				securedServer, // by default protocol is HTTP/1.1
				securedServer.protocol(HttpProtocol.H2),
				securedServer.protocol(HttpProtocol.HTTP11, HttpProtocol.H2)
		};

		HttpClient _client = HttpClient.create()
		                               .wiretap(true);
		HttpClient securedClient = _client.secure(spec -> spec.sslContext(clientCtx));

		HttpClient[] clients = new HttpClient[]{
				_client, // by default protocol is HTTP/1.1
				_client.protocol(HttpProtocol.H2C),
				_client.protocol(HttpProtocol.HTTP11, HttpProtocol.H2C),
				securedClient, // by default protocol is HTTP/1.1
				securedClient.protocol(HttpProtocol.H2),
				securedClient.protocol(HttpProtocol.HTTP11, HttpProtocol.H2)
		};

		Flux<HttpServer> f1 = Flux.fromArray(servers).concatMap(o -> Flux.just(o).repeat(clients.length - 1));
		Flux<HttpClient> f2 = Flux.fromArray(clients).repeat(servers.length - 1);

		return Flux.zip(f1, f2)
		           .map(Tuple2::toArray)
		           .collectList()
		           .block(Duration.ofSeconds(30))
		           .toArray(new Object[servers.length * clients.length][2]);
	}

	@Test
	public void testProtocolVariationsGetRequest() {
		HttpServerConfig serverConfig = server.configuration();
		HttpClientConfig clientConfig = client.configuration();
		List<HttpProtocol> serverProtocols = Arrays.asList(serverConfig.protocols());
		List<HttpProtocol> clientProtocols = Arrays.asList(clientConfig.protocols());

		DisposableServer disposableServer = null;
		try {
			disposableServer =
					server.port(0)
					      .handle((req, res) -> {
					          boolean secure = "https".equals(req.scheme());
					          if (serverConfig.isSecure() != secure) {
					              return res.status(400).send();
					          }
					          return res.sendString(Mono.just("Hello"));
					      })
					      .bindNow();

			Mono<String> response =
					client.port(disposableServer.port())
					      .get()
					      .uri("/")
					      .responseContent()
					      .aggregate()
					      .asString();

			if (serverConfig.isSecure() != clientConfig.isSecure()) {
				StepVerifier.create(response)
				            .expectError()
				            .verify(Duration.ofSeconds(30));
			}
			else if (serverProtocols.size() == 1 && serverProtocols.get(0) == HttpProtocol.H2C && clientProtocols.size() == 2) {
				StepVerifier.create(response)
				            .expectError()
				            .verify(Duration.ofSeconds(30));
			}
			else if (serverProtocols.containsAll(clientProtocols) || clientProtocols.containsAll(serverProtocols)) {
				StepVerifier.create(response)
				            .expectNext("Hello")
				            .expectComplete()
				            .verify(Duration.ofSeconds(30));
			}
			else {
				StepVerifier.create(response)
				            .expectError()
				            .verify(Duration.ofSeconds(30));
			}
		}
		finally {
			assertThat(disposableServer).isNotNull();
			disposableServer.disposeNow();
		}
	}

	@Test
	public void testProtocolVariationsPostRequest_1() {
		doTestProtocolVariationsPostRequest(false);
	}

	@Test
	public void testProtocolVariationsPostRequest_2() {
		doTestProtocolVariationsPostRequest(true);
	}

	public void doTestProtocolVariationsPostRequest(boolean externalThread) {
		HttpServerConfig serverConfig = server.configuration();
		HttpClientConfig clientConfig = client.configuration();
		List<HttpProtocol> serverProtocols = Arrays.asList(serverConfig.protocols());
		List<HttpProtocol> clientProtocols = Arrays.asList(clientConfig.protocols());

		DisposableServer disposableServer = null;
		try {
			disposableServer =
					server.port(0)
					      .handle((req, res) -> {
					          boolean secure = "https".equals(req.scheme());
					          if (serverConfig.isSecure() != secure) {
					              return res.status(400).send();
					          }
					          Flux<ByteBuf> publisher = req.receive().retain();
					          if (externalThread) {
					              publisher = publisher.subscribeOn(Schedulers.boundedElastic());
					          }
					          return res.send(publisher);
					      })
					      .bindNow();

			Mono<String> response =
					client.port(disposableServer.port())
					      .post()
					      .uri("/")
					      .send(ByteBufFlux.fromString(Mono.just("Hello")))
					      .responseContent()
					      .aggregate()
					      .asString();

			if (serverConfig.isSecure() != clientConfig.isSecure()) {
					StepVerifier.create(response)
					            .expectError()
					            .verify(Duration.ofSeconds(30));
			}
			else if (serverProtocols.size() == 1 && serverProtocols.get(0) == HttpProtocol.H2C && clientProtocols.size() == 2) {
				StepVerifier.create(response)
				            .expectError()
				            .verify(Duration.ofSeconds(30));
			}
			else if (serverProtocols.containsAll(clientProtocols) || clientProtocols.containsAll(serverProtocols)) {
				StepVerifier.create(response)
				            .expectNext("Hello")
				            .expectComplete()
				            .verify(Duration.ofSeconds(30));
			}
			else {
				StepVerifier.create(response)
				            .expectError()
				            .verify(Duration.ofSeconds(30));
			}
		}
		finally {
			assertThat(disposableServer).isNotNull();
			disposableServer.disposeNow();
		}
	}
}