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

import android.annotation.TargetApi
import android.content.Context
import android.graphics.Color
import android.graphics.drawable.ColorDrawable
import android.os.Build
import androidx.test.core.app.ApplicationProvider
import com.facebook.litho.SizeSpec.EXACTLY
import com.facebook.litho.SizeSpec.makeSizeSpec
import com.facebook.litho.config.ComponentsConfiguration
import com.facebook.litho.testing.LegacyLithoViewRule
import com.facebook.litho.testing.helper.ComponentTestHelper
import com.facebook.litho.testing.testrunner.LithoTestRunner
import com.facebook.litho.widget.DynamicPropsResetValueTester
import com.facebook.litho.widget.DynamicPropsResetValueTesterSpec
import com.facebook.rendercore.MountItem
import org.assertj.core.api.Assertions.assertThat
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.annotation.Config
import org.robolectric.annotation.LooperMode

@LooperMode(LooperMode.Mode.LEGACY)
@RunWith(LithoTestRunner::class)
class DynamicPropsTest {

  private lateinit var context: ComponentContext

  val config = ComponentsConfiguration.create().shouldAddHostViewForRootComponent(true).build()

  @JvmField @Rule val legacyLithoViewRule = LegacyLithoViewRule(config)

  @Before
  fun setup() {
    context = ComponentContext(ApplicationProvider.getApplicationContext<Context>())
  }

  @Test
  fun testDynamicAlphaApplied() {
    val startValue = 0.8f
    val alphaDV = DynamicValue(startValue)
    val lithoView =
        legacyLithoViewRule
            .attachToWindow()
            .setRoot(Column.create(context).widthPx(80).heightPx(80).alpha(alphaDV).build())
            .measure()
            .layout()
            .lithoView
    assertThat(lithoView.childCount).isEqualTo(1)
    val hostView = lithoView.getChildAt(0)
    assertThat(hostView.alpha).isEqualTo(startValue)
    alphaDV.set(0.5f)
    assertThat(hostView.alpha).isEqualTo(0.5f)
    alphaDV.set(0f)
    assertThat(hostView.alpha).isEqualTo(0f)
    alphaDV.set(1f)
    assertThat(hostView.alpha).isEqualTo(1f)
  }

  @Test
  fun testAttributesAndDynamicPropDuringUpdate() {
    val startValue = 0.8f
    val alphaDV = DynamicValue(startValue)
    val component1 =
        Column.create(context)
            .widthPx(80)
            .heightPx(80)
            .backgroundColor(Color.GREEN)
            .alpha(alphaDV)
            .build()
    val component2 =
        Column.create(context)
            .widthPx(80)
            .heightPx(80)
            .backgroundColor(Color.MAGENTA)
            .alpha(alphaDV)
            .build()
    legacyLithoViewRule
        .setRoot(component1)
        .setSizeSpecs(makeSizeSpec(80, EXACTLY), makeSizeSpec(80, EXACTLY))
        .attachToWindow()
        .measure()
        .layout()
    val lithoView = legacyLithoViewRule.lithoView

    // Ensure we have one view.
    assertThat(lithoView.childCount).isEqualTo(1)
    var hostView = lithoView.getChildAt(0)

    // Ensure alpha DV is correct
    assertThat(hostView.alpha).isEqualTo(startValue)

    // Ensure background attribute is present and has the correct value.
    assertThat(hostView.background).isNotNull
    assertThat((hostView.background as ColorDrawable).color).isEqualTo(Color.GREEN)

    // Mount component2, which is identical to component1, except with a different bg, invoking
    // an update sequence.
    legacyLithoViewRule.setRoot(component2)

    // Grab the host again
    hostView = lithoView.getChildAt(0)

    // Alter the alpha DV
    alphaDV.set(0.5f)

    // Ensure the DV is properly applied on the view
    assertThat(hostView.alpha).isEqualTo(0.5f)

    // Ensure background attribute is present and has the correct value.
    assertThat(hostView.background).isNotNull
    assertThat((hostView.background as ColorDrawable).color).isEqualTo(Color.MAGENTA)
  }

  @Test
  fun testDynamicTranslationApplied() {
    val startValueX = 100f
    val startValueY = -100f
    val translationXDV = DynamicValue(startValueX)
    val translationYDV = DynamicValue(startValueY)
    val lithoView =
        legacyLithoViewRule
            .attachToWindow()
            .setRoot(
                Column.create(context)
                    .widthPx(80)
                    .heightPx(80)
                    .translationX(translationXDV)
                    .translationY(translationYDV)
                    .build())
            .measure()
            .layout()
            .lithoView
    assertThat(lithoView.childCount).isEqualTo(1)
    val hostView = lithoView.getChildAt(0)
    assertThat(hostView.translationX).isEqualTo(startValueX)
    assertThat(hostView.translationY).isEqualTo(startValueY)
    translationXDV.set(50f)
    translationYDV.set(20f)
    assertThat(hostView.translationX).isEqualTo(50f)
    assertThat(hostView.translationY).isEqualTo(20f)
    translationXDV.set(-50f)
    translationYDV.set(-20f)
    assertThat(hostView.translationX).isEqualTo(-50f)
    assertThat(hostView.translationY).isEqualTo(-20f)
    translationXDV.set(0f)
    translationYDV.set(0f)
    assertThat(hostView.translationX).isEqualTo(0f)
    assertThat(hostView.translationY).isEqualTo(0f)
  }

  @Test
  fun testDynamicScaleApplied() {
    val startValueX = 1.5f
    val startValueY = -1.5f
    val scaleXDV = DynamicValue(startValueX)
    val scaleYDV = DynamicValue(startValueY)
    val lithoView =
        legacyLithoViewRule
            .attachToWindow()
            .setRoot(
                Column.create(context)
                    .widthPx(80)
                    .heightPx(80)
                    .scaleX(scaleXDV)
                    .scaleY(scaleYDV)
                    .build())
            .measure()
            .layout()
            .lithoView
    assertThat(lithoView.childCount).isEqualTo(1)
    val hostView = lithoView.getChildAt(0)
    assertThat(hostView.scaleX).isEqualTo(startValueX)
    assertThat(hostView.scaleY).isEqualTo(startValueY)
    scaleXDV.set(0.5f)
    scaleYDV.set(2f)
    assertThat(hostView.scaleX).isEqualTo(0.5f)
    assertThat(hostView.scaleY).isEqualTo(2f)
    scaleXDV.set(2f)
    scaleYDV.set(0.5f)
    assertThat(hostView.scaleX).isEqualTo(2f)
    assertThat(hostView.scaleY).isEqualTo(.5f)
    scaleXDV.set(0f)
    scaleYDV.set(0f)
    assertThat(hostView.scaleX).isEqualTo(0f)
    assertThat(hostView.scaleY).isEqualTo(0f)
  }

  @Test
  fun testDynamicBackgroundColorApplied() {
    val startValue = Color.RED
    val backgroundColorDV = DynamicValue(startValue)
    val lithoView =
        legacyLithoViewRule
            .attachToWindow()
            .setRoot(
                Column.create(context)
                    .widthPx(80)
                    .heightPx(80)
                    .backgroundColor(backgroundColorDV)
                    .build())
            .measure()
            .layout()
            .lithoView
    assertThat(lithoView.childCount).isEqualTo(1)
    val hostView = lithoView.getChildAt(0)
    assertThat(hostView.background).isInstanceOf(ColorDrawable::class.java)
    assertThat((hostView.background as ColorDrawable).color).isEqualTo(startValue)
    backgroundColorDV.set(Color.BLUE)
    assertThat((hostView.background as ColorDrawable).color).isEqualTo(Color.BLUE)
    backgroundColorDV.set(Color.GRAY)
    assertThat((hostView.background as ColorDrawable).color).isEqualTo(Color.GRAY)
    backgroundColorDV.set(Color.TRANSPARENT)
    assertThat((hostView.background as ColorDrawable).color).isEqualTo(Color.TRANSPARENT)
  }

  @Test
  fun testDynamicRotationApplied() {
    val startValue = 0f
    val rotationDV = DynamicValue(startValue)
    val lithoView =
        legacyLithoViewRule
            .attachToWindow()
            .setRoot(Column.create(context).widthPx(80).heightPx(80).rotation(rotationDV).build())
            .measure()
            .layout()
            .lithoView
    assertThat(lithoView.childCount).isEqualTo(1)
    val hostView = lithoView.getChildAt(0)
    assertThat(hostView.rotation).isEqualTo(startValue)
    rotationDV.set(364f)
    assertThat(hostView.rotation).isEqualTo(364f)
    rotationDV.set(520f)
    assertThat(hostView.rotation).isEqualTo(520f)
    rotationDV.set(-1f)
    assertThat(hostView.rotation).isEqualTo(-1f)
  }

  @Test
  fun testNullDynamicValue() {
    val nullIntegerValue: DynamicValue<Int>? = null
    val nullFloatValue: DynamicValue<Float>? = null
    val lithoView =
        ComponentTestHelper.mountComponent(
            context,
            Column.create(context)
                .widthPx(80)
                .heightPx(80)
                .backgroundColor(nullIntegerValue)
                .rotation(nullFloatValue)
                .build())
    assertThat(lithoView.background).isEqualTo(null)
    assertThat(lithoView.rotation).isEqualTo(0.0f)
  }

  private class DynamicElevationBuilder(
      c: ComponentContext,
      defStyleAttr: Int,
      defStyleRes: Int,
      private var _component: SpecGeneratedComponent
  ) : Component.Builder<DynamicElevationBuilder?>(c, defStyleAttr, defStyleRes, _component) {
    override fun build(): SpecGeneratedComponent = _component

    override fun getThis(): DynamicElevationBuilder = this

    override fun setComponent(component: Component) {
      this._component = component as SpecGeneratedComponent
    }
  }

  @Test
  @Config(sdk = [Build.VERSION_CODES.LOLLIPOP])
  @TargetApi(Build.VERSION_CODES.LOLLIPOP)
  fun testDynamicElevationApplied() {
    val lithoView = LithoView(context)
    val startValue = 1f
    val elevationDV = DynamicValue(startValue)
    val component =
        DynamicElevationBuilder(
                context,
                -1,
                -1,
                object : SpecGeneratedComponent("DynamicElevationTestComponent") {
                  override fun getMountType(): MountType = MountType.VIEW
                })
            .shadowElevation(elevationDV)
            .build()
    val dynamicPropsManager = DynamicPropsManager()
    dynamicPropsManager.onBindComponentToContent(
        component, context, component.commonDynamicProps, lithoView)
    assertThat(lithoView.elevation).isEqualTo(startValue)
    elevationDV.set(50f)
    assertThat(lithoView.elevation).isEqualTo(50f)
    elevationDV.set(-50f)
    assertThat(lithoView.elevation).isEqualTo(-50f)
  }

