/*
 * 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 androidx.test.core.app.ApplicationProvider
import com.facebook.litho.config.ComponentsConfiguration
import com.facebook.litho.stats.LithoStats
import com.facebook.litho.testing.LegacyLithoViewRule
import com.facebook.litho.testing.helper.ComponentTestHelper
import com.facebook.litho.testing.logging.TestComponentsLogger
import com.facebook.litho.testing.testrunner.LithoTestRunner
import com.facebook.litho.widget.TextInput
import java.util.concurrent.CountDownLatch
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.LooperMode
import org.robolectric.shadows.ShadowLooper

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

  private lateinit var resolveThreadShadowLooper: ShadowLooper
  private lateinit var layoutThreadShadowLooper: ShadowLooper
  private lateinit var context: ComponentContext
  private lateinit var testComponent: StateUpdateTestComponent
  private lateinit var componentTree: ComponentTree
  private lateinit var componentsLogger: ComponentsLogger
  private lateinit var lithoView: LithoView
  private lateinit var testComponentKey: String

  @JvmField @Rule val legacyLithoViewRule = LegacyLithoViewRule()

  @Before
  fun setup() {
    componentsLogger = TestComponentsLogger()
    context =
        ComponentContext(ApplicationProvider.getApplicationContext(), LOG_TAG, componentsLogger)
    resolveThreadShadowLooper = ComponentTestHelper.getDefaultResolveThreadShadowLooper()
    layoutThreadShadowLooper = ComponentTestHelper.getDefaultLayoutThreadShadowLooper()
    testComponent = StateUpdateTestComponent()
    testComponentKey = testComponent.key
    componentTree = ComponentTree.create(context, testComponent).build()
    lithoView = LithoView(context)
    lithoView.componentTree = componentTree
    lithoView.onAttachedToWindow()
    ComponentTestHelper.measureAndLayout(lithoView)
  }

  private fun runToEndOfTasks() {
    resolveThreadShadowLooper.runToEndOfTasks()
    layoutThreadShadowLooper.runToEndOfTasks()
  }

  private fun runOneTask() {
    if (this::resolveThreadShadowLooper.isInitialized) {
      resolveThreadShadowLooper.runOneTask()
    }
    layoutThreadShadowLooper.runOneTask()
  }

  @Test
  fun updateStateAsync_incrementsAsyncCountAndTotalCount() {
    val beforeSync = LithoStats.getComponentTriggeredSyncStateUpdateCount()
    val beforeAsync = LithoStats.getComponentTriggeredAsyncStateUpdateCount()
    val beforeTotal = LithoStats.getComponentAppliedStateUpdateCount()
    componentTree.updateStateAsync(
        testComponentKey, StateUpdateTestComponent.createIncrementStateUpdate(), "test", false)
    runToEndOfTasks()
    val afterSync = LithoStats.getComponentTriggeredSyncStateUpdateCount()
    val afterAsync = LithoStats.getComponentTriggeredAsyncStateUpdateCount()
    val afterTotal = LithoStats.getComponentAppliedStateUpdateCount()
    assertThat(afterSync - beforeSync).isEqualTo(0)
    assertThat(afterAsync - beforeAsync).isEqualTo(1)
    assertThat(afterTotal - beforeTotal).isEqualTo(1)
  }

  @Test
  fun updateStateSync_incrementsSyncAndTotalCount() {
    val beforeSync = LithoStats.getComponentTriggeredSyncStateUpdateCount()
    val beforeAsync = LithoStats.getComponentTriggeredAsyncStateUpdateCount()
    val beforeTotal = LithoStats.getComponentAppliedStateUpdateCount()
    componentTree.updateStateSync(
        testComponentKey, StateUpdateTestComponent.createIncrementStateUpdate(), "test", false)
    runToEndOfTasks()
    val afterSync = LithoStats.getComponentTriggeredSyncStateUpdateCount()
    val afterAsync = LithoStats.getComponentTriggeredAsyncStateUpdateCount()
    val afterTotal = LithoStats.getComponentAppliedStateUpdateCount()
    assertThat(afterSync - beforeSync).isEqualTo(1)
    assertThat(afterAsync - beforeAsync).isEqualTo(0)
    assertThat(afterTotal - beforeTotal).isEqualTo(1)
  }

  @Test
  fun setRoot_incrementsCalculateLayoutOnUIAndTotalCount() {
    val beforeLayoutCalculationCount = LithoStats.getComponentCalculateLayoutCount()
    val beforeLayoutCalculationOnUICount = LithoStats.getComponentCalculateLayoutOnUICount()
    componentTree.root = StateUpdateTestComponent()
    val afterLayoutCalculationCount = LithoStats.getComponentCalculateLayoutCount()
    val afterLayoutCalculationOnUICount = LithoStats.getComponentCalculateLayoutOnUICount()
    assertThat(afterLayoutCalculationCount - beforeLayoutCalculationCount).isEqualTo(1)
    assertThat(afterLayoutCalculationOnUICount - beforeLayoutCalculationOnUICount).isEqualTo(1)
  }

  @Test
  fun setRootAsync_incrementsCalculateLayoutTotalCount() {
    val beforeLayoutCalculationCount = LithoStats.getComponentCalculateLayoutCount()
    val beforeLayoutCalculationOnUICount = LithoStats.getComponentCalculateLayoutOnUICount()
    componentTree.setRootAsync(StateUpdateTestComponent())
    val latch = CountDownLatch(1)
    Thread {
          // We have to do this inside another thread otherwise the execution of
          // mChangeSetThreadShadowLooper will happen on Main Thread
          runOneTask()
          if (!ComponentsConfiguration.useSeparateThreadHandlersForResolveAndLayout) {
            runOneTask()
          }
          latch.countDown()
        }
        .start()
    latch.await()
    val afterLayoutCalculationCount = LithoStats.getComponentCalculateLayoutCount()
    val afterLayoutCalculationOnUICount = LithoStats.getComponentCalculateLayoutOnUICount()
    assertThat(afterLayoutCalculationCount - beforeLayoutCalculationCount).isEqualTo(1)
    assertThat(afterLayoutCalculationOnUICount - beforeLayoutCalculationOnUICount).isEqualTo(0)
  }

  @Test
  fun mount_incrementsMountCount() {
    val beforeMountCount = LithoStats.getComponentMountCount()
    val component = TextInput.create(legacyLithoViewRule.context).build()
    legacyLithoViewRule.attachToWindow().setRoot(component).measure().layout()
    val afterMountCount = LithoStats.getComponentMountCount()
    assertThat(afterMountCount - beforeMountCount).isEqualTo(1)
  }

  companion object {
    private const val LOG_TAG = "logTag"
  }
}


/*
 * 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 android.view.View
import com.facebook.litho.core.height
import com.facebook.litho.core.margin
import com.facebook.litho.core.width
import com.facebook.litho.kotlin.widget.Image
import com.facebook.litho.testing.LithoViewRule
import com.facebook.litho.testing.testrunner.LithoTestRunner
import com.facebook.litho.widget.Text
import org.assertj.core.api.Assertions.assertThat
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith

/** Tests for [LithoTooltipController]. */
@RunWith(LithoTestRunner::class)
class LithoTooltipControllerTest {

  @Rule @JvmField val lithoViewRule = LithoViewRule()

  @Test
  fun `shows tooltip on Text Spec`() {
    class ComponentWithTooltip(val tooltipHandle: Handle) : KComponent() {
      override fun ComponentScope.render(): Component? {
        return Row {
          child(
              handle(tooltipHandle) {
                Text.create(context).text("I'm an anchor!").kotlinStyle(Style.margin(10.px)).build()
              })
        }
      }
    }

    val handle = Handle()
    val tooltip = RecordingLithoTooltip()

    lithoViewRule.render { ComponentWithTooltip(tooltipHandle = handle) }

    LithoTooltipController.showTooltipOnHandle(lithoViewRule.context, tooltip, handle)

    assertThat(tooltip.calls).hasSize(1)
    val call = tooltip.calls[0]
    assertThat(call.container).isNotNull()
    assertThat(call.anchorBounds.top).isEqualTo(10)
    assertThat(call.anchorBounds.left).isEqualTo(10)
  }

  @Test
  fun `shows tooltip on Row`() {
    class ComponentWithTooltip(val tooltipHandle: Handle) : KComponent() {
      override fun ComponentScope.render(): Component? {
        return Row {
          child(
              handle(tooltipHandle) {
                Row(style = Style.margin(10.px)) {
                  Image(style = Style.margin(100.px), drawable = ColorDrawable(Color.BLUE))
                }
              })
        }
      }
    }

    val handle = Handle()
    val tooltip = RecordingLithoTooltip()

    lithoViewRule.render { ComponentWithTooltip(tooltipHandle = handle) }

    LithoTooltipController.showTooltipOnHandle(lithoViewRule.context, tooltip, handle)

    assertThat(tooltip.calls).hasSize(1)
    val call = tooltip.calls[0]
    assertThat(call.container).isNotNull()
    assertThat(call.anchorBounds.top).isEqualTo(10)
    assertThat(call.anchorBounds.left).isEqualTo(10)
  }

  @Test
  fun `shows tooltip on KComponent`() {
    class KComponentWithDrawable() : KComponent() {
      override fun ComponentScope.render(): Component? {
        return Image(
            style = Style.width(100.px).height(100.px), drawable = ColorDrawable(Color.BLUE))
      }
    }

    class ComponentWithTooltip(val tooltipHandle: Handle) : KComponent() {
      override fun ComponentScope.render(): Component? {
        return Row { child(handle(tooltipHandle) { KComponentWithDrawable() }) }
      }
    }

    val handle = Handle()
    val tooltip = RecordingLithoTooltip()

    lithoViewRule.render { ComponentWithTooltip(tooltipHandle = handle) }

    LithoTooltipController.showTooltipOnHandle(lithoViewRule.context, tooltip, handle)

    assertThat(tooltip.calls).hasSize(1)
    val call = tooltip.calls[0]
    assertThat(call.container).isNotNull()
    assertThat(call.anchorBounds.top).isEqualTo(0)
    assertThat(call.anchorBounds.left).isEqualTo(0)
    assertThat(call.anchorBounds.right).isEqualTo(100)
    assertThat(call.anchorBounds.bottom).isEqualTo(100)
  }

  data class ShowTooltipCall(
      val container: View?,
      val anchorBounds: Rect,
      val xOffset: Int = 0,
      val yOffset: Int = 0
  )

  class RecordingLithoTooltip : LithoTooltip {
    private val _calls = mutableListOf<ShowTooltipCall>()
    val calls: List<ShowTooltipCall>
      get() = _calls.toList()

    override fun showLithoTooltip(
        container: View?,
        anchorBounds: Rect,
        xOffset: Int,
        yOffset: Int
    ) {
      _calls.add(ShowTooltipCall(container, anchorBounds, xOffset, yOffset))
    }

