// 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.grid.router;

import static org.openqa.selenium.json.Json.MAP_TYPE;
import static org.openqa.selenium.remote.http.HttpMethod.GET;

import java.io.IOException;
import java.io.StringReader;
import java.io.UncheckedIOException;
import java.net.ConnectException;
import java.time.Duration;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import org.openqa.selenium.Capabilities;
import org.openqa.selenium.grid.commands.EventBusCommand;
import org.openqa.selenium.grid.commands.Hub;
import org.openqa.selenium.grid.commands.Standalone;
import org.openqa.selenium.grid.config.CompoundConfig;
import org.openqa.selenium.grid.config.Config;
import org.openqa.selenium.grid.config.MapConfig;
import org.openqa.selenium.grid.config.MemoizedConfig;
import org.openqa.selenium.grid.config.TomlConfig;
import org.openqa.selenium.grid.distributor.httpd.DistributorServer;
import org.openqa.selenium.grid.node.httpd.NodeServer;
import org.openqa.selenium.grid.router.httpd.RouterServer;
import org.openqa.selenium.grid.server.Server;
import org.openqa.selenium.grid.sessionmap.httpd.SessionMapServer;
import org.openqa.selenium.grid.sessionqueue.httpd.NewSessionQueueServer;
import org.openqa.selenium.grid.web.Values;
import org.openqa.selenium.json.Json;
import org.openqa.selenium.json.JsonOutput;
import org.openqa.selenium.net.PortProber;
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.support.ui.FluentWait;
import org.openqa.selenium.testing.Safely;
import org.openqa.selenium.testing.TearDownFixture;