  @Test
  fun commonDynamicProps_unbindAndRebindContent_resetValues() {
    val stateUpdateCaller = DynamicPropsResetValueTesterSpec.Caller()
    val component = DynamicPropsResetValueTester.create(context).caller(stateUpdateCaller).build()
    legacyLithoViewRule.setRoot(component).attachToWindow().measure().layout()
    val mountDelegateTarget = legacyLithoViewRule.lithoView.mountDelegateTarget
    var text1HostId: Long = -1
    var text2HostId: Long = -1
    for (i in 0 until mountDelegateTarget.mountItemCount) {
      val mountItem = mountDelegateTarget.getMountItemAt(i)
      if (mountItem != null) {
        val unit = LithoRenderUnit.getRenderUnit(mountItem)
        if (unit.component.simpleName == "Text") {
          val hostMarker = if (i != 0) mountItem.renderTreeNode.parent?.renderUnit?.id ?: -1 else -1
          if (text1HostId == -1L) {
            text1HostId = hostMarker
          } else if (text2HostId == -1L) {
            text2HostId = hostMarker
          }
        }
      }
    }
    lateinit var text1HostComponent: HostComponent
    lateinit var text2HostComponent: HostComponent
    lateinit var text1Host: ComponentHost
    lateinit var text2Host: ComponentHost
    for (i in 0 until mountDelegateTarget.mountItemCount) {
      val mountItem = mountDelegateTarget.getMountItemAt(i)
      if (mountItem != null) {
        val unit = LithoRenderUnit.getRenderUnit(mountItem)
        if (text1HostId == MountItem.getId(mountItem)) {
          text1HostComponent = unit.component as HostComponent
          text1Host = mountItem.content as ComponentHost
        }
        if (text2HostId == MountItem.getId(mountItem)) {
          text2HostComponent = unit.component as HostComponent
          text2Host = mountItem.content as ComponentHost
        }
      }
    }
    assertThat(text1HostComponent.hasCommonDynamicProps()).isTrue
    assertThat(text1Host.alpha).isEqualTo(DynamicPropsResetValueTesterSpec.ALPHA_TRANSPARENT)
    assertThat(text2HostComponent.hasCommonDynamicProps()).isFalse
    assertThat(text2Host.alpha).isEqualTo(DynamicPropsResetValueTesterSpec.ALPHA_OPAQUE)
    stateUpdateCaller.toggleShowChild()
    lateinit var stateUpdateText1HostComponent: HostComponent
    lateinit var stateUpdateText2HostComponent: HostComponent
    lateinit var stateUpdateText1Host: ComponentHost
    lateinit var stateUpdateText2Host: ComponentHost
    for (i in 0 until mountDelegateTarget.mountItemCount) {
      val mountItem = mountDelegateTarget.getMountItemAt(i)
      if (mountItem != null) {
        val unit = LithoRenderUnit.getRenderUnit(mountItem)
        if (text1HostId == MountItem.getId(mountItem)) {
          stateUpdateText1HostComponent = unit.component as HostComponent
          stateUpdateText1Host = mountItem.content as ComponentHost
        }
        if (text2HostId == MountItem.getId(mountItem)) {
          stateUpdateText2HostComponent = unit.component as HostComponent
          stateUpdateText2Host = mountItem.content as ComponentHost
        }
      }
    }
    assertThat(stateUpdateText1Host).isEqualTo(text1Host)
    assertThat(stateUpdateText2Host).isEqualTo(text2Host)
    assertThat(stateUpdateText1HostComponent.hasCommonDynamicProps()).isFalse
    assertThat(stateUpdateText1Host.alpha).isEqualTo(DynamicPropsResetValueTesterSpec.ALPHA_OPAQUE)
    assertThat(stateUpdateText2HostComponent.hasCommonDynamicProps()).isFalse
    assertThat(stateUpdateText2Host.alpha).isEqualTo(DynamicPropsResetValueTesterSpec.ALPHA_OPAQUE)
  }
}


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

import com.facebook.litho.testing.Whitebox
import com.facebook.litho.testing.testrunner.LithoTestRunner
import com.facebook.yoga.YogaConstants
import com.facebook.yoga.YogaEdge
import org.assertj.core.api.Assertions.assertThat
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith

@RunWith(LithoTestRunner::class)
class EdgesTest {

  private lateinit var edges: Edges

  @Before
  fun setup() {
    edges = Edges()
  }

  @Test
  fun testInsertingOneEdgeMultipleTimes() {
    edges[YogaEdge.TOP] = 1f
    edges[YogaEdge.TOP] = 2f
    edges[YogaEdge.TOP] = 3f
    edges[YogaEdge.TOP] = 4f
    edges[YogaEdge.TOP] = 5f
    var bits = 0.inv().toLong()
    bits = bits and (0xF.toLong() shl YogaEdge.TOP.intValue() * 4).inv()
    bits = bits or (0L shl YogaEdge.TOP.intValue() * 4)
    assertThat(edgesToValuesIndex).isEqualTo(bits)
    assertThat(valuesArray.size).isEqualTo(2)
    assertThat(valuesArray[0]).isEqualTo(5f)
    assertThat(YogaConstants.isUndefined(valuesArray[1])).isTrue
  }

  @Test
  fun testUnsettingAnEdge() {
    edges[YogaEdge.TOP] = 1f
    edges[YogaEdge.TOP] = 2f
    edges[YogaEdge.TOP] = YogaConstants.UNDEFINED
    val bits = 0.inv().toLong()
    assertThat(edgesToValuesIndex).isEqualTo(bits)
    assertThat(valuesArray.size).isEqualTo(2)
    assertThat(YogaConstants.isUndefined(valuesArray[0])).isTrue
    assertThat(YogaConstants.isUndefined(valuesArray[1])).isTrue
  }

  @Test
  fun testUnsettingNotTheFirstEdge() {
    edges[YogaEdge.TOP] = 1f
    edges[YogaEdge.LEFT] = 2f
    edges[YogaEdge.LEFT] = YogaConstants.UNDEFINED
    var bits = 0.inv().toLong()
    bits = bits and (0xF.toLong() shl YogaEdge.TOP.intValue() * 4).inv()
    bits = bits or (0L shl YogaEdge.TOP.intValue() * 4)
    assertThat(edgesToValuesIndex).isEqualTo(bits)
    assertThat(valuesArray.size).isEqualTo(2)
    assertThat(valuesArray[0]).isEqualTo(1f)
    assertThat(valuesArray[1]).isNaN
  }

  @Test
  fun testSettingMultipleEdgesIncreasesTheArray() {
    edges[YogaEdge.TOP] = 1f
    edges[YogaEdge.LEFT] = 2f
    edges[YogaEdge.ALL] = 5f
    var bits = 0.inv().toLong()
    bits = bits and (0xF.toLong() shl YogaEdge.TOP.intValue() * 4).inv()
    bits = bits and (0xF.toLong() shl YogaEdge.LEFT.intValue() * 4).inv()
    bits = bits and (0xF.toLong() shl YogaEdge.ALL.intValue() * 4).inv()
    bits = bits or (0L shl YogaEdge.TOP.intValue() * 4)
    bits = bits or (1L shl YogaEdge.LEFT.intValue() * 4)
    bits = bits or (2L shl YogaEdge.ALL.intValue() * 4)
    assertThat(edgesToValuesIndex).isEqualTo(bits)
    assertThat(valuesArray.size).isEqualTo(4)
    assertThat(valuesArray[0]).isEqualTo(1f)
    assertThat(valuesArray[1]).isEqualTo(2f)
    assertThat(valuesArray[2]).isEqualTo(5f)
    assertThat(valuesArray[3]).isNaN
  }

  @Test
  fun testUnsettingAndSettingNewEdgesReusesArraySpace() {
    edges[YogaEdge.TOP] = 1f
    edges[YogaEdge.LEFT] = 2f
    edges[YogaEdge.ALL] = 5f
    edges[YogaEdge.LEFT] = YogaConstants.UNDEFINED
    edges[YogaEdge.BOTTOM] = 4f
    var bits = 0.inv().toLong()
    bits = bits and (0xF.toLong() shl YogaEdge.TOP.intValue() * 4).inv()
    bits = bits and (0xF.toLong() shl YogaEdge.ALL.intValue() * 4).inv()
    bits = bits and (0xF.toLong() shl YogaEdge.BOTTOM.intValue() * 4).inv()
    bits = bits or (0L shl YogaEdge.TOP.intValue() * 4)
    bits = bits or (1L shl YogaEdge.BOTTOM.intValue() * 4)
    bits = bits or (2L shl YogaEdge.ALL.intValue() * 4)
    assertThat(edgesToValuesIndex).isEqualTo(bits)
    assertThat(valuesArray.size).isEqualTo(4)
    assertThat(valuesArray[0]).isEqualTo(1f)
    assertThat(valuesArray[1]).isEqualTo(4f)
    assertThat(valuesArray[2]).isEqualTo(5f)
    assertThat(valuesArray[3]).isNaN
  }

  @Test
  fun testAliasesAndResolveGetter() {
    edges[YogaEdge.ALL] = 10f
    assertThat(edges.getRaw(YogaEdge.LEFT)).isNaN
    assertThat(edges.getRaw(YogaEdge.TOP)).isNaN
    assertThat(edges.getRaw(YogaEdge.RIGHT)).isNaN
    assertThat(edges.getRaw(YogaEdge.BOTTOM)).isNaN
    assertThat(edges.getRaw(YogaEdge.ALL)).isEqualTo(10f)
    assertThat(edges[YogaEdge.LEFT]).isEqualTo(10f)
    assertThat(edges[YogaEdge.TOP]).isEqualTo(10f)
    assertThat(edges[YogaEdge.RIGHT]).isEqualTo(10f)
    assertThat(edges[YogaEdge.BOTTOM]).isEqualTo(10f)
    assertThat(edges[YogaEdge.ALL]).isEqualTo(10f)
  }

  @Test
  fun testSameObjectEquivalentTo() {
    assertThat(edges.isEquivalentTo(edges)).isEqualTo(true)
  }

  @Test
  fun testNullObjectEquivalentTo() {
    assertThat(edges.isEquivalentTo(null)).isEqualTo(false)
  }

  @Test
  fun testDifferentObjectWithSameContentEquivalentTo() {
    edges[YogaEdge.TOP] = 1f
    val mEdges2 = Edges()
    mEdges2[YogaEdge.TOP] = 1f
    assertThat(edges.isEquivalentTo(mEdges2)).isEqualTo(true)
  }

  @Test
  fun testDifferentObjectWithDifferentContentEquivalentTo() {
    edges[YogaEdge.LEFT] = 1f
    var mEdges2 = Edges()
    mEdges2[YogaEdge.TOP] = 1f
    assertThat(edges.isEquivalentTo(mEdges2)).isEqualTo(false)
    edges = Edges()
    edges[YogaEdge.TOP] = 1f
    mEdges2 = Edges()
    mEdges2[YogaEdge.TOP] = 2f
    assertThat(edges.isEquivalentTo(mEdges2)).isEqualTo(false)
  }

  private val edgesToValuesIndex: Long
    private get() = Whitebox.getInternalState<Any>(edges, "mEdgesToValuesIndex") as Long

  private val valuesArray: FloatArray
    private get() = Whitebox.getInternalState<Any>(edges, "mValues") as FloatArray
}


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

import com.facebook.litho.core.height
import com.facebook.litho.testing.LithoViewRule
import com.facebook.litho.testing.assertj.LithoViewAssert
import com.facebook.litho.testing.testrunner.LithoTestRunner
import com.facebook.litho.widget.Text
import com.facebook.rendercore.utils.MeasureSpecUtils.exactly
import com.facebook.rendercore.utils.MeasureSpecUtils.unspecified
import org.assertj.core.api.Assertions.assertThat
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.shadows.ShadowLooper

/** Tests for [EmptyComponent]. */
@RunWith(LithoTestRunner::class)
class EmptyComponentTest {

  @Rule @JvmField val lithoViewRule = LithoViewRule()

  @Test
  fun `two EmptyComponents should be equivalent and have equivalent props`() {
    val emptyComponent1 = EmptyComponent()
    val emptyComponent2 = EmptyComponent()

    assertThat(emptyComponent1.isEquivalentProps(emptyComponent2, false))
        .describedAs("isEquivalentProps check")
        .isEqualTo(true)
    assertThat(emptyComponent1.isEquivalentTo(emptyComponent2))
        .describedAs("isEquivalent check")
        .isEqualTo(true)
  }

  @Test
  fun `setting a null root renders an empty component`() {
    val lithoView = lithoViewRule.render { Text.create(context).text("Hello World").build() }
    lithoView.setSizeSpecs(exactly(1000), unspecified())

    LithoViewAssert.assertThat(lithoView.lithoView).hasVisibleText("Hello World")

    lithoViewRule.act(lithoView) { lithoView.setRoot(null) }
    lithoView.measure().layout()
    ShadowLooper.idleMainLooper()

    // The assertion below should work, but we don't perform incremental mount when the visible rect
    // is empty!
    // LithoViewAssert.assertThat(lithoView.lithoView).doesNotHaveVisibleText("Hello World")
    assertThat(lithoView.lithoView.height)
        .describedAs("LithoView height with null root")
        .isEqualTo(0)
  }
}


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