    fun clear() {
      _calls.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 android.content.Context
import android.graphics.Rect
import androidx.test.core.app.ApplicationProvider
import com.facebook.litho.testing.TestViewComponent
import com.facebook.litho.testing.helper.ComponentTestHelper
import com.facebook.litho.testing.inlinelayoutspec.InlineLayoutSpec
import com.facebook.litho.testing.testrunner.LithoTestRunner
import com.facebook.litho.widget.MountSpecLifecycleTester
import com.facebook.litho.widget.SimpleMountSpecTester
import com.facebook.rendercore.MountState
import org.assertj.core.api.Assertions.assertThat
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.kotlin.spy
import org.mockito.kotlin.times
import org.mockito.kotlin.verify

/**
 * Tests for [LithoView] and [MountState] to make sure mount only happens once when attaching the
 * view and setting the component.
 */
@RunWith(LithoTestRunner::class)
class LithoViewMountTest {

  private lateinit var context: ComponentContext
  private lateinit var lithoView: TestLithoView
  private lateinit var component: Component
  private lateinit var componentTree: ComponentTree

  @Before
  fun setup() {
    context = ComponentContext(ApplicationProvider.getApplicationContext<Context>())
    lithoView = spy(TestLithoView(context.androidContext))
    lithoView.resetRequestLayoutInvocationCount()
    component =
        object : InlineLayoutSpec() {
          override fun onCreateLayout(c: ComponentContext): Component =
              SimpleMountSpecTester.create(c).widthPx(100).heightPx(100).build()
        }
    componentTree =
        createComponentTree(useSpy = false, incMountEnabled = false, width = 100, height = 100)
  }

  @Test
  fun testIncrementalMountTriggeredAfterUnmountAllWithSameDimensions() {
    componentTree =
        createComponentTree(useSpy = true, incMountEnabled = true, width = 100, height = 100)
    val width = 50
    val height = 50
    lithoView.setMeasured(width, height)
    lithoView.componentTree = componentTree
    lithoView.onAttachedToWindow()
    lithoView.layout(0, 0, lithoView.measuredWidth, lithoView.measuredHeight)
    verify(lithoView).performIncrementalMountForVisibleBoundsChange()
    lithoView.unmountAllItems()
    lithoView.performLayout(false, 0, 0, width, height)
    verify(lithoView, times(2)).performIncrementalMountForVisibleBoundsChange()
  }

  @Test
  fun testOnlyRequestLayoutCalledUntilMeasured() {
    lithoView.componentTree = componentTree
    lithoView.onAttachedToWindow()
    assertThat(lithoView.requestLayoutInvocationCount).isEqualTo(1)
  }

  @Test
  fun testSetComponentAndAttachRequestsLayout() {
    lithoView.setMeasured(10, 10)
    lithoView.componentTree = componentTree
    lithoView.onAttachedToWindow()
    assertThat(lithoView.requestLayoutInvocationCount).isEqualTo(2)
  }

  @Test
  fun testSetSameSizeComponentAndAttachRequestsLayout() {
    lithoView.setMeasured(100, 100)
    lithoView.componentTree = componentTree
    lithoView.onAttachedToWindow()
    assertThat(lithoView.requestLayoutInvocationCount).isEqualTo(2)
  }

  @Test
  fun testSetComponentTwiceWithResetAndAttachRequestsLayout() {
    val ct = createComponentTree(useSpy = false, incMountEnabled = false, width = 100, height = 100)
    lithoView.componentTree = ct
    lithoView.setMeasured(100, 100)
    lithoView.onAttachedToWindow()
    assertThat(lithoView.requestLayoutInvocationCount).isEqualTo(2)
    lithoView.onDetachedFromWindow()
    lithoView.resetRequestLayoutInvocationCount()
    lithoView.componentTree = ct
    lithoView.onAttachedToWindow()
    assertThat(lithoView.requestLayoutInvocationCount).isEqualTo(1)
  }

  @Test
  fun testAttachAndSetSameSizeComponentRequestsLayout() {
    lithoView.setMeasured(100, 100)
    lithoView.onAttachedToWindow()
    lithoView.componentTree = componentTree
    assertThat(lithoView.requestLayoutInvocationCount).isEqualTo(1)
  }

  @Test
  fun testAttachAndSetComponentRequestsLayout() {
    lithoView.setMeasured(10, 10)
    lithoView.onAttachedToWindow()
    lithoView.componentTree = componentTree
    assertThat(lithoView.requestLayoutInvocationCount).isEqualTo(1)
  }

  @Test
  fun testReAttachRequestsLayout() {
    lithoView.setMeasured(100, 100)
    lithoView.componentTree = componentTree
    lithoView.onAttachedToWindow()
    assertThat(lithoView.requestLayoutInvocationCount).isEqualTo(2)
    lithoView.onDetachedFromWindow()
    lithoView.resetRequestLayoutInvocationCount()
    lithoView.onAttachedToWindow()
    assertThat(lithoView.requestLayoutInvocationCount).isEqualTo(1)
    val newComponentTree =
        createComponentTree(useSpy = false, incMountEnabled = false, width = 100, height = 100)
    lithoView.resetRequestLayoutInvocationCount()
    lithoView.componentTree = newComponentTree
    assertThat(lithoView.requestLayoutInvocationCount).isEqualTo(1)
  }

  @Test
  fun testSetHasTransientStateMountsEverythingIfIncrementalMountEnabled() {
    val child1 = TestViewComponent.create(context).build()
    val lifecycleTracker2 = LifecycleTracker()
    val child2 =
        MountSpecLifecycleTester.create(context).lifecycleTracker(lifecycleTracker2).build()
    val lithoView: LithoView =
        ComponentTestHelper.mountComponent(
            context,
            object : InlineLayoutSpec() {
              override fun onCreateLayout(c: ComponentContext): Component =
                  Column.create(c)
                      .child(Wrapper.create(c).delegate(child1).widthPx(10).heightPx(10))
                      .child(Wrapper.create(c).delegate(child2).widthPx(10).heightPx(10))
                      .build()
            },
            true,
            true)
    lithoView.notifyVisibleBoundsChanged(Rect(0, -10, 10, -5), true)
    assertThat(child1.isMounted).isFalse
    assertThat(lifecycleTracker2.isMounted).isFalse
    lithoView.setHasTransientState(true)
    assertThat(child1.isMounted).isTrue
    assertThat(lifecycleTracker2.isMounted).isTrue
  }

  @Test
  fun testUnmountAllCausesRemountOfComponentTreeOnLayout() {
    val child1 = TestViewComponent.create(context).build()
    val lifecycleTracker2 = LifecycleTracker()
    val child2 =
        MountSpecLifecycleTester.create(context).lifecycleTracker(lifecycleTracker2).build()
    val lithoView: LithoView =
        ComponentTestHelper.mountComponent(
            context,
            object : InlineLayoutSpec() {
              override fun onCreateLayout(c: ComponentContext): Component =
                  Column.create(c)
                      .child(Wrapper.create(c).delegate(child1).widthPx(10).heightPx(10))
                      .child(Wrapper.create(c).delegate(child2).widthPx(10).heightPx(10))
                      .build()
            },
            true,
            true)
    lithoView.performLayout(false, 0, 0, 100, 100)
    assertThat(child1.isMounted).isTrue
    assertThat(lifecycleTracker2.isMounted).isTrue
    lithoView.unmountAllItems()
    assertThat(child1.isMounted).isFalse
    assertThat(lifecycleTracker2.isMounted).isFalse
    lithoView.performLayout(false, 0, 0, 100, 100)
    assertThat(child1.isMounted).isTrue
    assertThat(lifecycleTracker2.isMounted).isTrue
  }

  @Test
  fun testPerformLayoutWithDifferentBoundsMountsEverything() {
    val child1 = TestViewComponent.create(context).build()
    val lifecycleTracker2 = LifecycleTracker()
    val child2 =
        MountSpecLifecycleTester.create(context).lifecycleTracker(lifecycleTracker2).build()
    val lithoView: LithoView =
        ComponentTestHelper.mountComponent(
            context,
            object : InlineLayoutSpec() {
              override fun onCreateLayout(c: ComponentContext): Component =
                  Column.create(c)
                      .child(Wrapper.create(c).delegate(child1).widthPx(10).heightPx(10))
                      .child(Wrapper.create(c).delegate(child2).widthPx(10).heightPx(10))
                      .build()
            },
            true,
            true)
    lithoView.notifyVisibleBoundsChanged(Rect(0, -10, 10, -5), true)
    assertThat(child1.isMounted).isFalse
    assertThat(lifecycleTracker2.isMounted).isFalse
    lithoView.performLayout(false, 0, 0, 200, 200)
    lithoView.viewTreeObserver.dispatchOnGlobalLayout()
    assertThat(child1.isMounted).isTrue
    assertThat(lifecycleTracker2.isMounted).isTrue
  }

  private fun createComponentTree(
      useSpy: Boolean,
      incMountEnabled: Boolean,
      width: Int,
      height: Int
  ): ComponentTree {
    val componentTree =
        ComponentTree.create(context, component)
            .incrementalMount(incMountEnabled)
            .layoutDiffing(false)
            .build()
    componentTree.setSizeSpec(
        SizeSpec.makeSizeSpec(width, SizeSpec.EXACTLY),
        SizeSpec.makeSizeSpec(height, SizeSpec.EXACTLY))
    return if (useSpy) spy(componentTree) else componentTree
  }

  private open class TestLithoView(context: Context?) : LithoView(context) {
    var requestLayoutInvocationCount = 0
      private set

    override fun requestLayout() {
      super.requestLayout()
      requestLayoutInvocationCount++
    }

    fun resetRequestLayoutInvocationCount() {
      requestLayoutInvocationCount = 0
    }

    fun setMeasured(width: Int, height: Int) {
      setMeasuredDimension(width, height)
    }
  }
}


/*
 * 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.core.width
import com.facebook.litho.kotlin.widget.Text
import com.facebook.litho.testing.LithoViewRule
import com.facebook.litho.testing.testrunner.LithoTestRunner
import com.facebook.litho.view.onClick
import com.facebook.litho.widget.ComponentWithTreeProp
import com.facebook.litho.widget.TextDrawable
import com.facebook.litho.widget.treeprops.SimpleTreeProp
import org.assertj.core.api.Assertions
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

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

  @JvmField @Rule val lithoViewRule = LithoViewRule()

  @Test
  fun onLithoViewRuleWithTreeProp_shouldPropagateTreeProp() {
    val component = ComponentWithTreeProp.create(lithoViewRule.context).build()
    val testLithoView =
        lithoViewRule.setTreeProp(SimpleTreeProp::class.java, SimpleTreeProp("test")).render {
          component
        }

    val item = testLithoView.lithoView.getMountItemAt(0).content
    assertThat(item).isInstanceOf(TextDrawable::class.java)
    assertThat((item as TextDrawable).text).isEqualTo("test")
  }

  @Test(expected = RuntimeException::class)
  fun onLithoViewRuleWithoutTreeProp_shouldThrowException() {
    val component = ComponentWithTreeProp.create(lithoViewRule.context).build()
    lithoViewRule.createTestLithoView().attachToWindow().setRoot(component).measure().layout()
  }

  @Test
  fun onLithoViewRuleExceptionOnBackgroundThread_shouldPropagateExceptionImmediately() {
    class TestComponent : KComponent() {
      override fun ComponentScope.render(): Component? {
        val randomState = useState { false }
        if (randomState.value) {
          throw Exception("Hi There!")
        }
        return Row(style = Style.width(100.px).height(100.px)) {
          child(Text(text = "some_other_text", style = Style.onClick { randomState.update(true) }))
        }
      }
    }

    val testLithoView = lithoViewRule.render { TestComponent() }
    val thrown: Throwable =
        Assertions.catchThrowable {
          lithoViewRule.act(testLithoView) { clickOnText("some_other_text") }
        }

    assertThat(thrown).isInstanceOf(RuntimeException::class.java)
    assertThat((thrown.stackTraceToString()).contains("Timed out!")).isFalse
    assertThat(thrown).hasStackTraceContaining("Hi There!")
  }
}


/*
 * 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.ViewGroup
import androidx.test.core.app.ApplicationProvider.getApplicationContext
import com.facebook.litho.config.ComponentsConfiguration
import com.facebook.litho.testing.LithoStatsRule
import com.facebook.litho.testing.assertj.LithoViewAssert.assertThat
import com.facebook.litho.testing.atMost
import com.facebook.litho.testing.exactly
import com.facebook.litho.testing.inlinelayoutspec.InlineLayoutSpec
import com.facebook.litho.testing.testrunner.LithoTestRunner
import com.facebook.litho.testing.unspecified
import com.facebook.litho.widget.SimpleMountSpecTester
import org.assertj.core.api.Assertions.assertThat
import org.assertj.core.api.Assumptions
import org.junit.After
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.rules.ExpectedException
import org.junit.runner.RunWith
import org.mockito.ArgumentMatchers.any
import org.mockito.ArgumentMatchers.anyBoolean
import org.mockito.ArgumentMatchers.anyInt
import org.mockito.kotlin.mock
import org.mockito.kotlin.never
import org.mockito.kotlin.verify
import org.robolectric.Shadows.shadowOf
import org.robolectric.shadows.ShadowView

@RunWith(LithoTestRunner::class)
class LithoViewTest {

  @JvmField @Rule val expectedException = ExpectedException.none()
  @JvmField @Rule val lithoStatsRule: LithoStatsRule = LithoStatsRule()

  private lateinit var lithoView: LithoView
  private lateinit var initialComponent: Component

  @Before
  fun setup() {
    initialComponent =
        object : InlineLayoutSpec() {
          override fun onCreateLayout(c: ComponentContext): Component {
            return SimpleMountSpecTester.create(c).widthPx(100).heightPx(100).build()
          }
        }
    lithoView = LithoView(getApplicationContext<Context>())
    lithoView.setComponent(initialComponent)
  }

  @After
  fun tearDown() {
    ComponentsConfiguration.isDebugModeEnabled = ComponentsConfiguration.IS_INTERNAL_BUILD
  }

  @Test
  fun measureBeforeBeingAttached() {
    lithoView.measure(unspecified(), unspecified())
    lithoView.layout(0, 0, lithoView.measuredWidth, lithoView.measuredHeight)

    // View got measured.
    assertThat(lithoView.measuredWidth).isGreaterThan(0)
    assertThat(lithoView.measuredHeight).isGreaterThan(0)

    // Attaching will automatically mount since we already have a layout fitting our size.
    val shadow: ShadowView = shadowOf(lithoView)
    shadow.callOnAttachedToWindow()
    assertThat(getInternalMountItems(lithoView)).isEqualTo(2)
  }

  @Test
  fun testNullLithoViewDimensions() {
    val component: Component =
        object : InlineLayoutSpec() {
          override fun onCreateLayout(c: ComponentContext): Component? {
            return null
          }
        }
    val nullLithoView = LithoView(getApplicationContext<Context>())
    nullLithoView.setComponent(component)
    nullLithoView.measure(unspecified(), unspecified())
    nullLithoView.layout(0, 0, nullLithoView.measuredWidth, nullLithoView.measuredHeight)
    assertThat(nullLithoView).hasMeasuredWidthOf(0).hasMeasuredHeightOf(0)
  }

  @Test
  fun testSuppressMeasureComponentTree() {
    val mockComponentTree: ComponentTree = mock()
    val width = 240
    val height = 400
    lithoView.componentTree = mockComponentTree
    lithoView.suppressMeasureComponentTree(true)
    lithoView.measure(exactly(width), exactly(height))
    verify(mockComponentTree, never()).measure(anyInt(), anyInt(), any(), anyBoolean())
    assertThat(lithoView).hasMeasuredWidthOf(width).hasMeasuredHeightOf(height)
  }

  @Test
  fun testDontThrowWhenLayoutStateIsNull() {
    val mockComponentTree: ComponentTree = mock()
    lithoView.componentTree = mockComponentTree
    lithoView.requestLayout()
    lithoView.notifyVisibleBoundsChanged()
  }

  /** This verifies that the width is 0 with normal layout params. */
  @Test
  fun measureWithLayoutParams() {
    val component: Component =
        object : InlineLayoutSpec() {
          override fun onCreateLayout(c: ComponentContext): Component {
            return SimpleMountSpecTester.create(c).widthPercent(100f).heightPx(100).build()
          }
        }
    lithoView = LithoView(getApplicationContext<Context>())
    lithoView.setComponent(component)
    lithoView.layoutParams = ViewGroup.LayoutParams(0, 200)
    lithoView.measure(unspecified(), exactly(200))
    lithoView.layout(0, 0, lithoView.measuredWidth, lithoView.measuredHeight)

    // View got measured.
    assertThat(lithoView.measuredWidth).isEqualTo(0)
    assertThat(lithoView.measuredHeight).isEqualTo(200)

    // Attaching will not mount anything as we have no width.
    val shadow: ShadowView = shadowOf(lithoView)
    shadow.callOnAttachedToWindow()

    // With no volume, ensure the component is not mounted.
    // When IM is blocked when rect is empty - nothing is mounted, so we expect 0 items.
    // When IM continues when rect is empty - the root host is mounted, so we expect 1 item.
    val totalExpectedMountedItems =
        if (ComponentsConfiguration.shouldContinueIncrementalMountWhenVisibileRectIsEmpty) 1 else 0
    assertThat(getInternalMountItems(lithoView)).isEqualTo(totalExpectedMountedItems)
  }

  /** This verifies that the width is correct with at most layout params. */
  @Test
  fun measureWithAtMostLayoutParams() {
    val component: Component =
        object : InlineLayoutSpec() {
          override fun onCreateLayout(c: ComponentContext): Component {
            return SimpleMountSpecTester.create(c).widthPercent(50f).heightPercent(10f).build()
          }
        }
    lithoView = LithoView(getApplicationContext<Context>())
    lithoView.setComponent(component)
    lithoView.layoutParams = RecyclerViewLayoutManagerOverrideParams(atMost(100), atMost(200))
    lithoView.measure(unspecified(), unspecified())
    lithoView.layout(0, 0, lithoView.measuredWidth, lithoView.measuredHeight)

    // View got measured.
    assertThat(lithoView.measuredWidth).isEqualTo(50)
    assertThat(lithoView.measuredHeight).isEqualTo(20)

    // Attaching will automatically mount since we already have a layout fitting our size.
    val shadow: ShadowView = shadowOf(lithoView)
    shadow.callOnAttachedToWindow()
    assertThat(getInternalMountItems(lithoView)).isEqualTo(2)
  }

  @Test
  fun testMeasureDoesNotComputeLayoutStateWhenSpecsAreExact() {
    lithoView = LithoView(getApplicationContext<Context>())
    lithoView.setComponent(SimpleMountSpecTester.create(lithoView.componentContext).build())
    lithoView.measure(exactly(100), exactly(100))
    assertThat(lithoView.measuredWidth).isEqualTo(100)
    assertThat(lithoView.measuredHeight).isEqualTo(100)
    assertThat(lithoView.componentTree?.mainThreadLayoutState).isNull()
    lithoView.layout(0, 0, 50, 50)
    val layoutState = lithoView.componentTree?.mainThreadLayoutState
    assertThat(layoutState).isNotNull
    assertThat(layoutState?.isCompatibleSize(50, 50)).isTrue
  }

  @Test
  fun testMeasureComputesLayoutStateWhenSpecsAreNotExact() {
    lithoView = LithoView(getApplicationContext<Context>())
    lithoView.setComponent(
        SimpleMountSpecTester.create(lithoView.componentContext).heightPx(100).build())
    lithoView.measure(exactly(100), atMost(100))
    assertThat(lithoView.measuredWidth).isEqualTo(100)
    assertThat(lithoView.measuredHeight).isEqualTo(100)
    assertThat(lithoView.componentTree?.mainThreadLayoutState).isNotNull
  }

  @Test
  fun forceLayout_whenForceLayoutIsSet_recomputesLayout() {
    lithoView.measure(exactly(100), atMost(100))
    lithoView.forceRelayout()
    lithoView.measure(exactly(100), atMost(100))
    assertThat(lithoStatsRule.getComponentCalculateLayoutCount())
        .describedAs("Should force layout.")
        .isEqualTo(2)
    lithoView.measure(exactly(100), atMost(100))
    assertThat(lithoStatsRule.getComponentCalculateLayoutCount())
        .describedAs("Should only force layout one time.")
        .isEqualTo(2)
  }

  @Test
  fun forceLayout_whenForceLayoutIsNotSet_doesNotRecomputeLayout() {
    lithoView.measure(exactly(100), atMost(100))
    lithoView.measure(exactly(100), atMost(100))
    lithoView.measure(exactly(100), atMost(100))
    assertThat(lithoStatsRule.getComponentCalculateLayoutCount())
        .describedAs("Should only compute the first layout as other layouts are not forced.")
        .isEqualTo(1)
  }

  @Test
  fun forceLayout_whenForceLayoutIsSetAndHasExactMeasurements_recomputesLayout() {
    lithoView.measure(exactly(100), exactly(100))
    lithoView.layout(0, 0, 100, 100)
    lithoView.forceRelayout()
    lithoView.measure(exactly(100), exactly(100))
    lithoView.layout(0, 0, 100, 100)
    assertThat(lithoStatsRule.getComponentCalculateLayoutCount())
        .describedAs("Should force layout.")
        .isEqualTo(2)
    lithoView.measure(exactly(100), exactly(100))
    lithoView.layout(0, 0, 100, 100)
    assertThat(lithoStatsRule.getComponentCalculateLayoutCount())
        .describedAs("Should only force layout one time.")
        .isEqualTo(2)
  }

  @Test
  fun forceLayout_whenForceLayoutIsNotSetAndHasExactMeasurements_doesNotRecomputeLayout() {
    lithoView.measure(exactly(100), exactly(100))
    lithoView.layout(0, 0, 100, 100)
    lithoView.measure(exactly(100), exactly(100))
    lithoView.layout(0, 0, 100, 100)
    lithoView.measure(exactly(100), exactly(100))
    lithoView.layout(0, 0, 100, 100)
    assertThat(lithoStatsRule.getComponentCalculateLayoutCount())
        .describedAs("Should only force layout one time.")
        .isEqualTo(1)
  }

  @Test
  fun rootComponent_returnsRootComponentWhenSet_viaSetComponent() {
    assertThat(lithoView.rootComponent).isEqualTo(initialComponent)
    val newRootComponent: Component =
        SimpleMountSpecTester.create(lithoView.componentContext).heightPx(12345).build()
    lithoView.setComponent(newRootComponent)
    assertThat(lithoView.rootComponent).isEqualTo(newRootComponent)
  }

  @Test
  fun rootComponent_returnsRootComponentWhenSet_viaSetComponentTree() {
    assertThat(lithoView.rootComponent).isEqualTo(initialComponent)
    val c = lithoView.componentContext
    val newRootComponent: Component = SimpleMountSpecTester.create(c).heightPx(12345).build()
    lithoView.componentTree = ComponentTree.create(c, newRootComponent).build()
    assertThat(lithoView.rootComponent).isEqualTo(newRootComponent)
  }

  @Test
  fun rootComponent_returnsNullComponentWhenNoComponentSet() {
    lithoView = LithoView(getApplicationContext<Context>())
    assertThat(lithoView.rootComponent).isNull()
  }

  // Measure + layout is necessary to ensure async operations have finished come assertion point
  @Test
  fun rootComponent_returnsNullWhenNoComponent_viaSetComponentTree() {
    Assumptions.assumeThat(lithoView.rootComponent).isEqualTo(initialComponent)
    lithoView.componentTree = null
    // Measure + layout is necessary to ensure async operations have finished come assertion point
    lithoView.measure(unspecified(), unspecified())
    lithoView.layout(0, 0, 50, 50)
    assertThat(lithoView.rootComponent).isNull()
  }

  // Measure + layout is necessary to ensure async operations have finished come assertion point
  @Test
  fun rootComponent_returnsEmptyComponentWhenNoComponent_viaSetComponent() {
    Assumptions.assumeThat(lithoView.rootComponent).isEqualTo(initialComponent)
    lithoView.setComponent(null)
    // Measure + layout is necessary to ensure async operations have finished come assertion point
    lithoView.measure(unspecified(), unspecified())
    lithoView.layout(0, 0, 50, 50)
    assertThat(lithoView.rootComponent).isInstanceOf(EmptyComponent::class.java)
  }

  private class RecyclerViewLayoutManagerOverrideParams(
      private val _widthMeasureSpec: Int,
      private val _heightMeasureSpec: Int
  ) : ViewGroup.LayoutParams(WRAP_CONTENT, WRAP_CONTENT), LithoView.LayoutManagerOverrideParams {
    override fun hasValidAdapterPosition(): Boolean = false

    override fun getWidthMeasureSpec(): Int = _widthMeasureSpec

    override fun getHeightMeasureSpec(): Int = _heightMeasureSpec
  }

  companion object {
    private fun getInternalMountItems(lithoView: LithoView): Int {
      return lithoView.mountDelegateTarget.mountItemCount
    }
  }
}


/*
 * 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.getApplicationContext
import com.facebook.litho.config.ComponentsConfiguration
import com.facebook.litho.kotlin.widget.Text
import com.facebook.litho.testing.LithoViewRule
import com.facebook.litho.testing.inlinelayoutspec.InlineLayoutSpec
import com.facebook.litho.testing.testrunner.LithoTestRunner
import com.facebook.litho.testing.unspecified
import com.facebook.litho.view.testKey
import com.facebook.litho.widget.ComponentContainerWithSize
import com.facebook.litho.widget.SimpleMountSpecTester
import com.facebook.litho.widget.Text
import org.assertj.core.api.Assertions.assertThat
import org.junit.Assume
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith

@RunWith(LithoTestRunner::class)
class LithoViewTestHelperTest {

  @get:Rule val lithoViewRule = LithoViewRule()

  @Before
  fun skipIfRelease() {
    Assume.assumeTrue(
        "These tests cover debug functionality and can only be run " + "for internal builds.",
        ComponentsConfiguration.IS_INTERNAL_BUILD)
  }

  @Test
  fun testBasicViewToString() {
    val component: Component =
        object : InlineLayoutSpec() {
          override fun onCreateLayout(c: ComponentContext): Component? {
            return SimpleMountSpecTester.create(c).widthPx(100).heightPx(100).build()
          }
        }
    val lithoView = LithoView(getApplicationContext<Context>())
    lithoView.setComponent(component)
    lithoView.measure(unspecified(), unspecified())
    val string = LithoViewTestHelper.viewToString(lithoView)
    assertThat(string)
        .containsPattern(
            """
              litho.InlineLayout\{\w+ V.E..... .. 0,0-100,100\}
                litho.SimpleMountSpecTester\{\w+ V.E..... .. 0,0-100,100\}
                """
                .trimIndent())
  }

  @Test
  fun testBasicRootInstanceToStringWithDepth() {
    val component: Component =
        object : InlineLayoutSpec() {
          override fun onCreateLayout(c: ComponentContext): Component? {
            return SimpleMountSpecTester.create(c).widthPx(100).heightPx(100).build()
          }
        }
    val lithoView = LithoView(getApplicationContext<Context>())
    lithoView.setComponent(component)
    lithoView.measure(unspecified(), unspecified())
    val root = DebugComponent.getRootInstance(lithoView)
    val string =
        LithoViewTestHelper.rootInstanceToString(root, false /* embedded */, 1 /* string depth */)
    assertThat(string)
        .containsPattern(
            """
               \n  litho.InlineLayout\{\w+ V.E..... .. 0,0-100,100}
                   litho.SimpleMountSpecTester\{\w+ V.E..... .. 0,0-100,100}
               """
                .trimIndent())
  }

  @Test
  fun viewToStringWithText() {
    val component: Component =
        object : InlineLayoutSpec() {
          override fun onCreateLayout(c: ComponentContext): Component? {
            return Column.create(c)
                .child(
                    SimpleMountSpecTester.create(c)
                        .testKey("test-drawable")
                        .widthPx(100)
                        .heightPx(100))
                .child(Text.create(c).widthPx(100).heightPx(100).text("Hello, World"))
                .build()
          }
        }
    val lithoView = LithoView(getApplicationContext<Context>())
    lithoView.setComponent(component)
    lithoView.measure(unspecified(), unspecified())
    lithoView.layout(0, 0, lithoView.measuredWidth, lithoView.measuredHeight)
    val string = LithoViewTestHelper.viewToString(lithoView)
    assertThat(string)
        .containsPattern(
            """
              litho.InlineLayout\{\w+ V.E..... .. 0,0-100,200\}
                litho.Column\{\w+ V.E..... .. 0,0-100,200\}
                  litho.SimpleMountSpecTester\{\w+ V.E..... .. 0,0-100,100 litho:id/test-drawable\}
                  litho.Text\{\w+ V.E..... .. 0,100-100,200 text="Hello, World"\}
                """
                .trimIndent())
  }

  @Test
  fun viewToStringForE2E_withExtraDescription_componentKeyIsPrinted() {
    val c = lithoViewRule.context
    val component: Component =
        Column.create(c)
            .key("column")
            .child(SimpleMountSpecTester.create(c).key("simple").widthPx(100).heightPx(100).build())
            .child(Text.create(c).key("text").widthPx(100).heightPx(100).text("Hello, World"))
            .build()

    val string =
        LithoViewTestHelper.viewToStringForE2E(
            lithoViewRule.render { component }.lithoView, 0, false) { debugComponent, sb ->
              sb.append(", key=").append(debugComponent.key)
            }
    assertThat(string)
        .containsPattern(
            """
              litho.Column\{\w+ V.E..... .. 0,0-1080,200, key=column}
                litho.SimpleMountSpecTester\{\w+ V.E..... .. 0,0-100,100, key=simple}
                litho.Text\{\w+ V.E..... .. 0,100-100,200 text="Hello, World", key=text}
                """
                .trimIndent())
  }

  @Test
  fun `when root Component uses sizes then TestHelper toString should contain nested tree with children`() {
    val view =
        lithoViewRule
            .render {
              ComponentContainerWithSize.create(context)
                  .component(
                      Column {
                        child(TextHolderComponent())
                        child(TextHolderComponent())
                      })
                  .build()
            }
            .lithoView

    val string =
        LithoViewTestHelper.viewToStringForE2E(view, 0, false) { debugComponent, sb ->
          sb.append(", key=").append(debugComponent.key)
        }

    assertThat(string)
        .containsPattern(
            """
              litho.ComponentContainerWithSize\{\w+ V.E..... .. 0,0-1080,82, key=null}
                litho.Column\{\w+ V.E..... .. 0,0-1080,82, key=null}
                  litho.TextHolderComponent\{\w+ V.E..... .. 0,0-1080,41, key=null}
                    litho.Text\{\w+ V.E..... .. 0,0-1080,41 litho:id/test-key text="hello", key=null}
                  litho.TextHolderComponent\{\w+ V.E..... .. 0,41-1080,82, key=null}
                    litho.Text\{\w+ V.E..... .. 0,0-1080,41 litho:id/test-key text="hello", key=null}
                    """
                .trimIndent())
  }

  @Test
  fun `when root Component uses sizes then TestHelper toString should contain delegated nested tree with children`() {
    val view =
        lithoViewRule
            .render {
              ComponentContainerWithSize.create(context)
                  .component(
                      DelegatingComponent(
                          component =
                              Column {
                                child(TextHolderComponent())
                                child(TextHolderComponent())
                              }))
                  .build()
            }
            .lithoView

    val string =
        LithoViewTestHelper.viewToStringForE2E(view, 0, false) { debugComponent, sb ->
          sb.append(", key=").append(debugComponent.key)
        }

    assertThat(string)
        .containsPattern(
            """
              litho.ComponentContainerWithSize\{\w+ V.E..... .. 0,0-1080,82, key=null}
                litho.DelegatingComponent\{\w+ V.E..... .. 0,0-1080,82, key=null}
                  litho.Column\{\w+ V.E..... .. 0,0-1080,82, key=null}
                    litho.TextHolderComponent\{\w+ V.E..... .. 0,0-1080,41, key=null}
                      litho.Text\{\w+ V.E..... .. 0,0-1080,41 litho:id/test-key text="hello", key=null}
                    litho.TextHolderComponent\{\w+ V.E..... .. 0,41-1080,82, key=null}
                      litho.Text\{\w+ V.E..... .. 0,0-1080,41 litho:id/test-key text="hello", key=null}
                      """
                .trimIndent())
  }

  @Test
  fun `when root Component uses sizes then TestHelper toString should contain nested tree`() {
    val view =
        lithoViewRule
            .render {
              ComponentContainerWithSize.create(context).component(ParentComponent()).build()
            }
            .lithoView

    val string =
        LithoViewTestHelper.viewToStringForE2E(view, 0, false) { debugComponent, sb ->
          sb.append(", key=").append(debugComponent.key)
        }

    assertThat(string)
        .containsPattern(
            """
              litho.ComponentContainerWithSize\{\w+ V.E..... .. 0,0-1080,41, key=null}
                litho.ParentComponent\{\w+ V.E..... .. 0,0-1080,41, key=null}
                  litho.TextHolderComponent\{\w+ V.E..... .. 0,0-1080,41, key=null}
                    litho.Text\{\w+ V.E..... .. 0,0-1080,41 litho:id/test-key text="hello", key=null}
                    """
                .trimIndent())
  }

  @Test
  fun `when child Component uses sizes then TestHelper toString should contain nested tree`() {
    val view =
        lithoViewRule
            .render {
              Column {
                child(
                    ComponentContainerWithSize.create(context).component(ParentComponent()).build())
              }
            }
            .lithoView

    val string =
        LithoViewTestHelper.viewToStringForE2E(view, 0, false) { debugComponent, sb ->
          sb.append(", key=").append(debugComponent.key)
        }

    assertThat(string)
        .containsPattern(
            """
              litho.Column\{\w+ V.E..... .. 0,0-1080,41, key=null}
                litho.ComponentContainerWithSize\{\w+ V.E..... .. 0,0-1080,41, key=null}
                  litho.ParentComponent\{\w+ V.E..... .. 0,0-1080,41, key=null}
                    litho.TextHolderComponent\{\w+ V.E..... .. 0,0-1080,41, key=null}
                      litho.Text\{\w+ V.E..... .. 0,0-1080,41 litho:id/test-key text="hello", key=null}
                      """
                .trimIndent())
  }

  @Test
  fun `when delegated root Component uses sizes then TestHelper toString should contain nested tree`() {
    val view =
        lithoViewRule
            .render {
              DelegatingComponent(
                  component =
                      ComponentContainerWithSize.create(context)
                          .component(ParentComponent())
                          .build())
            }
            .lithoView

    val string =
        LithoViewTestHelper.viewToStringForE2E(view, 0, false) { debugComponent, sb ->
          sb.append(", key=").append(debugComponent.key)
        }

    assertThat(string)
        .containsPattern(
            """
              litho.DelegatingComponent\{\w+ V.E..... .. 0,0-1080,41, key=null}
                litho.ComponentContainerWithSize\{\w+ V.E..... .. 0,0-1080,41, key=null}
                  litho.ParentComponent\{\w+ V.E..... .. 0,0-1080,41, key=null}
                    litho.TextHolderComponent\{\w+ V.E..... .. 0,0-1080,41, key=null}
                      litho.Text\{\w+ V.E..... .. 0,0-1080,41 litho:id/test-key text="hello", key=null}
                      """
                .trimIndent())
  }

  @Test
  fun `when delegated child Component uses sizes then TestHelper toString should contain nested tree`() {
    val view =
        lithoViewRule
            .render {
              Column {
                child(
                    DelegatingComponent(
                        component =
                            ComponentContainerWithSize.create(context)
                                .component(ParentComponent())
                                .build()))
              }
            }
            .lithoView

    val string =
        LithoViewTestHelper.viewToStringForE2E(view, 0, false) { debugComponent, sb ->
          sb.append(", key=").append(debugComponent.key)
        }

    assertThat(string)
        .containsPattern(
            """
              litho.Column\{\w+ V.E..... .. 0,0-1080,41, key=null}
                litho.DelegatingComponent\{\w+ V.E..... .. 0,0-1080,41, key=null}
                  litho.ComponentContainerWithSize\{\w+ V.E..... .. 0,0-1080,41, key=null}
                    litho.ParentComponent\{\w+ V.E..... .. 0,0-1080,41, key=null}
                      litho.TextHolderComponent\{\w+ V.E..... .. 0,0-1080,41, key=null}
                        litho.Text\{\w+ V.E..... .. 0,0-1080,41 litho:id/test-key text="hello", key=null}
                        """
                .trimIndent())
  }

  @Test
  fun `when leaf component is measured then TestHelper toString should contain the leaf node`() {
    val view =
        lithoViewRule
            .render {
              MeasuringComponent(
                  component = Text(text = "hello", style = Style.testKey("test-key")))
            }
            .lithoView

    val string =
        LithoViewTestHelper.viewToStringForE2E(view, 0, false) { debugComponent, sb ->
          sb.append(", key=").append(debugComponent.key)
        }

    assertThat(string)
        .containsPattern(
            """
              litho.MeasuringComponent\{\w+ V.E..... .. 0,0-1080,41, key=null}
                litho.Text\{\w+ V.E..... .. 0,0-1080,41 litho:id/test-key text="hello", key=null}
                """
                .trimIndent())
  }

  @Test
  fun `when delegated leaf component is measured then TestHelper toString should contain the leaf node`() {
    val view =
        lithoViewRule
            .render {
              MeasuringComponent(
                  component =
                      DelegatingComponent(
                          component = Text(text = "hello", style = Style.testKey("test-key"))))
            }
            .lithoView

    val string =
        LithoViewTestHelper.viewToStringForE2E(view, 0, false) { debugComponent, sb ->
          sb.append(", key=").append(debugComponent.key)
        }

    assertThat(string)
        .containsPattern(
            """
              litho.MeasuringComponent\{\w+ V.E..... .. 0,0-1080,41, key=null}
                litho.DelegatingComponent\{\w+ V.E..... .. 0,0-1080,41, key=null}
                  litho.Text\{\w+ V.E..... .. 0,0-1080,41 litho:id/test-key text="hello", key=null}
               """
                .trimIndent())
  }

  @Test
  fun `when leaf component is delegated measured then TestHelper toString should contain the leaf node`() {
    val view =
        lithoViewRule
            .render {
              DelegatingComponent(component = MeasuringComponent(component = TextHolderComponent()))
            }
            .lithoView

    val string =
        LithoViewTestHelper.viewToStringForE2E(view, 0, false) { debugComponent, sb ->
          sb.append(", key=").append(debugComponent.key)
        }

    assertThat(string)
        .containsPattern(
            """
              litho.DelegatingComponent\{\w+ V.E..... .. 0,0-1080,41, key=null}
                litho.MeasuringComponent\{\w+ V.E..... .. 0,0-1080,41, key=null}
                  litho.TextHolderComponent\{\w+ V.E..... .. 0,0-1080,41, key=null}
                    litho.Text\{\w+ V.E..... .. 0,0-1080,41 litho:id/test-key text="hello", key=null}
                    """
                .trimIndent())
  }

  @Test
  fun `when delegated component is measured as child then TestHelper toString should contain all the components`() {
    val view =
        lithoViewRule
            .render {
              Column {
                child(
                    DelegatingComponent(
                        component =
                            MeasuringComponent(
                                component =
                                    DelegatingComponent(
                                        component =
                                            Text(
                                                text = "hello",
                                                style = Style.testKey("test-key"))))))
              }
            }
            .lithoView

    val string =
        LithoViewTestHelper.viewToStringForE2E(view, 0, false) { debugComponent, sb ->
          sb.append(", key=").append(debugComponent.key)
        }

    assertThat(string)
        .containsPattern(
            """
              litho.Column\{\w+ V.E..... .. 0,0-1080,41, key=null}
                litho.DelegatingComponent\{\w+ V.E..... .. 0,0-1080,41, key=null}
                  litho.MeasuringComponent\{\w+ V.E..... .. 0,0-1080,41, key=null}
                    litho.DelegatingComponent\{\w+ V.E..... .. 0,0-1080,41, key=null}
                      litho.Text\{\w+ V.E..... .. 0,0-1080,41 litho:id/test-key text="hello", key=null}
               """
                .trimIndent())
  }

  class DelegatingComponent(val component: Component) : KComponent() {
    override fun ComponentScope.render(): Component {
      return component
    }
  }

  class ParentComponent : KComponent() {
    override fun ComponentScope.render(): Component {
      return TextHolderComponent()
    }
  }

  class TextHolderComponent : KComponent() {
    override fun ComponentScope.render(): Component {
      return Text(text = "hello", style = Style.testKey("test-key"))
    }
  }

  class MeasuringComponent(val component: Component) : KComponent() {
    override fun ComponentScope.render(): Component {
      component.measure(context, unspecified(), unspecified(), Size())
      return component
    }
  }
}


