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

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
import static org.junit.jupiter.api.Assertions.fail;
import static org.junit.jupiter.api.Assumptions.assumeFalse;
import static org.openqa.selenium.WaitingConditions.elementTextToContain;
import static org.openqa.selenium.WaitingConditions.elementTextToEqual;
import static org.openqa.selenium.WaitingConditions.elementValueToEqual;
import static org.openqa.selenium.support.ui.ExpectedConditions.visibilityOfElementLocated;
import static org.openqa.selenium.testing.drivers.Browser.FIREFOX;
import static org.openqa.selenium.testing.drivers.Browser.HTMLUNIT;
import static org.openqa.selenium.testing.drivers.Browser.IE;
import static org.openqa.selenium.testing.drivers.Browser.SAFARI;

import java.io.File;
import java.io.IOException;
import java.time.Duration;
import java.util.List;
import org.junit.jupiter.api.Test;
import org.openqa.selenium.interactions.Actions;
import org.openqa.selenium.support.ui.WebDriverWait;
import org.openqa.selenium.testing.Ignore;
import org.openqa.selenium.testing.JupiterTestBase;
import org.openqa.selenium.testing.NotYetImplemented;
import org.openqa.selenium.testing.TestUtilities;
import org.openqa.selenium.testing.drivers.WebDriverBuilder;

class CorrectEventFiringTest extends JupiterTestBase {

  private static void clickOnElementWhichRecordsEvents(WebDriver driver) {
    driver.findElement(By.id("plainButton")).click();
  }

  private static void assertEventFired(String eventName, WebDriver driver) {
    WebElement result = driver.findElement(By.id("result"));

    String text =
        new WebDriverWait(driver, Duration.ofSeconds(10))
            .until(elementTextToContain(result, eventName));
    boolean conditionMet = text.contains(eventName);

    assertThat(conditionMet).as("%s fired with text %s", eventName, text).isTrue();
  }

  private static void assertEventNotFired(String eventName, WebDriver driver) {
    WebElement result = driver.findElement(By.id("result"));
    String text = result.getText();
    assertThat(text).as("%s fired with text %s").doesNotContain(eventName);
  }

  private static boolean browserNeedsFocusOnThisOs(WebDriver driver) {
    // No browser yet demands focus on windows
    if (TestUtilities.getEffectivePlatform(driver).is(Platform.WINDOWS)) {
      return false;
    }

    if (Boolean.getBoolean("webdriver.focus.override")) {
      return false;
    }

    String browserName = getBrowserName(driver);
    return browserName.toLowerCase().contains("firefox");
  }

  private static String getBrowserName(WebDriver driver) {
    if (driver instanceof HasCapabilities) {
      return ((HasCapabilities) driver).getCapabilities().getBrowserName();
    }

    return driver.getClass().getName();
  }

  @Test
  @NotYetImplemented(SAFARI)
  public void testShouldFireFocusEventWhenClicking() {
    driver.get(pages.javascriptPage);

    clickOnElementWhichRecordsEvents(driver);

    assertEventFired("focus", driver);
  }

  @Test
  @NotYetImplemented(SAFARI)
  public void testShouldFireFocusEventInNonTopmostWindow() {
    WebDriver driver2 = new WebDriverBuilder().get();
    try {
      // topmost
      driver2.get(pages.javascriptPage);
      clickOnElementWhichRecordsEvents(driver2);
      assertEventFired("focus", driver2);

      // non-topmost
      driver.get(pages.javascriptPage);
      clickOnElementWhichRecordsEvents(driver);
      assertEventFired("focus", driver);

    } finally {
      driver2.quit();
    }
  }

  @Test
  @NotYetImplemented(SAFARI)
  public void testShouldFireClickEventWhenClicking() {
    driver.get(pages.javascriptPage);

    clickOnElementWhichRecordsEvents(driver);

    assertEventFired("click", driver);
  }

  @Test
  @NotYetImplemented(SAFARI)
  public void testShouldFireMouseDownEventWhenClicking() {
    driver.get(pages.javascriptPage);

    clickOnElementWhichRecordsEvents(driver);

    assertEventFired("mousedown", driver);
  }

  @Test
  @NotYetImplemented(SAFARI)
  public void testShouldFireMouseUpEventWhenClicking() {
    driver.get(pages.javascriptPage);

    clickOnElementWhichRecordsEvents(driver);

    assertEventFired("mouseup", driver);
  }