import com.facebook.litho.kotlin.widget.Text
import com.facebook.litho.sections.SectionContext
import com.facebook.litho.sections.widget.RecyclerCollectionComponent
import com.facebook.litho.testing.LithoViewRule
import com.facebook.litho.testing.assertj.LithoViewAssert
import com.facebook.litho.testing.testrunner.LithoTestRunner
import com.facebook.litho.widget.EventHandlerBindingComponent
import com.facebook.litho.widget.EventHandlerBindingComponentSpec
import com.facebook.litho.widget.EventHandlerBindingSection
import com.facebook.litho.widget.EventHandlerBindingSectionSpec
import java.util.concurrent.atomic.AtomicInteger
import org.assertj.core.api.Assertions.assertThat
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.annotation.LooperMode

/**
 * Tests to make sure we properly dispatch events to the latest versions of event handlers when
 * eventhandlers are used across trees.
 */
@LooperMode(LooperMode.Mode.LEGACY)
@RunWith(LithoTestRunner::class)
class EventHandlerRebindTest {

  @Rule @JvmField val lithoViewRule: LithoViewRule = LithoViewRule()

  /**
   * This test makes sure that we re-bind EventHandlers even if the particular row the EventHandler
   * is used in doesn't get re-rendered.
   *
   * BACKGROUND
   *
   * Say we have a Section which has a button as one of its rows. Say that button invokes a
   * clickHandler which is defined on the section, and that click handler references section @State
   * (or @Props). Then, if the section updates state and the only thing that has changed on the
   * button is the click handler, we don't want to re-render that button. But we do want to make
   * sure that when it's clicked, we see the latest state. To do this, we have mechanisms in Litho (
   * [EventHandlersController]) that make sure EventHandlers point to their latest versions.
   *
   * THE TESTS
   *
   * There are 4 groups of assertions:
   * 1. Ensure things are set up properly
   * 2. Update the counter (@State on the Section): ensure the button doesn't re-render but the
   *    event handler does reflect the latest state.
   * 3. Cause the button to re-render (by changing the textSize prop) and update the counter: ensure
   *    the button re-renders as expected and that the event handler reflects the latest state again
   * 4. Update the counter again: ensure the button doesn't re-render but the event handler reflects
   *    the latest state. This is the same as (2) but makes sure things still work after the button
   *    re-renders.
   */
  @Test
  fun `test section event handlers update even if row doesn't re-render`() {
    val updater = EventHandlerBindingSectionSpec.StateUpdater()
    val clickListener = RecordingClickListener()
    val numButtonRenders = AtomicInteger()
    val buttonCreator =
        EventHandlerBindingSectionSpec.ButtonCreator {
          RecordingButton(numRenders = numButtonRenders, textSize = 10.sp)
        }

    val testLithoView =
        lithoViewRule.render(widthPx = 1000, heightPx = 1000) {
          RecyclerCollectionComponent.create(context)
              .section(
                  EventHandlerBindingSection.create(SectionContext(context))
                      .stateUpdater(updater)
                      .onButtonClickListener(clickListener)
                      .buttonCreator(buttonCreator))
              .build()
        }

    lithoViewRule.act(testLithoView) { clickOnText("Click Me") }

    // 1. First assertions: just make sure everything is set up properly.

    LithoViewAssert.assertThat(testLithoView.lithoView).hasVisibleText("Counter: 0")
    assertThat(numButtonRenders.get())
        .describedAs("There should be one initial render of the button")
        .isEqualTo(1)
    assertThat(clickListener.events)
        .describedAs("There should be one click of the button")
        .hasSize(1)
    assertThat(clickListener.events[0])
        .describedAs("The latest counter is 0, so the event should have value 0")
        .isEqualTo(0)
    clickListener.clear()

    // 2. Update the counter (@State on the Section): ensure the button doesn't re-render but the
    // event handler does reflect the latest state.

    updater.updateCounterSync(1)
    lithoViewRule.idle()
    lithoViewRule.act(testLithoView) { clickOnText("Click Me") }

    LithoViewAssert.assertThat(testLithoView.lithoView).hasVisibleText("Counter: 1")
    assertThat(numButtonRenders.get())
        .describedAs(
            "Updating the counter shouldn't cause the button to re-render (if it does, " +
                "that invalidates what this test is testing, namely that the EventHandlers update " +
                "even without a row re-rendering)")
        .isEqualTo(1)
    assertThat(clickListener.events)
        .describedAs("There should be one click of the button")
        .hasSize(1)
    assertThat(clickListener.events[0])
        .describedAs("The latest counter is 1, so the event should have value 1")
        .isEqualTo(1)
    clickListener.clear()

    // 3. Update the counter and cause the button to re-render (by changing the textSize prop):
    // ensure the button re-renders as expected and that the event handler reflects the latest
    // state again
    updater.updateCounterSync(2)
    testLithoView.setRoot(
        RecyclerCollectionComponent.create(lithoViewRule.context)
            .section(
                EventHandlerBindingSection.create(SectionContext(lithoViewRule.context))
                    .stateUpdater(updater)
                    .onButtonClickListener(clickListener)
                    .buttonCreator(
                        EventHandlerBindingSectionSpec.ButtonCreator {
                          RecordingButton(numRenders = numButtonRenders, textSize = 20.sp)
                        }))
            .build())
    lithoViewRule.idle()
    lithoViewRule.act(testLithoView) { clickOnText("Click Me") }

    LithoViewAssert.assertThat(testLithoView.lithoView).hasVisibleText("Counter: 2")
    assertThat(numButtonRenders.get())
        .describedAs(
            "Since we updated the button background, it should re-render (if it does not re-render, " +
                "that invalidates the premise of the rest of this test)")
        .isEqualTo(2)
    assertThat(clickListener.events)
        .describedAs("There should be one click of the button")
        .hasSize(1)
    assertThat(clickListener.events[0])
        .describedAs("The latest counter is 2, so the event should have value 2")
        .isEqualTo(2)
    clickListener.clear()

    // 4. Update the counter again: ensure the button doesn't re-render but the event handler
    // reflects the latest state. This is the same as (2) but makes sure things still work after the
    // button re-renders.

    updater.updateCounterSync(3)
    lithoViewRule.idle()
    lithoViewRule.act(testLithoView) { clickOnText("Click Me") }

    LithoViewAssert.assertThat(testLithoView.lithoView).hasVisibleText("Counter: 3")
    assertThat(numButtonRenders.get())
        .describedAs(
            "Updating the counter shouldn't cause the button to re-render (if it does, " +
                "that invalidates what this test is testing, namely that the EventHandlers update " +
                "even without a row re-rendering)")
        .isEqualTo(2)
    assertThat(clickListener.events)
        .describedAs("There should be one click of the button")
        .hasSize(1)

    assertThat(clickListener.events[0])
        .describedAs("The latest counter is 3, so the event should have value 3")
        .isEqualTo(3)

    clickListener.clear()
  }

  /**
   * This test makes sure that we re-bind EventHandlers even if the particular row the EventHandler
   * is used in doesn't get re-rendered. It's purposefully structured the exact same as the sections
   * version of the test above.
   *
   * BACKGROUND
   *
   * Say we have a parent Component which hosts a nested LithoView, and within that LithoView is a
   * button. Say this parent also defines a ClickHandler which is passed to the button, and that
   * click handler references @State (or @Props) of the parent. Then, if the parent updates state
   * and the only thing that has changed on the button is the click handler, we don't want to
   * re-render that button. But we do want to make sure that when it's clicked, we see the latest
   * state. To do this, we have mechanisms in Litho ([EventHandlersController]) that make sure
   * EventHandlers point to their latest versions.
   *
   * THE TESTS
   *
   * There are 4 groups of assertions:
   * 1. Ensure things are set up properly
   * 2. Update the counter (@State on the parent Component): ensure the button doesn't re-render but
   *    the event handler does reflect the latest state.
   * 3. Cause the button to re-render (by changing the textSize prop) and update the counter: ensure
   *    the button re-renders as expected and that the event handler reflects the latest state again
   * 4. Update the counter again: ensure the button doesn't re-render but the event handler reflects
   *    the latest state. This is the same as (2) but makes sure things still work after the button
   *    re-renders.
   */
  @Test
  fun `test component event handlers update even if nested tree doesn't re-render`() {
    val updater = EventHandlerBindingComponentSpec.StateUpdater()
    val clickListener = RecordingClickListener()
    val numButtonRenders = AtomicInteger()
    val buttonCreator =
        EventHandlerBindingComponentSpec.ButtonCreator {
          RecordingButton(numRenders = numButtonRenders, textSize = 10.sp)
        }

    val testLithoView =
        lithoViewRule.render(widthPx = 1000, heightPx = 1000) {
          EventHandlerBindingComponent.create(context)
              .stateUpdater(updater)
              .onButtonClickListener(clickListener)
              .buttonCreator(buttonCreator)
              .build()
        }

    lithoViewRule.act(testLithoView) { clickOnText("Click Me") }

    // 1. First assertions: just make sure everything is set up properly.

    LithoViewAssert.assertThat(testLithoView.lithoView).hasVisibleText("Counter: 0")
    assertThat(numButtonRenders.get())
        .describedAs("There should be one initial render of the button")
        .isEqualTo(1)
    assertThat(clickListener.events)
        .describedAs("There should be one click of the button")
        .hasSize(1)
    assertThat(clickListener.events[0])
        .describedAs("The latest counter is 0, so the event should have value 0")
        .isEqualTo(0)
    clickListener.clear()

    // 2. Update the counter (@State on the root component): ensure the button doesn't re-render but
    // the event handler does reflect the latest state.

    updater.updateCounterSync(1)
    lithoViewRule.idle()
    lithoViewRule.act(testLithoView) { clickOnText("Click Me") }

    LithoViewAssert.assertThat(testLithoView.lithoView).hasVisibleText("Counter: 1")
    assertThat(numButtonRenders.get())
        .describedAs(
            "Updating the counter shouldn't cause the button to re-render (if it does, " +
                "that invalidates what this test is testing, namely that the EventHandlers update " +
                "even without a row re-rendering)")
        .isEqualTo(1)
    assertThat(clickListener.events)
        .describedAs("There should be one click of the button")
        .hasSize(1)
    assertThat(clickListener.events[0])
        .describedAs("The latest counter is 1, so the event should have value 1")
        .isEqualTo(1)
    clickListener.clear()

    // 3. Update the counter and cause the button to re-render (by changing the textSize prop):
    // ensure the button re-renders as expected and that the event handler reflects the latest
    // state again
    updater.updateCounterSync(2)
    testLithoView.setRoot(
        EventHandlerBindingComponent.create(lithoViewRule.context)
            .stateUpdater(updater)
            .onButtonClickListener(clickListener)
            .buttonCreator(
                EventHandlerBindingComponentSpec.ButtonCreator {
                  RecordingButton(numRenders = numButtonRenders, textSize = 20.sp)
                })
            .build())
    lithoViewRule.idle()
    lithoViewRule.act(testLithoView) { clickOnText("Click Me") }

    LithoViewAssert.assertThat(testLithoView.lithoView).hasVisibleText("Counter: 2")
    assertThat(numButtonRenders.get())
        .describedAs(
            "Since we updated the button background, it should re-render (if it does not re-render, " +
                "that invalidates the premise of the rest of this test)")
        .isEqualTo(2)
    assertThat(clickListener.events)
        .describedAs("There should be one click of the button")
        .hasSize(1)
    assertThat(clickListener.events[0])
        .describedAs("The latest counter is 2, so the event should have value 2")
        .isEqualTo(2)
    clickListener.clear()

    // 4. Update the counter again: ensure the button doesn't re-render but the event handler
    // reflects the latest state. This is the same as (2) but makes sure things still work after the
    // button re-renders.

    updater.updateCounterSync(3)
    lithoViewRule.idle()
    lithoViewRule.act(testLithoView) { clickOnText("Click Me") }

    LithoViewAssert.assertThat(testLithoView.lithoView).hasVisibleText("Counter: 3")
    assertThat(numButtonRenders.get())
        .describedAs(
            "Updating the counter shouldn't cause the button to re-render (if it does, " +
                "that invalidates what this test is testing, namely that the EventHandlers update " +
                "even without a row re-rendering)")
        .isEqualTo(2)
    assertThat(clickListener.events)
        .describedAs("There should be one click of the button")
        .hasSize(1)

    assertThat(clickListener.events[0])
        .describedAs("The latest counter is 3, so the event should have value 3")
        .isEqualTo(3)

    clickListener.clear()
  }