/*
 * 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.Rect
import com.facebook.litho.config.ComponentsConfiguration
import com.facebook.litho.config.ComponentsConfiguration.isIncrementalMountGloballyDisabled
import com.facebook.litho.core.height
import com.facebook.litho.core.width
import com.facebook.litho.kotlin.widget.Text
import com.facebook.litho.testing.LithoViewRule
import com.facebook.litho.testing.testrunner.LithoTestRunner
import com.facebook.litho.visibility.onVisible
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

@RunWith(LithoTestRunner::class)
class LithoViewVisibilityProcessingTest {

  @get:Rule
  val lithoViewRule =
      LithoViewRule(
          componentsConfiguration =
              ComponentsConfiguration.getDefaultComponentsConfiguration().apply {
                isIncrementalMountGloballyDisabled = true
              })

  @Test
  fun `should process visibility outputs with the current visible rect when mount state is dirty and incremental mount disabled`() {
    val firstComponentOnVisibleCounter = AtomicInteger()
    val secondComponentOnVisibleCounter = AtomicInteger()

    val firstComponent = MyTestComponent(firstComponentOnVisibleCounter)
    val secondComponent = MyTestComponent(secondComponentOnVisibleCounter)

    /* We start out by rendering two different components in two different litho views */
    val firstLithoView = lithoViewRule.render { firstComponent }.lithoView
    val secondLithoView = lithoViewRule.render { secondComponent }.lithoView

    /* we verify that both components on visible callback was triggered */
    assertThat(firstComponentOnVisibleCounter.get()).isEqualTo(1)
    assertThat(secondComponentOnVisibleCounter.get()).isEqualTo(1)

    /* we re-use the second litho view component tree, and set it in the first litho view. */
    firstLithoView.componentTree = secondLithoView.componentTree

    /* immediately after, we "fake" translate down the component out of the viewport, so that it is not visible; calling translate
     * is not possible in this test because it requires a wrapping view of the LithoView */
    firstLithoView.notifyVisibleBoundsChanged(Rect(0, 200, 200, 600), true)

    /* during the translation, the used rect should be the visible rect (and not the last stored one in the visibility extensions),
    and therefore no extra visible callback should be called. */
    assertThat(firstComponentOnVisibleCounter.get()).isEqualTo(1)
    assertThat(secondComponentOnVisibleCounter.get()).isEqualTo(1)
  }

  class MyTestComponent(private val atomicCounter: AtomicInteger) : KComponent() {

    override fun ComponentScope.render(): Component {

      return Row {
        child(
            Text(
                "Hello",
                style =
                    Style.height(200.px).width(200.px).onVisible {
                      atomicCounter.incrementAndGet()
                    }))
      }
    }
  }
}