public enum DeploymentTypes {
  STANDALONE {
    @Override
    public Deployment start(Capabilities capabilities, Config additionalConfig) {
      StringBuilder rawCaps = new StringBuilder();
      try (JsonOutput out = new Json().newOutput(rawCaps)) {
        out.setPrettyPrint(false).write(capabilities);
      }

      String[] rawConfig =
          new String[] {
            "[network]",
            "relax-checks = true",
            "",
            "[server]",
            "registration-secret = \"provolone\"",
            "",
            "[sessionqueue]",
            "session-request-timeout = 100",
            "session-retry-interval = 1"
          };
      Config config =
          new MemoizedConfig(
              new CompoundConfig(
                  additionalConfig,
                  new TomlConfig(new StringReader(String.join("\n", rawConfig)))));

      Server<?> server =
          new Standalone().asServer(new CompoundConfig(setRandomPort(), config)).start();
      waitUntilReady(server, Duration.ofSeconds(5));

      return new Deployment(server, server::stop);
    }
  },
  HUB_AND_NODE {
    @Override
    public Deployment start(Capabilities capabilities, Config additionalConfig) {
      StringBuilder rawCaps = new StringBuilder();
      try (JsonOutput out = new Json().newOutput(rawCaps)) {
        out.setPrettyPrint(false).write(capabilities);
      }

      int publish = PortProber.findFreePort();
      int subscribe = PortProber.findFreePort();

      String[] rawConfig =
          new String[] {
            "[events]",
            "publish = \"tcp://localhost:" + publish + "\"",
            "subscribe = \"tcp://localhost:" + subscribe + "\"",
            "",
            "[network]",
            "relax-checks = true",
            "",
            "[server]",
            "registration-secret = \"feta\"",
            "",
            "[sessionqueue]",
            "session-request-timeout = 100",
            "session-retry-interval = 1"
          };
      Config baseConfig =
          new MemoizedConfig(
              new CompoundConfig(
                  additionalConfig,
                  new TomlConfig(new StringReader(String.join("\n", rawConfig)))));

      Config hubConfig =
          new MemoizedConfig(
              new CompoundConfig(
                  setRandomPort(),
                  new MapConfig(Map.of("events", Map.of("bind", true))),
                  baseConfig));

      Server<?> hub = new Hub().asServer(hubConfig).start();

      MapConfig additionalNodeConfig = new MapConfig(Map.of("node", Map.of("hub", hub.getUrl())));

      Config nodeConfig =
          new MemoizedConfig(new CompoundConfig(additionalNodeConfig, setRandomPort(), baseConfig));
      Server<?> node = new NodeServer().asServer(nodeConfig).start();
      waitUntilReady(node, Duration.ofSeconds(5));

      waitUntilReady(hub, Duration.ofSeconds(5));

      return new Deployment(hub, hub::stop, node::stop);
    }
  },
  DISTRIBUTED {
    @Override
    public Deployment start(Capabilities capabilities, Config additionalConfig) {
      StringBuilder rawCaps = new StringBuilder();
      try (JsonOutput out = new Json().newOutput(rawCaps)) {
        out.setPrettyPrint(false).write(capabilities);
      }

      int publish = PortProber.findFreePort();
      int subscribe = PortProber.findFreePort();

      String[] rawConfig =
          new String[] {
            "[events]",
            "publish = \"tcp://localhost:" + publish + "\"",
            "subscribe = \"tcp://localhost:" + subscribe + "\"",
            "bind = false",
            "",
            "[network]",
            "relax-checks = true",
            "",
            "[server]",
            "",
            "registration-secret = \"colby\"",
            "",
            "[sessionqueue]",
            "session-request-timeout = 100",
            "session-retry-interval = 1"
          };

      Config sharedConfig =
          new MemoizedConfig(
              new CompoundConfig(
                  additionalConfig,
                  new TomlConfig(new StringReader(String.join("\n", rawConfig)))));

      Server<?> eventServer =
          new EventBusCommand()
              .asServer(
                  new MemoizedConfig(
                      new CompoundConfig(
                          new TomlConfig(
                              new StringReader(
                                  String.join(
                                      "\n",
                                      new String[] {
                                        "[events]",
                                        "publish = \"tcp://localhost:" + publish + "\"",
                                        "subscribe = \"tcp://localhost:" + subscribe + "\"",
                                        "bind = true"
                                      }))),
                          setRandomPort(),
                          sharedConfig)))
              .start();
      waitUntilReady(eventServer, Duration.ofSeconds(5));

      Server<?> newSessionQueueServer =
          new NewSessionQueueServer()
              .asServer(new MemoizedConfig(new CompoundConfig(setRandomPort(), sharedConfig)))
              .start();
      waitUntilReady(newSessionQueueServer, Duration.ofSeconds(5));
      Config newSessionQueueServerConfig =
          new TomlConfig(
              new StringReader(
                  String.join(
                      "\n",
                      new String[] {
                        "[sessionqueue]",
                        "hostname = \"localhost\"",
                        "port = " + newSessionQueueServer.getUrl().getPort()
                      })));

      Server<?> sessionMapServer =
          new SessionMapServer()
              .asServer(new MemoizedConfig(new CompoundConfig(setRandomPort(), sharedConfig)))
              .start();
      Config sessionMapConfig =
          new TomlConfig(
              new StringReader(
                  String.join(
                      "\n",
                      new String[] {
                        "[sessions]",
                        "hostname = \"localhost\"",
                        "port = " + sessionMapServer.getUrl().getPort()
                      })));

      Server<?> distributorServer =
          new DistributorServer()
              .asServer(
                  new MemoizedConfig(
                      new CompoundConfig(
                          setRandomPort(),
                          sessionMapConfig,
                          newSessionQueueServerConfig,
                          sharedConfig)))
              .start();
      Config distributorConfig =
          new TomlConfig(
              new StringReader(
                  String.join(
                      "\n",
                      new String[] {
                        "[distributor]",
                        "hostname = \"localhost\"",
                        "port = " + distributorServer.getUrl().getPort()
                      })));

      Server<?> router =
          new RouterServer()
              .asServer(
                  new MemoizedConfig(
                      new CompoundConfig(
                          setRandomPort(),
                          sessionMapConfig,
                          distributorConfig,
                          newSessionQueueServerConfig,
                          sharedConfig)))
              .start();

      MapConfig nodeConfig = new MapConfig(Map.of("node", Map.of("hub", router.getUrl())));

      Server<?> nodeServer =
          new NodeServer()
              .asServer(
                  new MemoizedConfig(
                      new CompoundConfig(
                          nodeConfig,
                          setRandomPort(),
                          sharedConfig,
                          sessionMapConfig,
                          distributorConfig,
                          newSessionQueueServerConfig)))
              .start();
      waitUntilReady(nodeServer, Duration.ofSeconds(5));

      waitUntilReady(router, Duration.ofSeconds(5));

      return new Deployment(
          router,
          router::stop,
          nodeServer::stop,
          distributorServer::stop,
          sessionMapServer::stop,
          newSessionQueueServer::stop,
          eventServer::stop);
    }
  };

  private static Config setRandomPort() {
    return new MapConfig(Map.of("server", Map.of("port", PortProber.findFreePort())));
  }

  private static void waitUntilReady(Server<?> server, Duration duration) {
    HttpClient client = HttpClient.Factory.createDefault().createClient(server.getUrl());

    try {
      new FluentWait<>(client)
          .withTimeout(duration)
          .pollingEvery(Duration.ofMillis(250))
          .ignoring(IOException.class)
          .ignoring(UncheckedIOException.class)
          .ignoring(ConnectException.class)
          .until(
              c -> {
                HttpResponse response = c.execute(new HttpRequest(GET, "/status"));
                Map<String, Object> status = Values.get(response, MAP_TYPE);
                return Boolean.TRUE.equals(
                    status != null && Boolean.parseBoolean(status.get("ready").toString()));
              });
    } finally {
      Safely.safelyCall(client::close);
    }
  }

  public abstract Deployment start(Capabilities capabilities, Config additionalConfig);

  public static class Deployment implements TearDownFixture {
    private final Server<?> server;
    private final List<TearDownFixture> tearDowns;

    private Deployment(Server<?> server, TearDownFixture... tearDowns) {
      this.server = server;
      this.tearDowns = Arrays.asList(tearDowns);
    }

    public Server<?> getServer() {
      return server;
    }

    @Override
    public void tearDown() throws Exception {
      tearDowns.parallelStream().forEach(Safely::safelyCall);
    }
  }
}
