/*
 * Copyright (c) Meta Platforms, Inc. and 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.rendercore.primitives

import android.content.Context
import android.util.SparseArray
import com.facebook.rendercore.BindData
import com.facebook.rendercore.ContentAllocator
import com.facebook.rendercore.MountDelegate
import com.facebook.rendercore.RenderUnit
import com.facebook.rendercore.Systracer

/**
 * MountBehavior defines how to allocate a [View]/[Drawable] and apply properties to it.
 *
 * @property id Unique id identifying the [RenderUnit] in the tree of Node it is part of.
 * @property description A description of the underlying [RenderUnit]. Mainly for debugging purposes
 *   such as tracing and logs. Maximum description length is 127 characters. Everything above that
 *   will be truncated.
 * @property contentAllocator Provides a [View]/[Drawable] content.
 * @property mountConfigurationCall A function that allows for applying properties to the content.
 */
class MountBehavior<ContentType : Any>(
    private val id: Long,
    private val description: String?,
    private val contentAllocator: ContentAllocator<ContentType>,
    private val mountConfigurationCall: MountConfigurationScope<ContentType>.() -> Unit
) {

  constructor(
      id: Long,
      contentAllocator: ContentAllocator<ContentType>,
      mountConfigurationCall: MountConfigurationScope<ContentType>.() -> Unit
  ) : this(id, null, contentAllocator, mountConfigurationCall)

  internal val renderUnit: PrimitiveRenderUnit<ContentType> by
      lazy(LazyThreadSafetyMode.NONE) {
        val mountConfigurationScope = MountConfigurationScope<ContentType>()
        mountConfigurationScope.mountConfigurationCall()

        object :
            PrimitiveRenderUnit<ContentType>(
                contentAllocator.renderType,
                mountConfigurationScope.fixedBinders,
                mountConfigurationScope.renderUnitExtras) {
          override fun getContentAllocator(): ContentAllocator<ContentType> {
            return this@MountBehavior.contentAllocator
          }

          override fun getId(): Long {
            return this@MountBehavior.id
          }

          override fun getDescription(): String {
            return this@MountBehavior.description?.take(MAX_DESCRIPTION_LENGTH)
                ?: super.getDescription()
          }
        }
      }
}

abstract class PrimitiveRenderUnit<ContentType>(
    renderType: RenderType,
    fixedMountBinders: List<DelegateBinder<*, ContentType, *>>,
    extras: SparseArray<Any?>? = null
) :
    RenderUnit<ContentType>(
        renderType,
        fixedMountBinders,
        emptyList(), // optional binders
        emptyList(), // attach binders
        extras) {

  /**
   * This method is an override that calls super impl to make it public on RenderUnit because it
   * needs to be called in PrimitiveLithoRenderUnit.
   */
  public override fun mountBinders(
      context: Context,
      content: ContentType,
      layoutData: Any?,
      bindData: BindData,
      tracer: Systracer
  ) = super.mountBinders(context, content, layoutData, bindData, tracer)

  /**
   * This method is an override that calls super impl to make it public on RenderUnit because it
   * needs to be called in PrimitiveLithoRenderUnit.
   */
  public override fun unmountBinders(
      context: Context,
      content: ContentType,
      layoutData: Any?,
      bindData: BindData,
      tracer: Systracer
  ) = super.unmountBinders(context, content, layoutData, bindData, tracer)

  /**
   * This method is an override that calls super impl to make it public on RenderUnit because it
   * needs to be called in PrimitiveLithoRenderUnit.
   */
  public override fun attachBinders(
      context: Context,
      content: ContentType,
      layoutData: Any?,
      bindData: BindData,
      tracer: Systracer
  ) = super.attachBinders(context, content, layoutData, bindData, tracer)

  /**
   * This method is an override that calls super impl to make it public on RenderUnit because it
   * needs to be called in PrimitiveLithoRenderUnit.
   */
  public override fun detachBinders(
      context: Context,
      content: ContentType,
      layoutData: Any?,
      bindData: BindData,
      tracer: Systracer
  ) = super.detachBinders(context, content, layoutData, bindData, tracer)

  /**
   * This method is an override that calls super impl to make it public on RenderUnit because it
   * needs to be called in PrimitiveLithoRenderUnit.
   */
  public override fun updateBinders(
      context: Context,
      content: ContentType,
      currentRenderUnit: RenderUnit<ContentType>,
      currentLayoutData: Any?,
      newLayoutData: Any?,
      mountDelegate: MountDelegate?,
      bindData: BindData,
      isAttached: Boolean,
      tracer: Systracer,
  ) =
      super.updateBinders(
          context,
          content,
          currentRenderUnit,
          currentLayoutData,
          newLayoutData,
          mountDelegate,
          bindData,
          isAttached,
          tracer,
      )
}


/*
 * Copyright (c) Meta Platforms, Inc. and 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.rendercore.primitives

import android.content.Context
import android.util.SparseArray
import androidx.annotation.IdRes
import com.facebook.rendercore.RenderUnit
import kotlin.reflect.KFunction2
import kotlin.reflect.KMutableProperty1

/** The implicit receiver for [MountBehavior.mountConfigurationCall]. */
class MountConfigurationScope<ContentType : Any> internal constructor() {

  internal val fixedBinders: List<RenderUnit.DelegateBinder<*, ContentType, *>>
    get() = _fixedBinders

  private val _fixedBinders: MutableList<RenderUnit.DelegateBinder<*, ContentType, *>> =
      mutableListOf<RenderUnit.DelegateBinder<*, ContentType, *>>()

  internal var renderUnitExtras: SparseArray<Any?>? = null

  /**
   * Stores the current binder description which is used by a binder defined within
   * withDescription{} block.
   */
  @PublishedApi internal var binderDescription: String? = null

  /**
   * Creates a binding between the value, and the contentâ€™s property. Allows for specifying custom
   * logic and handling complex cases.
   *
   * Additionally, any time the [deps] changes between updates, the existing [UnbindFunc.onUnbind]
   * cleanup callback will be invoked, and the new [bindCall] callback will be invoked.
   *
   * @param deps Should contain any props or state your [bindCall]/[UnbindFunc.onUnbind] callbacks
   *   use. For example, if you're using [bind] to set a background based on a color you get as a
   *   prop, [deps] should include that color.
   * @param bindCall A function that allows for applying properties to the content.
   */
  fun bind(vararg deps: Any?, bindCall: BindScope.(content: ContentType) -> UnbindFunc) {
    val bindScope = BindScope()
    _fixedBinders.add(
        RenderUnit.DelegateBinder.createDelegateBinder(
            deps,
            object : RenderUnit.Binder<Array<out Any?>, ContentType, UnbindFunc> {

              val lastBinderIndex: Int = _fixedBinders.size

              override fun shouldUpdate(
                  currentModel: Array<out Any?>?,
                  newModel: Array<out Any?>?,
                  currentLayoutData: Any?,
                  nextLayoutData: Any?
              ): Boolean {
                if (currentModel == null && newModel == null) {
                  // nothing has changed
                  return false
                }

                if (currentModel != null && newModel != null) {
                  // return true if model has changed
                  return !currentModel.contentDeepEquals(newModel)
                }

                // model was null and became non null or was non null and became null
                return true
              }

              override fun bind(
                  context: Context?,
                  content: ContentType,
                  model: Array<out Any?>?,
                  layoutData: Any?
              ): UnbindFunc {
                return bindScope.bindCall(content)
              }

              override fun unbind(
                  context: Context?,
                  content: ContentType,
                  model: Array<out Any?>?,
                  layoutData: Any?,
                  unbindFunc: UnbindFunc
              ) {
                unbindFunc.onUnbind()
              }

              override fun getDescription(): String {
                return binderDescription ?: "#$lastBinderIndex"
              }
            }))
  }

  /**
   * Creates a binding between the value, and the contentâ€™s property. Allows for specifying custom
   * logic and handling complex cases and for accessing in [bindCall] the layout data that was
   * generated during the layout pass.
   *
   * Additionally, any time the [deps] changes between updates, the existing [UnbindFunc.onUnbind]
   * cleanup callback will be invoked, and the new [bindCall] callback will be invoked.
   *
   * @param deps Should contain any props or state your [bindCall]/[UnbindFunc.onUnbind] callbacks
   *   use. For example, if you're using [bind] to set a background based on a color you get as a
   *   prop, [deps] should include that color.
   * @param bindCall A function that allows for applying properties to the content and accessing the
   *   layout data that was generated during the layout pass.
   */
  fun <LayoutDataT> bindWithLayoutData(
      vararg deps: Any?,
      bindCall: BindScope.(content: ContentType, layoutData: LayoutDataT) -> UnbindFunc
  ) {
    val bindScope = BindScope()
    _fixedBinders.add(
        RenderUnit.DelegateBinder.createDelegateBinder(
            deps,
            object : RenderUnit.Binder<Array<out Any?>, ContentType, UnbindFunc> {

              val lastBinderIndex: Int = _fixedBinders.size

              override fun shouldUpdate(
                  currentModel: Array<out Any?>?,
                  newModel: Array<out Any?>?,
                  currentLayoutData: Any?,
                  nextLayoutData: Any?
              ): Boolean {
                if (currentModel == null &&
                    newModel == null &&
                    currentLayoutData == null &&
                    nextLayoutData == null) {
                  // nothing has changed
                  return false
                }

                if (currentLayoutData != nextLayoutData) {
                  // layout data has changed
                  return true
                }

                if (currentModel != null && newModel != null) {
                  // return true if model has changed
                  return !currentModel.contentDeepEquals(newModel)
                }

                // model was null and became non null or was non null and became null
                return true
              }

              @Suppress("UNCHECKED_CAST")
              override fun bind(
                  context: Context?,
                  content: ContentType,
                  model: Array<out Any?>?,
                  layoutData: Any?
              ): UnbindFunc {
                return bindScope.bindCall(content, layoutData as LayoutDataT)
              }

              override fun unbind(
                  context: Context?,
                  content: ContentType,
                  model: Array<out Any?>?,
                  layoutData: Any?,
                  unbindFunc: UnbindFunc
              ) {
                unbindFunc.onUnbind()
              }

              override fun getDescription(): String {
                return binderDescription ?: "#$lastBinderIndex"
              }
            }))
  }

