// Licensed to the Software Freedom Conservancy (SFC) under one
// or more contributor license agreements.  See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership.  The SFC licenses this file
// to you 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.openqa.selenium.remote.internal;

import static io.netty.handler.codec.http.HttpHeaderNames.CONTENT_LENGTH;
import static io.netty.handler.codec.http.HttpHeaderNames.CONTENT_TYPE;
import static java.nio.charset.StandardCharsets.UTF_8;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assumptions.assumeThat;
import static org.openqa.selenium.remote.http.HttpMethod.GET;

import com.google.common.net.MediaType;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.PooledByteBufAllocator;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelOption;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.channel.epoll.Epoll;
import io.netty.channel.epoll.EpollEventLoopGroup;
import io.netty.channel.epoll.EpollServerDomainSocketChannel;
import io.netty.channel.kqueue.KQueue;
import io.netty.channel.kqueue.KQueueEventLoopGroup;
import io.netty.channel.kqueue.KQueueServerDomainSocketChannel;
import io.netty.channel.unix.DomainSocketAddress;
import io.netty.channel.unix.DomainSocketChannel;
import io.netty.channel.unix.ServerDomainSocketChannel;
import io.netty.handler.codec.http.DefaultFullHttpResponse;
import io.netty.handler.codec.http.FullHttpRequest;
import io.netty.handler.codec.http.FullHttpResponse;
import io.netty.handler.codec.http.HttpObjectAggregator;
import io.netty.handler.codec.http.HttpResponseStatus;
import io.netty.handler.codec.http.HttpServerCodec;
import io.netty.handler.codec.http.HttpServerKeepAliveHandler;
import io.netty.handler.codec.http.HttpVersion;
import java.io.IOException;
import java.net.SocketAddress;
import java.net.URI;
import java.net.URISyntaxException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.concurrent.atomic.AtomicReference;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.openqa.selenium.remote.http.ClientConfig;
import org.openqa.selenium.remote.http.Contents;
import org.openqa.selenium.remote.http.HttpClient;
import org.openqa.selenium.remote.http.HttpRequest;
import org.openqa.selenium.remote.http.HttpResponse;
import org.openqa.selenium.testing.Safely;

public abstract class DomainSocketsTestBase {

  private final AtomicReference<String> responseText = new AtomicReference<>();
  private EventLoopGroup group;
  private ChannelFuture future;
  private URI socket;

  protected abstract HttpClient.Factory createFactory();

  @BeforeEach
  public void setupUnixDomainSocketServer() throws IOException, URISyntaxException {
    Class<? extends ServerDomainSocketChannel> channelType = null;

    if (Epoll.isAvailable()) {
      group = new EpollEventLoopGroup(2);
      channelType = EpollServerDomainSocketChannel.class;
    } else if (KQueue.isAvailable()) {
      group = new KQueueEventLoopGroup(2);
      channelType = KQueueServerDomainSocketChannel.class;
    }

    assumeThat(group).isNotNull();
    assumeThat(channelType).isNotNull();

    ServerBootstrap bootstrap =
        new ServerBootstrap()
            .group(group)
            .option(ChannelOption.SO_BACKLOG, 1024)
            .childOption(ChannelOption.ALLOCATOR, PooledByteBufAllocator.DEFAULT)
            .channel(channelType)
            .childHandler(
                new ChannelInitializer<DomainSocketChannel>() {
                  @Override
                  protected void initChannel(DomainSocketChannel ch) {
                    ch.pipeline()
                        .addLast("http-codec", new HttpServerCodec())
                        .addLast("http-keep-alive", new HttpServerKeepAliveHandler())
                        .addLast("http-aggregator", new HttpObjectAggregator(Integer.MAX_VALUE))
                        .addLast(
                            new SimpleChannelInboundHandler<FullHttpRequest>() {
                              @Override
                              protected void channelRead0(
                                  ChannelHandlerContext ctx, FullHttpRequest req) {
                                byte[] bytes = responseText.get().getBytes(UTF_8);
                                ByteBuf text = Unpooled.wrappedBuffer(bytes);
                                FullHttpResponse res =
                                    new DefaultFullHttpResponse(
                                        HttpVersion.HTTP_1_1, HttpResponseStatus.OK, text);
                                res.headers()
                                    .set(CONTENT_TYPE, MediaType.PLAIN_TEXT_UTF_8.toString());
                                res.headers().set(CONTENT_LENGTH, bytes.length);

                                ctx.writeAndFlush(res);
                              }
                            });
                  }
                });

    Path temp = Files.createTempFile(Paths.get("/tmp"), "domain-socket-test", "socket");
    Files.deleteIfExists(temp);

    SocketAddress address = new DomainSocketAddress(temp.toFile());
    future = bootstrap.bind(address);

    this.socket = new URI("unix", null, null, 0, temp.toString(), null, null);
  }

  @AfterEach
  public void shutdown() {
    Safely.safelyCall(() -> group.shutdownGracefully());
    Safely.safelyCall(() -> future.channel().closeFuture().sync());
  }

  @Test
  void shouldBeAbleToConnectToAUnixDomainSocketUrl() {
    ClientConfig config = ClientConfig.defaultConfig().baseUri(socket);
    HttpClient client = createFactory().createClient(config);

    String emphaticCheeseEnjoyment = "I like cheese!";
    responseText.set(emphaticCheeseEnjoyment);

    HttpResponse res = client.execute(new HttpRequest(GET, "/do-you-like-cheese"));

    assertThat(Contents.string(res)).isEqualTo(emphaticCheeseEnjoyment);
  }
}