  private class RecordingButton(
      private val numRenders: AtomicInteger,
      private val textSize: Dimen
  ) : KComponent() {
    override fun ComponentScope.render(): Component? {
      numRenders.incrementAndGet()
      return Text(text = "Click Me", textSize = textSize)
    }
  }

  private class RecordingClickListener :
      EventHandlerBindingSectionSpec.OnButtonClickListener,
      EventHandlerBindingComponentSpec.OnButtonClickListener {
    private val _events: MutableList<Int> = mutableListOf()
    val events: List<Int>
      get() = _events.toList()

    override fun onClick(counter: Int) {
      _events.add(counter)
    }

    fun clear() {
      _events.clear()
    }
  }
}


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

import com.facebook.litho.testing.testrunner.LithoTestRunner
import org.assertj.core.api.Assertions.assertThat
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.kotlin.mock

@RunWith(LithoTestRunner::class)
class EventHandlerTest {

  private val hasEventDispatcher = mock<HasEventDispatcher>()

  @Test
  fun testIsEquivalentToWithNullHandler() {
    val eventHandler: EventHandler<*> = EventHandler<Any?>(hasEventDispatcher, 1)
    assertThat(eventHandler.isEquivalentTo(null)).isFalse
  }

  @Test
  fun testIsEquivalentToWithSameHandler() {
    val eventHandler: EventHandler<*> = EventHandler<Any?>(hasEventDispatcher, 1)
    assertThat(eventHandler.isEquivalentTo(eventHandler)).isTrue
  }

  @Test
  fun testIsEquivalentToWithDifferentIds() {
    val eventHandler1: EventHandler<*> = EventHandler<Any?>(hasEventDispatcher, 1)
    val eventHandler2: EventHandler<*> = EventHandler<Any?>(hasEventDispatcher, 2)
    assertThat(eventHandler1.isEquivalentTo(eventHandler2)).isFalse
    assertThat(eventHandler2.isEquivalentTo(eventHandler1)).isFalse
  }

  @Test
  fun testIsEquivalentToWithOneNullParams() {
    val eventHandler1: EventHandler<*> = EventHandler<Any?>(hasEventDispatcher, 1)
    val eventHandler2: EventHandler<*> = EventHandler<Any?>(hasEventDispatcher, 1, arrayOfNulls(0))
    assertThat(eventHandler1.isEquivalentTo(eventHandler2)).isFalse
    assertThat(eventHandler2.isEquivalentTo(eventHandler1)).isFalse
  }

  @Test
  fun testIsEquivalentToWithBothNullParams() {
    val eventHandler1: EventHandler<*> = EventHandler<Any?>(hasEventDispatcher, 1)
    val eventHandler2: EventHandler<*> = EventHandler<Any?>(hasEventDispatcher, 1)
    assertThat(eventHandler1.isEquivalentTo(eventHandler2)).isTrue
    assertThat(eventHandler2.isEquivalentTo(eventHandler1)).isTrue
  }

  @Test
  fun testIsEquivalentToWithDifferentLengthParams() {
    val eventHandler1: EventHandler<*> =
        EventHandler<Any?>(hasEventDispatcher, 1, arrayOf<Any>(1, 2, 3))
    val eventHandler2: EventHandler<*> =
        EventHandler<Any?>(hasEventDispatcher, 1, arrayOf<Any>(1, 2))
    assertThat(eventHandler1.isEquivalentTo(eventHandler2)).isFalse
    assertThat(eventHandler2.isEquivalentTo(eventHandler1)).isFalse
  }

  @Test
  fun testIsEquivalentToWithDifferentParams() {
    val eventHandler1: EventHandler<*> =
        EventHandler<Any?>(hasEventDispatcher, 1, arrayOf<Any>(1, 2, 3))
    val eventHandler2: EventHandler<*> =
        EventHandler<Any?>(hasEventDispatcher, 1, arrayOf<Any>(1, 3, 3))
    assertThat(eventHandler1.isEquivalentTo(eventHandler2)).isFalse
    assertThat(eventHandler2.isEquivalentTo(eventHandler1)).isFalse
  }

  @Test
  fun testIsEquivalentToWithFirstParamDifferent() {
    val eventHandler1: EventHandler<*> =
        EventHandler<Any?>(hasEventDispatcher, 1, arrayOf<Any>(1, 2, 3))
    val eventHandler2: EventHandler<*> =
        EventHandler<Any?>(hasEventDispatcher, 1, arrayOf<Any>(2, 2, 3))
    assertThat(eventHandler1.isEquivalentTo(eventHandler2)).isFalse
    assertThat(eventHandler2.isEquivalentTo(eventHandler1)).isFalse
  }

  @Test
  fun testIsEquivalentToWithDifferentHasEventDispatchInfos() {
    val eventHandler1: EventHandler<*> =
        EventHandler<Any?>(1, EventDispatchInfo(mock(), mock()), arrayOf<Any>(1, 2, 3))
    val eventHandler2: EventHandler<*> =
        EventHandler<Any?>(1, EventDispatchInfo(mock(), mock()), arrayOf<Any>(1, 2, 3))
    assertThat(eventHandler1.isEquivalentTo(eventHandler2)).isTrue
    assertThat(eventHandler2.isEquivalentTo(eventHandler1)).isTrue
  }
}


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

import com.facebook.litho.testing.testrunner.LithoTestRunner
import org.assertj.core.api.Assertions.assertThat
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.Mock
import org.mockito.kotlin.mock
import org.mockito.kotlin.whenever

@RunWith(LithoTestRunner::class)
class EventTriggersContainerTest {

  @Mock lateinit var TEST_HANDLE_1: Handle

  @Mock lateinit var TEST_HANDLE_2: Handle

  @Mock lateinit var mockEventTriggerWithKey: EventTrigger<*>

  @Mock lateinit var mockEventTriggerWithHandle: EventTrigger<*>

  @Mock lateinit var mockEventTriggerWithKeyAndHandle: EventTrigger<*>
  private lateinit var eventTriggersContainer: EventTriggersContainer

  @Before
  fun setUp() {
    TEST_HANDLE_1 = mock()
    TEST_HANDLE_2 = mock()
    mockEventTriggerWithKey = mock()
    whenever(mockEventTriggerWithKey.key).thenReturn(TEST_KEY_1)
    mockEventTriggerWithHandle = mock()
    whenever(mockEventTriggerWithHandle.handle).thenReturn(TEST_HANDLE_1)
    whenever(mockEventTriggerWithHandle.id).thenReturn(METHOD_ID_1)
    mockEventTriggerWithKeyAndHandle = mock()
    whenever(mockEventTriggerWithKeyAndHandle.key).thenReturn(TEST_KEY_2)
    whenever(mockEventTriggerWithKeyAndHandle.handle).thenReturn(TEST_HANDLE_2)
    whenever(mockEventTriggerWithKeyAndHandle.id).thenReturn(METHOD_ID_2)
    eventTriggersContainer = EventTriggersContainer()
  }

  @Test
  fun testKeyAndHandleStorage() {
    eventTriggersContainer.recordEventTrigger(mockEventTriggerWithKey)
    eventTriggersContainer.recordEventTrigger(mockEventTriggerWithHandle)
    eventTriggersContainer.recordEventTrigger(mockEventTriggerWithKeyAndHandle)
    assertThat(eventTriggersContainer.getEventTrigger(TEST_KEY_1))
        .isEqualTo(mockEventTriggerWithKey)
    assertThat(eventTriggersContainer.getEventTrigger(TEST_HANDLE_1, METHOD_ID_1))
        .isEqualTo(mockEventTriggerWithHandle)
    assertThat(eventTriggersContainer.getEventTrigger(TEST_KEY_2))
        .isEqualTo(mockEventTriggerWithKeyAndHandle)
    assertThat(eventTriggersContainer.getEventTrigger(TEST_HANDLE_2, METHOD_ID_2))
        .isEqualTo(mockEventTriggerWithKeyAndHandle)
  }

  @Test
  fun testInvalidStates() {
    eventTriggersContainer.recordEventTrigger(null)
    assertThat(eventTriggersContainer.getEventTrigger("invalid")).isNull()
    assertThat(eventTriggersContainer.getEventTrigger(Handle(), 0)).isNull()
  }

  @Test
  fun testClear() {
    eventTriggersContainer.recordEventTrigger(mockEventTriggerWithKey)
    eventTriggersContainer.recordEventTrigger(mockEventTriggerWithHandle)
    eventTriggersContainer.recordEventTrigger(mockEventTriggerWithKeyAndHandle)
    eventTriggersContainer.clear()
    assertThat(eventTriggersContainer.getEventTrigger(TEST_KEY_1)).isNull()
    assertThat(eventTriggersContainer.getEventTrigger(TEST_HANDLE_1, METHOD_ID_1)).isNull()
    assertThat(eventTriggersContainer.getEventTrigger(TEST_KEY_2)).isNull()
    assertThat(eventTriggersContainer.getEventTrigger(TEST_HANDLE_2, METHOD_ID_2)).isNull()
  }

  companion object {
    private const val TEST_KEY_1 = "test_key_1"
    private const val TEST_KEY_2 = "test_key_2"
    private const val METHOD_ID_1 = 1
    private const val METHOD_ID_2 = 2
  }
}


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

import com.facebook.litho.ExecutorLithoHandlerTest.QueuingExecutor
import com.facebook.litho.testing.testrunner.LithoTestRunner
import java.util.ArrayDeque
import java.util.concurrent.Executor
import org.assertj.core.api.Assertions.assertThat
import org.junit.Test
import org.junit.runner.RunWith

@RunWith(LithoTestRunner::class)
class ExecutorLithoHandlerTest {

  private val executor = QueuingExecutor()
  private val lithoHandler = ExecutorLithoHandler(executor)
  private var runCounter = 0
  private val runnable = Runnable { runCounter++ }

  @Test
  fun testIsTracing_returnsFalse() {
    assertThat(lithoHandler.isTracing).isFalse
  }

  @Test
  fun testPost_multipleInstancesOfSameRunnable_runsAll() {
    lithoHandler.post(runnable, TAG)
    lithoHandler.post(runnable, TAG)
    executor.runAllQueuedTasks()
    assertThat(runCounter).isEqualTo(2)
  }

  @Test
  fun testPost_multipleRunnables_runsAll() {
    val runnable2 = Runnable { runCounter += 2 }
    lithoHandler.post(runnable, TAG)
    lithoHandler.post(runnable2, TAG)
    executor.runAllQueuedTasks()
    assertThat(runCounter).isEqualTo(3)
  }

  @Test
  fun testRemove_removesAllInstances() {
    lithoHandler.post(runnable, TAG)
    lithoHandler.post(runnable, TAG)
    lithoHandler.post(runnable, TAG)
    lithoHandler.remove(runnable)
    executor.runAllQueuedTasks()
    assertThat(runCounter).isEqualTo(0)
  }

  @Test
  fun testRemoveThenPost_runsOnce() {
    lithoHandler.post(runnable, TAG)
    lithoHandler.post(runnable, TAG)
    lithoHandler.remove(runnable)
    lithoHandler.post(runnable, TAG)
    executor.runAllQueuedTasks()
    assertThat(runCounter).isEqualTo(1)
  }

  private class QueuingExecutor : Executor {
    private val queue = ArrayDeque<Runnable>()