  /**
   * Creates a binding between the value, and the contentâ€™s property.
   *
   * @param defaultValue value that will be set to the Content after unbind
   * @param setter function reference that will set the value on the content
   */
  fun <T> T.bindTo(setter: KFunction2<ContentType, T, *>, defaultValue: T) {
    _fixedBinders.add(
        RenderUnit.DelegateBinder.createDelegateBinder(
            this,
            object : RenderUnit.Binder<T, ContentType, Any?> {

              val lastBinderIndex: Int = _fixedBinders.size

              override fun shouldUpdate(
                  currentModel: T,
                  newModel: T,
                  currentLayoutData: Any?,
                  nextLayoutData: Any?
              ): Boolean {
                return currentModel != newModel
              }

              override fun bind(
                  context: Context?,
                  content: ContentType,
                  model: T,
                  layoutData: Any?
              ): Any? {
                setter(content, model)
                return null
              }

              override fun unbind(
                  context: Context?,
                  content: ContentType,
                  model: T,
                  layoutData: Any?,
                  bindData: Any?
              ) {
                setter(content, defaultValue)
              }

              override fun getDescription(): String {
                return binderDescription ?: "#$lastBinderIndex"
              }
            }))
  }

  /**
   * Creates a binding between the value, and the contentâ€™s property.
   *
   * @param defaultValue value that will be set to the Content after unbind
   * @param setter property reference that will set the value on the content
   */
  fun <T> T.bindTo(setter: KMutableProperty1<ContentType, T>, defaultValue: T) {
    _fixedBinders.add(
        RenderUnit.DelegateBinder.createDelegateBinder(
            this,
            object : RenderUnit.Binder<T, ContentType, Any?> {

              val lastBinderIndex: Int = _fixedBinders.size

              override fun shouldUpdate(
                  currentModel: T,
                  newModel: T,
                  currentLayoutData: Any?,
                  nextLayoutData: Any?
              ): Boolean {
                return currentModel != newModel
              }

              override fun bind(
                  context: Context?,
                  content: ContentType,
                  model: T,
                  layoutData: Any?
              ): Any? {
                setter.set(content, model)
                return null
              }

              override fun unbind(
                  context: Context?,
                  content: ContentType,
                  model: T,
                  layoutData: Any?,
                  bindData: Any?
              ) {
                setter.set(content, defaultValue)
              }

              override fun getDescription(): String {
                return binderDescription ?: "#$lastBinderIndex"
              }
            }))
  }

  /**
   * Creates a binding between the value, and the contentâ€™s property. The default value of the
   * property is assumed to be null, so after unbind, null value will be set to the Content.
   *
   * @param setter function reference that will set the value on the content
   */
  inline fun <T> T.bindTo(setter: KFunction2<ContentType, T?, *>) = bindTo(setter, null)

  /**
   * Creates a binding between the value, and the contentâ€™s property. The default value of the
   * property is assumed to be null, so after unbind, null value will be set to the Content.
   *
   * @param setter property reference that will set the value on the content
   */
  inline fun <T> T.bindTo(setter: KMutableProperty1<ContentType, T?>) = bindTo(setter, null)

  /**
   * Sets the description on the [RenderUnit.Binder] defined within [binderCall]. Descriptions are
   * used mainly for debugging purposes such as tracing and logs. Maximum description length is 127
   * characters. Everything above that will be truncated.
   */
  inline fun withDescription(
      description: String,
      binderCall: MountConfigurationScope<ContentType>.() -> Unit
  ) {
    try {
      binderDescription = description.take(RenderUnit.MAX_DESCRIPTION_LENGTH)
      this.binderCall()
    } finally {
      binderDescription = null
    }
  }

  /**
   * Adds extra data that will be associated with the underlying [RenderUnit].
   *
   * This is intended mostly for an internal usage such as configuring RenderCore Extensions.
   *
   * @param key The key at which the value will be stored. It must be unique integer generated by
   *   Android resource ID system (via XML).
   * @param value The value to store at a given key.
   */
  fun addExtra(@IdRes key: Int, value: Any?) {
    val extras = renderUnitExtras ?: SparseArray<Any?>().also { renderUnitExtras = it }
    extras.put(key, value)
  }

  /**
   * Creates a binding between the value, and the contentâ€™s property. Allows for specifying custom
   * logic and handling complex cases.
   *
   * It is an error to call [bind] without deps parameter.
   */
  // This deprecated-error function shadows the varargs overload so that the varargs version is not
  // used without key parameters.
  @Deprecated(BIND_NO_DEPS_ERROR, level = DeprecationLevel.ERROR)
  fun bind(bindCall: BindScope.(content: ContentType) -> UnbindFunc): Unit =
      throw IllegalStateException(BIND_NO_DEPS_ERROR)

  /**
   * Creates a binding between the value, and the contentâ€™s property. Allows for specifying custom
   * logic and handling complex cases.
   *
   * It is an error to call [bindWithLayoutData] without deps parameter.
   */
  // This deprecated-error function shadows the varargs overload so that the varargs version is not
  // used without key parameters.
  @Deprecated(BIND_WITH_LAYOUT_DATA_NO_DEPS_ERROR, level = DeprecationLevel.ERROR)
  fun <LayoutDataT> bindWithLayoutData(
      bindCall: BindScope.(content: ContentType, layoutData: LayoutDataT) -> UnbindFunc
  ): Unit = throw IllegalStateException(BIND_WITH_LAYOUT_DATA_NO_DEPS_ERROR)

  companion object {
    private const val BIND_NO_DEPS_ERROR =
        "bind must provide 'deps' parameter that determines whether the existing 'onUnbind' cleanup callback will be invoked, and the new 'bind' callback will be invoked"

    private const val BIND_WITH_LAYOUT_DATA_NO_DEPS_ERROR =
        "bindWithLayoutData must provide 'deps' parameter that determines whether the existing 'onUnbind' cleanup callback will be invoked, and the new 'bindWithLayoutData' callback will be invoked"
  }
}

/**
 * Interface for the [onUnbind] function: use [onUnbind] to define the cleanup function for your
 * content.
 */
fun interface UnbindFunc {
  fun onUnbind()
}

class BindScope {
  /**
   * Defines an unbind function to be invoked when the content needs to be updated or a [Primitive]
   * is detached.
   */
  inline fun onUnbind(crossinline unbindFunc: () -> Unit): UnbindFunc = UnbindFunc { unbindFunc() }
}


/*
 * Copyright (c) Meta Platforms, Inc. and 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.rendercore.primitives

import android.graphics.drawable.Drawable
import android.view.View
import com.facebook.rendercore.LayoutContext
import com.facebook.rendercore.LayoutResult
import com.facebook.rendercore.Node
import com.facebook.rendercore.SizeConstraints

/**
 * Primitive is the fundamental building block of a UI component that can be shared across
 * frameworks such as Bloks and Litho. It's two main responsibilities are:
 * - Laying itself out (along with laying out and placing children if applicable)
 * - Optionally, mounting and configuring a [View] or a [Drawable]
 *
 * A Primitive component must be immutable, and not cause side effects.
 *
 * @property layoutBehavior - defines how a Primitive lays out itself and its children (if present)
 * @property mountBehavior - defines how a Primitive mounts and configures a [View] or a [Drawable]
 *   associated with that Primitive
 */
class Primitive(val layoutBehavior: LayoutBehavior, private val mountBehavior: MountBehavior<*>) :
    Node<Any?> {
  /** Lazily creates and returns a [RenderUnit] configured with [mountBehavior]. */
  val renderUnit: PrimitiveRenderUnit<*>
    get() {
      return mountBehavior.renderUnit
    }

  override fun calculateLayout(
      context: LayoutContext<Any?>,
      widthSpec: Int,
      heightSpec: Int
  ): LayoutResult {
    val layoutScope = LayoutScope(context, context.consumePreviousLayoutDataForCurrentNode())
    return with(layoutBehavior) {
      layoutScope
          .layout(SizeConstraints.fromMeasureSpecs(widthSpec, heightSpec))
          .toNodeLayoutResult(widthSpec, heightSpec, renderUnit)
    }
  }
}


/*
 * Copyright (c) Meta Platforms, Inc. and 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.
 */

@file:JvmName("EquivalenceUtils")

package com.facebook.rendercore.primitives.utils

import android.util.SparseArray
import com.facebook.rendercore.primitives.Equivalence

/** Checks if two objects are equal. */
fun equals(a: Any?, b: Any?): Boolean {
  return a == b
}

/** Checks if two [SparseArray] objects are equal. */
fun equals(a: SparseArray<*>?, b: SparseArray<*>?): Boolean {
  if (a === b) {
    return true
  }

  if (a == null || b == null) {
    return false
  }

  val size = a.size()
  if (size != b.size()) {
    return false
  }

  for (index in 0 until size) {
    val key: Int = a.keyAt(index)
    if (!equals(a.valueAt(index), b.get(key))) {
      return false
    }
  }

  return true
}

/** Checks if two [Equivalence] objects are equivalent. */
fun <T : Equivalence<T>> isEquivalentTo(a: T?, b: T?): Boolean {
  return if (a === b) {
    true
  } else if (a == null || b == null) {
    false
  } else {
    a.isEquivalentTo(b)
  }
}

/**
 * Checks if two objects are equivalent if they implement [Equivalence] or compares the objects
 * field by field.
 */
@Suppress("UNCHECKED_CAST")
fun <T> isEqualOrEquivalentTo(a: T?, b: T?): Boolean {
  return if (a === b) {
    true
  } else if (a == null || b == null) {
    false
  } else if (a is Equivalence<*> && b is Equivalence<*>) {
    isEquivalentTo(a as Equivalence<Any>, b as Equivalence<Any>)
  } else {
    hasEquivalentFields(a, b)
  }
}

/** Checks if all private final fields in two objects are equivalent. */
fun hasEquivalentFields(a: Any, b: Any): Boolean {
  if (a === b) {
    return true
  }

  if (a.javaClass != b.javaClass) {
    return false
  }

  for (field in a.javaClass.declaredFields) {
    val val1: Any?
    val val2: Any?
    try {
      val wasAccessible = field.isAccessible
      if (!wasAccessible) {
        field.isAccessible = true
      }
      val1 = field[a]
      val2 = field[b]
      if (!wasAccessible) {
        field.isAccessible = false
      }
    } catch (e: IllegalAccessException) {
      throw IllegalStateException("Unable to get fields by reflection.", e)
    }

    if (!areObjectsEquivalent(val1, val2)) {
      return false
    }
  }

  return true
}

/** Checks if two objects are equivalent. */
@Suppress("UNCHECKED_CAST")
fun areObjectsEquivalent(val1: Any?, val2: Any?): Boolean {
  if (val1 === val2) {
    return true
  }

  if (val1 == null || val2 == null || val1.javaClass != val2.javaClass) {
    return false
  }

  return when {
    val1 is Float -> val1.compareTo(val2 as Float) == 0
    val1 is Double -> val1.compareTo(val2 as Double) == 0
    val1 is Equivalence<*> -> (val1 as Equivalence<Any>).isEquivalentTo(val2)
    val1.javaClass.isArray -> areArraysEquivalent(val1, val2)
    val1 is List<*> && val1 is RandomAccess -> areRandomAccessListsEquivalent(val1, val2 as List<*>)
    val1 is Collection<*> -> areCollectionsEquivalent(val1, val2 as Collection<*>)
    else -> val1 == val2
  }
}

