// 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.support.pagefactory;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.lang.reflect.Field;
import java.util.List;
import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.Test;
import org.openqa.selenium.By;
import org.openqa.selenium.SearchContext;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.support.AbstractFindByBuilder;
import org.openqa.selenium.support.ByIdOrName;
import org.openqa.selenium.support.FindAll;
import org.openqa.selenium.support.FindBy;
import org.openqa.selenium.support.FindBys;
import org.openqa.selenium.support.How;
import org.openqa.selenium.support.PageFactoryFinder;

@Tag("UnitTests")
class AnnotationsTest {

  public WebElement default_field;
  public List<WebElement> defaultList_field;

  @FindBy(how = How.NAME, using = "cheese")
  public WebElement longFindBy_field;

  @FindBy(how = How.NAME, using = "cheese")
  public List<WebElement> longFindAllBy_field;

  @FindBy(name = "cheese")
  public WebElement shortFindBy_field;

  @FindBy(name = "cheese")
  public List<WebElement> shortFindAllBy_field;

  @FindBys({@FindBy(how = How.NAME, using = "cheese"), @FindBy(id = "fruit")})
  public WebElement findBys_field;

  @FindAll({@FindBy(how = How.TAG_NAME, using = "div"), @FindBy(id = "fruit")})
  public WebElement findAll_field;

  @FindBy(how = How.NAME, using = "cheese")
  @FindBys({@FindBy(how = How.NAME, using = "cheese"), @FindBy(id = "fruit")})
  public WebElement findByAndFindBys_field;

  @FindBy(how = How.NAME, using = "cheese")
  @FindAll({@FindBy(how = How.NAME, using = "cheese"), @FindBy(id = "fruit")})
  public WebElement findAllAndFindBy_field;

  @FindAll({@FindBy(how = How.NAME, using = "cheese"), @FindBy(id = "fruit")})
  @FindBys({@FindBy(how = How.NAME, using = "cheese"), @FindBy(id = "fruit")})
  public WebElement findAllAndFindBys_field;

  @FindBy(id = "cheese", name = "fruit")
  public WebElement findByMultipleHows_field;

  @FindBy(id = "cheese", name = "fruit")
  public List<WebElement> findAllByMultipleHows_field;

  @FindBys({@FindBy(id = "cheese", name = "fruit"), @FindBy(id = "crackers")})
  public WebElement findBysMultipleHows_field;

  @FindAll({@FindBy(id = "cheese", name = "fruit"), @FindBy(id = "crackers")})
  public WebElement findAllMultipleHows_field;

  @FindBy(using = "cheese")
  public WebElement findByUnsetHow_field;

  @Retention(RetentionPolicy.RUNTIME)
  @Target({ElementType.FIELD, ElementType.TYPE})
  @PageFactoryFinder(FindByXXXX.FindByXXXXBuilder.class)
  public @interface FindByXXXX {

    class FindByXXXXBuilder extends AbstractFindByBuilder {

      @Override
      public By buildIt(Object annotation, Field field) {
        return new By() {
          @Override
          public List<WebElement> findElements(SearchContext context) {
            return null;
          }

          @Override
          public String toString() {
            return "FindByXXXX's By";
          }
        };
      }
    }
  }

  @FindByXXXX() public WebElement findBy_xxx;

  @Test
  void testDefault() throws Exception {
    assertThat(new Annotations(getClass().getField("default_field")).buildBy())
        .isEqualTo(new ByIdOrName("default_field"));
  }

  @Test
  void testDefaultList() throws Exception {
    assertThat(new Annotations(getClass().getField("defaultList_field")).buildBy())
        .isEqualTo(new ByIdOrName("defaultList_field"));
  }

  @Test
  void longFindBy() throws Exception {
    assertThat(new Annotations(getClass().getField("longFindBy_field")).buildBy())
        .isEqualTo(By.name("cheese"));
  }