    override fun execute(runnable: Runnable) {
      queue.add(runnable)
    }

    fun runAllQueuedTasks() {
      while (!queue.isEmpty()) {
        val runnable = queue.removeFirst()
        runnable.run()
      }
    }
  }

  companion object {
    private const val TAG = "testTag"
  }
}


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

import com.facebook.litho.testing.testrunner.LithoTestRunner
import org.assertj.core.api.Assertions.assertThat
import org.junit.Test
import org.junit.runner.RunWith

@RunWith(LithoTestRunner::class)
class FastMathTest {

  @Test
  fun testRoundPositiveUp() {
    assertThat(2).isEqualTo(FastMath.round(1.6f))
  }

  @Test
  fun testRoundPositiveDown() {
    assertThat(1).isEqualTo(FastMath.round(1.3f))
  }

  @Test
  fun testRoundZero() {
    assertThat(0).isEqualTo(FastMath.round(0f))
  }

  @Test
  fun testRoundNegativeUp() {
    assertThat(-1).isEqualTo(FastMath.round(-1.3f))
  }

  @Test
  fun testRoundNegativeDown() {
    assertThat(-2).isEqualTo(FastMath.round(-1.6f))
  }
}


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

import android.view.View
import com.facebook.litho.core.height
import com.facebook.litho.core.width
import com.facebook.litho.testing.LithoViewRule
import com.facebook.litho.testing.testrunner.LithoTestRunner
import com.facebook.litho.view.viewTag
import com.facebook.litho.view.wrapInView
import com.facebook.litho.visibility.onVisible
import java.lang.RuntimeException
import java.util.concurrent.atomic.AtomicReference
import org.assertj.core.api.Assertions.assertThat
import org.junit.Rule
import org.junit.Test
import org.junit.rules.ExpectedException
import org.junit.runner.RunWith

/** Tests for [ComponentContext.findViewWithTag]. */
@RunWith(LithoTestRunner::class)
class FindViewWithTagTest {

  @Rule @JvmField val lithoViewRule = LithoViewRule()
  @Rule @JvmField val expectedException = ExpectedException.none()

  @Test
  fun `findViewWithTag returns correct view when called from onVisible, if tag exists`() {
    class MyComponent(val viewRef: AtomicReference<View>) : KComponent() {
      override fun ComponentScope.render(): Component {
        return Column(
            style = Style.wrapInView().onVisible { viewRef.set(findViewWithTag("Find Me!")) }) {
              child(Column(style = Style.viewTag("not_this_one").width(100.dp).height(100.dp)))
              child(Column(style = Style.viewTag("Find Me!").width(100.dp).height(100.dp)))
            }
      }
    }

    val viewRef = AtomicReference<View>()
    lithoViewRule.render { MyComponent(viewRef = viewRef) }

    assertThat(viewRef.get()).isNotNull()
    assertThat(viewRef.get().tag).isEqualTo("Find Me!")
  }

  @Test
  fun `findViewWithTag returns null when tag doesn't exists`() {
    class MyComponent(val viewRef: AtomicReference<View>) : KComponent() {
      override fun ComponentScope.render(): Component {
        return Column(
            style =
                Style.wrapInView().onVisible { viewRef.set(findViewWithTag("I don't exist")) }) {
              child(Column(style = Style.viewTag("not_this_one").width(100.dp).height(100.dp)))
              child(Column(style = Style.viewTag("Find Me!").width(100.dp).height(100.dp)))
            }
      }
    }

    val viewRef = AtomicReference<View>()
    lithoViewRule.render { MyComponent(viewRef = viewRef) }

    assertThat(viewRef.get()).isNull()
  }

  @Test
  fun `findViewWithTag returns correct view for complex object tag, if tag exists`() {
    val handleTag1 = Handle()
    val handleTag2 = Handle()

    class MyComponent(val viewRef: AtomicReference<View>) : KComponent() {
      override fun ComponentScope.render(): Component {
        return Column(
            style = Style.wrapInView().onVisible { viewRef.set(findViewWithTag(handleTag2)) }) {
              child(Column(style = Style.viewTag(handleTag1).width(100.dp).height(100.dp)))
              child(Column(style = Style.viewTag(handleTag2).width(100.dp).height(100.dp)))
            }
      }
    }

    val viewRef = AtomicReference<View>()
    lithoViewRule.render { MyComponent(viewRef = viewRef) }

    assertThat(viewRef.get()).isNotNull()
    assertThat(viewRef.get().tag).isEqualTo(handleTag2)
  }

  @Test
  fun `findViewWithTag throws when called with incorrect ComponentContext`() {
    expectedException.expect(RuntimeException::class.java)
    expectedException.expectMessage("render")

    ComponentContext(lithoViewRule.context).findViewWithTag<View>("Some Tag")
  }

  @Test
  fun `findViewWithTag throws when called with incorrect ComponentScope`() {
    expectedException.expect(RuntimeException::class.java)
    expectedException.expectMessage("render")

    ComponentScope(ComponentContext(lithoViewRule.context)).findViewWithTag<View>("Some Tag")
  }
}


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

import android.content.Context
import androidx.test.core.app.ApplicationProvider
import com.facebook.litho.GetSimpleNameTest.TestWrapperComponent
import com.facebook.litho.testing.testrunner.LithoTestRunner
import com.facebook.litho.widget.SimpleMountSpecTester
import org.assertj.core.api.Assertions.assertThat
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith

@RunWith(LithoTestRunner::class)
class GetSimpleNameTest {

  private class TestWrapperComponent(private val delegate: Component) :
      SpecGeneratedComponent("TestWrapper") {
    override fun getSimpleNameDelegate(): Component = delegate
  }

  private lateinit var context: ComponentContext

  @Before
  fun setUp() {
    context = ComponentContext(ApplicationProvider.getApplicationContext<Context>())
  }

  @Test
  fun testGetSimpleName() {
    val testComponent = SimpleMountSpecTester.create(context).build()
    assertThat(testComponent.simpleName).isEqualTo("SimpleMountSpecTester")
  }

  @Test
  fun testGetSimpleNameWithOneWrapper() {
    val inner = SimpleMountSpecTester.create(context).build()
    val wrapper = TestWrapperComponent(inner)
    assertThat(wrapper.simpleName).isEqualTo("TestWrapper(SimpleMountSpecTester)")
  }

  @Test
  fun testGetSimpleNameWithMultipleWrapper() {
    val inner = SimpleMountSpecTester.create(context).build()
    val wrapper = TestWrapperComponent(inner)
    val wrapper2 = TestWrapperComponent(wrapper)
    val wrapper3 = TestWrapperComponent(wrapper2)
    assertThat(wrapper3.simpleName).isEqualTo("TestWrapper(SimpleMountSpecTester)")
  }
}


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

import android.content.Context
import android.util.Pair
import androidx.test.core.app.ApplicationProvider.getApplicationContext
import com.facebook.litho.LithoKeyTestingUtil.getScopedComponentInfos
import com.facebook.litho.annotations.OnCreateLayout
import com.facebook.litho.config.TempComponentsConfigurations
import com.facebook.litho.testing.TestViewComponent
import com.facebook.litho.testing.inlinelayoutspec.InlineLayoutSpec
import com.facebook.litho.testing.logging.TestComponentsReporter
import com.facebook.litho.testing.testrunner.LithoTestRunner
import com.facebook.litho.testing.unspecified
import com.facebook.litho.widget.CardClip
import com.facebook.litho.widget.EditText
import com.facebook.litho.widget.SimpleMountSpecTester
import com.facebook.litho.widget.Text
import com.facebook.litho.widget.TextInput
import com.facebook.rendercore.LogLevel
import org.assertj.core.api.Assertions.assertThat
import org.junit.After
import org.junit.Assert
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith

@RunWith(LithoTestRunner::class)
class GlobalKeyTest {
  private var mContext: ComponentContext? = null
  private var mComponentsReporter: TestComponentsReporter? = null

  @Before
  fun setup() {
    TempComponentsConfigurations.setShouldAddHostViewForRootComponent(true)
    mComponentsReporter = TestComponentsReporter()
    mContext = ComponentContext(getApplicationContext<Context>())
    ComponentsReporter.provide(mComponentsReporter!!)
  }

  @Test
  fun testComponentKey() {
    val component: Component = SimpleMountSpecTester.create(mContext).build()
    Assert.assertEquals(component.key, component.typeId.toString() + "")
  }

  @Test
  fun testComponentManualKey() {
    val component: Component = SimpleMountSpecTester.create(mContext).key("someKey").build()
    Assert.assertEquals(component.key, "someKey")
  }

  @Test
  fun testRootComponentGlobalKey() {
    val component: Component =
        SimpleMountSpecTester.create(mContext).widthDip(10f).heightDip(10f).build()
    val lithoView = getLithoView(component)
    val scopedComponentInfos = getScopedComponentInfos(lithoView)
    val rootScopedComponentInfo = scopedComponentInfos["SimpleMountSpecTester"]!![0]
    val scopedContext = rootScopedComponentInfo.context
    assertThat(scopedContext).isNotNull
    assertThat(scopedContext.globalKey).isEqualTo(component.key)
  }

  @Test
  fun testRootComponentGlobalKeyManualKey() {
    val component: Component =
        SimpleMountSpecTester.create(mContext).widthDip(10f).heightDip(10f).key("someKey").build()
    val lithoView = getLithoView(component)
    val scopedComponentInfos = getScopedComponentInfos(lithoView)
    val rootScopedComponentInfo = scopedComponentInfos["SimpleMountSpecTester"]!![0]
    val scopedContext = rootScopedComponentInfo.context
    assertThat(scopedContext).isNotNull
    assertThat(scopedContext.globalKey).isEqualTo("\$someKey")
  }

  @Test
  fun testMultipleChildrenComponentKey() {
    val component = multipleChildrenComponent
    val layoutSpecId = component.typeId
    val nestedLayoutSpecId = layoutSpecId - 1
    val column: Component = Column.create(mContext).build()
    val columnSpecId = column.typeId
    val lithoView = getLithoView(component)
    val scopedComponentInfos = getScopedComponentInfos(lithoView)

    // Text
    val expectedGlobalKeyText =
        ComponentKeyUtils.getKeyWithSeparatorForTest(layoutSpecId, columnSpecId, "$[Text2]")
    val scopedComponentInfoText = scopedComponentInfos["Text"]!![0]
    Assert.assertEquals(scopedComponentInfoText.context.globalKey, expectedGlobalKeyText)
    // TestViewComponent in child layout
    val expectedGlobalKeyTestViewComponent =
        ComponentKeyUtils.getKeyWithSeparatorForTest(
            layoutSpecId, columnSpecId, nestedLayoutSpecId, columnSpecId, "$[TestViewComponent1]")
    val scopedComponentInfoTestViewComponent = scopedComponentInfos["TestViewComponent"]!![0]
    Assert.assertEquals(
        scopedComponentInfoTestViewComponent.context.globalKey, expectedGlobalKeyTestViewComponent)
    // CardClip in child
    val expectedGlobalKeyCardClip =
        ComponentKeyUtils.getKeyWithSeparatorForTest(
            layoutSpecId,
            columnSpecId,
            nestedLayoutSpecId,
            columnSpecId,
            columnSpecId,
            "$[CardClip1]")
    val scopedComponentInfoCardClip = scopedComponentInfos["CardClip"]!![0]
    Assert.assertEquals(scopedComponentInfoCardClip.context.globalKey, expectedGlobalKeyCardClip)

    // Text in child
    val expectedGlobalKeyTextChild =
        ComponentKeyUtils.getKeyWithSeparatorForTest(
            layoutSpecId, columnSpecId, nestedLayoutSpecId, columnSpecId, "$[Text1]")
    val scopedComponentInfoTextChild = scopedComponentInfos["Text"]!![1]
    Assert.assertEquals(scopedComponentInfoTextChild.context.globalKey, expectedGlobalKeyTextChild)

    // CardClip
    val expectedGlobalKeyCardClip2 =
        ComponentKeyUtils.getKeyWithSeparatorForTest(
            layoutSpecId, columnSpecId, columnSpecId, "$[CardClip2]")
    val scopedComponentInfoCardClip2 = scopedComponentInfos["CardClip"]!![1]
    Assert.assertEquals(scopedComponentInfoCardClip2.context.globalKey, expectedGlobalKeyCardClip2)

    // TestViewComponent
    val expectedGlobalKeyTestViewComponent2 =
        ComponentKeyUtils.getKeyWithSeparatorForTest(
            layoutSpecId, columnSpecId, "$[TestViewComponent2]")
    val scopedComponentInfoTestViewComponent2 = scopedComponentInfos["TestViewComponent"]!![1]
    Assert.assertEquals(
        scopedComponentInfoTestViewComponent2.context.globalKey,
        expectedGlobalKeyTestViewComponent2)
  }