/** Checks if two arrays are equivalent. */
@Suppress("UNCHECKED_CAST")
private fun areArraysEquivalent(val1: Any, val2: Any): Boolean {
  return when (val1) {
    is ByteArray -> val1.contentEquals(val2 as ByteArray)
    is ShortArray -> val1.contentEquals(val2 as ShortArray)
    is CharArray -> val1.contentEquals(val2 as CharArray)
    is IntArray -> val1.contentEquals(val2 as IntArray)
    is LongArray -> val1.contentEquals(val2 as LongArray)
    is FloatArray -> val1.contentEquals(val2 as FloatArray)
    is DoubleArray -> val1.contentEquals(val2 as DoubleArray)
    is BooleanArray -> val1.contentEquals(val2 as BooleanArray)
    else -> {
      val array1 = val1 as Array<Any>
      val array2 = val2 as Array<Any>

      if (array1.size != array2.size) {
        return false
      }

      val size = array1.size
      for (i in 0 until size) {
        if (!areObjectsEquivalent(array1[i], array2[i])) {
          return false
        }
      }

      return true
    }
  }
}

/** Checks if two random access lists are equivalent. */
private fun areRandomAccessListsEquivalent(val1: List<*>, val2: List<*>): Boolean {
  if (val1.size != val2.size) {
    return false
  }

  for (index in 0 until val1.size) {
    if (!areObjectsEquivalent(val1[index], val2[index])) {
      return false
    }
  }

  return true
}

/** Checks if two collections are equivalent. */
private fun areCollectionsEquivalent(val1: Collection<*>, val2: Collection<*>): Boolean {
  if (val1.size != val2.size) {
    return false
  }

  val iterator1 = val1.iterator()
  val iterator2 = val2.iterator()

  while (iterator1.hasNext()) {
    val elem1 = iterator1.next()
    val elem2 = iterator2.next()
    if (!areObjectsEquivalent(elem1, elem2)) {
      return false
    }
  }

  return true
}


/*
 * Copyright (c) Meta Platforms, Inc. and 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

interface BaseThreadLooperController {
  fun init()

  fun clean()

  fun runOneTaskSync()

  fun runToEndOfTasksSync()

  fun runToEndOfTasksAsync(): TimeOutSemaphore?
}


/*
 * Copyright (c) Meta Platforms, Inc. and 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

import com.facebook.litho.Component
import com.facebook.litho.ComponentScope
import com.facebook.litho.KComponent

class EmptyComponentForTests : KComponent() {

  override fun ComponentScope.render(): Component? {
    return null
  }
}


/*
 * Copyright (c) Meta Platforms, Inc. and 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

import android.app.Activity
import android.content.Context
import android.os.Looper
import android.view.View
import android.view.View.MeasureSpec
import android.view.ViewGroup
import androidx.annotation.StringRes
import androidx.annotation.VisibleForTesting
import androidx.test.core.app.ApplicationProvider.getApplicationContext
import com.facebook.litho.Component
import com.facebook.litho.ComponentContext
import com.facebook.litho.ComponentScope
import com.facebook.litho.ComponentTree
import com.facebook.litho.LayoutState
import com.facebook.litho.LithoLayoutResult
import com.facebook.litho.LithoView
import com.facebook.litho.StateHandler
import com.facebook.litho.TreeProps
import com.facebook.litho.componentsfinder.findAllComponentsInLithoView
import com.facebook.litho.componentsfinder.findComponentInLithoView
import com.facebook.litho.componentsfinder.findDirectComponentInLithoView
import com.facebook.litho.config.ComponentsConfiguration
import com.facebook.litho.testing.viewtree.ViewPredicates
import com.facebook.litho.testing.viewtree.ViewTree
import com.facebook.rendercore.MountItemsPool
import com.google.common.base.Predicate
import kotlin.reflect.KClass
import org.junit.rules.TestRule
import org.junit.runner.Description
import org.junit.runners.model.Statement
import org.robolectric.Robolectric
import org.robolectric.Shadows.shadowOf

/**
 * This test utility allows clients to test assertion on the view hierarchy rendered by a Litho
 * components. The utility has methods to override the default {@link LithoView}, {@link
 * ComponentTree}, width, and height specs.
 *
 * ```
 *  @RunWith(AndroidJUnit4::class)
 *  class LithoSampleTest {
 *
 *  @Rule @JvmField val lithoViewRule = LegacyLithoViewRule()
 *  @Test
 *  fun test() {
 *    lithoViewRule.render { TestComponent() }
 *
 *    // or you can use setRoot/measure/layout for more fine-grained control
 *    val lithoViewTest = lithoViewRule.attachToWindow().setRoot(TestComponent()).measure().layout()
 *    // Test your assertions on the litho view.
 *    }
 * }
 * ```
 */