/*
 * 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 androidx.test.core.app.ApplicationProvider.getApplicationContext
import com.facebook.litho.LogTreePopulatorTest.MyKey
import com.facebook.litho.testing.logging.TestComponentsLogger
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.kotlin.mock
import org.mockito.kotlin.verify
import org.mockito.kotlin.verifyNoMoreInteractions

@RunWith(LithoTestRunner::class)
class LogTreePopulatorTest {

  private lateinit var context: ComponentContext

  @Before
  fun setup() {
    context = ComponentContext(getApplicationContext(), "test", TestComponentsLogger())
  }

  @Test
  fun testCustomTreePropLogger() {
    val logger: ComponentsLogger =
        object : TestComponentsLogger() {
          override fun getExtraAnnotations(
              treePropertyProvider: TreePropertyProvider
          ): Map<String, String>? {
            return mapOf("my_key" to treePropertyProvider.getProperty(MyKey::class.java).toString())
          }
        }
    val event: PerfEvent = mock()
    context.treeProps = TreeProps().apply { put(MyKey::class.java, 1_337) }
    LogTreePopulator.populatePerfEventFromLogger(context, logger, event)
    verify(event).markerAnnotate("my_key", "1337")
  }

  @Test
  fun testSkipOnEmptyTag() {
    val logger: TestComponentsLogger =
        object : TestComponentsLogger() {
          override fun getExtraAnnotations(
              treePropertyProvider: TreePropertyProvider
          ): Map<String, String>? {
            return mapOf("my_key" to treePropertyProvider.getProperty(MyKey::class.java).toString())
          }
        }
    val event: PerfEvent = mock()
    context.treeProps = TreeProps().apply { put(MyKey::class.java, 1_337) }
    val noLogTagContext = ComponentContext(getApplicationContext(), null, null)
    val perfEvent = LogTreePopulator.populatePerfEventFromLogger(noLogTagContext, logger, event)
    assertThat(perfEvent).isNull()
    assertThat(logger.canceledPerfEvents).containsExactly(event)
    verifyNoMoreInteractions(event)
  }

  @Test
  fun testNullTreePropLogger() {
    val logger: ComponentsLogger =
        object : TestComponentsLogger() {
          override fun getExtraAnnotations(
              treePropertyProvider: TreePropertyProvider
          ): Map<String, String>? = null
        }
    val event: PerfEvent = mock()
    context.treeProps = TreeProps().apply { put(MyKey::class.java, 1_337) }
    LogTreePopulator.populatePerfEventFromLogger(context, logger, event)
    verify(event).markerAnnotate("log_tag", "test")
    verifyNoMoreInteractions(event)
  }

  @Test
  fun testSkipNullPerfEvent() {
    val logger = TestComponentsLogger()
    assertThat(LogTreePopulator.populatePerfEventFromLogger(context, logger, null)).isNull()
  }

  private class MyKey
}


/*
 * 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 com.facebook.rendercore.MountDelegate
import com.facebook.rendercore.MountDelegateTarget
import com.facebook.rendercore.RenderTreeNode
import com.facebook.rendercore.extensions.MountExtension
import java.lang.IllegalStateException
import org.assertj.core.api.Assertions.assertThat
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.kotlin.mock
import org.mockito.kotlin.whenever

@RunWith(LithoTestRunner::class)
class MountDelegateCanPreventMountTest {

  @Test
  fun testIsLockedForMount() {
    // Create test LayoutOutputs with unique ids
    val lithoRenderUnit1: LithoRenderUnit = mock()
    val layoutOutput1: RenderTreeNode = mock()
    whenever(layoutOutput1.renderUnit).thenReturn(lithoRenderUnit1)
    whenever(lithoRenderUnit1.id).thenReturn(1L)
    val lithoRenderUnit2: LithoRenderUnit = mock()
    val layoutOutput2: RenderTreeNode = mock()
    whenever(layoutOutput2.renderUnit).thenReturn(lithoRenderUnit2)
    whenever(lithoRenderUnit2.id).thenReturn(2L)
    val mountDelegateTarget = mock<MountDelegateTarget>()
    val mountDelegate = MountDelegate(mountDelegateTarget, ComponentsSystrace.systrace)

    // When no extensions are present, calls to isLockedForMount default to true
    assertThat(mountDelegate.isLockedForMount(layoutOutput1)).isTrue
    assertThat(mountDelegate.isLockedForMount(layoutOutput2)).isTrue

    // When no extensions prevent mounting, calls to isLockedForMount default to true
    val mountExtension: MountExtension<*, *> = mock()
    mountDelegate.registerMountExtension(mountExtension)
    assertThat(mountDelegate.isLockedForMount(layoutOutput1)).isTrue
    assertThat(mountDelegate.isLockedForMount(layoutOutput2)).isTrue

    // When an extension can prevent mounting, calls to isLockedForMount default to false.
    val mountDelegateExtensionPreventMount: MountExtension<*, *> = mock()
    whenever(mountDelegateExtensionPreventMount.canPreventMount()).thenReturn(true)
    mountDelegate.registerMountExtension(mountDelegateExtensionPreventMount)
    assertThat(mountDelegate.isLockedForMount(layoutOutput1)).isFalse
    assertThat(mountDelegate.isLockedForMount(layoutOutput2)).isFalse

    // Acquiring a lock for a given LayoutOutput is reflected by isLockedForMount
    mountDelegate.acquireMountRef(layoutOutput1)
    assertThat(mountDelegate.isLockedForMount(layoutOutput1)).isTrue
    assertThat(mountDelegate.isLockedForMount(layoutOutput2)).isFalse

    // Releasing the lock is reflected by isLockedForMount
    mountDelegate.releaseMountRef(layoutOutput1)
    assertThat(mountDelegate.isLockedForMount(layoutOutput1)).isFalse
    assertThat(mountDelegate.isLockedForMount(layoutOutput2)).isFalse
  }

  @Test(expected = IllegalStateException::class)
  fun testImbalancedLocking() {
    val lithoRenderUnit = mock<LithoRenderUnit>()
    val layoutOutput1 = mock<RenderTreeNode>()
    whenever(layoutOutput1.renderUnit).thenReturn(lithoRenderUnit)
    whenever(lithoRenderUnit.id).thenReturn(1L)
    val mountDelegateTarget = mock<MountDelegateTarget>()
    val mountDelegate = MountDelegate(mountDelegateTarget, ComponentsSystrace.systrace)
    val mountDelegateExtensionPreventMount: MountExtension<*, *> = mock()
    whenever(mountDelegateExtensionPreventMount.canPreventMount()).thenReturn(true)
    mountDelegate.registerMountExtension(mountDelegateExtensionPreventMount)
    mountDelegate.acquireMountRef(layoutOutput1)
    mountDelegate.releaseMountRef(layoutOutput1)
    mountDelegate.releaseMountRef(layoutOutput1)
  }
}


/*
 * 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.graphics.Rect
import android.util.SparseArray
import android.view.View
import androidx.core.view.ViewCompat
import androidx.test.core.app.ApplicationProvider.getApplicationContext
import com.facebook.litho.MountSpecLithoRenderUnit.Companion.STATE_UNKNOWN
import com.facebook.litho.testing.TestDrawableComponent
import com.facebook.litho.testing.inlinelayoutspec.InlineLayoutSpec
import com.facebook.litho.testing.testrunner.LithoTestRunner
import com.facebook.litho.widget.SimpleMountSpecTester
import com.facebook.rendercore.MountItem
import com.facebook.rendercore.RenderTreeNode
import org.assertj.core.api.Assertions.assertThat
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith

/** Tests [MountItem] */
@RunWith(LithoTestRunner::class)
class MountItemTest {

