/*
 * Copyright (c) Facebook, Inc. and its affiliates.
 *
 * Licensed 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 com.facebook.litho.testing.assertj;

import android.graphics.drawable.Drawable;
import androidx.annotation.DrawableRes;
import androidx.annotation.StringRes;
import com.facebook.litho.Component;
import com.facebook.litho.ComponentContext;
import com.facebook.litho.LithoView;
import com.facebook.litho.testing.Whitebox;
import com.facebook.litho.testing.helper.ComponentTestHelper;
import com.facebook.litho.testing.state.StateUpdatesTestHelper;
import com.facebook.litho.testing.subcomponents.InspectableComponent;
import com.facebook.litho.testing.subcomponents.SubComponent;
import java.util.List;
import org.assertj.core.api.AbstractAssert;
import org.assertj.core.api.Java6Assertions;
import org.assertj.core.api.ListAssert;
import org.assertj.core.api.iterable.Extractor;
import org.assertj.core.util.CheckReturnValue;

/**
 * Assertion methods for {@link Component}s.
 *
 * <p>To create an instance of this class, invoke <code>
 * {@link ComponentAssert#assertThat(ComponentContext, Component)}</code> or <code>
 * {@link ComponentAssert#assertThat(Component.Builder)}</code>.
 *
 * <p>Alternatively, use {@link LithoAssertions} which provides entry points to all Litho AssertJ
 * helpers.
 */
public final class ComponentAssert extends AbstractAssert<ComponentAssert, Component> {

  private final ComponentContext mComponentContext;

  public static ComponentAssert assertThat(ComponentContext componentContext, Component component) {
    return new ComponentAssert(componentContext, component);
  }

  public static ComponentAssert assertThat(Component.Builder<?> builder) {
    // mContext is freed up during build() so we need to get a reference to it before.
    final ComponentContext context = Whitebox.getInternalState(builder, "mContext");
    return new ComponentAssert(context, builder.build());
  }

  /** Performs a state update and returns the new view. */
  public LithoViewAssert afterStateUpdate() {
    return LithoViewAssert.assertThat(
        StateUpdatesTestHelper.getViewAfterStateUpdate(mComponentContext, actual));
  }

  public LithoViewAssert withStateUpdate(final StateUpdatesTestHelper.StateUpdater updater) {
    return LithoViewAssert.assertThat(
        StateUpdatesTestHelper.getViewAfterStateUpdate(mComponentContext, actual, updater));
  }

  private ComponentAssert(ComponentContext c, Component actual) {
    super(actual, ComponentAssert.class);
    mComponentContext = c;
    mComponentContext.setLayoutStateContextForTesting();
  }

  private LithoView mountComponent() {
    return ComponentTestHelper.mountComponent(mComponentContext, actual);
  }

  private LithoViewAssert assertThatLithoView() {
    return LithoViewAssert.assertThat(mountComponent());
  }

  /**
   * Assert that the given component has no sub-components.
   *
   * @deprecated Use {@link #extractingSubComponents} instead.
   */
  @Deprecated
  public ComponentAssert hasNoSubComponents() {
    final List<SubComponent> subComponents =
        ComponentTestHelper.getSubComponents(mComponentContext, actual);
    Java6Assertions.assertThat(subComponents)
        .overridingErrorMessage(
            "Expected Component not to have any sub " + "components, but found %d.",
            subComponents.size())
        .isEmpty();

    return this;
  }

  /**
   * Assert that the given component contains the provided sub-component.
   *
   * @deprecated Use {@link #extractingSubComponents} instead.
   */
  @Deprecated
  public ComponentAssert containsSubComponent(SubComponent subComponent) {
    final List<SubComponent> subComponents =
        ComponentTestHelper.getSubComponents(mComponentContext, actual);
    Java6Assertions.assertThat(subComponents)
        .overridingErrorMessage(
            "Expected to find <%s> as sub component of <%s>, " + "but couldn't find it in %s.",
            subComponent, actual, subComponents)
        .contains(subComponent);

    return this;
  }

  /**
   * Assert that the given component does <strong>not</strong> contain the provided sub-component.
   *
   * @deprecated Use {@link #extractingSubComponents} instead.
   */
  @Deprecated
  public ComponentAssert doesNotContainSubComponent(SubComponent subComponent) {
    final List<SubComponent> subComponents =
        ComponentTestHelper.getSubComponents(mComponentContext, actual);
    Java6Assertions.assertThat(subComponents)
        .overridingErrorMessage(
            "Did not expect to find <%s> as sub component of <%s>, " + "but it was present.",
            subComponent, actual)
        .doesNotContain(subComponent);

    return this;
  }

  /** Assert that any view in the given Component has the provided content description. */
  public ComponentAssert hasContentDescription(String contentDescription) {
    assertThatLithoView().hasContentDescription(contentDescription);

    return this;
  }

  /**
   * Assert that the given component contains the drawable identified by the provided drawable
   * resource id.
   */
  public ComponentAssert hasVisibleDrawable(@DrawableRes int drawableRes) {
    assertThatLithoView().hasVisibleDrawable(drawableRes);

    return this;
  }