@Deprecated("Please use LithoViewRule and TestLithoView instead")
class LegacyLithoViewRule
@JvmOverloads
constructor(
    val componentsConfiguration: ComponentsConfiguration? = null,
    val themeResId: Int? = null
) : TestRule {

  init {
    ComponentsConfiguration.isDebugModeEnabled = true
  }

  val componentTree: ComponentTree
    get() {
      if (_componentTree == null) {
        _componentTree =
            ComponentTree.create(context).componentsConfiguration(componentsConfiguration).build()
      }
      return _componentTree ?: throw AssertionError("Set to null by another thread")
    }

  val lithoView: LithoView
    get() {
      if (_lithoView == null) {
        _lithoView = LithoView(context)
      }

      if (_lithoView?.componentTree == null) {
        _lithoView?.componentTree = componentTree
      }
      return _lithoView ?: throw AssertionError("Set to null by another thread")
    }

  val committedLayoutState: LayoutState?
    @VisibleForTesting(otherwise = VisibleForTesting.NONE)
    get() = componentTree.committedLayoutState

  val currentRootNode: LithoLayoutResult?
    @VisibleForTesting(otherwise = VisibleForTesting.NONE)
    get() = committedLayoutState?.rootLayoutResult

  var widthSpec = DEFAULT_WIDTH_SPEC
  var heightSpec = DEFAULT_HEIGHT_SPEC
  lateinit var context: ComponentContext
  lateinit var stateHandler: StateHandler
  private var _lithoView: LithoView? = null
  private var _componentTree: ComponentTree? = null
  private var threadLooperController: BaseThreadLooperController = ThreadLooperController()
  private val interactionsScope = InteractionsScope()

  override fun apply(base: Statement, description: Description): Statement {
    return object : Statement() {
      override fun evaluate() {
        ensureThreadLooperType()

        try {
          if (themeResId != null) {
            val activity = Robolectric.buildActivity(Activity::class.java).create().get()
            activity.setTheme(themeResId)
            context = ComponentContext(activity)
          } else {
            context = ComponentContext(getApplicationContext<Context>())
          }
          stateHandler = StateHandler()
          threadLooperController.init()
          base.evaluate()
        } finally {
          threadLooperController.clean()
          MountItemsPool.clear()
          context.clearCalculationStateContext()
          _componentTree = null
          _lithoView = null
          widthSpec = DEFAULT_WIDTH_SPEC
          heightSpec = DEFAULT_HEIGHT_SPEC
        }
      }
    }
  }

  private fun ensureThreadLooperType() {
    if (ComponentsConfiguration.isSplitResolveAndLayoutWithSplitHandlers() &&
        threadLooperController is ThreadLooperController) {
      threadLooperController = ResolveAndLayoutThreadLooperController()
    } else if (!ComponentsConfiguration.isSplitResolveAndLayoutWithSplitHandlers() &&
        threadLooperController is ResolveAndLayoutThreadLooperController) {
      threadLooperController = ThreadLooperController()
    }
  }

  fun useContext(c: ComponentContext): LegacyLithoViewRule {
    context = c
    return this
  }

  /** Sets a new [LithoView] which should be used to render. */
  fun useLithoView(lithoView: LithoView): LegacyLithoViewRule {
    _lithoView = lithoView
    if (lithoView.componentContext !== context) {
      throw RuntimeException(
          "You must use the same ComponentContext for the LithoView as what is on the LegacyLithoViewRule @Rule!")
    }
    lithoView.componentTree.let {
      if (it == null && _componentTree != null) {
        lithoView.componentTree = _componentTree
      } else {
        _componentTree = it
      }
    }
    return this
  }

  /** Sets a new [ComponentTree] which should be used to render. */
  fun useComponentTree(componentTree: ComponentTree?): LegacyLithoViewRule {
    _componentTree = componentTree
    lithoView.componentTree = componentTree
    return this
  }

  /** Sets the new root [Component] to render. */
  fun setRoot(component: Component?): LegacyLithoViewRule {
    componentTree.setRoot(component)
    return this
  }

  /** Sets the new root [Component.Builder] to render. */
  fun setRoot(builder: Component.Builder<*>): LegacyLithoViewRule {
    componentTree.setRoot(builder.build())
    return this
  }

  /** Sets the new root [Component] to render asynchronously. */
  fun setRootAsync(component: Component?): LegacyLithoViewRule {
    componentTree.setRootAsync(component)
    return this
  }

  /** Sets the new root [Component.Builder] to render asynchronously. */
  fun setRootAsync(builder: Component.Builder<*>): LegacyLithoViewRule {
    componentTree.setRootAsync(builder.build())
    return this
  }

  /** Sets the new root [Component] with new size spec to render. */
  fun setRootAndSizeSpecSync(
      component: Component?,
      widthSpec: Int,
      heightSpec: Int
  ): LegacyLithoViewRule {
    this.widthSpec = widthSpec
    this.heightSpec = heightSpec
    componentTree.setRootAndSizeSpecSync(component, this.widthSpec, this.heightSpec)
    return this
  }

  /** Sets a new width and height which should be used to render. */
  fun setSizePx(widthPx: Int, heightPx: Int): LegacyLithoViewRule {
    widthSpec = MeasureSpec.makeMeasureSpec(widthPx, MeasureSpec.EXACTLY)
    heightSpec = MeasureSpec.makeMeasureSpec(heightPx, MeasureSpec.EXACTLY)
    return this
  }

  /** Sets a new width spec and height spec which should be used to render. */
  fun setSizeSpecs(widthSpec: Int, heightSpec: Int): LegacyLithoViewRule {
    this.widthSpec = widthSpec
    this.heightSpec = heightSpec
    return this
  }

  /** Sets a new [TreeProp] for the next layout pass. */
  fun setTreeProp(klass: Class<*>, instance: Any?): LegacyLithoViewRule {
    val props = context.treeProps ?: TreeProps()
    props.put(klass, instance)
    context.treeProps = props
    return this
  }

  /** Explicitly calls measure on the current root [LithoView] */
  fun measure(): LegacyLithoViewRule {
    lithoView.measure(widthSpec, heightSpec)
    return this
  }

  /**
   * Explicitly calls layout on the current root [LithoView]. If there are any async events
   * triggered by layout use together with [idle]
   */
  fun layout(): LegacyLithoViewRule {
    val lithoView: LithoView = lithoView
    lithoView.layout(0, 0, lithoView.measuredWidth, lithoView.measuredHeight)
    return this
  }

  fun dispatchGlobalLayout(): LegacyLithoViewRule {
    lithoView.notifyVisibleBoundsChanged()

    return this
  }

  /** Explicitly attaches current root [LithoView] */
  fun attachToWindow(): LegacyLithoViewRule {
    lithoView.onAttachedToWindowForTest()
    return this
  }

  /** Explicitly detaches current root [LithoView] */
  fun detachFromWindow(): LegacyLithoViewRule {
    lithoView.onDetachedFromWindowForTest()
    return this
  }

  /** Explicitly releases current root [LithoView] */
  fun release(): LegacyLithoViewRule {
    lithoView.release()
    return this
  }

  /** Sets the new root to render. */
  fun render(componentFunction: ComponentScope.() -> Component?) {
    attachToWindow()
        .setRoot(with(ComponentScope(context)) { componentFunction() })
        .measure()
        .layout()
  }

  /**
   * Finds the first [View] with the specified tag in the rendered hierarchy, returning null if is
   * doesn't exist.
   */
  fun findViewWithTagOrNull(tag: Any): View? {
    return findViewWithTagTransversal(lithoView, tag)
  }

  /**
   * Finds the first [View] with the specified tag in the rendered hierarchy, throwing if it doesn't
   * exist.
   */
  fun findViewWithTag(tag: Any): View {
    return findViewWithTagOrNull(tag) ?: throw RuntimeException("Did not find view with tag '$tag'")
  }

  private fun findViewWithTagTransversal(view: View?, tag: Any): View? {
    if (view == null || view.tag == tag) {
      return view
    }
    if (view is ViewGroup) {
      for (i in 0..view.childCount) {
        val child = findViewWithTagTransversal(view.getChildAt(i), tag)
        if (child != null) {
          return child
        }
      }
    }
    return null
  }

  /**
   * Finds the first [View] with the specified text in the rendered hierarchy, returning null if is
   * doesn't exist.
   */
  fun findViewWithTextOrNull(text: String): View? {
    val viewTree = ViewTree.of(lithoView)
    return findViewWithPredicateOrNull(viewTree, ViewPredicates.hasVisibleText(text))
  }

  /**
   * Finds the first [View] with the specified text in the rendered hierarchy, throwing if it
   * doesn't exist.
   */
  fun findViewWithText(text: String): View {
    return findViewWithTextOrNull(text)
        ?: throw RuntimeException("Did not find view with text '$text'")
  }
  /**
   * Finds the first [View] with the specified content description in the rendered hierarchy,
   * returning null if is doesn't exist.
   */
  fun findViewWithContentDescriptionOrNull(contentDescription: String): View? {
    val viewTree = ViewTree.of(lithoView)
    return findViewWithPredicateOrNull(
        viewTree, ViewPredicates.hasContentDescription(contentDescription))
  }

  /**
   * Finds the first [View] with the specified content description in the rendered hierarchy,
   * throwing if it doesn't exist.
   */
  fun findViewWithContentDescription(contentDescription: String): View {
    return findViewWithContentDescriptionOrNull(contentDescription)
        ?: throw RuntimeException("Did not find view with contentDescription '$contentDescription'")
  }

  /**
   * Finds the first [View] with the specified text in the rendered hierarchy, returning null if is
   * doesn't exist.
   */
  fun findViewWithTextOrNull(@StringRes resourceId: Int): View? {
    val viewTree = ViewTree.of(lithoView)
    val text = getApplicationContext<Context>().resources.getString(resourceId)
    return findViewWithPredicateOrNull(viewTree, ViewPredicates.hasVisibleText(text))
  }

  /**
   * Finds the first [View] with the specified text in the rendered hierarchy, throwing if it
   * doesn't exist.
   */
  fun findViewWithText(@StringRes resourceId: Int): View =
      findViewWithText(getApplicationContext<Context>().resources.getString(resourceId))

  /**
   * Finds the first [View] with the specified content description in the rendered hierarchy,
   * returning null if is doesn't exist.
   */
  fun findViewWithContentDescriptionOrNull(@StringRes resourceId: Int): View? {
    val viewTree = ViewTree.of(lithoView)
    val contentDescription = getApplicationContext<Context>().resources.getString(resourceId)
    return findViewWithPredicateOrNull(
        viewTree, ViewPredicates.hasContentDescription(contentDescription))
  }

  /**
   * Finds the first [View] with the specified content description in the rendered hierarchy,
   * throwing if it doesn't exist.
   */
  fun findViewWithContentDescription(@StringRes resourceId: Int): View =
      findViewWithContentDescription(
          getApplicationContext<Context>().resources.getString(resourceId))

  /** Returns a component of the given class only if it is a direct child of the root component */
  fun findDirectComponent(clazz: KClass<out Component>): Component? {
    return findDirectComponentInLithoView(lithoView, clazz)
  }

  /** Returns a component of the given class only if it is a direct child of the root component */
  fun findDirectComponent(clazz: Class<out Component?>): Component? {
    return findDirectComponentInLithoView(lithoView, clazz)
  }

  /** Returns a component of the given class from the ComponentTree or null if not found */
  fun findComponent(clazz: KClass<out Component>): Component? {
    return findComponentInLithoView(lithoView, clazz)
  }

  /** Returns a component of the given class from the ComponentTree or null if not found */
  fun findComponent(clazz: Class<out Component?>): Component? {
    return findComponentInLithoView(lithoView, clazz)
  }

  /**
   * Returns a list of all components of the given classes from the ComponentTree or an empty list
   * if not found
   */
  fun findAllComponents(vararg clazz: KClass<out Component>): List<Component> {
    return findAllComponentsInLithoView(lithoView, *clazz)
  }

  /**
   * Returns a list of all components of the given classes from the ComponentTree or an empty list
   * if not found
   */
  fun findAllComponents(vararg clazz: Class<out Component?>): List<Component> {
    return findAllComponentsInLithoView(lithoView, *clazz)
  }

  private fun findViewWithPredicateOrNull(viewTree: ViewTree, predicate: Predicate<View>): View? {
    return viewTree.findChild(predicate)?.last()
  }

  /**
   * Perform any interactions defined in the [InteractionScope] or on the [LegacyLithoViewRule].
   *
   * During tests we need to make sure that everything is in sync in the Main Thread and in the
   * Background Thread, just like in real life use case. This functions takes off the responsibility
   * from you to use the Loopers and manage the thread synchronisation. You only need to pass here
   * one of the defined interactions from [LegacyLithoViewRule] or [InteractionScope], and we will
   * take care of all of the rest
   */
  fun act(action: InteractionsScope.() -> Unit): LegacyLithoViewRule {
    interactionsScope.action()
    idle()
    return this
  }

  /**
   * Runs through all tasks on the background thread and main lopper, blocking until it completes.
   * Use if there are any async events triggered by layout ( ie visibility events) to manually drain
   * the queue
   */
  fun idle() {
    runToEndOfBackgroundTasks()
    shadowOf(Looper.getMainLooper()).idle()
  }

  /**
   * Runs through all tasks on the background thread only, not touching the main lopper, blocking
   * until it completes.
   */
  fun runToEndOfBackgroundTasks() {
    threadLooperController.runToEndOfTasksSync()
  }

  /**
   * Class which exposes interactions that can take place on a view. Exposing interactions in this
   * class ensures that they are only accessible within [act], where the proper threading is taken
   * into account to properly update the components and views.
   */
  inner class InteractionsScope internal constructor() {
    /**
     * Clicks on a [View] with the specified text in the rendered hierarchy, throwing if the view
     * doesn't exists
     */
    fun clickOnText(text: String): Boolean = findViewWithText(text).performClick()

    /**
     * Clicks on a [View] with the specified tag in the rendered hierarchy, throwing if the view
     * doesn't exists
     */
    fun clickOnTag(tag: String): Boolean = findViewWithTag(tag).performClick()

    /**
     * Clicks on a [View] with the specified tag in the rendered hierarchy, throwing if the view
     * doesn't exists
     */
    fun clickOnContentDescription(contentDescription: String): Boolean =
        findViewWithContentDescription(contentDescription).performClick()

    /**
     * Clicks on the root view, if it has click handling, throwing if the view is not clickable.
     *
     * See other functions such as [clickOnText], [clickOnTag], and [clickOnContentDescription] if
     * you'd like more specifically click on portions of the view.
     */
    fun clickOnRootView() {
      checkNotNull(shadowOf(lithoView).onClickListener) {
        "No click handling found on root view.  The root view must be clickable in order to use this function."
      }
      lithoView.performClick()
    }

    /**
     * Clicks on a [View] with the specified text in the rendered hierarchy, throwing if the view
     * doesn't exists
     */
    fun clickOnText(@StringRes resourceId: Int): Boolean =
        findViewWithText(resourceId).performClick()

    /**
     * Clicks on a [View] with the specified tag in the rendered hierarchy, throwing if the view
     * doesn't exists
     */
    fun clickOnTag(@StringRes resourceId: Int): Boolean = findViewWithTag(resourceId).performClick()

    /**
     * Clicks on a [View] with the specified tag in the rendered hierarchy, throwing if the view
     * doesn't exists
     */
    fun clickOnContentDescription(@StringRes resourceId: Int): Boolean =
        findViewWithContentDescription(resourceId).performClick()
  }

  companion object {
    @VisibleForTesting(otherwise = VisibleForTesting.NONE)
    @JvmStatic
    fun getRootLayout(
        rule: LegacyLithoViewRule,
        component: Component?,
        widthSpec: Int,
        heightSpec: Int
    ): LithoLayoutResult? {
      return rule
          .attachToWindow()
          .setRootAndSizeSpecSync(component, widthSpec, heightSpec)
          .measure()
          .layout()
          .currentRootNode
    }

    @VisibleForTesting(otherwise = VisibleForTesting.NONE)
    @JvmStatic
    fun getRootLayout(rule: LegacyLithoViewRule, component: Component?): LithoLayoutResult? {
      return rule.attachToWindow().setRoot(component).measure().layout().currentRootNode
    }
  }
}