  @Test
  fun testSiblingsUniqueKeyRequirement() {
    val component: Component =
        object : InlineLayoutSpec() {
          @OnCreateLayout
          override fun onCreateLayout(c: ComponentContext): Component? {
            return Column.create(c)
                .child(Text.create(c).text("").key("sameKey"))
                .child(Text.create(c).text("").key("sameKey"))
                .build()
          }
        }
    getLithoView(component)
    val expectedError =
        ("The manual key " +
            "sameKey you are setting on " +
            "this Text is a duplicate and will be changed into a unique one. This will " +
            "result in unexpected behavior if you don't change it.")
    assertThat(mComponentsReporter!!.loggedMessages)
        .contains(Pair.create(LogLevel.WARNING, expectedError))
  }

  @Test
  fun testSiblingsManualKeyDeduplication() {
    val component: Component =
        object : InlineLayoutSpec() {
          @OnCreateLayout
          override fun onCreateLayout(c: ComponentContext): Component? {
            return Column.create(c)
                .child(EditText.create(c).text("").key("sameKey").widthDip(10f).heightDip(10f))
                .child(EditText.create(c).text("").widthDip(10f).heightDip(10f))
                .child(EditText.create(c).text("").key("sameKey").widthDip(10f).heightDip(10f))
                .child(EditText.create(c).text("").key("sameKey").widthDip(10f).heightDip(10f))
                .build()
          }
        }
    val lithoView = getLithoView(component)
    val scopedComponentInfos = getScopedComponentInfos(lithoView)
    val column: Component = Column.create(mContext).build()
    val columnSpecId = column.typeId
    val layoutSpecId = component.typeId
    val expectedKey0 =
        ComponentKeyUtils.getKeyWithSeparatorForTest(layoutSpecId, columnSpecId, "\$sameKey")
    val firstEditTextSCI = scopedComponentInfos["EditText"]!![0]
    assertThat(firstEditTextSCI.context.globalKey).isEqualTo(expectedKey0)
    val expectedKey2 =
        ComponentKeyUtils.getKeyWithSeparatorForTest(layoutSpecId, columnSpecId, "\$sameKey!1")
    val secondEditTextSCI = scopedComponentInfos["EditText"]!![2]
    assertThat(secondEditTextSCI.context.globalKey).isEqualTo(expectedKey2)
    val expectedKey3 =
        ComponentKeyUtils.getKeyWithSeparatorForTest(layoutSpecId, columnSpecId, "\$sameKey!2")
    val thirdEditTextSCI = scopedComponentInfos["EditText"]!![3]
    assertThat(thirdEditTextSCI.context.globalKey).isEqualTo(expectedKey3)
  }

  @Test
  fun testSiblingsOfDifferentTypesManualKeyDeduplication() {
    val component: Component =
        object : InlineLayoutSpec() {
          @OnCreateLayout
          override fun onCreateLayout(c: ComponentContext): Component? {
            return Column.create(c)
                .child(Text.create(c).text("").key("sameKey").widthDip(10f).heightDip(10f))
                .child(Text.create(c).text("").widthDip(10f).heightDip(10f))
                .child(TestViewComponent.create(c).widthDip(10f).heightDip(10f).key("sameKey"))
                .child(
                    TextInput.create(c).initialText("").key("sameKey").widthDip(10f).heightDip(10f))
                .build()
          }
        }
    val lithoView = getLithoView(component)
    val column: Component = Column.create(mContext).build()
    val columnSpecId = column.typeId
    val layoutSpecId = component.typeId
    val scopedComponentInfos = getScopedComponentInfos(lithoView)
    val expectedKey0 =
        ComponentKeyUtils.getKeyWithSeparatorForTest(layoutSpecId, columnSpecId, "\$sameKey")
    val firstTextSCI = scopedComponentInfos["Text"]!![0]
    assertThat(firstTextSCI.context.globalKey).isEqualTo(expectedKey0)
    val expectedKey2 =
        ComponentKeyUtils.getKeyWithSeparatorForTest(layoutSpecId, columnSpecId, "\$sameKey!1")
    val firstTextViewComponentSCI = scopedComponentInfos["TestViewComponent"]!![0]
    assertThat(firstTextViewComponentSCI.context.globalKey).isEqualTo(expectedKey2)
    val expectedKey3 =
        ComponentKeyUtils.getKeyWithSeparatorForTest(layoutSpecId, columnSpecId, "\$sameKey!2")
    val firstTextInputSCI = scopedComponentInfos["TextInput"]!![0]
    assertThat(firstTextInputSCI.context.globalKey).isEqualTo(expectedKey3)
  }

  @Test
  fun testColumnSiblingsUniqueKeyRequirement() {
    val component: Component =
        object : InlineLayoutSpec() {
          @OnCreateLayout
          override fun onCreateLayout(c: ComponentContext): Component? {
            return Column.create(c)
                .child(Column.create(c).key("sameKey"))
                .child(Column.create(c).key("sameKey"))
                .build()
          }
        }
    getLithoView(component)
    val expectedError =
        ("The manual key " +
            "sameKey you are setting on " +
            "this Column is a duplicate and will be changed into a unique one. This will " +
            "result in unexpected behavior if you don't change it.")
    assertThat(mComponentsReporter!!.loggedMessages)
        .contains(Pair.create(LogLevel.WARNING, expectedError))
  }

  @Test
  fun testAutogenSiblingsUniqueKeys() {
    val component: Component =
        object : InlineLayoutSpec() {
          @OnCreateLayout
          override fun onCreateLayout(c: ComponentContext): Component? {
            return Column.create(c)
                .child(Text.create(mContext).widthDip(10f).heightDip(10f).text(""))
                .child(Text.create(mContext).widthDip(10f).heightDip(10f).text(""))
                .build()
          }
        }
    val layoutSpecId = component.typeId
    val text: Component = Text.create(mContext).text("").build()
    val textSpecId = text.typeId
    val column: Component = Column.create(mContext).build()
    val columnTypeId = column.typeId
    val lithoView = getLithoView(component)
    val scopedComponentInfos = getScopedComponentInfos(lithoView)
    val expectedKey0 =
        ComponentKeyUtils.getKeyWithSeparatorForTest(layoutSpecId, columnTypeId, textSpecId)
    val firstTextSCI = scopedComponentInfos["Text"]!![0]
    assertThat(firstTextSCI.context.globalKey).isEqualTo(expectedKey0)
    val expectedKey1 =
        ComponentKeyUtils.getKeyWithSeparatorForTest(layoutSpecId, columnTypeId, "$textSpecId!1")
    val secondTextSCI = scopedComponentInfos["Text"]!![1]
    assertThat(secondTextSCI.context.globalKey).isEqualTo(expectedKey1)
  }

  @Test
  fun testAutogenSiblingsUniqueKeysSkipsManualKeys() {
    val component: Component =
        object : InlineLayoutSpec() {
          @OnCreateLayout
          override fun onCreateLayout(c: ComponentContext): Component? {
            return Column.create(c)
                .child(Text.create(c).text("").widthDip(10f).heightDip(10f))
                .child(Text.create(c).text("").widthDip(10f).heightDip(10f))
                .child(Text.create(c).text("").widthDip(10f).heightDip(10f).key("manual_key"))
                .child(Text.create(c).text("").widthDip(10f).heightDip(10f))
                .build()
          }
        }
    val lithoView = getLithoView(component)
    val scopedComponentInfos = getScopedComponentInfos(lithoView)
    val firstSCI = scopedComponentInfos["Text"]!![0]
    val secondSCI = scopedComponentInfos["Text"]!![1]
    val fourthSCI = scopedComponentInfos["Text"]!![3]
    val firstKey = firstSCI.context.globalKey
    val secondKey = secondSCI.context.globalKey
    val fourthKey = fourthSCI.context.globalKey
    assertThat(firstKey).isNotBlank
    assertThat(secondKey).isEqualTo("$firstKey!1")
    // The third key is a manual key, so will have no impact on the unique suffix
    assertThat(fourthKey).isEqualTo("$firstKey!2")
  }

  @Test
  fun testAutogenColumnSiblingsUniqueKeys() {
    val component: Component =
        object : InlineLayoutSpec() {
          @OnCreateLayout
          override fun onCreateLayout(c: ComponentContext): Component? {
            return Column.create(c)
                .child(
                    Column.create(mContext)
                        .child(Text.create(c).widthDip(10f).heightDip(10f).text("")))
                .child(
                    Column.create(mContext)
                        .child(Text.create(c).widthDip(10f).heightDip(10f).text("")))
                .build()
          }
        }
    val layoutSpecId = component.typeId
    val text: Component = Text.create(mContext).text("").build()
    val textSpecId = text.typeId
    val column: Component = Column.create(mContext).build()
    val columnTypeId = column.typeId
    val lithoView = getLithoView(component)
    val scopedComponentInfos = getScopedComponentInfos(lithoView)
    val expectedKey0 =
        ComponentKeyUtils.getKeyWithSeparatorForTest(
            layoutSpecId, columnTypeId, columnTypeId, textSpecId)
    val firstTextSCI = scopedComponentInfos["Text"]!![0]
    assertThat(firstTextSCI.context.globalKey).isEqualTo(expectedKey0)
    val expectedKey1 =
        ComponentKeyUtils.getKeyWithSeparatorForTest(
            layoutSpecId, columnTypeId, "$columnTypeId!1", textSpecId)
    val secondTextSCI = scopedComponentInfos["Text"]!![1]
    assertThat(secondTextSCI.context.globalKey).isEqualTo(expectedKey1)
  }

  @Test
  fun testAutogenSiblingsUniqueKeysNested() {
    val component: Component =
        object : InlineLayoutSpec() {
          @OnCreateLayout
          override fun onCreateLayout(c: ComponentContext): Component {
            return Column.create(c)
                .child(Text.create(mContext).widthDip(10f).heightDip(10f).text(""))
                .child(Text.create(mContext).widthDip(10f).heightDip(10f).text(""))
                .build()
          }
        }
    val root: Component =
        object : InlineLayoutSpec() {
          @OnCreateLayout
          override fun onCreateLayout(c: ComponentContext): Component {
            return Column.create(c)
                .child(component)
                .child(Text.create(mContext).widthDip(10f).heightDip(10f).text("test"))
                .child(Text.create(mContext).widthDip(10f).heightDip(10f).text("test"))
                .build()
          }
        }
    val layoutSpecId = root.typeId
    val nestedLayoutSpecId = component.typeId
    val text: Component = Text.create(mContext).text("").build()
    val textSpecId = text.typeId
    val column: Component = Column.create(mContext).build()
    val columnTypeId = column.typeId
    val lithoView = getLithoView(root)
    val scopedComponentInfos = getScopedComponentInfos(lithoView)
    val expectedKey0 =
        ComponentKeyUtils.getKeyWithSeparatorForTest(
            layoutSpecId, columnTypeId, nestedLayoutSpecId, columnTypeId, textSpecId)
    val firstTextSCI = scopedComponentInfos["Text"]!![0]
    assertThat(firstTextSCI.context.globalKey).isEqualTo(expectedKey0)
    val expectedKey1 =
        ComponentKeyUtils.getKeyWithSeparatorForTest(
            layoutSpecId, columnTypeId, nestedLayoutSpecId, columnTypeId, "$textSpecId!1")
    val secondTextSCI = scopedComponentInfos["Text"]!![1]
    assertThat(secondTextSCI.context.globalKey).isEqualTo(expectedKey1)
    val expectedKey2 =
        ComponentKeyUtils.getKeyWithSeparatorForTest(layoutSpecId, columnTypeId, textSpecId)
    val thirdTextSCI = scopedComponentInfos["Text"]!![2]
    assertThat(thirdTextSCI.context.globalKey).isEqualTo(expectedKey2)
    val expectedKey3 =
        ComponentKeyUtils.getKeyWithSeparatorForTest(layoutSpecId, columnTypeId, "$textSpecId!1")
    val fourthTextSCI = scopedComponentInfos["Text"]!![3]
    assertThat(fourthTextSCI.context.globalKey).isEqualTo(expectedKey3)
  }