  /** Assert that the given component contains the drawable provided. */
  public ComponentAssert hasVisibleDrawable(Drawable drawable) {
    assertThatLithoView().hasVisibleDrawable(drawable);

    return this;
  }

  /** Inverse of {@link #hasVisibleDrawable(Drawable)} */
  public ComponentAssert doesNotHaveVisibleDrawable(Drawable drawable) {
    assertThatLithoView().doesNotHaveVisibleDrawable(drawable);

    return this;
  }

  /** Inverse of {@link #hasVisibleDrawable(int)} */
  public ComponentAssert doesNotHaveVisibleDrawable(@DrawableRes int drawableRes) {
    assertThatLithoView().doesNotHaveVisibleDrawable(drawableRes);

    return this;
  }

  /** Assert that the given component has the exact text provided. */
  public ComponentAssert hasVisibleText(String text) {
    assertThatLithoView().hasVisibleText(text);

    return this;
  }

  /** Assert that the given component has the exact text identified by resource id. */
  public ComponentAssert hasVisibleText(@StringRes int resourceId) {
    assertThatLithoView().hasVisibleText(resourceId);

    return this;
  }

  /** Inverse of {@link #hasVisibleText(String)} */
  public ComponentAssert doesNotHaveVisibleText(String text) {
    assertThatLithoView().doesNotHaveVisibleText(text);

    return this;
  }

  /** Inverse of {@link #hasVisibleText(int)} */
  public ComponentAssert doesNotHaveVisibleText(@StringRes int resourceId) {
    assertThatLithoView().doesNotHaveVisibleText(resourceId);

    return this;
  }

  /** Assert that the given component contains the provided pattern. */
  public ComponentAssert hasVisibleTextMatching(String pattern) {
    assertThatLithoView().hasVisibleTextMatching(pattern);

    return this;
  }

  /**
   * Assert that the view tag is present for the given index.
   *
   * @param tagId Index of the view tag.
   * @param tagValue View tag value.
   */
  public ComponentAssert hasViewTag(int tagId, Object tagValue) {
    assertThatLithoView().hasViewTag(tagId, tagValue);

    return this;
  }

  /**
   * Verifies that the component contains the exact list of provided sub-components.
   *
   * @deprecated Use {@link #extractingSubComponents} instead.
   */
  @Deprecated
  public ComponentAssert hasSubComponents(SubComponent... subComponents) {
    final List<SubComponent> mountedSubComponents =
        ComponentTestHelper.getSubComponents(mComponentContext, actual);

    Java6Assertions.assertThat(mountedSubComponents).containsExactly(subComponents);

    return this;
  }

  /**
   * Verifies that the component contains only the given sub-components and nothing else, in order.
   *
   * @deprecated Use {@link #extractingSubComponents} instead.
   */
  @Deprecated
  public ComponentAssert containsOnlySubComponents(SubComponent... subComponents) {
    final List<SubComponent> mountedSubComponents =
        ComponentTestHelper.getSubComponents(mComponentContext, actual);

    Java6Assertions.assertThat(mountedSubComponents).containsOnly(subComponents);

    return this;
  }

  /**
   * Extract values from the underlying component based on the {@link Extractor} provided.
   *
   * @param extractor The extractor applied to the Component.
   * @param <A> Type of the value extracted.
   * @return ListAssert for the extracted values.
   */
  @CheckReturnValue
  public <A> ListAssert<A> extracting(Extractor<Component, List<A>> extractor) {
    final List<A> value = extractor.extract(actual);
    return new ListAssert<>(value);
  }

  /** Extract the sub components from the underlying Component, returning a ListAssert over it. */
  @CheckReturnValue
  public ListAssert<InspectableComponent> extractingSubComponents(ComponentContext c) {
    return extracting(SubComponentExtractor.subComponents(c));
  }

  /**
   * Extract the sub components recursively from the underlying Component, returning a ListAssert
   * over it.
   */
  @CheckReturnValue
  public ListAssert<InspectableComponent> extractingSubComponentsDeeply(ComponentContext c) {
    return extracting(SubComponentDeepExtractor.subComponentsDeeply(c));
  }

  public ComponentAssert extractingSubComponentAt(int index) {
    InspectableComponent component =
        SubComponentExtractor.subComponents(mComponentContext).extract(actual).get(index);
    return new ComponentAssert(mComponentContext, component.getComponent());
  }

  /**
   * Assert that a given {@link Component} renders to null, i.e. its <code>onCreateLayout
   * </code> method resolves to a {@link ComponentContext#NULL_LAYOUT}.
   */
  public ComponentAssert wontRender() {
    Java6Assertions.assertThat(Component.willRender(mComponentContext, actual))
        .overridingErrorMessage("Expected Component to render to null, but it did not.")
        .isFalse();

    return this;
  }

  /**
   * Assert that a given {@link Component} produces a layout that's not equivalent to {@link
   * ComponentContext#NULL_LAYOUT}.
   */
  public ComponentAssert willRender() {
    Java6Assertions.assertThat(Component.willRender(mComponentContext, actual))
        .overridingErrorMessage("Expected Component to not render to null, but it did.")
        .isTrue();

    return this;
  }

  /** @see #wontRender() */
  public ComponentAssert willNotRender() {
    return wontRender();
  }
}