  private lateinit var mountItem: MountItem
  private lateinit var component: SpecGeneratedComponent
  private lateinit var componentHost: ComponentHost
  private lateinit var content: Any
  private lateinit var contentDescription: CharSequence
  private lateinit var viewTag: Any
  private lateinit var viewTags: SparseArray<Any>
  private lateinit var clickHandler: EventHandler<ClickEvent>
  private lateinit var longClickHandler: EventHandler<LongClickEvent>
  private lateinit var focusChangeHandler: EventHandler<FocusChangedEvent>
  private lateinit var touchHandler: EventHandler<TouchEvent>
  private lateinit var dispatchPopulateAccessibilityEventHandler:
      EventHandler<DispatchPopulateAccessibilityEventEvent>
  private var flags = 0
  private lateinit var context: ComponentContext
  private lateinit var nodeInfo: NodeInfo

  @Before
  fun setup() {
    context = ComponentContext(getApplicationContext<Context>())
    component =
        object : InlineLayoutSpec() {
          override fun onCreateLayout(c: ComponentContext): Component =
              SimpleMountSpecTester.create(c).build()
        }
    componentHost = ComponentHost(getApplicationContext<Context>())
    content = View(getApplicationContext())
    contentDescription = "contentDescription"
    viewTag = "tag"
    viewTags = SparseArray()
    clickHandler = EventHandler<ClickEvent>(component, 5)
    longClickHandler = EventHandler<LongClickEvent>(component, 3)
    focusChangeHandler = EventHandler<FocusChangedEvent>(component, 9)
    touchHandler = EventHandler<TouchEvent>(component, 1)
    dispatchPopulateAccessibilityEventHandler =
        EventHandler<DispatchPopulateAccessibilityEventEvent>(component, 7)
    flags = 114
    nodeInfo = NodeInfo()
    nodeInfo.contentDescription = contentDescription
    nodeInfo.clickHandler = clickHandler
    nodeInfo.longClickHandler = longClickHandler
    nodeInfo.focusChangeHandler = focusChangeHandler
    nodeInfo.touchHandler = touchHandler
    nodeInfo.viewTag = viewTag
    nodeInfo.viewTags = viewTags
    mountItem = create(content)
  }