  private fun getLithoView(component: Component): LithoView {
    val lithoView = LithoView(mContext)
    lithoView.setComponent(component)
    lithoView.measure(unspecified(640), unspecified(480))
    lithoView.layout(0, 0, lithoView.measuredWidth, lithoView.measuredHeight)
    return lithoView
  }

  @After
  fun restoreConfiguration() {
    TempComponentsConfigurations.restoreShouldAddHostViewForRootComponent()
  }

  companion object {
    private val multipleChildrenComponent: Component
      private get() {
        val color = -0x10000
        val testGlobalKeyChildComponent: Component =
            object : InlineLayoutSpec() {
              @OnCreateLayout
              override fun onCreateLayout(c: ComponentContext): Component {
                return Column.create(c)
                    .child(
                        TestViewComponent.create(c)
                            .widthDip(10f)
                            .heightDip(10f)
                            .key("[TestViewComponent1]"))
                    .child(
                        Column.create(c)
                            .backgroundColor(color)
                            .child(
                                CardClip.create(c).widthDip(10f).heightDip(10f).key("[CardClip1]")))
                    .child(Text.create(c).text("Test").widthDip(10f).heightDip(10f).key("[Text1]"))
                    .build()
              }
            }
        val testGlobalKeyChild: Component =
            object : InlineLayoutSpec() {
              @OnCreateLayout
              override fun onCreateLayout(c: ComponentContext): Component {
                return Column.create(c)
                    .child(Text.create(c).text("test").widthDip(10f).heightDip(10f).key("[Text2]"))
                    .child(testGlobalKeyChildComponent)
                    .child(
                        Column.create(c)
                            .backgroundColor(color)
                            .child(
                                CardClip.create(c).widthDip(10f).heightDip(10f).key("[CardClip2]")))
                    .child(
                        TestViewComponent.create(c)
                            .widthDip(10f)
                            .heightDip(10f)
                            .key("[TestViewComponent2]"))
                    .build()
              }
            }
        return testGlobalKeyChild
      }
  }
}


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

import android.content.Context
import android.view.View
import androidx.test.core.app.ApplicationProvider.getApplicationContext
import com.facebook.litho.LithoKeyTestingUtil.getScopedComponentInfos
import com.facebook.litho.annotations.OnCreateLayout
import com.facebook.litho.config.TempComponentsConfigurations
import com.facebook.litho.testing.TestViewComponent
import com.facebook.litho.testing.inlinelayoutspec.InlineLayoutSpec
import com.facebook.litho.testing.testrunner.LithoTestRunner
import com.facebook.litho.widget.CardClip
import com.facebook.litho.widget.Text
import org.assertj.core.api.Assertions.assertThat
import org.junit.After
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith

@RunWith(LithoTestRunner::class)
class GlobalKeyWithoutRootHostTest {
  private var mContext: ComponentContext? = null

  @Before
  fun setup() {
    TempComponentsConfigurations.setShouldAddHostViewForRootComponent(false)
    mContext = ComponentContext(getApplicationContext<Context>())
  }

  @Test
  fun testMultipleChildrenComponentKey() {
    val component = multipleChildrenComponent
    val layoutSpecId = component.typeId
    val nestedLayoutSpecId = layoutSpecId - 1
    val column: Component = Column.create(mContext).build()
    val columnSpecId = column.typeId
    val lithoView = getLithoView(component)
    val globalKeysInfo = getScopedComponentInfos(lithoView)

    // Text
    val expectedGlobalKeyText =
        ComponentKeyUtils.getKeyWithSeparatorForTest(layoutSpecId, columnSpecId, "$[Text2]")
    val scopedComponentInfoText = globalKeysInfo["Text"]!![0]
    assertThat(scopedComponentInfoText.context.globalKey).isEqualTo(expectedGlobalKeyText)
    // TestViewComponent in child layout
    val expectedGlobalKeyTestViewComponent =
        ComponentKeyUtils.getKeyWithSeparatorForTest(
            layoutSpecId, columnSpecId, nestedLayoutSpecId, columnSpecId, "$[TestViewComponent1]")
    val scopedComponentInfoTestViewComponent = globalKeysInfo["TestViewComponent"]!![0]
    assertThat(scopedComponentInfoTestViewComponent.context.globalKey)
        .isEqualTo(expectedGlobalKeyTestViewComponent)
    // CardClip in child
    val expectedGlobalKeyCardClip =
        ComponentKeyUtils.getKeyWithSeparatorForTest(
            layoutSpecId,
            columnSpecId,
            nestedLayoutSpecId,
            columnSpecId,
            columnSpecId,
            "$[CardClip1]")
    val scopedComponentInfoCardClip = globalKeysInfo["CardClip"]!![0]
    assertThat(scopedComponentInfoCardClip.context.globalKey).isEqualTo(expectedGlobalKeyCardClip)

    // Text in child
    val expectedGlobalKeyTextChild =
        ComponentKeyUtils.getKeyWithSeparatorForTest(
            layoutSpecId, columnSpecId, nestedLayoutSpecId, columnSpecId, "$[Text1]")
    val scopedComponentInfoTextChild = globalKeysInfo["Text"]!![1]
    assertThat(scopedComponentInfoTextChild.context.globalKey).isEqualTo(expectedGlobalKeyTextChild)

    // CardClip
    val expectedGlobalKeyCardClip2 =
        ComponentKeyUtils.getKeyWithSeparatorForTest(
            layoutSpecId, columnSpecId, columnSpecId, "$[CardClip2]")
    val scopedComponentInfoCardClip2 = globalKeysInfo["CardClip"]!![1]
    assertThat(scopedComponentInfoCardClip2.context.globalKey).isEqualTo(expectedGlobalKeyCardClip2)

    // TestViewComponent
    val expectedGlobalKeyTestViewComponent2 =
        ComponentKeyUtils.getKeyWithSeparatorForTest(
            layoutSpecId, columnSpecId, "$[TestViewComponent2]")
    val scopedComponentInfoTestViewComponent2 = globalKeysInfo["TestViewComponent"]!![1]
    assertThat(scopedComponentInfoTestViewComponent2.context.globalKey)
        .isEqualTo(expectedGlobalKeyTestViewComponent2)
  }

  private fun getLithoView(component: Component): LithoView {
    val lithoView = LithoView(mContext)
    lithoView.setComponent(component)
    lithoView.measure(
        View.MeasureSpec.makeMeasureSpec(640, View.MeasureSpec.UNSPECIFIED),
        View.MeasureSpec.makeMeasureSpec(480, View.MeasureSpec.UNSPECIFIED))
    lithoView.layout(0, 0, lithoView.measuredWidth, lithoView.measuredHeight)
    return lithoView
  }

  @After
  fun restoreConfiguration() {
    TempComponentsConfigurations.restoreShouldAddHostViewForRootComponent()
  }

  companion object {
    private val multipleChildrenComponent: Component
      private get() {
        val color = -0x10000
        val testGlobalKeyChildComponent: Component =
            object : InlineLayoutSpec() {
              @OnCreateLayout
              override fun onCreateLayout(c: ComponentContext): Component {
                return Column.create(c)
                    .child(
                        TestViewComponent.create(c)
                            .widthDip(10f)
                            .heightDip(10f)
                            .key("[TestViewComponent1]"))
                    .child(
                        Column.create(c)
                            .backgroundColor(color)
                            .child(
                                CardClip.create(c).widthDip(10f).heightDip(10f).key("[CardClip1]")))
                    .child(Text.create(c).text("Test").widthDip(10f).heightDip(10f).key("[Text1]"))
                    .build()
              }
            }
        val testGlobalKeyChild: Component =
            object : InlineLayoutSpec() {
              @OnCreateLayout
              override fun onCreateLayout(c: ComponentContext): Component {
                return Column.create(c)
                    .child(Text.create(c).text("test").widthDip(10f).heightDip(10f).key("[Text2]"))
                    .child(testGlobalKeyChildComponent)
                    .child(
                        Column.create(c)
                            .backgroundColor(color)
                            .child(
                                CardClip.create(c).widthDip(10f).heightDip(10f).key("[CardClip2]")))
                    .child(
                        TestViewComponent.create(c)
                            .widthDip(10f)
                            .heightDip(10f)
                            .key("[TestViewComponent2]"))
                    .build()
              }
            }
        return testGlobalKeyChild
      }
  }
}


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

import com.facebook.litho.testing.testrunner.LithoTestRunner
import org.assertj.core.api.Assertions.assertThat
import org.assertj.core.util.Lists
import org.junit.Test
import org.junit.runner.RunWith

/** Tests for the hooks state (useState) integration with StateHandler. */
@RunWith(LithoTestRunner::class)
class HooksStateHandlerTest {

  @Test
  fun copyHandler_copyingEmptyStateHandler_createsEmptyStateHandler() {
    val first = StateHandler()
    val second = StateHandler(first)
    assertThat(second.hasUncommittedUpdates()).isFalse
    assertThat(second.isEmpty).isTrue
  }

  @Test
  fun commit_copyingHandlerWithPendingStateUpdateAndCommittingIt_copiesAllOldStateValuesAndAppliesStateUpdate() {
    val bazState = Any()
    var kStateContainer = KStateContainer.withNewState(null, "test")
    kStateContainer = KStateContainer.withNewState(kStateContainer, 4)
    kStateContainer = KStateContainer.withNewState(kStateContainer, bazState)
    val first = StateHandler()
    first.addStateContainer(GLOBAL_KEY, kStateContainer)
    first.queueHookStateUpdate(
        GLOBAL_KEY,
        object : HookUpdater {
          override fun getUpdatedStateContainer(currentState: KStateContainer): KStateContainer =
              KStateContainer.withNewState(currentState, "newValue")
        })
    assertThat(first.hasUncommittedUpdates()).isTrue
    val second = StateHandler(first)
    assertThat(first.hasUncommittedUpdates()).isTrue
    assertThat(second.hasUncommittedUpdates()).isTrue
    val secondKstate = second.getStateContainer(GLOBAL_KEY) as KStateContainer?
    second.keepStateContainerForGlobalKey(GLOBAL_KEY)
    assertThat(secondKstate?.states)
        .hasSize(4)
        .isEqualTo(Lists.newArrayList("test", 4, bazState, "newValue"))
    first.commit(second)
    assertThat(first.hasUncommittedUpdates()).isFalse
    val firstState = first.getStateContainer(GLOBAL_KEY) as KStateContainer?
    assertThat(firstState?.states)
        .hasSize(4)
        .isEqualTo(Lists.newArrayList("test", 4, bazState, "newValue"))
  }

