// 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;

import static java.util.Collections.emptyMap;
import static java.util.Collections.singletonList;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
import static org.openqa.selenium.json.Json.JSON_UTF_8;
import static org.openqa.selenium.json.Json.MAP_TYPE;
import static org.openqa.selenium.remote.Browser.CHROME;
import static org.openqa.selenium.remote.Browser.FIREFOX;

import com.google.common.collect.ImmutableMap;
import java.io.File;
import java.io.IOException;
import java.net.URI;
import java.net.URL;
import java.time.Duration;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;
import java.util.stream.Collectors;
import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.Test;
import org.openqa.selenium.Capabilities;
import org.openqa.selenium.ImmutableCapabilities;
import org.openqa.selenium.Platform;
import org.openqa.selenium.SessionNotCreatedException;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.chrome.ChromeOptions;
import org.openqa.selenium.devtools.HasDevTools;
import org.openqa.selenium.firefox.FirefoxOptions;
import org.openqa.selenium.ie.InternetExplorerOptions;
import org.openqa.selenium.json.Json;
import org.openqa.selenium.remote.http.ClientConfig;
import org.openqa.selenium.remote.http.Contents;
import org.openqa.selenium.remote.http.HttpRequest;
import org.openqa.selenium.remote.http.HttpResponse;
import org.openqa.selenium.remote.service.DriverService;

@Tag("UnitTests")
class RemoteWebDriverBuilderTest {

  private static final SessionId SESSION_ID = new SessionId(UUID.randomUUID());
  private static final HttpResponse CANNED_SESSION_RESPONSE =
      new HttpResponse()
          .setContent(
              Contents.asJson(
                  ImmutableMap.of(
                      "value",
                      ImmutableMap.of(
                          "sessionId",
                          SESSION_ID,
                          // Primula is a canned cheese. Boom boom!
                          "capabilities",
                          new ImmutableCapabilities("se:cheese", "primula")))));

  @Test
  void justCallingBuildWithoutSettingAnyOptionsIsAnError() {
    RemoteWebDriverBuilder builder = RemoteWebDriver.builder();
    assertThatExceptionOfType(SessionNotCreatedException.class).isThrownBy(builder::build);
  }

  @Test
  void mustSpecifyAtLeastOneSetOfOptions() {
    List<List<Capabilities>> caps = new ArrayList<>();

    RemoteWebDriver.builder()
        .oneOf(new FirefoxOptions())
        .address("http://localhost:34576")
        .connectingWith(
            config ->
                req -> {
                  caps.add(listCapabilities(req));
                  return CANNED_SESSION_RESPONSE;
                })
        .build();

    assertThat(caps).hasSize(1);
    List<Capabilities> caps0 = caps.get(0);
    assertThat(caps0.get(0).getBrowserName()).isEqualTo(FIREFOX.browserName());
  }

  @Test
  void settingAGlobalCapabilityCountsAsAnOption() {
    AtomicBoolean match = new AtomicBoolean(false);

    RemoteWebDriver.builder()
        .setCapability("se:cheese", "anari")
        .address("http://localhost:34576")
        .connectingWith(
            config ->
                req -> {
                  listCapabilities(req).stream()
                      .map(caps -> "anari".equals(caps.getCapability("se:cheese")))
                      .reduce(Boolean::logicalOr)
                      .ifPresent(match::set);
                  return CANNED_SESSION_RESPONSE;
                })
        .build();

    assertThat(match.get()).isTrue();
  }

  @Test
  void shouldForbidGlobalCapabilitiesFromClobberingFirstMatchCapabilities() {
    RemoteWebDriverBuilder builder =
        RemoteWebDriver.builder()
            .oneOf(new ImmutableCapabilities("se:cheese", "stinking bishop"))
            .setCapability("se:cheese", "cheddar")
            .address("http://localhost:38746");

    assertThatExceptionOfType(IllegalArgumentException.class).isThrownBy(builder::build);
  }

  @Test
  void requireAllOptionsAreW3CCompatible() {
    RemoteWebDriverBuilder builder =
        RemoteWebDriver.builder()
            .setCapability("cheese", "casu marzu")
            .address("http://localhost:45734");

    assertThatExceptionOfType(IllegalArgumentException.class).isThrownBy(builder::build);
  }