/*
 * Copyright (c) Meta Platforms, Inc. and 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

import android.app.Activity
import android.content.Context
import android.os.Looper
import androidx.test.core.app.ApplicationProvider.getApplicationContext
import com.facebook.litho.Component
import com.facebook.litho.ComponentContext
import com.facebook.litho.ComponentScope
import com.facebook.litho.ComponentTree
import com.facebook.litho.LithoLifecycleProvider
import com.facebook.litho.LithoView
import com.facebook.litho.TreeProps
import com.facebook.litho.config.ComponentsConfiguration
import com.facebook.rendercore.MountItemsPool
import com.facebook.rendercore.utils.MeasureSpecUtils.exactly
import org.junit.rules.TestRule
import org.junit.runner.Description
import org.junit.runners.model.Statement
import org.robolectric.Robolectric
import org.robolectric.Shadows.shadowOf

/**
 * This test utility allows clients to create a [TestLithoView] instance that allows to test
 * assertion on the view hierarchy rendered by a Litho components.
 *
 * ```
 *  @RunWith(AndroidJUnit4::class)
 *  class LithoSampleTest {
 *
 *  @Rule @JvmField val lithoViewRule = LithoViewRule()
 *  @Test
 *  fun test() {
 *   val testLithoView =  lithoViewRule.render { TestComponent() }
 *
 *    // or you can use setRoot/measure/layout for more fine-grained control
 *    val testLithoView = lithoViewRule.createTestLithoView().attachToWindow().setRoot(TestComponent()).measure().layout()
 *    // Test your assertions on the TestLithoView instance.
 *    }
 * }
 * ```
 */
class LithoViewRule
@JvmOverloads
constructor(
    val componentsConfiguration: ComponentsConfiguration? = null,
    val themeResId: Int? = null,
    private val lithoLifecycleProvider: (() -> LithoLifecycleProvider)? = null
) : TestRule {
  lateinit var context: ComponentContext
  private var threadLooperController: BaseThreadLooperController = ThreadLooperController()

  init {
    ComponentsConfiguration.isDebugModeEnabled = true
  }

  override fun apply(base: Statement, description: Description): Statement {
    return object : Statement() {
      override fun evaluate() {
        ensureThreadLooperType()

        try {
          if (themeResId != null) {
            val activity = Robolectric.buildActivity(Activity::class.java).create().get()
            activity.setTheme(themeResId)
            context = ComponentContext(activity)
          } else {
            context = ComponentContext(getApplicationContext<Context>())
          }

          threadLooperController.init()
          base.evaluate()
        } finally {
          threadLooperController.clean()
          MountItemsPool.clear()
          context.clearCalculationStateContext()
        }
      }
    }
  }

  private fun ensureThreadLooperType() {
    if (ComponentsConfiguration.isSplitResolveAndLayoutWithSplitHandlers() &&
        threadLooperController is ThreadLooperController) {
      threadLooperController = ResolveAndLayoutThreadLooperController()
    } else if (!ComponentsConfiguration.isSplitResolveAndLayoutWithSplitHandlers() &&
        threadLooperController is ResolveAndLayoutThreadLooperController) {
      threadLooperController = ThreadLooperController()
    }
  }

  fun useContext(c: ComponentContext): LithoViewRule {
    context = c
    return this
  }

  /** Sets a new [TreeProp] for the next layout pass. */
  fun setTreeProp(klass: Class<*>, instance: Any?): LithoViewRule {
    val props = context.treeProps ?: TreeProps()
    props.put(klass, instance)
    context.treeProps = props
    return this
  }

  /**
   * Creates new TestLithoView holder responsible for keeping an instance of LithoView, allowing to
   * find Views/Components or perform assertions on it. For simple Component rendering without
   * fine-grained control, use [render]
   */
  @JvmOverloads
  fun createTestLithoView(
      lithoView: LithoView? = null,
      componentTree: ComponentTree? = null,
      widthPx: Int? = null,
      heightPx: Int? = null,
      componentFunction: (ComponentScope.() -> Component?)? = null
  ): TestLithoView {
    val testLithoView =
        TestLithoView(context, componentsConfiguration, lithoLifecycleProvider?.invoke())
    componentTree?.let { testLithoView.useComponentTree(componentTree) }
    lithoView?.let { testLithoView.useLithoView(lithoView) }
    if (widthPx != null || heightPx != null) {
      val widthSpec = if (widthPx != null) exactly(widthPx) else DEFAULT_WIDTH_SPEC
      val heightSpec = if (heightPx != null) exactly(heightPx) else DEFAULT_HEIGHT_SPEC
      testLithoView.setSizeSpecs(widthSpec, heightSpec)
    }

    componentFunction?.let {
      with(ComponentScope(context)) { componentFunction() }
          ?.let { component -> testLithoView.setRoot(component) }
    }
    return testLithoView
  }

  /** Sets the new root to render. */
  @JvmOverloads
  fun render(
      lithoView: LithoView? = null,
      componentTree: ComponentTree? = null,
      widthPx: Int? = null,
      heightPx: Int? = null,
      componentFunction: ComponentScope.() -> Component?
  ): TestLithoView {
    val testLithoView =
        createTestLithoView(
            lithoView = lithoView,
            componentTree = componentTree,
            widthPx = widthPx,
            heightPx = heightPx,
            componentFunction = componentFunction)
    return testLithoView.attachToWindow().measure().layout()
  }

  /**
   * Perform any interactions defined in the [InteractionScope] or on the [LithoViewRule].
   *
   * During tests we need to make sure that everything is in sync in the Main Thread and in the
   * Background Thread, just like in real life use case. This functions takes off the responsibility
   * from you to use the Loopers and manage the thread synchronisation. You only need to pass here
   * one of the defined interactions from [LithoViewRule] or [InteractionScope], and we will take
   * care of all of the rest
   */
  fun act(
      testLithoView: TestLithoView,
      action: TestLithoView.InteractionsScope.() -> Unit
  ): LithoViewRule {
    testLithoView.InteractionsScope().action()
    idle()
    return this
  }

  /**
   * Runs through all tasks on the background thread and main lopper, blocking until it completes.
   * Use if there are any async events triggered by layout ( ie visibility events) to manually drain
   * the queue
   */
  fun idle() {
    threadLooperController.runToEndOfTasksSync()
    shadowOf(Looper.getMainLooper()).idle()
  }
}


/*
 * Copyright (c) Meta Platforms, Inc. and 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

import android.view.View

/** Shorthand for creating a [View.MeasureSpec.EXACTLY] measure spec. */
fun exactly(px: Int): Int = View.MeasureSpec.makeMeasureSpec(px, View.MeasureSpec.EXACTLY)

/** Shorthand for creating a [View.MeasureSpec.AT_MOST] measure spec. */
fun atMost(px: Int): Int = View.MeasureSpec.makeMeasureSpec(px, View.MeasureSpec.AT_MOST)

/** Shorthand for creating a [View.MeasureSpec.UNSPECIFIED] measure spec. */
@JvmOverloads
fun unspecified(px: Int = 0): Int =
    View.MeasureSpec.makeMeasureSpec(px, View.MeasureSpec.UNSPECIFIED)


/*
 * Copyright (c) Meta Platforms, Inc. and 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

import android.os.Looper
import com.facebook.litho.ComponentTree
import org.robolectric.Shadows

class ResolveAndLayoutThreadLooperController : BaseThreadLooperController {

  private lateinit var resolveController: ThreadLooperController
  private lateinit var layoutController: ThreadLooperController

  private var isInitialized = false

  override fun init() {
    resolveController =
        ThreadLooperController(
            Shadows.shadowOf(
                Whitebox.invokeMethod<Any>(
                    ComponentTree::class.java, "getDefaultResolveThreadLooper") as Looper))
    layoutController = ThreadLooperController() // default layout thread thread looper

    resolveController.init()
    layoutController.init()

    isInitialized = true
  }

  override fun clean() {
    if (!isInitialized) {
      return
    }

    resolveController.clean()
    layoutController.clean()

    isInitialized = false
  }

  override fun runOneTaskSync() {
    resolveController.runOneTaskSync()
    layoutController.runOneTaskSync()
  }

  override fun runToEndOfTasksSync() {
    resolveController.runToEndOfTasksSync()
    layoutController.runToEndOfTasksSync()
  }

  override fun runToEndOfTasksAsync(): TimeOutSemaphore? {
    resolveController.runToEndOfTasksAsync()
    return layoutController.runToEndOfTasksAsync()
  }
}


/*
 * Copyright (c) Meta Platforms, Inc. and 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

import androidx.recyclerview.widget.RecyclerView
import com.facebook.litho.Component
import com.facebook.litho.LithoView
import com.facebook.litho.sections.widget.SectionBinderTarget
import com.facebook.litho.widget.Binder
import com.facebook.litho.widget.Recycler
import com.facebook.litho.widget.RecyclerBinder
import kotlin.reflect.KClass

/**
 * A wrapper class that exposes methods specific to testing collection components.
 *
 * @property recyclerBinder The [RecyclerBinder] from the list being tested.
 */
class TestCollection {

  private var recyclerBinder: RecyclerBinder

  constructor(recycler: Recycler) {
    val binder =
        Whitebox.getInternalState<Binder<RecyclerView>>(recycler, "binder") as SectionBinderTarget

    this.recyclerBinder = Whitebox.getInternalState(binder, "mRecyclerBinder")
  }

  /**
   * Get the number of items in the lazy collection. This is not the same as the number of items
   * that are displayed.
   */
  val itemCount: Int
    get() = recyclerBinder.itemCount

  /** All items in the lazy collection */
  val items: List<TestCollectionItem>
    get() =
        (0 until itemCount).map {
          TestCollectionItem(this, recyclerBinder.getComponentTreeHolderAt(it), it)
        }

  /** Get the first visible item index, returns -1 when there are no components */
  val firstVisibleIndex: Int
    get() = recyclerBinder.findFirstVisibleItemPosition()

  /** Get the first fully visible item index, returns -1 when there are no components */
  val firstFullyVisibleIndex: Int
    get() = recyclerBinder.findFirstFullyVisibleItemPosition()

  /** Get the last visible item index, returns -1 when there are no components */
  val lastVisibleIndex: Int
    get() = recyclerBinder.findLastVisibleItemPosition()

  /** Get the last Fully visible item index, returns -1 when there are no components */
  val lastFullyVisibleIndex: Int
    get() = recyclerBinder.findLastFullyVisibleItemPosition()

  /** Get all visible items */
  val visibleItems: List<TestCollectionItem>
    get() =
        if (recyclerBinder.findFirstVisibleItemPosition() != RecyclerView.NO_POSITION) {
          items.slice(
              recyclerBinder.findFirstVisibleItemPosition()..recyclerBinder
                      .findLastVisibleItemPosition())
        } else {
          listOf()
        }