  @Test
  void longFindAllBy() throws Exception {
    assertThat(new Annotations(getClass().getField("longFindAllBy_field")).buildBy())
        .isEqualTo(By.name("cheese"));
  }

  @Test
  void shortFindBy() throws Exception {
    assertThat(new Annotations(getClass().getField("shortFindBy_field")).buildBy())
        .isEqualTo(By.name("cheese"));
  }

  @Test
  void shortFindAllBy() throws Exception {
    assertThat(new Annotations(getClass().getField("shortFindAllBy_field")).buildBy())
        .isEqualTo(By.name("cheese"));
  }

  @Test
  void findBys() throws Exception {
    assertThat(new Annotations(getClass().getField("findBys_field")).buildBy())
        .isEqualTo(new ByChained(By.name("cheese"), By.id("fruit")));
  }

  @Test
  void findAll() throws Exception {
    assertThat(new Annotations(getClass().getField("findAll_field")).buildBy())
        .isEqualTo(new ByAll(By.tagName("div"), By.id("fruit")));
  }

  @Test
  void findByAndFindBys() {
    assertThatExceptionOfType(IllegalArgumentException.class)
        .describedAs("Expected field annotated with both @FindBy and @FindBys to throw exception")
        .isThrownBy(() -> new Annotations(getClass().getField("findByAndFindBys_field")).buildBy());
  }

  @Test
  void findAllAndFindBy() {
    assertThatExceptionOfType(IllegalArgumentException.class)
        .describedAs("Expected field annotated with both @FindAll and @FindBy to throw exception")
        .isThrownBy(() -> new Annotations(getClass().getField("findByAndFindBys_field")).buildBy());
  }

  @Test
  void findAllAndFindBys() {
    assertThatExceptionOfType(IllegalArgumentException.class)
        .describedAs("Expected field annotated with both @FindAll and @FindBys to throw exception")
        .isThrownBy(() -> new Annotations(getClass().getField("findByAndFindBys_field")).buildBy());
  }

  @Test
  void findByMultipleHows() {
    assertThatExceptionOfType(IllegalArgumentException.class)
        .describedAs("Expected field annotated with invalid @FindBy to throw error")
        .isThrownBy(
            () -> new Annotations(getClass().getField("findByMultipleHows_field")).buildBy());
  }

  @Test
  void findAllByMultipleHows() {
    assertThatExceptionOfType(IllegalArgumentException.class)
        .describedAs(
            "Expected field annotated with @FindAllBy containing bad @FindAllBy to throw error")
        .isThrownBy(
            () -> new Annotations(getClass().getField("findAllByMultipleHows_field")).buildBy());
  }

  @Test
  void findBysMultipleHows() {
    assertThatExceptionOfType(IllegalArgumentException.class)
        .describedAs("Expected field annotated with @FindBys containing bad @FindBy to throw error")
        .isThrownBy(
            () -> new Annotations(getClass().getField("findBysMultipleHows_field")).buildBy());
  }

  @Test
  void findAllMultipleHows() {
    assertThatExceptionOfType(IllegalArgumentException.class)
        .describedAs("Expected field annotated with @FindAll containing bad @FindBy to throw error")
        .isThrownBy(
            () -> new Annotations(getClass().getField("findAllMultipleHows_field")).buildBy());
  }

  @Test
  void findByUnsetHowIsEquivalentToFindById() throws Exception {
    assertThat(new Annotations(getClass().getField("findByUnsetHow_field")).buildBy())
        .isEqualTo(By.id("cheese"));
  }

  /*
   * Example of how teams making their own @FindBy alike would experience a general purpose
   * capability.
   *
   * @See @FindByXXXX (above)
   */
  @Test
  void findBySomethingElse() throws Exception {
    assertThat(new Annotations(getClass().getField("findBy_xxx")).buildBy().toString())
        .isEqualTo("FindByXXXX's By");
  }
}