  @Test
  @NotYetImplemented(SAFARI)
  public void testShouldFireMouseOverEventWhenClicking() {
    driver.get(pages.javascriptPage);

    clickOnElementWhichRecordsEvents(driver);

    assertEventFired("mouseover", driver);
  }

  /**
   * This isn't quite right. We just loaded the page, and the native mouse is assumed to be at 0,0
   * (or the last location the mouse was in) In order to click the element, the mouse will have to
   * move towards it.
   */
  @Test
  @Ignore(FIREFOX)
  @NotYetImplemented(SAFARI)
  public void testShouldFireMouseMoveEventWhenClicking() {
    driver.get(pages.simpleTestPage);
    // Move the mouse cursor to somewhere pretty far down the page
    new Actions(driver).moveToElement(driver.findElement(By.id("span"))).perform();

    driver.get(pages.javascriptPage);
    clickOnElementWhichRecordsEvents(driver);

    assertEventFired("mousemove", driver);
  }

  @Test
  void testShouldNotThrowIfEventHandlerThrows() {
    driver.get(pages.javascriptPage);

    try {
      driver.findElement(By.id("throwing-mouseover")).click();
    } catch (WebDriverException e) {
      fail("Error in event handler should not have propagated: " + e);
    }
  }

  @Test
  @NotYetImplemented(SAFARI)
  public void testShouldFireEventsInTheRightOrder() {
    driver.get(pages.javascriptPage);

    clickOnElementWhichRecordsEvents(driver);

    String text = driver.findElement(By.id("result")).getText();

    int lastIndex = -1;
    for (String event : new String[] {"mousedown", "focus", "mouseup", "click"}) {
      int index = text.indexOf(event);

      assertThat(index).as(event + " did not fire at all").isNotEqualTo(-1);
      assertThat(index).as(event + " did not fire in the correct order").isGreaterThan(lastIndex);
      lastIndex = index;
    }
  }

  @Test
  @NotYetImplemented(SAFARI)
  public void testsShouldIssueMouseDownEvents() {
    driver.get(pages.javascriptPage);
    driver.findElement(By.id("mousedown")).click();

    assertEventFired("mouse down", driver);
    String result = driver.findElement(By.id("result")).getText();
    assertThat(result).isEqualTo("mouse down");
  }

  @Test
  @NotYetImplemented(SAFARI)
  public void testShouldIssueClickEvents() {
    driver.get(pages.javascriptPage);
    driver.findElement(By.id("mouseclick")).click();

    WebElement result = driver.findElement(By.id("result"));
    wait.until($ -> result.getText().equals("mouse click"));
  }

  @Test
  @NotYetImplemented(SAFARI)
  public void testShouldIssueMouseUpEvents() {
    driver.get(pages.javascriptPage);
    driver.findElement(By.id("mouseup")).click();

    WebElement result = driver.findElement(By.id("result"));
    wait.until($ -> result.getText().equals("mouse up"));
  }

  @Test
  @NotYetImplemented(SAFARI)
  public void testMouseEventsShouldBubbleUpToContainingElements() {
    driver.get(pages.javascriptPage);
    driver.findElement(By.id("child")).click();

    WebElement result = driver.findElement(By.id("result"));
    wait.until($ -> result.getText().equals("mouse down"));
  }

  @Test
  @Ignore(FIREFOX)
  @NotYetImplemented(SAFARI)
  public void testShouldEmitOnChangeEventsWhenSelectingElements() {
    driver.get(pages.javascriptPage);
    // Intentionally not looking up the select tag. See selenium r7937 for details.
    List<WebElement> allOptions = driver.findElements(By.xpath("//select[@id='selector']//option"));

    String initialTextValue = driver.findElement(By.id("result")).getText();

    WebElement foo = allOptions.get(0);
    WebElement bar = allOptions.get(1);

    foo.click();
    assertThat(driver.findElement(By.id("result")).getText()).isEqualTo(initialTextValue);
    bar.click();
    assertThat(driver.findElement(By.id("result")).getText()).isEqualTo("bar");
  }

  @Test
  @NotYetImplemented(SAFARI)
  public void testShouldEmitOnClickEventsWhenSelectingElements() {
    driver.get(pages.javascriptPage);
    // Intentionally not looking up the select tag. See selenium r7937 for details.
    List<WebElement> allOptions =
        driver.findElements(By.xpath("//select[@id='selector2']//option"));

    WebElement foo = allOptions.get(0);
    WebElement bar = allOptions.get(1);

    foo.click();
    assertThat(driver.findElement(By.id("result")).getText()).isEqualTo("foo");
    bar.click();
    assertThat(driver.findElement(By.id("result")).getText()).isEqualTo("bar");
  }