  /** Get all fully visible items */
  val fullyVisibleItems: List<TestCollectionItem>
    get() =
        if (recyclerBinder.findFirstVisibleItemPosition() != RecyclerView.NO_POSITION) {
          items.slice(
              recyclerBinder.findFirstFullyVisibleItemPosition()..recyclerBinder
                      .findLastFullyVisibleItemPosition())
        } else {
          listOf()
        }

  /** Convenience property for getting the first item */
  val firstItem: TestCollectionItem
    get() = items.first()

  /** Convenience property for getting the first visible item */
  val firstVisibleItem: TestCollectionItem
    get() = visibleItems.first()

  /** Convenience property for getting the last item */
  val lastItem: TestCollectionItem
    get() = items.last()

  /** Convenience property for getting the last visible item */
  val lastVisibleItem: TestCollectionItem
    get() = visibleItems.last()

  /** Convenience function for getting the item at an index */
  inline fun getItemAtIndex(index: Int): TestCollectionItem = items[index]

  /**
   * Performs a shallow search to Find the first item, with the given component type; in the
   * LazyCollection.
   */
  inline fun findFirstItem(clazz: Class<out Component>): TestCollectionItem? =
      items.find { it.component::class.java == clazz }

  /** @see findFirstItem */
  inline fun findFirstItem(clazz: KClass<out Component>): TestCollectionItem? =
      findFirstItem(clazz.java)

  /**
   * Performs a shallow search for items, with the given component type; from the LazyCollection.
   */
  @PublishedApi
  internal inline fun findItems(classes: List<Class<out Component>>): List<TestCollectionItem> =
      items.filter { classes.contains(it.component::class.java) }

  /** @see findItems */
  inline fun findItems(vararg clazz: Class<out Component>): List<TestCollectionItem> =
      findItems(clazz.toList())

  /** @see findItems */
  inline fun findItems(vararg clazz: KClass<out Component>): List<TestCollectionItem> =
      findItems(clazz.map { it.java })

  /** Retrieve the visible mounted children. */
  fun getLithoViews(): List<LithoView> {
    return (firstVisibleIndex..lastVisibleIndex).mapNotNull {
      recyclerView.layoutManager?.findViewByPosition(it) as? LithoView
    }
  }

  internal val recyclerView: RecyclerView
    get() {
      val recyclerView: RecyclerView? = Whitebox.getInternalState(recyclerBinder, "mMountedView")

      return requireNotNull(recyclerView) {
        """
        We could not find a mounted recycler view.  This normally happens if you did not provide an
        explicit size when rendering your component for test.  To fix this, add a width and height
        when rendering the component: `lithoViewRule.render(widthPx = 100, heightPx = 100) { ... }`.
      """
            .trimIndent()
      }
    }
}


/*
 * Copyright (c) Meta Platforms, Inc. and 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

import android.view.View
import android.widget.ImageView
import android.widget.TextView
import androidx.recyclerview.widget.RecyclerView
import com.facebook.litho.Component
import com.facebook.litho.ComponentHost
import com.facebook.litho.DebugComponent
import com.facebook.litho.LithoViewTestHelper
import com.facebook.litho.testing.viewtree.ViewExtractors
import com.facebook.litho.testing.viewtree.ViewTree

object TestCollectionDescriptionHelper {

  private const val DEPTH_3_LEADING_SPACE = "      "
  private const val ITEM_STARTING_DEPTH = 3

  fun collectionToString(testCollection: TestCollection): String {
    var hierarchyString = testCollection.recyclerView.toString()
    testCollection.items.forEach { item ->
      hierarchyString = "${hierarchyString}${itemToString(item)}"
    }
    return hierarchyString
  }

  private fun itemToString(item: TestCollectionItem): String {
    val testCollectionItemHeader =
        "\nâ””â”€â”€ index ${item.index}: Collection Item (id: ${item.id}, visibility: ${getVisibility(item)})"

    val itemDescription =
        if (item.renderInfo.rendersView()) getViewInfo(item, item.recyclerView)
        else getComponentInfo(item)
    return "$testCollectionItemHeader${itemDescription}"
  }

  private fun getComponentInfo(item: TestCollectionItem): String {
    val debugComponent =
        DebugComponent.getRootInstance(item.componentTree)
            ?: return fallbackComponentPrintInfo(item.component)

    return LithoViewTestHelper.rootInstanceToString(debugComponent, false, ITEM_STARTING_DEPTH)
  }

  private fun fallbackComponentPrintInfo(component: Component): String {
    // This can be null when working with a mock, so we fallback in that case to the class name info
    val componentString = component.toString() ?: component::class.java.simpleName
    return "\n$DEPTH_3_LEADING_SPACE$componentString{${component.hashCode()}}\n"
  }

  private fun getViewInfo(item: TestCollectionItem, recyclerView: RecyclerView): String {
    val itemView =
        recyclerView.findViewHolderForAdapterPosition(item.index)?.itemView
            ?: return "\n${DEPTH_3_LEADING_SPACE}Found null item view (no additional information available)\n"
    val viewTreeString =
        ViewTree.of(itemView).makeString(ITEM_STARTING_DEPTH, ::viewExtraTextExtractor)

    return "$viewTreeString\n"
  }

  private fun viewExtraTextExtractor(view: View?): String? {
    if (view is TextView || view is ComponentHost) {
      val textOutput =
          ViewExtractors.GET_TEXT_FUNCTION.apply(view)?.takeUnless {
            it.startsWith("No text found")
          }
      if (textOutput != null) {
        return textOutput
      }
    }

    if (view is ImageView || view is ComponentHost) {
      val textOutput =
          ViewExtractors.GET_DRAWABLE_FUNCTION.apply(view)?.takeUnless {
            it.startsWith("No drawable found")
          }
      if (textOutput != null) {
        return textOutput
      }
    }

    return null
  }

  private fun getVisibility(item: TestCollectionItem): String =
      when {
        item.isFullyVisible && item.isVisible -> "full" // checks both to handle mocks in odd states
        item.isVisible -> "partial"
        else -> "none"
      }
}


/*
 * Copyright (c) Meta Platforms, Inc. and 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

import com.facebook.litho.Component
import com.facebook.litho.widget.ComponentTreeHolder
import com.facebook.litho.widget.RecyclerBinder

/**
 * A wrapper class that exposes methods specific to testing collection children.
 *
 * @property componentTreeHolder The [ComponentTreeHolder] that holds information about the child.
 * @property index The item's index in the collection.
 * @property component The [Component] contained in this child.
 * @property id The id of the child in the collection.
 * @property isSticky True if the component can stick to the top of the collection.
 * @property isFullSpan True if the component spans the whole grid
 * @property spanSize Number of columns the component spans in a grid
 */
data class TestCollectionItem(
    private val testCollection: TestCollection,
    private val componentTreeHolder: ComponentTreeHolder,
    val index: Int,
) {

  internal val recyclerView
    get() = testCollection.recyclerView

  internal val renderInfo
    get() = componentTreeHolder.renderInfo

  internal val componentTree
    get() = componentTreeHolder.componentTree

  val id
    get() = renderInfo.getCustomAttribute(RecyclerBinder.ID_CUSTOM_ATTR_KEY)

  val component
    get() = renderInfo.component

  val isVisible
    get() = index in with(testCollection) { firstVisibleIndex..lastVisibleIndex }

  val isFullyVisible
    get() = index in with(testCollection) { firstFullyVisibleIndex..lastFullyVisibleIndex }

  val isSticky
    get() = renderInfo.isSticky

  val isFullSpan: Boolean
    get() = renderInfo.isFullSpan

  val spanSize: Int
    get() = renderInfo.spanSize
}


/*
 * Copyright (c) Meta Platforms, Inc. and 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

import android.content.Context
import android.view.View
import android.view.ViewGroup
import androidx.annotation.StringRes
import androidx.annotation.VisibleForTesting
import androidx.test.core.app.ApplicationProvider.getApplicationContext
import com.facebook.litho.Component
import com.facebook.litho.ComponentContext
import com.facebook.litho.ComponentTree
import com.facebook.litho.LayoutState
import com.facebook.litho.LithoLayoutResult
import com.facebook.litho.LithoLifecycleProvider
import com.facebook.litho.LithoView
import com.facebook.litho.componentsfinder.findAllComponentsInLithoView
import com.facebook.litho.componentsfinder.findComponentInLithoView
import com.facebook.litho.componentsfinder.findDirectComponentInLithoView
import com.facebook.litho.componentsfinder.getRootComponentInLithoView
import com.facebook.litho.config.ComponentsConfiguration
import com.facebook.litho.testing.viewtree.ViewPredicates
import com.facebook.litho.testing.viewtree.ViewTree
import com.facebook.litho.widget.Recycler
import com.facebook.litho.widget.collection.LazyCollection
import com.google.common.base.Predicate
import kotlin.reflect.KClass
import org.robolectric.Shadows

/**
 * Holder class for the result of [LithoViewRule.render] call, exposing methods to for finding the
 * views/components and assertions
 *
 * ```
 *  @RunWith(AndroidJUnit4::class)
 *  class LithoSampleTest {
 *
 *  @Rule @JvmField val lithoViewRule = LithoViewRule()
 *  @Test
 *  fun test() {
 *   val testLithoView =  lithoViewRule.render { TestComponent() }
 *
 *   LithoAssertions.assertThat(testLithoView)
 *   .willRenderContent()
 *   .containsComponents(AnotherTestComponent::class)
 *   .containsContentDescription(R.string.content_descr)
 *
 *   lithoViewRule.act(testLithoView) { clickOnTag("test_tag") }
 *
 *   LithoAssertions.assertThat(testLithoView)
 *   .containsComponents(NewTestComponent::class)
 *
 *    }
 * }
 * ```
 */