  @Test
  fun commit_copyingHandlerWithMultiplePendingStateUpdatesAndCommittingIt_copiesAllOldStateValuesAndAppliesAllStateUpdates() {
    val bazState = Any()
    val first = StateHandler()
    var kStateContainer = KStateContainer.withNewState(null, "test")
    kStateContainer = KStateContainer.withNewState(kStateContainer, 4)
    kStateContainer = KStateContainer.withNewState(kStateContainer, bazState)
    first.addStateContainer(GLOBAL_KEY, kStateContainer)
    first.queueHookStateUpdate(
        GLOBAL_KEY,
        object : HookUpdater {
          override fun getUpdatedStateContainer(currentState: KStateContainer): KStateContainer =
              KStateContainer.withNewState(currentState, "newValue").copyAndMutate(1, 5)
        })
    first.queueHookStateUpdate(
        GLOBAL_KEY,
        object : HookUpdater {
          override fun getUpdatedStateContainer(currentState: KStateContainer): KStateContainer =
              currentState.copyAndMutate(1, 1 + currentState.states[1] as Int)
        })
    assertThat(first.hasUncommittedUpdates()).isTrue
    val second = StateHandler(first)
    assertThat(first.hasUncommittedUpdates()).isTrue
    assertThat(second.hasUncommittedUpdates()).isTrue
    assertThat(second.stateContainers).hasSize(1)
    val secondKstate = second.getStateContainer(GLOBAL_KEY) as KStateContainer?
    second.keepStateContainerForGlobalKey(GLOBAL_KEY)
    assertThat(secondKstate?.states)
        .hasSize(4)
        .isEqualTo(Lists.newArrayList("test", 6, bazState, "newValue"))
    first.commit(second)
    assertThat(first.hasUncommittedUpdates()).isFalse
    val kState = first.getStateContainer(GLOBAL_KEY) as KStateContainer?
    assertThat(kState?.states)
        .hasSize(4)
        .isEqualTo(Lists.newArrayList("test", 6, bazState, "newValue"))
  }

  @Test
  fun commit_committingHandlerFirstWithPartOfStateUpdatesAndSecondTimeWithAllStateUpdates_atFirstAppliesOnlyStateUpdatesFromFirstCommitAndKeepOthersPendingAndThenAppliesAllStateUpdates() {
    val bazState = Any()
    val first = StateHandler()
    var kStateContainer = KStateContainer.withNewState(null, "test")
    kStateContainer = KStateContainer.withNewState(kStateContainer, 4)
    kStateContainer = KStateContainer.withNewState(kStateContainer, bazState)
    first.addStateContainer(GLOBAL_KEY, kStateContainer)
    first.queueHookStateUpdate(
        GLOBAL_KEY,
        object : HookUpdater {
          override fun getUpdatedStateContainer(currentState: KStateContainer): KStateContainer =
              KStateContainer.withNewState(currentState, "newValue").copyAndMutate(1, 5)
        })
    first.queueHookStateUpdate(
        GLOBAL_KEY,
        object : HookUpdater {
          override fun getUpdatedStateContainer(currentState: KStateContainer): KStateContainer =
              currentState.copyAndMutate(1, currentState.states[1] as Int + 1)
        })
    val second = StateHandler(first)
    first.queueHookStateUpdate(
        GLOBAL_KEY,
        object : HookUpdater {
          override fun getUpdatedStateContainer(currentState: KStateContainer): KStateContainer =
              currentState.copyAndMutate(1, currentState.states[1] as Int + 1)
        })
    val third = StateHandler(first)
    third.keepStateContainerForGlobalKey(GLOBAL_KEY)
    val secondKstate = second.getStateContainer(GLOBAL_KEY) as KStateContainer?
    second.keepStateContainerForGlobalKey(GLOBAL_KEY)
    assertThat(secondKstate?.states)
        .hasSize(4)
        .isEqualTo(Lists.newArrayList("test", 6, bazState, "newValue"))
    first.commit(second)
    assertThat(first.hasUncommittedUpdates()).isTrue
    val kState = first.getStateContainer(GLOBAL_KEY) as KStateContainer?
    assertThat(kState?.states)
        .hasSize(4)
        .isEqualTo(Lists.newArrayList("test", 6, bazState, "newValue"))
    first.commit(third)
    assertThat(first.hasUncommittedUpdates()).isFalse
    val firstStateUpdated = first.getStateContainer(GLOBAL_KEY) as KStateContainer?
    assertThat(firstStateUpdated?.states)
        .hasSize(4)
        .isEqualTo(Lists.newArrayList("test", 7, bazState, "newValue"))
  }

  companion object {
    private const val GLOBAL_KEY = "globalKey"
  }
}


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

import android.graphics.Color
import android.graphics.Rect
import android.graphics.drawable.ColorDrawable
import com.facebook.litho.testing.testrunner.LithoTestRunner
import com.facebook.rendercore.MountDelegate
import com.facebook.rendercore.MountDelegateInput
import com.facebook.rendercore.MountDelegateTarget
import com.facebook.rendercore.RenderCoreSystrace
import com.facebook.rendercore.RenderTreeNode
import com.facebook.rendercore.RenderUnit
import com.facebook.rendercore.incrementalmount.IncrementalMountExtension
import com.facebook.rendercore.incrementalmount.IncrementalMountExtensionInput
import com.facebook.rendercore.incrementalmount.IncrementalMountOutput
import com.facebook.rendercore.testing.DrawableWrapperUnit
import java.util.ArrayList
import java.util.LinkedHashMap
import org.assertj.core.api.Assertions.assertThat
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.kotlin.anyOrNull
import org.mockito.kotlin.mock
import org.mockito.kotlin.whenever

@RunWith(LithoTestRunner::class)
class IncrementalMountExtensionTest {

  @Test
  fun onIteratingOnIncrementMountOutputs_shouldIterateByInsertionOrder() {
    val input = TestInput(10)
    input.incrementalMountOutputs.forEachIndexed { i, output ->
      assertThat(output.id).isEqualTo(input.getMountableOutputAt(i).renderUnit.id)
    }
  }

  @Test
  fun testDirtyMountWithEmptyRect() {
    val lithoView = mock<LithoView>()
    whenever(lithoView.height).thenReturn(50)
    val mountDelegate = mock<MountDelegate>()
    val mountDelegateTarget = mock<MountDelegateTarget>()
    whenever(mountDelegate.tracer).thenReturn(RenderCoreSystrace.getInstance())
    whenever(mountDelegate.mountDelegateTarget).thenReturn(mountDelegateTarget)
    val extension = IncrementalMountExtension.getInstance()
    val extensionState = extension.createExtensionState(mountDelegate)
    val state = extensionState.state
    mountDelegate.registerMountExtension(extension)
    val incrementalMountExtensionInput = TestInput(10)
    extension.beforeMount(extensionState, incrementalMountExtensionInput, Rect(0, 0, 10, 50))
    for (i in 0 until incrementalMountExtensionInput.mountableOutputCount) {
      val node = incrementalMountExtensionInput.getMountableOutputAt(i)
      extension.beforeMountItem(extensionState, node, i)
    }
    extension.afterMount(extensionState)
    assertThat(IncrementalMountExtension.getPreviousBottomsIndex(state)).isEqualTo(0)
    assertThat(IncrementalMountExtension.getPreviousTopsIndex(state)).isEqualTo(5)
    val incrementalMountExtensionInput2 = TestInput(3)
    extension.beforeMount(extensionState, incrementalMountExtensionInput2, Rect(0, 0, 0, 0))
    for (i in 0 until incrementalMountExtensionInput2.mountableOutputCount) {
      val node = incrementalMountExtensionInput2.getMountableOutputAt(i)
      extension.beforeMountItem(extensionState, node, i)
    }
    extension.afterMount(extensionState)
    extension.onVisibleBoundsChanged(extensionState, Rect(0, 0, 10, 50))
    assertThat(IncrementalMountExtension.getPreviousBottomsIndex(state)).isEqualTo(0)
    assertThat(IncrementalMountExtension.getPreviousTopsIndex(state)).isEqualTo(3)
  }

  @Test
  fun testDirtyMountWithEmptyRect_leftRightMatch() {
    val lithoView = mock<LithoView>()
    whenever(lithoView.height).thenReturn(50)
    val mountDelegate = mock<MountDelegate>()
    val mountDelegateTarget = mock<MountDelegateTarget>()
    whenever(mountDelegate.tracer).thenReturn(RenderCoreSystrace.getInstance())
    whenever(mountDelegate.mountDelegateTarget).thenReturn(mountDelegateTarget)
    val extension = IncrementalMountExtension.getInstance()
    mountDelegate.registerMountExtension(extension)
    val extensionState = extension.createExtensionState(mountDelegate)
    val state = extensionState.state
    mountDelegate.registerMountExtension(extension)
    val incrementalMountExtensionInput = TestInput(10)
    extension.beforeMount(extensionState, incrementalMountExtensionInput, Rect(0, 0, 10, 50))
    for (i in 0 until incrementalMountExtensionInput.mountableOutputCount) {
      val node = incrementalMountExtensionInput.getMountableOutputAt(i)
      extension.beforeMountItem(extensionState, node, i)
    }
    extension.afterMount(extensionState)
    assertThat(IncrementalMountExtension.getPreviousBottomsIndex(state)).isEqualTo(0)
    assertThat(IncrementalMountExtension.getPreviousTopsIndex(state)).isEqualTo(5)
    val incrementalMountExtensionInput2 = TestInput(3)
    extension.beforeMount(extensionState, incrementalMountExtensionInput2, Rect(0, 0, 10, 0))
    for (i in 0 until incrementalMountExtensionInput2.mountableOutputCount) {
      val node = incrementalMountExtensionInput2.getMountableOutputAt(i)
      extension.beforeMountItem(extensionState, node, i)
    }
    extension.afterMount(extensionState)
    extension.onVisibleBoundsChanged(extensionState, Rect(0, 0, 10, 50))
    assertThat(IncrementalMountExtension.getPreviousBottomsIndex(state)).isEqualTo(0)
    assertThat(IncrementalMountExtension.getPreviousTopsIndex(state)).isEqualTo(3)
  }

  internal inner class TestInput(private val count: Int) :
      IncrementalMountExtensionInput, MountDelegateInput {
    private val mountableOutputs: MutableList<RenderTreeNode> = ArrayList()
    private val _incrementalMountOutputs: MutableMap<Long, IncrementalMountOutput> = LinkedHashMap()
    private val tops: MutableList<IncrementalMountOutput> = ArrayList()
    private val bottoms: MutableList<IncrementalMountOutput> = ArrayList()

    override fun getMountableOutputCount(): Int = count

    override fun getOutputsOrderedByTopBounds(): List<IncrementalMountOutput> = tops

    override fun getOutputsOrderedByBottomBounds(): List<IncrementalMountOutput> = bottoms

    override fun getIncrementalMountOutputForId(id: Long): IncrementalMountOutput? =
        _incrementalMountOutputs[id]

    override fun getIncrementalMountOutputs(): Collection<IncrementalMountOutput> =
        _incrementalMountOutputs.values

    override fun getIncrementalMountOutputCount(): Int = count

    override fun getPositionForId(id: Long): Int = 0

    override fun renderUnitWithIdHostsRenderTrees(id: Long): Boolean = true

    override fun getMountableOutputAt(position: Int): RenderTreeNode = mountableOutputs[position]

    init {
      for (i in 0 until count) {
        val bounds = Rect(0, i * 10, 10, (i + 1) * 10)
        val renderTreeNode = mock<RenderTreeNode>()
        whenever(renderTreeNode.layoutData).thenReturn(mock<LithoAnimtableItem>())
        whenever(renderTreeNode.getAbsoluteBounds(anyOrNull<Rect>())).thenReturn(bounds)
        val renderUnit: RenderUnit<*> = DrawableWrapperUnit(ColorDrawable(Color.BLACK), i.toLong())
        whenever(renderTreeNode.renderUnit).thenReturn(renderUnit)
        mountableOutputs.add(renderTreeNode)
        val incrementalMountOutput =
            IncrementalMountOutput(
                i.toLong(),
                i,
                bounds,
                false,
                if (i != 0) _incrementalMountOutputs[(i - 1).toLong()] else null)
        _incrementalMountOutputs[incrementalMountOutput.id] = incrementalMountOutput
        tops.add(incrementalMountOutput)
        bottoms.add(incrementalMountOutput)
      }
    }
  }
}