  private fun create(content: Any): MountItem =
      MountItemTestHelper.create(
          component, content, nodeInfo, null, flags, ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_YES)

  @Test
  fun testIsBound() {
    mountItem.setIsBound(true)
    assertThat(mountItem.isBound).isTrue
    mountItem.setIsBound(false)
    assertThat(mountItem.isBound).isFalse
  }

  @Test
  fun testGetters() {
    assertThat(LithoRenderUnit.getRenderUnit(mountItem).component).isSameAs(component)
    assertThat(mountItem.content).isSameAs(content)
    assertThat(LithoRenderUnit.getRenderUnit(mountItem).nodeInfo?.contentDescription)
        .isSameAs(contentDescription)
    assertThat(LithoRenderUnit.getRenderUnit(mountItem).nodeInfo?.clickHandler)
        .isSameAs(clickHandler)
    assertThat(LithoRenderUnit.getRenderUnit(mountItem).nodeInfo?.focusChangeHandler)
        .isSameAs(focusChangeHandler)
    assertThat(LithoRenderUnit.getRenderUnit(mountItem).nodeInfo?.touchHandler)
        .isSameAs(touchHandler)
    assertThat(LithoRenderUnit.getRenderUnit(mountItem).flags).isEqualTo(flags)
    assertThat(LithoRenderUnit.getRenderUnit(mountItem).importantForAccessibility)
        .isEqualTo(ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_YES)
  }

  @Test
  fun testFlags() {
    flags =
        (LithoRenderUnit.LAYOUT_FLAG_DUPLICATE_PARENT_STATE or
            LithoRenderUnit.LAYOUT_FLAG_DISABLE_TOUCHABLE)
    mountItem = create(content)
    assertThat(
            LithoRenderUnit.isDuplicateParentState(LithoRenderUnit.getRenderUnit(mountItem).flags))
        .isTrue
    assertThat(LithoRenderUnit.isTouchableDisabled(LithoRenderUnit.getRenderUnit(mountItem).flags))
        .isTrue
    flags = 0
    mountItem = create(content)
    assertThat(
            LithoRenderUnit.isDuplicateParentState(LithoRenderUnit.getRenderUnit(mountItem).flags))
        .isFalse
    assertThat(LithoRenderUnit.isTouchableDisabled(LithoRenderUnit.getRenderUnit(mountItem).flags))
        .isFalse
  }

  @Test
  fun testViewFlags() {
    val view = View(getApplicationContext())
    view.isClickable = true
    view.isEnabled = true
    view.isLongClickable = true
    view.isFocusable = false
    view.isSelected = false
    mountItem = create(view)
    assertThat(
            LithoMountData.isViewClickable(
                LithoMountData.getMountData(mountItem).defaultAttributeValuesFlags))
        .isTrue
    assertThat(
            LithoMountData.isViewEnabled(
                LithoMountData.getMountData(mountItem).defaultAttributeValuesFlags))
        .isTrue
    assertThat(
            LithoMountData.isViewLongClickable(
                LithoMountData.getMountData(mountItem).defaultAttributeValuesFlags))
        .isTrue
    assertThat(
            LithoMountData.isViewFocusable(
                LithoMountData.getMountData(mountItem).defaultAttributeValuesFlags))
        .isFalse
    assertThat(
            LithoMountData.isViewSelected(
                LithoMountData.getMountData(mountItem).defaultAttributeValuesFlags))
        .isFalse
    view.isClickable = false
    view.isEnabled = false
    view.isLongClickable = false
    view.isFocusable = true
    view.isSelected = true
    mountItem = create(view)
    assertThat(
            LithoMountData.isViewClickable(
                LithoMountData.getMountData(mountItem).defaultAttributeValuesFlags))
        .isFalse
    assertThat(
            LithoMountData.isViewEnabled(
                LithoMountData.getMountData(mountItem).defaultAttributeValuesFlags))
        .isFalse
    assertThat(
            LithoMountData.isViewLongClickable(
                LithoMountData.getMountData(mountItem).defaultAttributeValuesFlags))
        .isFalse
    assertThat(
            LithoMountData.isViewFocusable(
                LithoMountData.getMountData(mountItem).defaultAttributeValuesFlags))
        .isTrue
    assertThat(
            LithoMountData.isViewSelected(
                LithoMountData.getMountData(mountItem).defaultAttributeValuesFlags))
        .isTrue
  }

  @Test
  fun testIsAccessibleWithNullComponent() {
    val mountItem = create(content)
    assertThat(LithoRenderUnit.getRenderUnit(mountItem).isAccessible).isFalse
  }

  @Test
  fun testIsAccessibleWithAccessibleComponent() {
    val mountItem: MountItem =
        MountItemTestHelper.create(
            TestDrawableComponent.create(context, true, true, true /* implementsAccessibility */)
                .build(),
            content,
            nodeInfo,
            null,
            flags,
            ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_AUTO)
    assertThat(LithoRenderUnit.getRenderUnit(mountItem).isAccessible).isTrue
  }