class TestLithoView
internal constructor(
    val context: ComponentContext,
    val componentsConfiguration: ComponentsConfiguration? = null,
    private val lithoLifecycleProvider: LithoLifecycleProvider? = null
) {
  val componentTree: ComponentTree
    get() {
      if (_componentTree == null) {
        _componentTree =
            ComponentTree.create(context)
                .withLithoLifecycleProvider(lithoLifecycleProvider)
                .componentsConfiguration(componentsConfiguration)
                .build()
      }
      return _componentTree ?: throw AssertionError("Set to null by another thread")
    }

  val lithoView: LithoView
    get() {
      if (_lithoView == null) {
        _lithoView = LithoView(context)
      }

      if (_lithoView?.componentTree == null) {
        _lithoView?.componentTree = componentTree
      }
      return _lithoView ?: throw AssertionError("Set to null by another thread")
    }

  val rootComponent: Component?
    get() = getRootComponentInLithoView(lithoView)

  val committedLayoutState: LayoutState?
    @VisibleForTesting(otherwise = VisibleForTesting.NONE)
    get() = componentTree.committedLayoutState

  val currentRootNode: LithoLayoutResult?
    @VisibleForTesting(otherwise = VisibleForTesting.NONE)
    get() = committedLayoutState?.rootLayoutResult

  private var widthSpec = DEFAULT_WIDTH_SPEC
  private var heightSpec = DEFAULT_HEIGHT_SPEC
  private var _lithoView: LithoView? = null
  private var _componentTree: ComponentTree? = null

  /** Sets a new [LithoView] which should be used to render. */
  fun useLithoView(lithoView: LithoView): TestLithoView {
    _lithoView = lithoView
    if (lithoView.componentContext !== context) {
      throw RuntimeException(
          "You must use the same ComponentContext for the LithoView as what is on the LithoViewRule @Rule!")
    }
    lithoView.componentTree.let {
      if (it == null && _componentTree != null) {
        lithoView.componentTree = _componentTree
      } else {
        _componentTree = it
      }
    }
    return this
  }

  /** Sets a new [ComponentTree] which should be used to render. */
  fun useComponentTree(componentTree: ComponentTree?): TestLithoView {
    _componentTree = componentTree
    lithoView.componentTree = componentTree
    return this
  }
  /** Sets the new root [Component] to render. */
  fun setRoot(component: Component?): TestLithoView {
    componentTree.setRoot(component)
    return this
  }

  /** Sets the new root [Component.Builder] to render. */
  fun setRoot(builder: Component.Builder<*>): TestLithoView {
    componentTree.setRoot(builder.build())
    return this
  }

  /** Sets the new root [Component] to render asynchronously. */
  fun setRootAsync(component: Component?): TestLithoView {
    componentTree.setRootAsync(component)
    return this
  }

  /** Sets the new root [Component.Builder] to render asynchronously. */
  fun setRootAsync(builder: Component.Builder<*>): TestLithoView {
    componentTree.setRootAsync(builder.build())
    return this
  }

  /** Sets the new root [Component] with new size spec to render. */
  fun setRootAndSizeSpecSync(
      component: Component?,
      widthSpec: Int,
      heightSpec: Int
  ): TestLithoView {
    this.widthSpec = widthSpec
    this.heightSpec = heightSpec
    componentTree.setRootAndSizeSpecSync(component, this.widthSpec, this.heightSpec)
    return this
  }

  /** Sets a new width and height which should be used to render. */
  fun setSizePx(widthPx: Int, heightPx: Int): TestLithoView {
    widthSpec = View.MeasureSpec.makeMeasureSpec(widthPx, View.MeasureSpec.EXACTLY)
    heightSpec = View.MeasureSpec.makeMeasureSpec(heightPx, View.MeasureSpec.EXACTLY)
    return this
  }

  /** Sets a new width spec and height spec which should be used to render. */
  fun setSizeSpecs(widthSpec: Int, heightSpec: Int): TestLithoView {
    this.widthSpec = widthSpec
    this.heightSpec = heightSpec
    return this
  }
  /** Explicitly calls measure on the current root [LithoView] */
  fun measure(): TestLithoView {
    lithoView.measure(widthSpec, heightSpec)
    return this
  }

  /**
   * Explicitly calls layout on the current root [LithoView]. If there are any async events
   * triggered by layout use together with [idle]
   */
  fun layout(): TestLithoView {
    val lithoView: LithoView = lithoView
    lithoView.layout(0, 0, lithoView.measuredWidth, lithoView.measuredHeight)
    return this
  }

  /** Explicitly attaches current root [LithoView] */
  fun attachToWindow(): TestLithoView {
    lithoView.onAttachedToWindowForTest()
    return this
  }

  /** Explicitly detaches current root [LithoView] */
  fun detachFromWindow(): TestLithoView {
    lithoView.onDetachedFromWindowForTest()
    return this
  }

  fun markAsNeedsRemount() {
    lithoView.setMountStateAsNeedingRemount()
  }

  fun setComponentTree(componentTree: ComponentTree) {
    lithoView.componentTree = componentTree
  }

  /** Explicitly releases current root [LithoView] */
  fun release(): TestLithoView {
    lithoView.release()
    return this
  }

  /**
   * Finds the first [View] with the specified tag in the rendered hierarchy, returning null if is
   * doesn't exist.
   */
  fun findViewWithTagOrNull(tag: Any): View? {
    return findViewWithTagTransversal(lithoView, tag)
  }

  /**
   * Finds the first [View] with the specified tag in the rendered hierarchy, throwing if it doesn't
   * exist.
   */
  fun findViewWithTag(tag: Any): View {
    return findViewWithTagOrNull(tag) ?: throw RuntimeException("Did not find view with tag '$tag'")
  }

  private fun findViewWithTagTransversal(view: View?, tag: Any): View? {
    if (view == null || view.tag == tag) {
      return view
    }
    if (view is ViewGroup) {
      for (i in 0..view.childCount) {
        val child = findViewWithTagTransversal(view.getChildAt(i), tag)
        if (child != null) {
          return child
        }
      }
    }
    return null
  }

  /**
   * Finds the first [View] with the specified text in the rendered hierarchy, returning null if is
   * doesn't exist.
   */
  fun findViewWithTextOrNull(text: String): View? {
    val viewTree = ViewTree.of(lithoView)
    return findViewWithPredicateOrNull(viewTree, ViewPredicates.hasVisibleText(text))
  }

  /**
   * Finds the first [View] with the specified text in the rendered hierarchy, throwing if it
   * doesn't exist.
   */
  fun findViewWithText(text: String): View {
    return findViewWithTextOrNull(text)
        ?: throw RuntimeException("Did not find view with text '$text'")
  }
  /**
   * Finds the first [View] with the specified content description in the rendered hierarchy,
   * returning null if is doesn't exist.
   */
  fun findViewWithContentDescriptionOrNull(contentDescription: String): View? {
    val viewTree = ViewTree.of(lithoView)
    return findViewWithPredicateOrNull(
        viewTree, ViewPredicates.hasContentDescription(contentDescription))
  }

  /**
   * Finds the first [View] with the specified content description in the rendered hierarchy,
   * throwing if it doesn't exist.
   */
  fun findViewWithContentDescription(contentDescription: String): View {
    return findViewWithContentDescriptionOrNull(contentDescription)
        ?: throw RuntimeException("Did not find view with contentDescription '$contentDescription'")
  }

  /**
   * Finds the first [View] with the specified text in the rendered hierarchy, returning null if is
   * doesn't exist.
   */
  fun findViewWithTextOrNull(@StringRes resourceId: Int): View? {
    val viewTree = ViewTree.of(lithoView)
    val text = getApplicationContext<Context>().resources.getString(resourceId)
    return findViewWithPredicateOrNull(viewTree, ViewPredicates.hasVisibleText(text))
  }

  /**
   * Finds the first [View] with the specified text in the rendered hierarchy, throwing if it
   * doesn't exist.
   */
  fun findViewWithText(@StringRes resourceId: Int): View =
      findViewWithText(getApplicationContext<Context>().resources.getString(resourceId))

  /**
   * Finds the first [View] with the specified content description in the rendered hierarchy,
   * returning null if is doesn't exist.
   */
  fun findViewWithContentDescriptionOrNull(@StringRes resourceId: Int): View? {
    val viewTree = ViewTree.of(lithoView)
    val contentDescription = getApplicationContext<Context>().resources.getString(resourceId)
    return findViewWithPredicateOrNull(
        viewTree, ViewPredicates.hasContentDescription(contentDescription))
  }

  /**
   * Finds the first [View] with the specified content description in the rendered hierarchy,
   * throwing if it doesn't exist.
   */
  fun findViewWithContentDescription(@StringRes resourceId: Int): View =
      findViewWithContentDescription(
          getApplicationContext<Context>().resources.getString(resourceId))

  /** Returns a component of the given class only if it is a direct child of the root component */
  fun findDirectComponent(clazz: KClass<out Component>): Component? {
    return findDirectComponentInLithoView(lithoView, clazz)
  }

  /** Returns a component of the given class only if it is a direct child of the root component */
  fun findDirectComponent(clazz: Class<out Component?>): Component? {
    return findDirectComponentInLithoView(lithoView, clazz)
  }

  /** Returns a component of the given class from the ComponentTree or null if not found */
  fun findComponent(clazz: KClass<out Component>): Component? {
    return findComponentInLithoView(lithoView, clazz)
  }

  /** Returns a component of the given class from the ComponentTree or null if not found */
  fun findComponent(clazz: Class<out Component?>): Component? {
    return findComponentInLithoView(lithoView, clazz)
  }

  /**
   * Returns a list of all components of the given classes from the ComponentTree or an empty list
   * if not found
   */
  fun findAllComponents(vararg clazz: KClass<out Component>): List<Component> {
    return findAllComponentsInLithoView(lithoView, *clazz)
  }

  /**
   * Returns a list of all components of the given classes from the ComponentTree or an empty list
   * if not found
   */
  fun findAllComponents(vararg clazz: Class<out Component?>): List<Component> {
    return findAllComponentsInLithoView(lithoView, *clazz)
  }

  private fun findViewWithPredicateOrNull(viewTree: ViewTree, predicate: Predicate<View>): View? {
    return viewTree.findChild(predicate)?.last()
  }

  /** Returns the first [LazyCollection] from the ComponentTree, or null if not found. */
  fun findCollectionComponent(): TestCollection? {
    val recycler = findComponent(Recycler::class.java) as Recycler? ?: return null
    return TestCollection(recycler)
  }

  /**
   * Class which exposes interactions that can take place on a view. Exposing interactions in this
   * class ensures that they are only accessible within [act], where the proper threading is taken
   * into account to properly update the components and views.
   */
  inner class InteractionsScope internal constructor() {
    /**
     * Clicks on a [View] with the specified text in the rendered hierarchy, throwing if the view
     * doesn't exists
     */
    fun clickOnText(text: String): Boolean = findViewWithText(text).performClick()

    /**
     * Clicks on a [View] with the specified tag in the rendered hierarchy, throwing if the view
     * doesn't exists
     */
    fun clickOnTag(tag: String): Boolean = findViewWithTag(tag).performClick()

    /**
     * Clicks on a [View] with the specified tag in the rendered hierarchy, throwing if the view
     * doesn't exists
     */
    fun clickOnContentDescription(contentDescription: String): Boolean =
        findViewWithContentDescription(contentDescription).performClick()

    /**
     * Clicks on the root view, if it has click handling, throwing if the view is not clickable.
     *
     * See other functions such as [clickOnText], [clickOnTag], and [clickOnContentDescription] if
     * you'd like more specifically click on portions of the view.
     */
    fun clickOnRootView() {
      checkNotNull(Shadows.shadowOf(lithoView).onClickListener) {
        "No click handling found on root view.  The root view must be clickable in order to use this function."
      }
      lithoView.performClick()
    }

    /**
     * Clicks on a [View] with the specified text in the rendered hierarchy, throwing if the view
     * doesn't exists
     */
    fun clickOnText(@StringRes resourceId: Int): Boolean =
        findViewWithText(resourceId).performClick()

    /**
     * Clicks on a [View] with the specified tag in the rendered hierarchy, throwing if the view
     * doesn't exists
     */
    fun clickOnTag(@StringRes resourceId: Int): Boolean = findViewWithTag(resourceId).performClick()

    /**
     * Clicks on a [View] with the specified tag in the rendered hierarchy, throwing if the view
     * doesn't exists
     */
    fun clickOnContentDescription(@StringRes resourceId: Int): Boolean =
        findViewWithContentDescription(resourceId).performClick()
  }
}