  @Test
  void shouldRejectOldJsonWireProtocolNames() {
    RemoteWebDriverBuilder builder =
        RemoteWebDriver.builder()
            .oneOf(new ImmutableCapabilities("platform", Platform.getCurrent()))
            .address("http://localhost:35856");

    assertThatExceptionOfType(IllegalArgumentException.class).isThrownBy(builder::build);
  }

  @Test
  void shouldAllowMetaDataToBeSet() {
    AtomicBoolean seen = new AtomicBoolean(false);

    RemoteWebDriver.builder()
        .oneOf(new FirefoxOptions())
        .addMetadata("cloud:options", "merhaba")
        .address("http://localhost:34576")
        .connectingWith(
            config ->
                req -> {
                  Map<String, Object> payload = new Json().toType(Contents.string(req), MAP_TYPE);
                  seen.set("merhaba".equals(payload.getOrDefault("cloud:options", "")));
                  return CANNED_SESSION_RESPONSE;
                })
        .build();

    assertThat(seen.get()).isTrue();
  }

  @Test
  void doesNotAllowFirstMatchToBeUsedAsAMetadataNameAsItIsConfusing() {
    RemoteWebDriverBuilder builder = RemoteWebDriver.builder();
    assertThatExceptionOfType(IllegalArgumentException.class)
        .isThrownBy(() -> builder.addMetadata("firstMatch", "cheese"));
  }

  @Test
  void doesNotAllowAlwaysMatchToBeUsedAsAMetadataNameAsItIsConfusing() {
    RemoteWebDriverBuilder builder = RemoteWebDriver.builder();
    assertThatExceptionOfType(IllegalArgumentException.class)
        .isThrownBy(() -> builder.addMetadata("alwaysMatch", "cheese"));
  }

  @Test
  void doesNotAllowCapabilitiesToBeUsedAsAMetadataName() {
    RemoteWebDriverBuilder builder = RemoteWebDriver.builder();
    assertThatExceptionOfType(IllegalArgumentException.class)
        .isThrownBy(() -> builder.addMetadata("capabilities", "cheese"));
  }

  @Test
  void shouldAllowCapabilitiesToBeSetGlobally() {
    AtomicBoolean seen = new AtomicBoolean(false);

    RemoteWebDriver.builder()
        .oneOf(new FirefoxOptions())
        .setCapability("se:option", "cheese")
        .address("http://localhost:34576")
        .connectingWith(
            config ->
                req -> {
                  listCapabilities(req).stream()
                      .map(caps -> "cheese".equals(caps.getCapability("se:option")))
                      .reduce(Boolean::logicalAnd)
                      .ifPresent(seen::set);
                  return CANNED_SESSION_RESPONSE;
                })
        .build();

    assertThat(seen.get()).isTrue();
  }

  @Test
  void ifARemoteUrlIsGivenThatIsUsedForTheSession() {
    URI uri = URI.create("http://localhost:7575");
    AtomicReference<URI> seen = new AtomicReference<>();

    RemoteWebDriver.builder()
        .oneOf(new FirefoxOptions())
        .address(uri)
        .connectingWith(
            config ->
                req -> {
                  seen.set(config.baseUri());
                  return CANNED_SESSION_RESPONSE;
                })
        .build();

    assertThat(seen.get()).isEqualTo(uri);
  }

  @Test
  void shouldUseGivenDriverServiceForUrlIfProvided() throws IOException {
    URI uri = URI.create("http://localhost:9898");
    URL url = uri.toURL();

    DriverService service =
        new FakeDriverService() {
          @Override
          public URL getUrl() {
            return url;
          }
        };

    AtomicReference<URI> seen = new AtomicReference<>();
    RemoteWebDriver.builder()
        .oneOf(new FirefoxOptions())
        .withDriverService(service)
        .connectingWith(
            config -> {
              seen.set(config.baseUri());
              return req -> CANNED_SESSION_RESPONSE;
            })
        .build();

    assertThat(seen.get()).isEqualTo(uri);
  }

  @Test
  void settingBothDriverServiceAndUrlIsAnError() throws IOException {
    RemoteWebDriverBuilder builder =
        RemoteWebDriver.builder().withDriverService(new FakeDriverService());

    assertThatExceptionOfType(IllegalArgumentException.class)
        .isThrownBy(() -> builder.address("http://localhost:89789"));
  }