  @Test
  @Ignore(value = IE, reason = "Only fires the onchange event when the checkbox loses the focus")
  @NotYetImplemented(SAFARI)
  public void testShouldEmitOnChangeEventsWhenChangingTheStateOfACheckbox() {
    driver.get(pages.javascriptPage);
    WebElement checkbox = driver.findElement(By.id("checkbox"));

    checkbox.click();
    WebElement result = driver.findElement(By.id("result"));
    wait.until(elementTextToEqual(result, "checkbox thing"));
  }

  @Test
  void testShouldEmitClickEventWhenClickingOnATextInputElement() {
    driver.get(pages.javascriptPage);

    WebElement clicker = driver.findElement(By.id("clickField"));
    clicker.click();

    wait.until(elementValueToEqual(clicker, "Clicked"));
    assertThat(clicker.getAttribute("value")).isEqualTo("Clicked");
  }

  @Test
  @NotYetImplemented(SAFARI)
  public void testShouldFireTwoClickEventsWhenClickingOnALabel() {
    driver.get(pages.javascriptPage);

    driver.findElement(By.id("labelForCheckbox")).click();

    WebElement result = driver.findElement(By.id("result"));
    wait.until(elementTextToContain(result, "labelclick chboxclick"));
  }

  @Test
  @NotYetImplemented(SAFARI)
  public void testClearingAnElementShouldCauseTheOnChangeHandlerToFire() {
    driver.get(pages.javascriptPage);

    WebElement element = driver.findElement(By.id("clearMe"));
    element.clear();

    WebElement result = driver.findElement(By.id("result"));
    assertThat(result.getText()).isEqualTo("Cleared");
  }

  @Test
  void testSendingKeysToAnotherElementShouldCauseTheBlurEventToFire() {
    assumeFalse(browserNeedsFocusOnThisOs(driver));

    driver.get(pages.javascriptPage);
    WebElement element = driver.findElement(By.id("theworks"));
    element.sendKeys("foo");
    WebElement element2 = driver.findElement(By.id("changeable"));
    element2.sendKeys("bar");
    assertEventFired("blur", driver);
  }

  @Test
  @Ignore(value = SAFARI, reason = "Allows only one instance")
  public void testSendingKeysToAnotherElementShouldCauseTheBlurEventToFireInNonTopmostWindow() {
    assumeFalse(browserNeedsFocusOnThisOs(driver));

    WebDriver driver2 = new WebDriverBuilder().get();
    try {
      // topmost
      driver2.get(pages.javascriptPage);
      WebElement element = driver2.findElement(By.id("theworks"));
      element.sendKeys("foo");
      WebElement element2 = driver2.findElement(By.id("changeable"));
      element2.sendKeys("bar");
      assertEventFired("blur", driver2);

      // non-topmost
      driver.get(pages.javascriptPage);
      element = driver.findElement(By.id("theworks"));
      element.sendKeys("foo");
      element2 = driver.findElement(By.id("changeable"));
      element2.sendKeys("bar");
      assertEventFired("blur", driver);

    } finally {
      driver2.quit();
    }

    driver.get(pages.javascriptPage);
    WebElement element = driver.findElement(By.id("theworks"));
    element.sendKeys("foo");
    WebElement element2 = driver.findElement(By.id("changeable"));
    element2.sendKeys("bar");
    assertEventFired("blur", driver);
  }

  @Test
  void testSendingKeysToAnElementShouldCauseTheFocusEventToFire() {
    assumeFalse(browserNeedsFocusOnThisOs(driver));

    driver.get(pages.javascriptPage);
    WebElement element = driver.findElement(By.id("theworks"));
    element.sendKeys("foo");
    assertEventFired("focus", driver);
  }

  @Test
  @NotYetImplemented(SAFARI)
  public void testSendingKeysToAFocusedElementShouldNotBlurThatElement() {
    assumeFalse(browserNeedsFocusOnThisOs(driver));

    driver.get(pages.javascriptPage);
    WebElement element = driver.findElement(By.id("theworks"));
    element.click();

    // Wait until focused
    boolean focused = false;
    WebElement result = driver.findElement(By.id("result"));
    for (int i = 0; i < 5; ++i) {
      String fired = result.getText();
      if (fired.contains("focus")) {
        focused = true;
        break;
      }
      try {
        Thread.sleep(200);
      } catch (InterruptedException e) {
        throw new RuntimeException(e);
      }
    }
    if (!focused) {
      fail("Clicking on element didn't focus it in time - can't proceed so failing");
    }

    element.sendKeys("a");
    assertEventNotFired("blur", driver);
  }

