/*
 * Copyright © 2020-2021 Apple Inc. and the ServiceTalk project 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 io.servicetalk.http.netty;

import io.servicetalk.http.api.BlockingHttpClient;
import io.servicetalk.http.api.HttpResponseStatus;
import io.servicetalk.http.api.HttpServerBuilder;
import io.servicetalk.http.api.SingleAddressHttpClientBuilder;
import io.servicetalk.test.resources.DefaultTestCerts;
import io.servicetalk.transport.api.ClientSslConfigBuilder;
import io.servicetalk.transport.api.HostAndPort;
import io.servicetalk.transport.api.ServerContext;
import io.servicetalk.transport.api.ServerSslConfigBuilder;

import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;

import java.net.InetSocketAddress;
import java.net.SocketOption;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;

import static io.netty.util.internal.PlatformDependent.isOsx;
import static io.servicetalk.http.netty.HttpProtocol.HTTP_1;
import static io.servicetalk.http.netty.HttpProtocol.HTTP_2;
import static io.servicetalk.http.netty.HttpProtocol.toConfigs;
import static io.servicetalk.test.resources.DefaultTestCerts.serverPemHostname;
import static io.servicetalk.transport.api.ServiceTalkSocketOptions.TCP_FASTOPEN_BACKLOG;
import static io.servicetalk.transport.api.ServiceTalkSocketOptions.TCP_FASTOPEN_CONNECT;
import static io.servicetalk.transport.netty.internal.AddressUtils.localAddress;
import static io.servicetalk.transport.netty.internal.AddressUtils.serverHostAndPort;
import static java.util.Arrays.asList;
import static java.util.Collections.emptyMap;
import static java.util.Collections.singletonList;
import static java.util.Collections.singletonMap;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assumptions.assumeFalse;

class TcpFastOpenTest {

    @SuppressWarnings("unused")
    private static List<Arguments> sslProviders() {
        List<Arguments> args = new ArrayList<>();
        for (Map<?, Object> serverListenOptions : asList(emptyMap(), serverTcpFastOpenOptions())) {
            for (Map<?, Object> clientOptions : asList(emptyMap(), clientTcpFastOpenOptions())) {
                args.add(Arguments.of(singletonList(HTTP_1), false, serverListenOptions, clientOptions));
                args.add(Arguments.of(singletonList(HTTP_1), true, serverListenOptions, clientOptions));
                args.add(Arguments.of(singletonList(HTTP_2), false, serverListenOptions, clientOptions));
                args.add(Arguments.of(singletonList(HTTP_2), true, serverListenOptions, clientOptions));
                args.add(Arguments.of(asList(HTTP_2, HTTP_1), true, serverListenOptions, clientOptions));
                args.add(Arguments.of(asList(HTTP_1, HTTP_2), true, serverListenOptions, clientOptions));
            }
        }
        return args;
    }

    @SuppressWarnings("rawtypes")
    static Map<SocketOption, Object> clientTcpFastOpenOptions() {
        return singletonMap(TCP_FASTOPEN_CONNECT, true);
    }

    @SuppressWarnings("rawtypes")
    static Map<SocketOption, Object> serverTcpFastOpenOptions() {
        return singletonMap(TCP_FASTOPEN_BACKLOG, 1);
    }

    @ParameterizedTest(name =
            "{displayName} [{index}] protocols={0}, secure={1}, serverListenOptions={2}, clientOptions={3}")
    @MethodSource("sslProviders")
    void requestSucceedsEvenIfTcpFastOpenNotEnabledOrSupported(final Collection<HttpProtocol> protocols,
            final boolean secure,
            @SuppressWarnings("rawtypes") final Map<SocketOption, Object> serverListenOptions,
            @SuppressWarnings("rawtypes") final Map<SocketOption, Object> clientOptions) throws Exception {
        assumeTcpFastOpen(clientOptions);

        HttpServerBuilder serverBuilder = HttpServers.forAddress(localAddress(0))
                .protocols(toConfigs(protocols));
        if (secure) {
            serverBuilder.sslConfig(new ServerSslConfigBuilder(DefaultTestCerts::loadServerPem,
                    DefaultTestCerts::loadServerKey).build());
        }
        for (@SuppressWarnings("rawtypes") Entry<SocketOption, Object> entry : serverListenOptions.entrySet()) {
            @SuppressWarnings("unchecked")
            SocketOption<Object> option = entry.getKey();
            serverBuilder.listenSocketOption(option, entry.getValue());
        }
        try (ServerContext serverContext = serverBuilder.listenBlockingAndAwait(
                (ctx, request, responseFactory) -> responseFactory.ok());
             BlockingHttpClient client = newClient(serverContext, protocols, secure, clientOptions)) {
            assertEquals(HttpResponseStatus.OK, client.request(client.get("/")).status());
        }
    }

    private static BlockingHttpClient newClient(final ServerContext serverContext,
            final Collection<HttpProtocol> protocols, final boolean secure,
            @SuppressWarnings("rawtypes") final Map<SocketOption, Object> clientOptions) {
        SingleAddressHttpClientBuilder<HostAndPort, InetSocketAddress> builder =
                HttpClients.forSingleAddress(serverHostAndPort(serverContext))
                        .protocols(toConfigs(protocols));
        if (secure) {
            builder.sslConfig(new ClientSslConfigBuilder(DefaultTestCerts::loadServerCAPem)
                    .sniHostname(serverPemHostname()).build());
        }
        for (@SuppressWarnings("rawtypes") Entry<SocketOption, Object> entry : clientOptions.entrySet()) {
            @SuppressWarnings("unchecked")
            SocketOption<Object> option = entry.getKey();
            builder.socketOption(option, entry.getValue());
        }
        return builder.buildBlocking();
    }

    static void assumeTcpFastOpen(@SuppressWarnings("rawtypes") final Map<SocketOption, Object> clientOptions) {
        // FIXME: remove after https://github.com/netty/netty/pull/11588 is released
        if (clientOptions.containsKey(TCP_FASTOPEN_CONNECT)) {
            assumeFalse(isOsx(), "TCP Fast Open is not supported on macOS yet");
        }
    }
}