  @Test
  fun testIsAccessibleWithDisabledAccessibleComponent() {
    val mountItem: MountItem =
        MountItemTestHelper.create(
            TestDrawableComponent.create(context, true, true, true /* implementsAccessibility */)
                .build(),
            content,
            nodeInfo,
            null,
            flags,
            ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_NO)
    assertThat(LithoRenderUnit.getRenderUnit(mountItem).isAccessible).isFalse
  }

  @Test
  fun testIsAccessibleWithAccessibilityEventHandler() {
    val mountItem: MountItem =
        MountItemTestHelper.create(
            TestDrawableComponent.create(context, true, true, true /* implementsAccessibility */)
                .build(),
            content,
            nodeInfo,
            null,
            flags,
            ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_AUTO)
    assertThat(LithoRenderUnit.getRenderUnit(mountItem).isAccessible).isTrue
  }

  @Test
  fun testIsAccessibleWithNonAccessibleComponent() {
    assertThat(LithoRenderUnit.getRenderUnit(mountItem).isAccessible).isFalse
  }

  @Test
  fun testUpdateDoesntChangeFlags() {
    val unit: LithoRenderUnit =
        MountSpecLithoRenderUnit.create(0, component, null, context, nodeInfo, 0, 0, STATE_UNKNOWN)
    val node: RenderTreeNode = create(unit, Rect(0, 0, 0, 0), null, null)
    val view = View(getApplicationContext())
    val mountItem = MountItem(node, view)
    mountItem.setMountData(LithoMountData(view))
    assertThat(
            LithoMountData.isViewClickable(
                LithoMountData.getMountData(mountItem).defaultAttributeValuesFlags))
        .isFalse
    view.isClickable = true
    mountItem.update(node)
    assertThat(
            LithoMountData.isViewClickable(
                LithoMountData.getMountData(mountItem).defaultAttributeValuesFlags))
        .isFalse
  }
}