  @Test
  @NotYetImplemented(HTMLUNIT)
  @NotYetImplemented(IE)
  @NotYetImplemented(SAFARI)
  public void testClickingAnUnfocusableChildShouldNotBlurTheParent() {
    driver.get(pages.javascriptPage);
    // Click on parent, giving it the focus.
    WebElement parent = driver.findElement(By.id("hideOnBlur"));
    parent.click();
    assertEventNotFired("blur", driver);
    // Click on child. It is not focusable, so focus should stay on the parent.
    driver.findElement(By.id("hideOnBlurChild")).click();
    assertThat(parent.isDisplayed())
        .as("#hideOnBlur should still be displayed after click")
        .isTrue();
    assertEventNotFired("blur", driver);
    // Click elsewhere, and let the element disappear.
    driver.findElement(By.id("result")).click();
    assertEventFired("blur", driver);
  }

  @Test
  void testSubmittingFormFromFormElementShouldFireOnSubmitForThatForm() {
    driver.get(pages.javascriptPage);
    WebElement formElement = driver.findElement(By.id("submitListeningForm"));
    formElement.submit();
    assertEventFired("form-onsubmit", driver);
  }

  @Test
  void testSubmittingFormFromFormInputSubmitElementShouldFireOnSubmitForThatForm() {
    driver.get(pages.javascriptPage);
    WebElement submit = driver.findElement(By.id("submitListeningForm-submit"));
    submit.submit();
    assertEventFired("form-onsubmit", driver);
  }

  @Test
  void
      testSubmittingFormFromFormInputTextElementShouldFireOnSubmitForThatFormAndNotClickOnThatInput() {
    driver.get(pages.javascriptPage);
    WebElement submit = driver.findElement(By.id("submitListeningForm-submit"));
    submit.submit();
    assertEventFired("form-onsubmit", driver);
    assertEventNotFired("text-onclick", driver);
  }

  @Test
  void testUploadingFileShouldFireOnChangeEvent() throws IOException {
    driver.get(pages.formPage);
    WebElement uploadElement = driver.findElement(By.id("upload"));
    WebElement result = driver.findElement(By.id("fileResults"));
    assertThat(result.getText()).isEmpty();

    File file = File.createTempFile("test", "txt");
    file.deleteOnExit();

    uploadElement.sendKeys(file.getAbsolutePath());
    // Shift focus to something else because send key doesn't make the focus leave
    driver.findElement(By.id("id-name1")).click();

    assertThat(result.getText()).isEqualTo("changed");
  }

  private String getTextFromElementOnceAvailable(String elementId) {
    return wait.until(visibilityOfElementLocated(By.id(elementId))).getText();
  }

  @Test
  void testShouldReportTheXAndYCoordinatesWhenClicking() {
    driver.get(pages.clickEventPage);

    WebElement element = driver.findElement(By.id("eventish"));
    element.click();

    String clientX = getTextFromElementOnceAvailable("clientX");
    String clientY = getTextFromElementOnceAvailable("clientY");

    assertThat(clientX).isNotEqualTo("0");
    assertThat(clientY).isNotEqualTo("0");
  }

  @Test
  void testClickEventsShouldBubble() {
    driver.get(pages.clicksPage);
    driver.findElement(By.id("bubblesFrom")).click();
    boolean eventBubbled =
        (Boolean) ((JavascriptExecutor) driver).executeScript("return !!window.bubbledClick;");
    assertThat(eventBubbled).as("Event bubbled").isTrue();
  }

  @Test
  @Ignore(HTMLUNIT)
  public void testClickOverlappingElements() {
    driver.get(appServer.whereIs("click_tests/overlapping_elements.html"));
    WebElement element = driver.findElement(By.id("under"));
    // TODO: change to ElementClickInterceptedException
    assertThatExceptionOfType(WebDriverException.class).isThrownBy(element::click);
  }

  @Test
  @Ignore(HTMLUNIT)
  @NotYetImplemented(SAFARI)
  public void testClickAnElementThatDisappear() {
    driver.get(appServer.whereIs("click_tests/disappearing_element.html"));
    driver.findElement(By.id("over")).click();
    assertThat(driver.findElement(By.id("log")).getText())
        .startsWith(
            "Log:\n"
                + "mousedown in over (handled by over)\n"
                + "mousedown in over (handled by body)\n"
                + "mouseup in under (handled by under)\n"
                + "mouseup in under (handled by body)");
  }
}
