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

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
import static org.assertj.core.api.Assertions.fail;
import static org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy;
import static org.assertj.core.api.Assumptions.assumeThat;
import static org.openqa.selenium.testing.drivers.Browser.CHROME;

import com.google.common.util.concurrent.Uninterruptibles;
import java.time.Duration;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import org.assertj.core.api.Assumptions;
import org.junit.jupiter.api.Test;
import org.openqa.selenium.Capabilities;
import org.openqa.selenium.JavascriptExecutor;
import org.openqa.selenium.SessionNotCreatedException;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebDriverException;
import org.openqa.selenium.chromium.ChromiumNetworkConditions;
import org.openqa.selenium.chromium.HasCasting;
import org.openqa.selenium.chromium.HasCdp;
import org.openqa.selenium.chromium.HasNetworkConditions;
import org.openqa.selenium.chromium.HasPermissions;
import org.openqa.selenium.net.PortProber;
import org.openqa.selenium.remote.RemoteWebDriverBuilder;
import org.openqa.selenium.remote.http.ClientConfig;
import org.openqa.selenium.testing.Ignore;
import org.openqa.selenium.testing.JupiterTestBase;
import org.openqa.selenium.testing.NoDriverBeforeTest;

class ChromeDriverFunctionalTest extends JupiterTestBase {

  @Test
  @NoDriverBeforeTest
  public void builderGeneratesDefaultChromeOptions() {
    // This test won't pass if we want to use Chrome in a non-standard location
    Assumptions.assumeThat(System.getProperty("webdriver.chrome.binary")).isNull();

    localDriver = ChromeDriver.builder().build();
    Capabilities capabilities = ((ChromeDriver) localDriver).getCapabilities();

    assertThat(localDriver.manage().timeouts().getImplicitWaitTimeout()).isEqualTo(Duration.ZERO);
    assertThat(capabilities.getCapability("browserName")).isEqualTo("chrome");
  }

  @Test
  @NoDriverBeforeTest
  public void builderOverridesDefaultChromeOptions() {
    ChromeOptions options = (ChromeOptions) CHROME.getCapabilities();
    options.setImplicitWaitTimeout(Duration.ofMillis(1));
    localDriver = ChromeDriver.builder().oneOf(options).build();
    assertThat(localDriver.manage().timeouts().getImplicitWaitTimeout())
        .isEqualTo(Duration.ofMillis(1));
  }

  @Test
  @NoDriverBeforeTest
  public void driverOverridesDefaultClientConfig() {
    assertThatThrownBy(
            () -> {
              ClientConfig clientConfig =
                  ClientConfig.defaultConfig().readTimeout(Duration.ofSeconds(0));
              localDriver =
                  new ChromeDriver(
                      ChromeDriverService.createDefaultService(),
                      (ChromeOptions) CHROME.getCapabilities(),
                      clientConfig);
            })
        .isInstanceOf(SessionNotCreatedException.class);
  }

  @Test
  void builderWithClientConfigThrowsException() {
    ClientConfig clientConfig = ClientConfig.defaultConfig().readTimeout(Duration.ofMinutes(1));
    RemoteWebDriverBuilder builder =
        ChromeDriver.builder().oneOf(CHROME.getCapabilities()).config(clientConfig);

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

  @Test
  @Ignore(value = CHROME, reason = "https://bugs.chromium.org/p/chromedriver/issues/detail?id=4350")
  void canSetPermission() {
    HasPermissions permissions = (HasPermissions) driver;

    driver.get(pages.clicksPage);
    String CLIPBOARD_READ = "clipboard-read";
    assumeThat(checkPermission(driver, CLIPBOARD_READ)).isEqualTo("prompt");
    String CLIPBOARD_WRITE = "clipboard-write";
    assumeThat(checkPermission(driver, CLIPBOARD_WRITE)).isEqualTo("granted");

    permissions.setPermission(CLIPBOARD_READ, "denied");
    permissions.setPermission(CLIPBOARD_WRITE, "prompt");

    assertThat(checkPermission(driver, CLIPBOARD_READ)).isEqualTo("denied");
    assertThat(checkPermission(driver, CLIPBOARD_WRITE)).isEqualTo("prompt");
  }

  public String checkPermission(WebDriver driver, String permission) {
    @SuppressWarnings("unchecked")
    Map<String, Object> result =
        (Map<String, Object>)
            ((JavascriptExecutor) driver)
                .executeAsyncScript(
                    "callback = arguments[arguments.length - 1];"
                        + "callback(navigator.permissions.query({"
                        + "name: arguments[0]"
                        + "}));",
                    permission);
    return result.get("state").toString();
  }

  @Test
  @Ignore(gitHubActions = true)
  void canCast() {
    HasCasting caster = (HasCasting) driver;

    // Does not get list the first time it is called
    caster.getCastSinks();
    Uninterruptibles.sleepUninterruptibly(Duration.ofMillis(1500));
    List<Map<String, String>> castSinks = caster.getCastSinks();

    // Can not call these commands if there are no sinks available
    if (!castSinks.isEmpty()) {
      String deviceName = castSinks.get(0).get("name");

      caster.startTabMirroring(deviceName);
      caster.stopCasting(deviceName);
    }
  }

  @Test
  @Ignore(gitHubActions = true)
  public void canCastOnDesktop() {
    HasCasting caster = (HasCasting) driver;

    // Does not get list the first time it is called
    caster.getCastSinks();
    Uninterruptibles.sleepUninterruptibly(Duration.ofMillis(1500));
    List<Map<String, String>> castSinks = caster.getCastSinks();

    // Can not call these commands if there are no sinks available
    if (!castSinks.isEmpty()) {
      String deviceName = castSinks.get(0).get("name");

      caster.startDesktopMirroring(deviceName);
      caster.stopCasting(deviceName);
    }
  }

  @Test
  void canManageNetworkConditions() {
    HasNetworkConditions conditions = (HasNetworkConditions) driver;

    ChromiumNetworkConditions networkConditions = new ChromiumNetworkConditions();
    networkConditions.setLatency(Duration.ofMillis(200));

    conditions.setNetworkConditions(networkConditions);
    assertThat(conditions.getNetworkConditions().getLatency()).isEqualTo(Duration.ofMillis(200));

    conditions.deleteNetworkConditions();

    try {
      conditions.getNetworkConditions();
      fail("If Network Conditions were deleted, should not be able to get Network Conditions");
    } catch (WebDriverException e) {
      if (!e.getMessage().contains("network conditions must be set before it can be retrieved")) {
        throw e;
      }
    }
  }

  @Test
  void canExecuteCdpCommands() {
    HasCdp cdp = (HasCdp) driver;

    Map<String, Object> parameters = Map.of("url", pages.simpleTestPage);
    cdp.executeCdpCommand("Page.navigate", parameters);

    assertThat(driver.getTitle()).isEqualTo("Hello WebDriver");
  }

  @Test
  @NoDriverBeforeTest
  void shouldLaunchSuccessfullyWithArabicDate() {
    try {
      Locale arabicLocale = new Locale("ar", "EG");
      Locale.setDefault(arabicLocale);

      int port = PortProber.findFreePort();
      ChromeDriverService.Builder builder = new ChromeDriverService.Builder();
      builder.usingPort(port);
      builder.build();

    } catch (Exception e) {
      throw e;
    } finally {
      Locale.setDefault(Locale.US);
    }
  }
}