/*
 * 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.LifecycleStep.StepInfo
import com.facebook.litho.testing.LegacyLithoViewRule
import com.facebook.litho.testing.LithoStatsRule
import com.facebook.litho.testing.exactly
import com.facebook.litho.testing.testrunner.LithoTestRunner
import com.facebook.litho.testing.unspecified
import com.facebook.litho.widget.MountSpecLifecycleTester
import com.facebook.litho.widget.MountSpecWithMountUnmountAssertion
import com.facebook.litho.widget.MountSpecWithMountUnmountAssertionSpec
import com.facebook.litho.widget.PreallocatedMountSpecLifecycleTester
import com.facebook.litho.widget.RecordsShouldUpdate
import com.facebook.litho.widget.SimpleStateUpdateEmulator
import com.facebook.litho.widget.SimpleStateUpdateEmulatorSpec
import com.facebook.litho.widget.Text
import com.facebook.rendercore.MountItemsPool
import com.facebook.rendercore.RunnableHandler
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
import org.robolectric.annotation.LooperMode
import org.robolectric.shadows.ShadowLooper

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

  @JvmField @Rule val legacyLithoViewRule: LegacyLithoViewRule = LegacyLithoViewRule()

  @JvmField @Rule val lithoStatsRule: LithoStatsRule = LithoStatsRule()

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

  @Test
  fun lifecycle_onSetComponentWithoutLayout_shouldNotCallLifecycleMethods() {
    val lifecycleTracker = LifecycleTracker()
    val component =
        MountSpecLifecycleTester.create(legacyLithoViewRule.context)
            .lifecycleTracker(lifecycleTracker)
            .build()
    legacyLithoViewRule.setRoot(component).idle()
    assertThat(lifecycleTracker.steps)
        .describedAs("Only render lifecycle methods should be called")
        .containsExactly(
            LifecycleStep.ON_CREATE_INITIAL_STATE,
            LifecycleStep.ON_CREATE_TREE_PROP,
            LifecycleStep.ON_CALCULATE_CACHED_VALUE,
            LifecycleStep.ON_PREPARE)
  }

  @Test
  fun lifecycle_onLayout_shouldCallLifecycleMethods() {
    val lifecycleTracker = LifecycleTracker()
    val component =
        MountSpecLifecycleTester.create(legacyLithoViewRule.context)
            .lifecycleTracker(lifecycleTracker)
            .intrinsicSize(Size(800, 600))
            .build()
    legacyLithoViewRule.setRoot(component)
    legacyLithoViewRule.attachToWindow().measure().layout()
    assertThat(lifecycleTracker.steps)
        .describedAs("Should call the lifecycle methods in expected order")
        .containsExactly(
            LifecycleStep.ON_CREATE_INITIAL_STATE,
            LifecycleStep.ON_CREATE_TREE_PROP,
            LifecycleStep.ON_CALCULATE_CACHED_VALUE,
            LifecycleStep.ON_PREPARE,
            LifecycleStep.ON_MEASURE,
            LifecycleStep.ON_BOUNDS_DEFINED,
            LifecycleStep.ON_ATTACHED,
            LifecycleStep.ON_CREATE_MOUNT_CONTENT,
            LifecycleStep.ON_MOUNT,
            LifecycleStep.ON_BIND)
  }

  @Test
  fun lifecycle_onLayoutWithExactSize_shouldCallLifecycleMethodsExceptMeasure() {
    val lifecycleTracker = LifecycleTracker()
    val component =
        MountSpecLifecycleTester.create(legacyLithoViewRule.context)
            .lifecycleTracker(lifecycleTracker)
            .intrinsicSize(Size(800, 600))
            .build()
    legacyLithoViewRule.setRoot(component)
    legacyLithoViewRule.setSizePx(600, 800).attachToWindow().measure().layout()
    assertThat(lifecycleTracker.steps)
        .describedAs("Should call the lifecycle methods in expected order")
        .containsExactly(
            LifecycleStep.ON_CREATE_INITIAL_STATE,
            LifecycleStep.ON_CREATE_TREE_PROP,
            LifecycleStep.ON_CALCULATE_CACHED_VALUE,
            LifecycleStep.ON_PREPARE,
            LifecycleStep.ON_BOUNDS_DEFINED,
            LifecycleStep.ON_ATTACHED,
            LifecycleStep.ON_CREATE_MOUNT_CONTENT,
            LifecycleStep.ON_MOUNT,
            LifecycleStep.ON_BIND)
  }

  @Test
  fun lifecycle_onDetach_shouldCallLifecycleMethods() {
    val lifecycleTracker = LifecycleTracker()
    val component =
        MountSpecLifecycleTester.create(legacyLithoViewRule.context)
            .intrinsicSize(Size(800, 600))
            .lifecycleTracker(lifecycleTracker)
            .build()
    legacyLithoViewRule.setRoot(component)
    legacyLithoViewRule.attachToWindow().measure().layout()
    lifecycleTracker.reset()
    legacyLithoViewRule.detachFromWindow()
    assertThat(lifecycleTracker.steps)
        .describedAs("Should only call")
        .containsExactly(LifecycleStep.ON_UNBIND)
  }

  @Test
  fun lifecycle_onReAttach_shouldCallLifecycleMethods() {
    val lifecycleTracker = LifecycleTracker()
    val component =
        MountSpecLifecycleTester.create(legacyLithoViewRule.context)
            .lifecycleTracker(lifecycleTracker)
            .intrinsicSize(Size(800, 600))
            .build()
    legacyLithoViewRule.setRoot(component)
    legacyLithoViewRule.attachToWindow().measure().layout().detachFromWindow()
    lifecycleTracker.reset()
    legacyLithoViewRule.attachToWindow().measure().layout()
    assertThat(lifecycleTracker.steps)
        .describedAs("Should only call")
        .containsExactly(LifecycleStep.ON_BIND)
  }

  @Test
  fun lifecycle_onRemeasureWithSameSpecs_shouldNotCallLifecycleMethods() {
    val lifecycleTracker = LifecycleTracker()
    val component =
        MountSpecLifecycleTester.create(legacyLithoViewRule.context)
            .intrinsicSize(Size(800, 600))
            .lifecycleTracker(lifecycleTracker)
            .build()
    legacyLithoViewRule.setRoot(component)
    legacyLithoViewRule.attachToWindow().measure().layout()
    lifecycleTracker.reset()
    legacyLithoViewRule.measure()
    assertThat(lifecycleTracker.steps)
        .describedAs("No lifecycle methods should be called")
        .isEmpty()
  }

  @Test
  fun lifecycle_onRemeasureWithDifferentSpecs_shouldCallLifecycleMethods() {
    val lifecycleTracker = LifecycleTracker()
    val component =
        MountSpecLifecycleTester.create(legacyLithoViewRule.context)
            .intrinsicSize(Size(800, 600))
            .lifecycleTracker(lifecycleTracker)
            .build()
    legacyLithoViewRule.setRoot(component)
    legacyLithoViewRule.attachToWindow().measure().layout()
    lifecycleTracker.reset()
    legacyLithoViewRule.setSizeSpecs(exactly(800), unspecified()).measure()
    assertThat(lifecycleTracker.steps)
        .describedAs("Should call the lifecycle methods in expected order")
        .containsExactly(LifecycleStep.ON_MEASURE, LifecycleStep.ON_BOUNDS_DEFINED)
  }

  @Test
  fun lifecycle_onRemeasureWithExactSize_shouldNotCallLifecycleMethods() {
    val lifecycleTracker = LifecycleTracker()
    val component =
        MountSpecLifecycleTester.create(legacyLithoViewRule.context)
            .lifecycleTracker(lifecycleTracker)
            .intrinsicSize(Size(800, 600))
            .build()
    legacyLithoViewRule.setRoot(component)
    legacyLithoViewRule.attachToWindow().measure().layout()
    lifecycleTracker.reset()
    legacyLithoViewRule.setSizePx(800, 600).measure()
    assertThat(lifecycleTracker.steps)
        .describedAs(
            "No lifecycle methods should be called because EXACT measures should skip layout calculation.")
        .isEmpty()
  }

  @Test
  fun lifecycle_onReLayoutAfterMeasureWithExactSize_shouldCallLifecycleMethods() {
    val lifecycleTracker = LifecycleTracker()
    val component =
        MountSpecLifecycleTester.create(legacyLithoViewRule.context)
            .lifecycleTracker(lifecycleTracker)
            .intrinsicSize(Size(800, 600))
            .build()
    legacyLithoViewRule.setRoot(component)
    legacyLithoViewRule.attachToWindow().measure().layout()
    lifecycleTracker.reset()
    legacyLithoViewRule.setSizePx(800, 600).measure().layout()
    assertThat(lifecycleTracker.steps)
        .describedAs("Should call the lifecycle methods in expected order")
        .containsExactly(
            LifecycleStep.ON_BOUNDS_DEFINED,
            LifecycleStep.ON_UNBIND,
            LifecycleStep.ON_UNMOUNT,
            LifecycleStep.ON_MOUNT,
            LifecycleStep.ON_BIND)
  }

  @Test
  fun lifecycle_onReLayoutAfterMeasureWithExactSizeAsNonRoot_shouldCallLifecycleMethods() {
    val lifecycleTracker = LifecycleTracker()
    val component =
        MountSpecLifecycleTester.create(legacyLithoViewRule.context)
            .intrinsicSize(Size(800, 600))
            .lifecycleTracker(lifecycleTracker)
            .build()
    legacyLithoViewRule.setRoot(Column.create(legacyLithoViewRule.context).child(component).build())
    legacyLithoViewRule.attachToWindow().measure().layout()
    lifecycleTracker.reset()
    legacyLithoViewRule.setSizePx(800, 600).measure().layout()
    assertThat(lifecycleTracker.steps)
        .describedAs("Should call the lifecycle methods in expected order")
        .containsExactly(
            LifecycleStep.ON_MEASURE,
            LifecycleStep.ON_BOUNDS_DEFINED,
            LifecycleStep.ON_UNBIND,
            LifecycleStep.ON_UNMOUNT,
            LifecycleStep.ON_MOUNT,
            LifecycleStep.ON_BIND)
  }

  @Test
  fun lifecycle_onSetShallowCopy_shouldNotCallLifecycleMethods() {
    val lifecycleTracker = LifecycleTracker()
    val component =
        MountSpecLifecycleTester.create(legacyLithoViewRule.context)
            .intrinsicSize(Size(800, 600))
            .lifecycleTracker(lifecycleTracker)
            .build()
    legacyLithoViewRule.setRoot(component)
    legacyLithoViewRule.attachToWindow().measure().layout()
    lifecycleTracker.reset()
    legacyLithoViewRule.setRoot(component.makeShallowCopy())
    assertThat(lifecycleTracker.steps)
        .describedAs("No lifecycle methods should be called")
        .isEmpty()
  }

  @Test
  fun lifecycle_onRemeasureWithCompatibleSpecs_shouldNotRemount() {
    val lifecycleTracker = LifecycleTracker()
    val component =
        MountSpecLifecycleTester.create(legacyLithoViewRule.context)
            .intrinsicSize(Size(800, 600))
            .lifecycleTracker(lifecycleTracker)
            .build()
    legacyLithoViewRule.setRoot(component).measure().layout().attachToWindow()
    lifecycleTracker.reset()

    // Force measure call to propagate to ComponentTree
    legacyLithoViewRule.lithoView.requestLayout()
    legacyLithoViewRule.measure().layout()
    assertThat(lifecycleTracker.steps)
        .describedAs("No lifecycle methods should be called because measure was compatible")
        .isEmpty()
  }

  @Test
  fun lifecycle_onSetSemanticallySimilarComponent_shouldCallLifecycleMethods() {
    val lifecycleTracker = LifecycleTracker()
    val component =
        MountSpecLifecycleTester.create(legacyLithoViewRule.context)
            .intrinsicSize(Size(800, 600))
            .lifecycleTracker(lifecycleTracker)
            .build()
    legacyLithoViewRule.setRoot(component)
    legacyLithoViewRule.attachToWindow().measure().layout()
    lifecycleTracker.reset()
    val newLifecycleTracker = LifecycleTracker()
    legacyLithoViewRule.setRoot(
        MountSpecLifecycleTester.create(legacyLithoViewRule.context)
            .intrinsicSize(Size(800, 600))
            .lifecycleTracker(newLifecycleTracker)
            .build())
    legacyLithoViewRule.measure().layout()
    assertThat(lifecycleTracker.steps)
        .describedAs("Should call the lifecycle methods on old instance in expected order")
        .containsExactly(LifecycleStep.ON_UNBIND, LifecycleStep.ON_UNMOUNT)
    assertThat(newLifecycleTracker.steps)
        .describedAs("Should call the lifecycle methods on new instance in expected order")
        .containsExactly(
            LifecycleStep.ON_CREATE_TREE_PROP,
            LifecycleStep.ON_CALCULATE_CACHED_VALUE,
            LifecycleStep.ON_PREPARE,
            LifecycleStep.ON_MEASURE,
            LifecycleStep.ON_BOUNDS_DEFINED,
            LifecycleStep.ON_MOUNT,
            LifecycleStep.ON_BIND)
  }

  @Test
  fun onSetRootWithPreallocatedMountContent_shouldCallLifecycleMethods() {
    val looper = ShadowLooper.getLooperForThread(Thread.currentThread())
    val tree =
        ComponentTree.create(legacyLithoViewRule.context)
            .shouldPreallocateMountContentPerMountSpec(true)
            .preAllocateMountContentHandler(RunnableHandler.DefaultHandler(looper))
            .build()
    legacyLithoViewRule.useComponentTree(tree)
    val info: List<StepInfo> = ArrayList<StepInfo>()
    val component =
        PreallocatedMountSpecLifecycleTester.create(legacyLithoViewRule.context).steps(info).build()
    legacyLithoViewRule.componentTree.setRootAndSizeSpecSync(
        component, legacyLithoViewRule.widthSpec, legacyLithoViewRule.heightSpec)
    legacyLithoViewRule.measure()
    ShadowLooper.runUiThreadTasks()
    assertThat(LifecycleStep.getSteps(info))
        .describedAs("Should call the lifecycle methods on new instance in expected order")
        .containsExactly(
            LifecycleStep.ON_PREPARE,
            LifecycleStep.ON_MEASURE,
            LifecycleStep.ON_BOUNDS_DEFINED,
            LifecycleStep.ON_ATTACHED)
  }

  @Test
  fun onSetRootWithPreallocatedMountContent_shouldCallLifecycleMethodsInRenderCore() {
    val looper = ShadowLooper.getLooperForThread(Thread.currentThread())
    val tree =
        ComponentTree.create(legacyLithoViewRule.context)
            .shouldPreallocateMountContentPerMountSpec(true)
            .preAllocateMountContentHandler(RunnableHandler.DefaultHandler(looper))
            .build()
    legacyLithoViewRule.useComponentTree(tree)
    val info: List<StepInfo> = ArrayList<StepInfo>()
    val component =
        PreallocatedMountSpecLifecycleTester.create(legacyLithoViewRule.context).steps(info).build()
    legacyLithoViewRule.componentTree.setRootAndSizeSpecSync(
        component, legacyLithoViewRule.widthSpec, legacyLithoViewRule.heightSpec)
    legacyLithoViewRule.measure()
    ShadowLooper.runUiThreadTasks()
    assertThat(LifecycleStep.getSteps(info))
        .describedAs("Should call the lifecycle methods on new instance in expected order")
        .containsExactly(
            LifecycleStep.ON_PREPARE,
            LifecycleStep.ON_MEASURE,
            LifecycleStep.ON_BOUNDS_DEFINED,
            LifecycleStep.ON_ATTACHED)
    assertThat(MountItemsPool.getMountItemPools().size)
        .describedAs("Should contain only 1 content pool")
        .isEqualTo(1)
  }

  @Test
  fun shouldUpdate_shouldUpdateIsCalled_prevAndNextAreInRightOrder() {
    val firstObject = Any()
    val shouldUpdateCalls: List<Diff<Any>> = ArrayList()
    legacyLithoViewRule
        .setRoot(
            RecordsShouldUpdate.create(legacyLithoViewRule.context)
                .shouldUpdateCalls(shouldUpdateCalls)
                .testProp(firstObject)
                .build())
        .measure()
        .layout()
        .attachToWindow()
    val secondObject = Any()
    legacyLithoViewRule
        .setRoot(
            RecordsShouldUpdate.create(legacyLithoViewRule.context)
                .shouldUpdateCalls(shouldUpdateCalls)
                .testProp(secondObject)
                .build())
        .measure()
        .layout()
    assertThat(shouldUpdateCalls).hasSize(1)
    assertThat(shouldUpdateCalls[0].previous).isEqualTo(firstObject)
    assertThat(shouldUpdateCalls[0].next).isEqualTo(secondObject)
  }

  /*
   * This case comes from a specific bug where when we shallow-copy components (which we do when we
   * update state) we were setting mHasManualKey to false even if there was a manual key which would
   * cause us to generate different keys between layouts.
   */
  @Test
  fun lifecycle_stateUpdateWithMultipleChildrenOfSameTypeAndManualKeys_doesNotRecreateInitialState() {
    val info_child1 = LifecycleTracker()
    val info_child2 = LifecycleTracker()
    val stateUpdater = SimpleStateUpdateEmulatorSpec.Caller()
    val root =
        Column.create(legacyLithoViewRule.context)
            .child(
                MountSpecLifecycleTester.create(legacyLithoViewRule.context)
                    .intrinsicSize(Size(800, 600))
                    .lifecycleTracker(info_child1)
                    .key("some_key"))
            .child(
                MountSpecLifecycleTester.create(legacyLithoViewRule.context)
                    .intrinsicSize(Size(800, 600))
                    .lifecycleTracker(info_child2)
                    .key("other_key"))
            .child(
                SimpleStateUpdateEmulator.create(legacyLithoViewRule.context).caller(stateUpdater))
            .build()
    legacyLithoViewRule.useComponentTree(
        ComponentTree.create(legacyLithoViewRule.context).isReconciliationEnabled(false).build())
    legacyLithoViewRule.setRoot(root).attachToWindow().measure().layout()
    val mountDelegateTarget = legacyLithoViewRule.lithoView.getMountDelegateTarget()
    assertThat(mountDelegateTarget.getMountItemCount()).isGreaterThan(1)
    info_child1.reset()
    info_child2.reset()
    stateUpdater.increment()
    assertThat(info_child1.steps)
        .describedAs("Should not recreate initial state.")
        .doesNotContain(LifecycleStep.ON_CREATE_INITIAL_STATE)
    assertThat(info_child2.steps)
        .describedAs("Should not recreate initial state.")
        .doesNotContain(LifecycleStep.ON_CREATE_INITIAL_STATE)
  }

  /**
   * This test case captures the scenario where unmount can get called on a component for which
   * mount was never invoked. 1. A layout is mounted. 2. The next layout update does not cause a new
   * mount pass. 3. When the next mount pass is triggered, an item is unmounted as it is out of the
   * view port. 4. The unmount must be called on the old (currently) mounted component.
   */
  @Test
  fun whenItemsAreUmounted_thenUnmountMustbeInvokedOnTheCurrentlyMountedComponent() {
    val c = legacyLithoViewRule.context
    val initialComponent =
        Column.create(c)
            .heightPx(200)
            .child(Text.create(c).text("1").heightPx(100))
            .child(
                MountSpecWithMountUnmountAssertion.create(c)
                    .container(MountSpecWithMountUnmountAssertionSpec.Container())
                    .heightPx(100))
            .build()
    val initialComponentTree = ComponentTree.create(c, initialComponent).build()
    val lithoView = LithoView(c.androidContext)

    // Mount a layout with the component.
    lithoView.componentTree = initialComponentTree
    lithoView.measure(exactly(100), exactly(200))
    lithoView.layout(0, 0, 100, 200)

    // Assert that the view is mounted
    assertThat(lithoView.childCount).isEqualTo(1)
    val newComponent =
        Column.create(c)
            .heightPx(200)
            .child(Text.create(c).text("1").heightPx(100))
            .child(
                MountSpecWithMountUnmountAssertion.create(c)
                    .container(MountSpecWithMountUnmountAssertionSpec.Container())
                    .heightPx(100)
                    .build())
            .build()

    // Create a new component tree so that state is recreated
    // but use the id maps, and component tree id from the initial
    // tree so that new ids match the current one
    val newComponentTree =
        ComponentTree.create(c, newComponent)
            .overrideRenderUnitIdMap(initialComponentTree)
            .overrideComponentTreeId(initialComponentTree.mId)
            .build()
    lithoView.componentTree = newComponentTree

    // Mount a new layout, but with a shorter height, to make the item unmount
    lithoView.measure(exactly(100), exactly(95))
    lithoView.layout(0, 0, 100, 95)

    // Assert that the items is unmounted.
    assertThat(lithoView.childCount).isEqualTo(0)
  }

  @Test
  fun mountTimeLifecycleMethodsShouldBeCalledInExpectedOrder() {
    val root =
        Column.create(legacyLithoViewRule.context)
            .child(
                MountSpecWithMountUnmountAssertion.create(legacyLithoViewRule.context)
                    .viewTag("tag")
                    .hasTagSet(true)
                    .container(MountSpecWithMountUnmountAssertionSpec.Container()))
            .build()
    legacyLithoViewRule.attachToWindow().setRoot(root).measure().layout()
    legacyLithoViewRule.lithoView.unmountAllItems()
  }
}