  @Test
  void oneOfWillClearOutTheCurrentlySetCapabilities() {
    AtomicBoolean allOk = new AtomicBoolean();

    RemoteWebDriver.builder()
        .oneOf(new FirefoxOptions())
        .addAlternative(new InternetExplorerOptions())
        .oneOf(new ChromeOptions())
        .address("http://localhost:34576")
        .connectingWith(
            config ->
                req -> {
                  List<Capabilities> caps = listCapabilities(req);
                  allOk.set(
                      caps.size() == 1
                          && caps.get(0).getBrowserName().equals(CHROME.browserName()));
                  return CANNED_SESSION_RESPONSE;
                })
        .build();

    assertThat(allOk.get()).isTrue();
  }

  @Test
  void shouldBeAbleToSetClientConfigDirectly() {
    URI uri = URI.create("http://localhost:5763");

    AtomicReference<URI> seen = new AtomicReference<>();

    RemoteWebDriver.builder()
        .address(uri.toString())
        .oneOf(new FirefoxOptions())
        .connectingWith(
            config -> {
              seen.set(config.baseUri());
              return req -> CANNED_SESSION_RESPONSE;
            })
        .build();

    assertThat(seen.get()).isEqualTo(uri);
  }

  @Test
  void shouldSetRemoteHostUriOnClientConfigIfSet() {
    URI uri = URI.create("http://localhost:6546");
    ClientConfig config = ClientConfig.defaultConfig().baseUri(uri);

    AtomicReference<URI> seen = new AtomicReference<>();

    RemoteWebDriver.builder()
        .config(config)
        .oneOf(new FirefoxOptions())
        .connectingWith(
            c -> {
              seen.set(c.baseUri());
              return req -> CANNED_SESSION_RESPONSE;
            })
        .build();

    assertThat(seen.get()).isEqualTo(uri);
  }

  @Test
  void shouldThrowErrorIfCustomConfigIfSetForLocalDriver() {
    ClientConfig config = ClientConfig.defaultConfig().readTimeout(Duration.ofMinutes(4));

    RemoteWebDriverBuilder builder =
        RemoteWebDriver.builder()
            .oneOf(new ImmutableCapabilities("browser", "selenium-test"))
            .config(config)
            .connectingWith(clientConfig -> req -> CANNED_SESSION_RESPONSE);

    assertThatIllegalArgumentException()
        .isThrownBy(builder::build)
        .withMessage("ClientConfig instances do not work for Local Drivers");
  }

  @Test
  void shouldSetSessionIdFromW3CResponse() {
    RemoteWebDriver driver =
        (RemoteWebDriver)
            RemoteWebDriver.builder()
                .oneOf(new FirefoxOptions())
                .address("http://localhost:34576")
                .connectingWith(config -> req -> CANNED_SESSION_RESPONSE)
                .build();

    assertThat(driver.getSessionId()).isEqualTo(SESSION_ID);
  }

  @Test
  void commandsShouldBeSentWithW3CHeaders() {
    AtomicBoolean allOk = new AtomicBoolean(false);

    RemoteWebDriver.builder()
        .oneOf(new FirefoxOptions())
        .address("http://localhost:34576")
        .connectingWith(
            config ->
                req -> {
                  allOk.set(
                      "no-cache".equals(req.getHeader("Cache-Control"))
                          && JSON_UTF_8.equals(req.getHeader("Content-Type")));
                  return CANNED_SESSION_RESPONSE;
                })
        .build();

    assertThat(allOk.get()).isTrue();
  }

  @Test
  void
      shouldUseWebDriverInfoToFindAMatchingDriverImplementationForRequestedCapabilitiesIfRemoteUrlNotSet() {
    WebDriver driver =
        RemoteWebDriver.builder()
            .oneOf(new ImmutableCapabilities("browser", "selenium-test"))
            .connectingWith(config -> req -> CANNED_SESSION_RESPONSE)
            .build();

    assertThat(driver).isInstanceOf(FakeWebDriverInfo.FakeWebDriver.class);
  }