@JvmField
val DEFAULT_WIDTH_SPEC: Int = View.MeasureSpec.makeMeasureSpec(1080, View.MeasureSpec.EXACTLY)
@JvmField
val DEFAULT_HEIGHT_SPEC: Int = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED)


/*
 * Copyright (c) Meta Platforms, Inc. and 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

import android.os.Looper
import com.facebook.litho.ComponentTree
import java.util.concurrent.ArrayBlockingQueue
import java.util.concurrent.BlockingQueue
import org.robolectric.Shadows
import org.robolectric.annotation.LooperMode
import org.robolectric.shadows.ShadowLooper

/**
 * Helper class extracting looper functionality from [BackgroundLayoutLooperRule] that can be reused
 * in other TestRules
 */
class ThreadLooperController(
    val layoutLooper: ShadowLooper =
        Shadows.shadowOf(
            Whitebox.invokeMethod<Any>(ComponentTree::class.java, "getDefaultLayoutThreadLooper")
                as Looper)
) : BaseThreadLooperController {
  private lateinit var messageQueue: BlockingQueue<Message>
  private var isInitialized = false

  override fun init() {
    messageQueue = ArrayBlockingQueue(100)
    val layoutLooperThread = LayoutLooperThread(layoutLooper, messageQueue)
    layoutLooperThread.start()

    isInitialized = true
  }

  override fun clean() {
    if (!isInitialized) {
      return
    }

    quitSync()
    if (ShadowLooper.looperMode() != LooperMode.Mode.PAUSED) {
      layoutLooper.quitUnchecked()
    }
    isInitialized = false
  }

  private fun quitSync() {
    val semaphore = TimeOutSemaphore(0)
    messageQueue.add(Message(MessageType.QUIT, semaphore))
    semaphore.acquire()
  }

  /**
   * Runs one task on the background thread, blocking until it completes successfully or throws an
   * exception.
   */
  override fun runOneTaskSync() {
    val semaphore = TimeOutSemaphore(0)
    messageQueue.add(Message(MessageType.DRAIN_ONE, semaphore))
    semaphore.acquire()
  }

  /**
   * Runs through all tasks on the background thread, blocking until it completes successfully or
   * throws an exception.
   */
  override fun runToEndOfTasksSync() {
    val semaphore = TimeOutSemaphore(0)
    messageQueue.add(Message(MessageType.DRAIN_ALL, semaphore))
    semaphore.acquire()
  }

  override fun runToEndOfTasksAsync(): TimeOutSemaphore? {
    val semaphore = TimeOutSemaphore(0)
    messageQueue.add(Message(MessageType.DRAIN_ALL, semaphore))
    return semaphore
  }
}

enum class MessageType {
  DRAIN_ONE,
  DRAIN_ALL,
  QUIT
}

class Message constructor(val messageType: MessageType, val semaphore: TimeOutSemaphore)

@SuppressWarnings("CatchGeneralException")
private class LayoutLooperThread(layoutLooper: ShadowLooper, messages: BlockingQueue<Message>) :
    Thread(
        Runnable {
          var keepGoing = true
          while (keepGoing) {
            val message: Message =
                try {
                  messages.take()
                } catch (e: InterruptedException) {
                  throw RuntimeException(e)
                }
            try {
              when (message.messageType) {
                MessageType.DRAIN_ONE -> {
                  layoutLooper.runOneTask()
                  message.semaphore.release()
                }
                MessageType.DRAIN_ALL -> {
                  layoutLooper.runToEndOfTasks()
                  message.semaphore.release()
                }
                MessageType.QUIT -> {
                  keepGoing = false
                  message.semaphore.release()
                }
              }
            } catch (e: Exception) {
              message.semaphore.setException(e)
              message.semaphore.release()
            }
          }
        })


/*
 * Copyright (c) Meta Platforms, Inc. and 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

import android.content.Context
import com.facebook.rendercore.ContentAllocator
import com.facebook.rendercore.MountItemsPool

class TrackedItemPool(lifecycle: Any, size: Int) : MountItemsPool.DefaultItemPool(lifecycle, size) {
  var currentSize: Int = 0
    private set

  override fun acquire(c: Context?, poolableMountContent: ContentAllocator<*>?): Any? {
    return super.acquire(c, poolableMountContent).apply {
      if (this != null) {
        currentSize--
      }
    }
  }

  override fun release(item: Any): Boolean {
    return super.release(item).apply {
      if (this) {
        currentSize++
      }
    }
  }
}


/*
 * Copyright (c) Meta Platforms, Inc. and 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.api

import com.facebook.litho.ActionAttributeKey
import java.lang.Appendable

/**
 * Prints all the available node information into a string.
 *
 * [maxDepth] can be used to configure how deep into the hierarchy to print. **0** will print just
 * this node, and [Int.MAX_VALUE] effectively prints the whole hierarchy. By default, the whole
 * hierarchy will be printed.
 */
fun TestNodeSelection.printToString(maxDepth: Int = Int.MAX_VALUE): String {
  return buildString { printTo(this, maxDepth) }
}

/**
 * Prints all the matching nodes in the collection into a string.
 *
 * [maxDepth] can be used to configure how deep into the hierarchy of each node to print. **0** will
 * print just the node, and [Int.MAX_VALUE] effectively prints the whole hierarchy. By default, this
 * does not print the sub-hierarchy of the matching modes.
 *
 * @see [TestNodeSelection.printToString]
 */
fun TestNodeCollectionSelection.printToString(maxDepth: Int = 0): String {
  return buildString { printTo(this, maxDepth) }
}

/**
 * Prints all the available node information into the given [appendable].
 *
 * A typical use-case for this API could be printing the node hierarchy into a stream or file, as
 * the case may be, using the [java.io.OutputStreamWriter] or [java.io.FileWriter] APIs accordingly
 *
 * [maxDepth] can be used to configure how deep into the hierarchy to print. **0** will print just
 * this node, and [Int.MAX_VALUE] effectively prints the whole hierarchy. By default, the whole
 * hierarchy will be printed.
 *
 * @see [TestNodeSelection.printToString]
 */
fun TestNodeSelection.printTo(appendable: Appendable, maxDepth: Int = Int.MAX_VALUE) {
  fetchTestNode("Failed: printNode").printTo(appendable, maxDepth)
}

/**
 * Prints all the matching nodes in the collection into the given [appendable].
 *
 * [maxDepth] can be used to configure how deep into the hierarchy of each node to print. **0** will
 * print just the node, and [Int.MAX_VALUE] effectively prints the whole hierarchy. By default, this
 * does not print the sub-hierarchy of the matching modes.
 *
 * @see [TestNodeSelection.printTo]
 * @see [TestNodeCollectionSelection.printToString]
 */
fun TestNodeCollectionSelection.printTo(appendable: Appendable, maxDepth: Int = 0) {
  val nodes = fetchMatchingNodes()
  appendable.appendLine("Found ${nodes.size} matching node(s)")
  nodes.printTo(appendable, maxDepth)
}

internal fun List<TestNode>.printTo(appendable: Appendable, maxDepth: Int = 0) {
  forEach { node -> node.printTo(appendable, maxDepth) }
}

internal fun TestNode.printTo(appendable: Appendable, maxDepth: Int = Int.MAX_VALUE) {
  printTo(appendable, indentLevel = 0, maxIndentLevel = maxDepth, hasNextSibling = false)
}

private fun TestNode.printTo(
    appendable: Appendable,
    indentLevel: Int,
    maxIndentLevel: Int,
    hasNextSibling: Boolean
) {
  require(maxIndentLevel >= indentLevel) { "maxIndentLevel cannot be less than indentLevel" }

  val levelPrefix = if (indentLevel == 0) "" else " ".repeat(indentLevel)

  // 1. Set node prefix
  val nodePrefix = buildString {
    append(levelPrefix)
    if (indentLevel != 0) {
      append("|")
    }
    append("-")
  }

  appendable.append(nodePrefix)

  // 2. Append name
  val normalizedComponentName = componentType.simpleName
  appendable.append(normalizedComponentName)

  // 3. Append meta properties
  val metaProps = listOfNotNull(Pair("children", children.size).takeIf { children.isNotEmpty() })
  if (metaProps.isNotEmpty()) {
    appendable.appendLine("(${metaProps.joinToString(", ") { (k, v) -> "$k=$v"}})")
  } else {
    appendable.appendLine()
  }

  // 4. Append attributes
  val nonActionAttributes =
      attributes
          .filter { (key, _) -> key !is ActionAttributeKey }
          .mapNotNull { (key, value) ->
            value?.let { Pair(key.description, formatPrettyValue(value)) }
          }

  val attributePrefix = buildString {
    append(levelPrefix)
    append(if (hasNextSibling) "|  " else "   ")
  }

  nonActionAttributes.forEach { (key, value) ->
    appendable.append(attributePrefix)
    appendable.appendLine("$key = $value")
  }

  // 5. Append Action attributes
  val actionAttributes =
      attributes.filter { (key, value) -> key is ActionAttributeKey && value != null }

  if (actionAttributes.isNotEmpty()) {
    appendable.append(attributePrefix)
    appendable.append("Actions = ")
    appendable.appendLine(
        actionAttributes.joinToString(separator = " , ", prefix = "[", postfix = "]") {
          it.first.description
        })
  }

  if (indentLevel == maxIndentLevel) return

  children.forEachIndexed { index, child ->
    child.printTo(
        appendable = appendable,
        indentLevel = indentLevel + 1,
        maxIndentLevel = maxIndentLevel,
        hasNextSibling = children.lastIndex != index)
  }
}

/**
 * This helps to have less clutter when printing values.
 *
 * For example, if the given [value] is a Collection of a single value, it will only print the first
 * value, instead of the whole array representation.
 */
private fun formatPrettyValue(value: Any?): String? {
  return if (value is Collection<*>) {
    if (value.size > 1) value.toString() else value.first()?.toString()
  } else {
    value?.toString()
  }
}