  @Test
  void shouldAugmentDriverIfPossible() {
    HttpResponse response =
        new HttpResponse()
            .setContent(
                Contents.asJson(
                    ImmutableMap.of(
                        "value",
                        ImmutableMap.of(
                            "sessionId",
                            SESSION_ID,
                            "capabilities",
                            new ImmutableCapabilities("firefox", "caps")))));

    Augmenter augmenter =
        new Augmenter()
            .addDriverAugmentation("firefox", HasMagicNumbers.class, (c, exe) -> () -> 1);
    WebDriver driver =
        RemoteWebDriver.builder()
            .oneOf(new FirefoxOptions())
            .augmentUsing(augmenter)
            .address("http://localhost:34576")
            .connectingWith(config -> req -> response)
            .build();

    int number = ((HasMagicNumbers) driver).getMagicNumber();

    assertThat(driver).isInstanceOf(HasMagicNumbers.class);
    assertThat(number).isEqualTo(1);
  }

  @Test
  void shouldAugmentDriverWhenUsingDriverService() throws IOException {
    URI uri = URI.create("http://localhost:9898");
    URL url = uri.toURL();

    DriverService service =
        new FakeDriverService() {
          @Override
          public URL getUrl() {
            return url;
          }
        };

    HttpResponse response =
        new HttpResponse()
            .setContent(
                Contents.asJson(
                    ImmutableMap.of(
                        "value",
                        ImmutableMap.of(
                            "sessionId",
                            SESSION_ID,
                            "capabilities",
                            new ImmutableCapabilities("firefox", "caps")))));

    Augmenter augmenter =
        new Augmenter()
            .addDriverAugmentation("firefox", HasMagicNumbers.class, (c, exe) -> () -> 1);
    WebDriver driver =
        RemoteWebDriver.builder()
            .oneOf(new FirefoxOptions())
            .withDriverService(service)
            .augmentUsing(augmenter)
            .connectingWith(config -> req -> response)
            .build();

    int number = ((HasMagicNumbers) driver).getMagicNumber();

    assertThat(driver).isInstanceOf(HasMagicNumbers.class);
    assertThat(number).isEqualTo(1);
  }

  @Test
  void shouldAugmentWithDevToolsWhenUsingDriverService() throws IOException {
    URI uri = URI.create("http://localhost:9898");
    URL url = uri.toURL();

    DriverService service =
        new FakeDriverService() {
          @Override
          public URL getUrl() {
            return url;
          }
        };

    HttpResponse response =
        new HttpResponse()
            .setContent(
                Contents.asJson(
                    ImmutableMap.of(
                        "value",
                        ImmutableMap.of(
                            "sessionId",
                            SESSION_ID,
                            "capabilities",
                            new ImmutableCapabilities(
                                "chrome", "caps",
                                "browserName", "chrome",
                                "goog:chromeOptions", uri.toString())))));

    WebDriver driver =
        RemoteWebDriver.builder()
            .oneOf(new ChromeOptions())
            .withDriverService(service)
            .augmentUsing(new Augmenter())
            .connectingWith(config -> req -> response)
            .build();

    assertThat(driver).isInstanceOf(HasDevTools.class);
  }

  @SuppressWarnings("unchecked")
  private List<Capabilities> listCapabilities(HttpRequest request) {
    Map<String, Object> converted = new Json().toType(Contents.string(request), MAP_TYPE);
    Map<String, Object> w3cCaps = (Map<String, Object>) converted.get("capabilities");
    Map<String, Object> always =
        (Map<String, Object>) w3cCaps.getOrDefault("alwaysMatch", emptyMap());
    Capabilities alwaysMatch = new ImmutableCapabilities(always);
    List<Map<String, Object>> first =
        (List<Map<String, Object>>) w3cCaps.getOrDefault("firstMatch", singletonList(emptyMap()));

    return first.stream()
        .map(ImmutableCapabilities::new)
        .map(alwaysMatch::merge)
        .collect(Collectors.toList());
  }

  static class FakeDriverService extends DriverService {
    private boolean started;

    FakeDriverService() throws IOException {
      super(new File("."), 0, DEFAULT_TIMEOUT, null, null);
    }

    @Override
    public void start() {
      started = true;
    }

    @Override
    public boolean isRunning() {
      return started;
    }

    @Override
    protected void waitUntilAvailable() {
      // return immediately
    }
  }
}
