/*
 * 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.sections.SectionContext
import com.facebook.litho.sections.common.SingleComponentSection
import com.facebook.litho.sections.widget.RecyclerCollectionComponent
import com.facebook.litho.testing.LegacyLithoViewRule
import com.facebook.litho.testing.testrunner.LithoTestRunner
import com.facebook.litho.widget.MountSpecTriggerTester
import com.facebook.litho.widget.Text
import java.util.ArrayList
import java.util.concurrent.atomic.AtomicReference
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 MountSpecTriggerTest {

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

  @Test
  fun mountSpec_setRootAndTriggerEvent_eventIsTriggered() {
    val componentContext = legacyLithoViewRule.context
    val triggerObjectRef = AtomicReference<Any>()
    val triggerHandle = Handle()
    val info: List<StepInfo> = ArrayList<StepInfo>()
    val component =
        MountSpecTriggerTester.create(componentContext)
            .steps(info)
            .triggerObjectRef(triggerObjectRef)
            .handle(triggerHandle)
            .build()
    legacyLithoViewRule.setRoot(component).idle()
    legacyLithoViewRule.attachToWindow().measure().layout()
    val bazObject = Any()
    MountSpecTriggerTester.triggerTestEvent(
        legacyLithoViewRule.componentTree.context, triggerHandle, bazObject)
    assertThat(LifecycleStep.getSteps(info))
        .describedAs("Should call @OnTrigger method")
        .containsExactly(LifecycleStep.ON_TRIGGER)
    assertThat(triggerObjectRef.get())
        .describedAs("Event object is correctly passed")
        .isEqualTo(bazObject)
  }

  @Test
  fun mountSpec_setRootAndTriggerEvent_eventIsTriggered_handle_used_in_child() {
    val componentContext = legacyLithoViewRule.context
    val triggerObjectRef = AtomicReference<Any>()
    val triggerHandle = Handle()
    val info: List<StepInfo> = ArrayList<StepInfo>()
    val component =
        Column.create(componentContext)
            .child(Text.create(componentContext).text("Sample"))
            .child(
                MountSpecTriggerTester.create(componentContext)
                    .steps(info)
                    .triggerObjectRef(triggerObjectRef)
                    .handle(triggerHandle)
                    .build())
            .build()
    legacyLithoViewRule.setRoot(component)
    legacyLithoViewRule.attachToWindow().measure().layout()
    val bazObject = Any()
    MountSpecTriggerTester.triggerTestEvent(
        legacyLithoViewRule.componentTree.context, triggerHandle, bazObject)
    assertThat(LifecycleStep.getSteps(info))
        .describedAs("Should call @OnTrigger method")
        .containsExactly(LifecycleStep.ON_TRIGGER)
    assertThat(triggerObjectRef.get())
        .describedAs("Event object is correctly passed")
        .isEqualTo(bazObject)
  }

  @Test
  fun mountSpec_setRootAndTriggerEvent_eventIsTriggered_handle_used_in_nested_tree_root() {
    val componentContext = legacyLithoViewRule.context
    val triggerObjectRef = AtomicReference<Any>()
    val triggerHandle = Handle()
    val info: List<StepInfo> = ArrayList<StepInfo>()
    val component =
        Column.create(componentContext)
            .child(Text.create(componentContext).text("Sample"))
            .child(
                RecyclerCollectionComponent.create(componentContext)
                    .heightPx(150)
                    .section(
                        SingleComponentSection.create(SectionContext(componentContext))
                            .component(
                                MountSpecTriggerTester.create(componentContext)
                                    .steps(info)
                                    .triggerObjectRef(triggerObjectRef)
                                    .handle(triggerHandle)
                                    .build())))
            .build()
    legacyLithoViewRule.setRoot(component)
    legacyLithoViewRule.attachToWindow().measure().layout()
    val bazObject = Any()
    MountSpecTriggerTester.triggerTestEvent(
        legacyLithoViewRule.componentTree.context, triggerHandle, bazObject)
    assertThat(LifecycleStep.getSteps(info))
        .describedAs("Should call @OnTrigger method")
        .containsExactly(LifecycleStep.ON_TRIGGER)
    assertThat(triggerObjectRef.get())
        .describedAs("Event object is correctly passed")
        .isEqualTo(bazObject)
  }

  @Test
  fun mountSpec_setRootAndTriggerEvent_eventIsTriggered_handle_used_in_nested_tree_deeper_in_hierarchy() {
    val componentContext = legacyLithoViewRule.context
    val triggerObjectRef = AtomicReference<Any>()
    val triggerHandle = Handle()
    val info: List<StepInfo> = ArrayList<StepInfo>()
    val component =
        Column.create(componentContext)
            .child(Text.create(componentContext).text("Sample"))
            .child(
                RecyclerCollectionComponent.create(componentContext)
                    .heightPx(150)
                    .section(
                        SingleComponentSection.create(SectionContext(componentContext))
                            .component(
                                Column.create(componentContext)
                                    .child(Text.create(componentContext).text("Nested tree child"))
                                    .child(
                                        MountSpecTriggerTester.create(componentContext)
                                            .steps(info)
                                            .triggerObjectRef(triggerObjectRef)
                                            .handle(triggerHandle)
                                            .build()))))
            .build()
    legacyLithoViewRule.setRoot(component)
    legacyLithoViewRule.attachToWindow().measure().layout()
    val bazObject = Any()
    MountSpecTriggerTester.triggerTestEvent(
        legacyLithoViewRule.componentTree.context, triggerHandle, bazObject)
    assertThat(LifecycleStep.getSteps(info))
        .describedAs("Should call @OnTrigger method")
        .containsExactly(LifecycleStep.ON_TRIGGER)
    assertThat(triggerObjectRef.get())
        .describedAs("Event object is correctly passed")
        .isEqualTo(bazObject)
  }
}


/*
 * 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.TestDrawableComponent
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.yoga.YogaAlign
import com.facebook.yoga.YogaEdge
import com.facebook.yoga.YogaJustify
import org.assertj.core.api.Assertions.assertThat
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith

@RunWith(LithoTestRunner::class)
class MountStateBoundsTest {

  private lateinit var context: ComponentContext

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

  @Test
  fun testMountedDrawableBounds() {
    val lithoView =
        ComponentTestHelper.mountComponent(
            context,
            object : InlineLayoutSpec() {
              override fun onCreateLayout(c: ComponentContext): Component =
                  TestDrawableComponent.create(c).widthPx(10).heightPx(10).build()
            })
    assertThat(lithoView.drawables[0].bounds).isEqualTo(Rect(0, 0, 10, 10))
  }

  @Test
  fun testMountedViewBounds() {
    val lithoView =
        ComponentTestHelper.mountComponent(
            context,
            object : InlineLayoutSpec() {
              override fun onCreateLayout(c: ComponentContext): Component =
                  TestViewComponent.create(c).widthPx(10).heightPx(10).build()
            })
    val mountedView = lithoView.getChildAt(0)
    assertThat(Rect(mountedView.left, mountedView.top, mountedView.right, mountedView.bottom))
        .isEqualTo(Rect(0, 0, 10, 10))
  }

  @Test
  fun testInnerComponentHostBounds() {
    val lithoView =
        ComponentTestHelper.mountComponent(
            context,
            object : InlineLayoutSpec() {
              override fun onCreateLayout(c: ComponentContext): Component =
                  Column.create(c)
                      .child(
                          Column.create(c)
                              .widthPx(20)
                              .heightPx(20)
                              .wrapInView()
                              .child(TestDrawableComponent.create(c).widthPx(10).heightPx(10)))
                      .build()
            })
    val host = lithoView.getChildAt(0) as ComponentHost
    assertThat(host.drawables[0].bounds).isEqualTo(Rect(0, 0, 10, 10))
    assertThat(Rect(host.left, host.top, host.right, host.bottom)).isEqualTo(Rect(0, 0, 20, 20))
  }

  @Test
  fun testDoubleInnerComponentHostBounds() {
    val lithoView =
        ComponentTestHelper.mountComponent(
            context,
            object : InlineLayoutSpec() {
              override fun onCreateLayout(c: ComponentContext): Component =
                  Column.create(c)
                      .alignItems(YogaAlign.FLEX_END)
                      .justifyContent(YogaJustify.FLEX_END)
                      .child(
                          Column.create(c)
                              .widthPx(100)
                              .heightPx(100)
                              .paddingPx(YogaEdge.ALL, 20)
                              .wrapInView()
                              .child(
                                  Column.create(c)
                                      .widthPx(60)
                                      .heightPx(60)
                                      .wrapInView()
                                      .child(
                                          TestDrawableComponent.create(c)
                                              .widthPx(20)
                                              .heightPx(20)
                                              .marginPx(YogaEdge.ALL, 20))))
                      .build()
            },
            200,
            200)
    val host = lithoView.getChildAt(0) as ComponentHost
    val nestedHost = host.getChildAt(0) as ComponentHost
    assertThat(Rect(host.left, host.top, host.right, host.bottom))
        .isEqualTo(Rect(100, 100, 200, 200))
    assertThat(nestedHost.drawables[0].bounds).isEqualTo(Rect(20, 20, 40, 40))
    assertThat(Rect(nestedHost.left, nestedHost.top, nestedHost.right, nestedHost.bottom))
        .isEqualTo(Rect(20, 20, 80, 80))
  }
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.facebook.litho

import android.content.Context
import androidx.test.core.app.ApplicationProvider
import com.facebook.litho.config.TempComponentsConfigurations
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.SimpleMountSpecTester
import org.assertj.core.api.Assertions.assertThat
import org.junit.After
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith

@RunWith(LithoTestRunner::class)
class MountStateFocusableTest {

  private lateinit var context: ComponentContext
  private var focusableDefault = false

  @Before
  fun setup() {
    TempComponentsConfigurations.setShouldAddHostViewForRootComponent(true)
    context =
        ComponentContext(
            androidx.test.core.app.ApplicationProvider.getApplicationContext<Context>())
    focusableDefault = ComponentHost(context).isFocusable
  }

  @Test
  fun testInnerComponentHostFocusable() {
    val lithoView =
        ComponentTestHelper.mountComponent(
            context,
            object : InlineLayoutSpec() {
              override fun onCreateLayout(c: ComponentContext): Component? =
                  Column.create(c)
                      .child(Column.create(c).focusable(true).child(TestViewComponent.create(c)))
                      .build()
            })
    assertThat(lithoView.childCount).isEqualTo(1)
    // TODO(T16959291): The default varies between internal and external test runs, which indicates
    // that our Robolectric setup is not actually identical. Until we can figure out why,
    // we will compare against the dynamic default instead of asserting false.
    assertThat(lithoView.isFocusable).isEqualTo(focusableDefault)
    val innerHost = lithoView.getChildAt(0) as ComponentHost
    assertThat(innerHost.isFocusable).isTrue
  }

  @Test
  fun testRootHostFocusable() {
    val lithoView =
        ComponentTestHelper.mountComponent(
            context,
            object : InlineLayoutSpec() {
              override fun onCreateLayout(c: ComponentContext): Component? =
                  Column.create(c).focusable(true).child(SimpleMountSpecTester.create(c)).build()
            })
    assertThat(lithoView.childCount).isEqualTo(1)
    val innerHost = lithoView.getChildAt(0)
    assertThat(innerHost.isFocusable).isTrue
  }

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


/*
 * 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 com.facebook.litho.animation.AnimatedProperties
import com.facebook.litho.sections.SectionContext
import com.facebook.litho.sections.common.TestSingleComponentListSection
import com.facebook.litho.sections.widget.ListRecyclerConfiguration
import com.facebook.litho.sections.widget.RecyclerBinderConfiguration
import com.facebook.litho.sections.widget.RecyclerCollectionComponent
import com.facebook.litho.testing.LegacyLithoViewRule
import com.facebook.litho.testing.TransitionTestRule
import com.facebook.litho.testing.exactly
import com.facebook.litho.testing.testrunner.LithoTestRunner
import com.facebook.litho.widget.LithoViewFactory
import com.facebook.litho.widget.MountSpecLifecycleTester
import com.facebook.litho.widget.SectionsRecyclerView
import com.facebook.litho.widget.TestAnimationsComponent
import com.facebook.litho.widget.TestComponentProvider
import com.facebook.yoga.YogaAlign
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 MountStateIncrementalMountWithTransitionsTest {

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

  private val stateCaller = StateCaller()

  @Test
  fun incrementalMount_componentOffScreen_mountIfAnimating() {
    val lifecycleTracker1 = LifecycleTracker()
    val lifecycleTracker2 = LifecycleTracker()
    val lifecycleTracker3 = LifecycleTracker()
    val trackers = ArrayList<LifecycleTracker>()
    trackers.add(lifecycleTracker1)
    trackers.add(lifecycleTracker2)
    trackers.add(lifecycleTracker3)
    val root = getPartiallyVisibleRootWithAnimatingComponents(trackers)
    legacyLithoViewRule.setRoot(root).setSizeSpecs(exactly(1_040), exactly(60))
    legacyLithoViewRule.attachToWindow().measure().layout()
    val lithoViews: List<LithoView> = ArrayList()
    val sectionsRecyclerView = legacyLithoViewRule.lithoView.getChildAt(0) as SectionsRecyclerView
    sectionsRecyclerView.obtainLithoViewChildren(lithoViews)

    // grab the 2nd litho-view in the list (0 = sticky header, 1 = 1st litho view)
    val animatingLithoView = lithoViews[2]
    animatingLithoView.onAttachedToWindowForTest()
    stateCaller.update()
    legacyLithoViewRule.idle()
    assertThat(lifecycleTracker1.steps).contains(LifecycleStep.ON_MOUNT)
    assertThat(lifecycleTracker2.steps).contains(LifecycleStep.ON_MOUNT)
    assertThat(lifecycleTracker3.steps).contains(LifecycleStep.ON_MOUNT)
  }

  @Test
  fun incrementalMount_animatingComponentWithChildrenLithoView_mountLithoViewsOffScreen() {

    /*
      TODO: Fix the broken test for RCMS

      Root cause is litho MountState applyMountBinders()

      LMS calls TE onMountItem after bind, and after bounds are set.
      RCMS also calls it but before bind and before bounds are set (by design).

      So, in the test:

      LMS, after the Recycler is mounted, bound, and its size set the TE calls
      notify on it which causes the internal LV's to mount.

      RCMS, after the Recycler is mounted, but before being bound, and setting
      size, TE calls notify on it; this means that the size of the view is zero,
      and when the AnimatedRootHost.notifyVisibleBoundsChanged() nothing is mounted
      in the LV
    */
    val lifecycleTracker0 = LifecycleTracker()
    val lifecycleTracker1 = LifecycleTracker()
    val component0 =
        MountSpecLifecycleTester.create(legacyLithoViewRule.context)
            .lifecycleTracker(lifecycleTracker0)
            .intrinsicSize(Size(10, 40))
            .build()
    val animatingComponents: MutableList<Component> = ArrayList()
    animatingComponents.add(
        MountSpecLifecycleTester.create(legacyLithoViewRule.context)
            .lifecycleTracker(lifecycleTracker1)
            .intrinsicSize(Size(40, 40))
            .widthPx(40)
            .heightPx(40)
            .build())
    val sectionContext = SectionContext(legacyLithoViewRule.context)
    val binderConfig =
        RecyclerBinderConfiguration.create().lithoViewFactory(lithoViewFactory).build()
    val config =
        ListRecyclerConfiguration.create()
            .orientation(0)
            .recyclerBinderConfiguration(binderConfig)
            .build()
    val childOfAnimatingComponent =
        RecyclerCollectionComponent.create(legacyLithoViewRule.context)
            .widthPx(40)
            .heightPx(40)
            .section(
                TestSingleComponentListSection.create(sectionContext)
                    .data(animatingComponents)
                    .build())
            .recyclerConfiguration(config)
            .build()
    val partiallyVisibleAnimatingComponent: TestComponentProvider =
        object : TestComponentProvider {
          override fun getComponent(componentContext: ComponentContext, state: Boolean): Component =
              if (!state) {
                Column.create(componentContext)
                    .alignItems(YogaAlign.FLEX_END)
                    .child(
                        Column.create(componentContext)
                            .flexGrow(1f)
                            .child(component0)
                            .transitionKey("transitionkey_root")
                            .build())
                    .build()
              } else {
                Column.create(componentContext)
                    .alignItems(YogaAlign.FLEX_START)
                    .child(
                        Column.create(componentContext)
                            .flexGrow(1f)
                            .child(component0)
                            .child(childOfAnimatingComponent)
                            .transitionKey("transitionkey_root")
                            .build())
                    .build()
              }
        }
    val root =
        TestAnimationsComponent(
            stateCaller,
            Transition.sequence(
                Transition.create("transitionkey_root")
                    .animator(Transition.timing(144))
                    .animate(AnimatedProperties.X))) { state ->
              requireNotNull(partiallyVisibleAnimatingComponent.getComponent(context, state))
            }
    legacyLithoViewRule.setRoot(root).setSizeSpecs(exactly(40), exactly(80))
    legacyLithoViewRule.attachToWindow().measure().layout()
    assertThat(lifecycleTracker0.isMounted).isTrue
    assertThat(lifecycleTracker1.isMounted).isFalse
    stateCaller.update()
    legacyLithoViewRule.idle()
    assertThat(lifecycleTracker1.isMounted).isTrue
  }

  fun getPartiallyVisibleRootWithAnimatingComponents(
      animatingComponentTrackers: List<LifecycleTracker>
  ): Component {
    val animatingComponents: MutableList<Component> = ArrayList()
    for (tracker in animatingComponentTrackers) {
      animatingComponents.add(
          MountSpecLifecycleTester.create(legacyLithoViewRule.context)
              .lifecycleTracker(tracker)
              .intrinsicSize(Size(40, 40))
              .build())
    }
    val partiallyVisibleAnimatingComponent: TestComponentProvider =
        object : TestComponentProvider {
          override fun getComponent(componentContext: ComponentContext, state: Boolean): Component {
            val builder =
                Column.create(componentContext)
                    .alignItems(if (state) YogaAlign.FLEX_START else YogaAlign.FLEX_END)
            val size = animatingComponents.size
            for (i in 0 until size) {
              builder.child(
                  Row.create(componentContext)
                      .child(animatingComponents[i])
                      .heightDip(40f)
                      .widthDip(40f)
                      .backgroundColor(Color.parseColor("#ee1111"))
                      .transitionKey("transitionkey_$i")
                      .viewTag(RED_TRANSITION_KEY)
                      .build())
            }
            return builder.build()
          }
        }
    val transitions = arrayOfNulls<Transition>(animatingComponents.size)
    val size = animatingComponents.size
    for (i in 0 until size) {
      transitions[i] =
          Transition.create("transitionkey_$i")
              .animator(Transition.timing(144))
              .animate(AnimatedProperties.X)
    }
    val component =
        TestAnimationsComponent(stateCaller, Transition.sequence(*transitions)) { state ->
          requireNotNull(partiallyVisibleAnimatingComponent.getComponent(context, state))
        }
    val data: MutableList<Component> = ArrayList()
    data.add(
        Row.create(legacyLithoViewRule.context)
            .heightDip(40f)
            .widthDip(40f)
            .backgroundColor(Color.parseColor("#ee1111"))
            .build())
    data.add(component)
    val sectionContext = SectionContext(legacyLithoViewRule.context)
    val binderConfig =
        RecyclerBinderConfiguration.create().lithoViewFactory(lithoViewFactory).build()
    val config =
        ListRecyclerConfiguration.create().recyclerBinderConfiguration(binderConfig).build()
    val root =
        RecyclerCollectionComponent.create(legacyLithoViewRule.context)
            .section(TestSingleComponentListSection.create(sectionContext).data(data).build())
            .recyclerConfiguration(config)
            .build()
    return root
  }

  private val lithoViewFactory: LithoViewFactory
    get() = LithoViewFactory { context -> LithoView(context) }

  companion object {
    const val RED_TRANSITION_KEY = "red"
    const val GREEN_TRANSITION_KEY = "green"
    const val BLUE_TRANSITION_KEY = "blue"
  }
}


/*
 * 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.LegacyLithoViewRule
import com.facebook.litho.testing.testrunner.LithoTestRunner
import com.facebook.litho.widget.CardHeaderComponent
import org.assertj.core.api.Assertions.assertThat
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith

@RunWith(LithoTestRunner::class)
class MountStateMountTest {

  @JvmField @Rule val legacyLithoViewRule: LegacyLithoViewRule = LegacyLithoViewRule()
  private lateinit var context: ComponentContext

  @Before
  fun setup() {
    context = legacyLithoViewRule.context
  }

  @Test
  fun unmountAll_mountStateNeedsRemount() {
    val root =
        Column.create(context).child(CardHeaderComponent.create(context).title("Title")).build()
    legacyLithoViewRule.setRoot(root).attachToWindow().measure().layout()
    val mountDelegateTarget = legacyLithoViewRule.lithoView.mountDelegateTarget
    assertThat(mountDelegateTarget.needsRemount()).isFalse
    legacyLithoViewRule.lithoView.unmountAllItems()
    assertThat(mountDelegateTarget.needsRemount()).isTrue
  }
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.facebook.litho

import android.content.Context
import androidx.test.core.app.ApplicationProvider
import com.facebook.litho.config.TempComponentsConfigurations
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.SimpleMountSpecTester
import org.assertj.core.api.Assertions.assertThat
import org.junit.After
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith

@RunWith(LithoTestRunner::class)
class MountStateRemountEventHandlerTest {

  private lateinit var context: ComponentContext

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

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

  @Test
  fun testReuseLongClickListenerOnSameView() {
    // Needed because of (legacy) usage two different InlineLayoutSpec that the test wants to treat
    // as the same component type.
    val COMPONENT_IDENTITY = 12_345
    val lithoView =
        ComponentTestHelper.mountComponent(
            context,
            object : InlineLayoutSpec(COMPONENT_IDENTITY) {
              override fun onCreateLayout(c: ComponentContext): Component =
                  Column.create(c)
                      .longClickHandler(c.newEventHandler(1) as? EventHandler<LongClickEvent>)
                      .child(SimpleMountSpecTester.create(c))
                      .child(SimpleMountSpecTester.create(c))
                      .build()
            })
    assertThat(lithoView.childCount).isEqualTo(1)
    val longClickListener =
        LithoViewAttributesExtension.getComponentLongClickListener(lithoView.getChildAt(0))
    assertThat(longClickListener).isNotNull
    lithoView.componentTree?.root =
        object : InlineLayoutSpec(COMPONENT_IDENTITY) {
          override fun onCreateLayout(c: ComponentContext): Component =
              Column.create(c)
                  .longClickHandler(c.newEventHandler(1) as? EventHandler<LongClickEvent>)
                  .child(SimpleMountSpecTester.create(c))
                  .child(SimpleMountSpecTester.create(c))
                  .build()
        }
    assertThat(
            longClickListener ===
                LithoViewAttributesExtension.getComponentLongClickListener(lithoView.getChildAt(0)))
        .isTrue
  }

  @Test
  fun testReuseFocusChangeListenerListenerOnSameView() {
    // Needed because of (legacy) usage two different InlineLayoutSpec that the test wants to treat
    // as the same component type.
    val COMPONENT_IDENTITY = 12_345
    val lithoView =
        ComponentTestHelper.mountComponent(
            context,
            object : InlineLayoutSpec(COMPONENT_IDENTITY) {
              override fun onCreateLayout(c: ComponentContext): Component =
                  Column.create(c)
                      .focusChangeHandler(c.newEventHandler(1) as? EventHandler<FocusChangedEvent>)
                      .child(SimpleMountSpecTester.create(c))
                      .child(SimpleMountSpecTester.create(c))
                      .build()
            })
    assertThat(lithoView.childCount).isEqualTo(1)
    val focusChangeListener =
        LithoViewAttributesExtension.getComponentFocusChangeListener(lithoView.getChildAt(0))
    assertThat(focusChangeListener).isNotNull
    lithoView.componentTree?.root =
        object : InlineLayoutSpec(COMPONENT_IDENTITY) {
          override fun onCreateLayout(c: ComponentContext): Component =
              Column.create(c)
                  .focusChangeHandler(c.newEventHandler(1) as? EventHandler<FocusChangedEvent>)
                  .child(SimpleMountSpecTester.create(c))
                  .child(SimpleMountSpecTester.create(c))
                  .build()
        }
    assertThat(
            focusChangeListener ===
                LithoViewAttributesExtension.getComponentFocusChangeListener(
                    lithoView.getChildAt(0)))
        .isTrue
  }

  @Test
  fun testReuseTouchListenerOnSameView() {
    // Needed because of (legacy) usage two different InlineLayoutSpec that the test wants to treat
    // as the same component type.
    val COMPONENT_IDENTITY = 12_345
    val lithoView =
        ComponentTestHelper.mountComponent(
            context,
            object : InlineLayoutSpec(COMPONENT_IDENTITY) {
              override fun onCreateLayout(c: ComponentContext): Component =
                  Column.create(c)
                      .touchHandler(c.newEventHandler(1) as? EventHandler<TouchEvent>)
                      .child(SimpleMountSpecTester.create(c))
                      .child(SimpleMountSpecTester.create(c))
                      .build()
            })
    assertThat(lithoView.childCount).isEqualTo(1)
    val touchListener =
        LithoViewAttributesExtension.getComponentTouchListener(lithoView.getChildAt(0))
    assertThat(touchListener).isNotNull
    lithoView.componentTree?.root =
        object : InlineLayoutSpec(COMPONENT_IDENTITY) {
          override fun onCreateLayout(c: ComponentContext): Component =
              Column.create(c)
                  .touchHandler(c.newEventHandler(2) as? EventHandler<TouchEvent>)
                  .child(SimpleMountSpecTester.create(c))
                  .child(SimpleMountSpecTester.create(c))
                  .build()
        }
    assertThat(LithoViewAttributesExtension.getComponentTouchListener(lithoView.getChildAt(0)))
        .isEqualTo(touchListener)
  }

  @Test
  fun testUnsetClickHandler() {
    // Needed because of (legacy) usage two different InlineLayoutSpec that the test wants to treat
    // as the same component type.
    val COMPONENT_IDENTITY = 12_345
    val lithoView =
        ComponentTestHelper.mountComponent(
            context,
            object : InlineLayoutSpec(COMPONENT_IDENTITY) {
              override fun onCreateLayout(c: ComponentContext): Component =
                  Column.create(c)
                      .clickHandler(c.newEventHandler(1) as? EventHandler<ClickEvent>)
                      .child(SimpleMountSpecTester.create(c))
                      .child(SimpleMountSpecTester.create(c))
                      .build()
            })
    assertThat(lithoView.getChildAt(0).isClickable).isTrue
    lithoView.componentTree?.root =
        object : InlineLayoutSpec(COMPONENT_IDENTITY) {
          override fun onCreateLayout(c: ComponentContext): Component =
              Column.create(c)
                  .child(SimpleMountSpecTester.create(c))
                  .child(SimpleMountSpecTester.create(c))
                  .build()
        }
    assertThat(lithoView.isClickable).isFalse
  }

  @Test
  fun testUnsetLongClickHandler() {
    // Needed because of (legacy) usage two different InlineLayoutSpec that the test wants to treat
    // as the same component type.
    val COMPONENT_IDENTITY = 12_345
    val lithoView =
        ComponentTestHelper.mountComponent(
            context,
            object : InlineLayoutSpec(COMPONENT_IDENTITY) {
              override fun onCreateLayout(c: ComponentContext): Component =
                  Column.create(c)
                      .longClickHandler(c.newEventHandler(1) as? EventHandler<LongClickEvent>)
                      .child(SimpleMountSpecTester.create(c))
                      .child(SimpleMountSpecTester.create(c))
                      .build()
            })
    assertThat(lithoView.childCount).isEqualTo(1)
    assertThat(LithoViewAttributesExtension.getComponentLongClickListener(lithoView.getChildAt(0)))
        .isNotNull
    lithoView.componentTree?.root =
        object : InlineLayoutSpec(COMPONENT_IDENTITY) {
          override fun onCreateLayout(c: ComponentContext): Component =
              Column.create(c)
                  .child(SimpleMountSpecTester.create(c))
                  .child(SimpleMountSpecTester.create(c))
                  .build()
        }
    assertThat(lithoView.childCount).isEqualTo(0)
    val listener = LithoViewAttributesExtension.getComponentLongClickListener(lithoView)
    assertThat(listener).isNull()
  }

  @Test
  fun testUnsetFocusChangeHandler() {
    // Needed because of (legacy) usage two different InlineLayoutSpec that the test wants to treat
    // as the same component type.
    val COMPONENT_IDENTITY = 12_345
    val lithoView =
        ComponentTestHelper.mountComponent(
            context,
            object : InlineLayoutSpec(COMPONENT_IDENTITY) {
              override fun onCreateLayout(c: ComponentContext): Component =
                  Column.create(c)
                      .focusChangeHandler(c.newEventHandler(1) as? EventHandler<FocusChangedEvent>)
                      .child(SimpleMountSpecTester.create(c))
                      .child(SimpleMountSpecTester.create(c))
                      .build()
            })
    assertThat(lithoView.childCount).isEqualTo(1)
    assertThat(
            LithoViewAttributesExtension.getComponentFocusChangeListener(lithoView.getChildAt(0)))
        .isNotNull
    lithoView.componentTree?.root =
        object : InlineLayoutSpec(COMPONENT_IDENTITY) {
          override fun onCreateLayout(c: ComponentContext): Component =
              Column.create(c)
                  .child(SimpleMountSpecTester.create(c))
                  .child(SimpleMountSpecTester.create(c))
                  .build()
        }
    assertThat(lithoView.childCount).isEqualTo(0)
    val listener = LithoViewAttributesExtension.getComponentFocusChangeListener(lithoView)
    assertThat(listener).isNull()
  }

  @Test
  fun testUnsetTouchHandler() {
    // Needed because of (legacy) usage two different InlineLayoutSpec that the test wants to treat
    // as the same component type.
    val COMPONENT_IDENTITY = 12_345
    val lithoView =
        ComponentTestHelper.mountComponent(
            context,
            object : InlineLayoutSpec(COMPONENT_IDENTITY) {
              override fun onCreateLayout(c: ComponentContext): Component =
                  Column.create(c)
                      .touchHandler(c.newEventHandler(1) as? EventHandler<TouchEvent>)
                      .child(SimpleMountSpecTester.create(c))
                      .child(SimpleMountSpecTester.create(c))
                      .build()
            })
    lithoView.componentTree?.root =
        object : InlineLayoutSpec(COMPONENT_IDENTITY) {
          override fun onCreateLayout(c: ComponentContext): Component =
              Column.create(c)
                  .child(SimpleMountSpecTester.create(c))
                  .child(SimpleMountSpecTester.create(c))
                  .build()
        }
    assertThat(lithoView.childCount).isEqualTo(0)
    val listener = LithoViewAttributesExtension.getComponentTouchListener(lithoView)
    assertThat(listener).isNull()
  }

  @Test
  fun testSetClickHandler() {
    // Needed because of (legacy) usage two different InlineLayoutSpec that the test wants to treat
    // as the same component type.
    val COMPONENT_IDENTITY = 12_345
    val lithoView =
        ComponentTestHelper.mountComponent(
            context,
            object : InlineLayoutSpec(COMPONENT_IDENTITY) {
              override fun onCreateLayout(c: ComponentContext): Component =
                  Column.create(c)
                      .child(SimpleMountSpecTester.create(c))
                      .child(SimpleMountSpecTester.create(c))
                      .build()
            })
    assertThat(lithoView.childCount).isEqualTo(0)
    assertThat(lithoView.isClickable).isFalse
    lithoView.componentTree?.root =
        object : InlineLayoutSpec(COMPONENT_IDENTITY) {
          override fun onCreateLayout(c: ComponentContext): Component =
              Column.create(c)
                  .clickHandler(c.newEventHandler(1) as? EventHandler<ClickEvent>)
                  .child(SimpleMountSpecTester.create(c))
                  .child(SimpleMountSpecTester.create(c))
                  .build()
        }
    assertThat(lithoView.childCount).isEqualTo(1)
    assertThat(lithoView.getChildAt(0).isClickable).isTrue
  }

  @Test
  fun testSetLongClickHandler() {
    // Needed because of (legacy) usage two different InlineLayoutSpec that the test wants to treat
    // as the same component type.
    val COMPONENT_IDENTITY = 12_345
    val lithoView =
        ComponentTestHelper.mountComponent(
            context,
            object : InlineLayoutSpec(COMPONENT_IDENTITY) {
              override fun onCreateLayout(c: ComponentContext): Component =
                  Column.create(c)
                      .child(SimpleMountSpecTester.create(c))
                      .child(SimpleMountSpecTester.create(c))
                      .build()
            })
    assertThat(lithoView.childCount).isEqualTo(0)
    assertThat(LithoViewAttributesExtension.getComponentLongClickListener(lithoView)).isNull()
    lithoView.componentTree?.root =
        object : InlineLayoutSpec(COMPONENT_IDENTITY) {
          override fun onCreateLayout(c: ComponentContext): Component =
              Column.create(c)
                  .longClickHandler(c.newEventHandler(1) as? EventHandler<LongClickEvent>)
                  .child(SimpleMountSpecTester.create(c))
                  .child(SimpleMountSpecTester.create(c))
                  .build()
        }
    assertThat(lithoView.childCount).isEqualTo(1)
    val listener =
        LithoViewAttributesExtension.getComponentLongClickListener(lithoView.getChildAt(0))
    assertThat(listener).isNotNull
    assertThat(listener?.eventHandler).isNotNull
  }

  @Test
  fun testSetFocusChangeHandler() {
    // Needed because of (legacy) usage two different InlineLayoutSpec that the test wants to treat
    // as the same component type.
    val COMPONENT_IDENTITY = 12_345
    val lithoView =
        ComponentTestHelper.mountComponent(
            context,
            object : InlineLayoutSpec(COMPONENT_IDENTITY) {
              override fun onCreateLayout(c: ComponentContext): Component =
                  Column.create(c)
                      .child(SimpleMountSpecTester.create(c))
                      .child(SimpleMountSpecTester.create(c))
                      .build()
            })
    assertThat(lithoView.childCount).isEqualTo(0)
    assertThat(LithoViewAttributesExtension.getComponentFocusChangeListener(lithoView)).isNull()
    lithoView.componentTree?.root =
        object : InlineLayoutSpec(COMPONENT_IDENTITY) {
          override fun onCreateLayout(c: ComponentContext): Component =
              Column.create(c)
                  .focusChangeHandler(c.newEventHandler(1) as? EventHandler<FocusChangedEvent>)
                  .child(SimpleMountSpecTester.create(c))
                  .child(SimpleMountSpecTester.create(c))
                  .build()
        }
    assertThat(lithoView.childCount).isEqualTo(1)
    val listener =
        LithoViewAttributesExtension.getComponentFocusChangeListener(lithoView.getChildAt(0))
    assertThat(listener).isNotNull
    assertThat(listener?.eventHandler).isNotNull
  }

  @Test
  fun testSetTouchHandler() {
    // Needed because of (legacy) usage two different InlineLayoutSpec that the test wants to treat
    // as the same component type.
    val COMPONENT_IDENTITY = 12_345
    val lithoView =
        ComponentTestHelper.mountComponent(
            context,
            object : InlineLayoutSpec(COMPONENT_IDENTITY) {
              override fun onCreateLayout(c: ComponentContext): Component =
                  Column.create(c)
                      .child(SimpleMountSpecTester.create(c))
                      .child(SimpleMountSpecTester.create(c))
                      .build()
            })
    assertThat(lithoView.childCount).isEqualTo(0)
    assertThat(LithoViewAttributesExtension.getComponentTouchListener(lithoView)).isNull()
    lithoView.componentTree?.root =
        object : InlineLayoutSpec(COMPONENT_IDENTITY) {
          override fun onCreateLayout(c: ComponentContext): Component =
              Column.create(c)
                  .touchHandler(c.newEventHandler(1) as? EventHandler<TouchEvent>)
                  .child(SimpleMountSpecTester.create(c))
                  .child(SimpleMountSpecTester.create(c))
                  .build()
        }
    assertThat(lithoView.childCount).isEqualTo(1)
    val listener = LithoViewAttributesExtension.getComponentTouchListener(lithoView.getChildAt(0))
    assertThat(listener).isNotNull
    assertThat(listener?.eventHandler).isNotNull
  }
}


/*
 * 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.res.Configuration
import android.graphics.Color
import androidx.test.core.app.ApplicationProvider
import com.facebook.litho.testing.LegacyLithoViewRule
import com.facebook.litho.testing.TestDrawableComponent
import com.facebook.litho.testing.atMost
import com.facebook.litho.testing.exactly
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.MountSpecLifecycleTester
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.mockito.kotlin.doReturn
import org.mockito.kotlin.spy

@RunWith(LithoTestRunner::class)
class MountStateRemountInPlaceTest {

  @JvmField @Rule val legacyLithoViewRule = LegacyLithoViewRule()
  private lateinit var context: ComponentContext
  private lateinit var componentsLogger: TestComponentsLogger

  @Before
  fun setup() {
    componentsLogger = TestComponentsLogger()
    context = ComponentContext(ApplicationProvider.getApplicationContext(), "tag", componentsLogger)
  }

  @Test
  fun testMountUnmountWithShouldUpdate() {
    val firstComponent = TestDrawableComponent.create(context).color(Color.RED).build()
    val lithoView =
        ComponentTestHelper.mountComponent(
            context, Column.create(context).child(firstComponent).build())
    assertThat(firstComponent.wasOnMountCalled()).isTrue
    assertThat(firstComponent.wasOnBindCalled()).isTrue
    assertThat(firstComponent.wasOnUnmountCalled()).isFalse
    val secondComponent = TestDrawableComponent.create(context).color(Color.BLUE).build()
    lithoView.componentTree?.root = Column.create(context).child(secondComponent).build()
    assertThat(secondComponent.wasOnMountCalled()).isTrue
    assertThat(secondComponent.wasOnBindCalled()).isTrue
    assertThat(firstComponent.wasOnUnmountCalled()).isTrue
  }

  @Test
  fun testRebindWithNoShouldUpdate() {
    val firstComponent = TestDrawableComponent.create(context).build()
    val lithoView =
        ComponentTestHelper.mountComponent(
            context, Column.create(context).child(firstComponent).build())
    assertThat(firstComponent.wasOnMountCalled()).isTrue
    assertThat(firstComponent.wasOnBindCalled()).isTrue
    assertThat(firstComponent.wasOnUnmountCalled()).isFalse
    val secondComponent = TestDrawableComponent.create(context).build()
    lithoView.componentTree?.root = Column.create(context).child(secondComponent).build()
    assertThat(secondComponent.wasOnMountCalled()).isFalse
    assertThat(secondComponent.wasOnBindCalled()).isTrue
    assertThat(firstComponent.wasOnUnmountCalled()).isFalse
  }

  @Test
  fun testMountUnmountWithNewOrientation() {
    val tracker = LifecycleTracker()
    val root = MountSpecLifecycleTester.create(context).lifecycleTracker(tracker).build()
    legacyLithoViewRule
        .setRoot(root)
        .attachToWindow()
        .setSizeSpecs(exactly(10), exactly(20))
        .measure()
        .layout()
    assertThat(tracker.steps)
        .describedAs("Should call lifecycle methods")
        .contains(LifecycleStep.ON_BIND, LifecycleStep.ON_MOUNT)
        .doesNotContain(LifecycleStep.ON_UNMOUNT)
    tracker.reset()
    legacyLithoViewRule.setRootAndSizeSpecSync(root, exactly(20), exactly(10)).measure().layout()
    assertThat(tracker.steps)
        .describedAs("Should call lifecycle methods")
        .contains(
            LifecycleStep.ON_UNBIND,
            LifecycleStep.ON_UNMOUNT,
            LifecycleStep.ON_MOUNT,
            LifecycleStep.ON_BIND)
  }

  @Test
  fun mountState_onNoForceShouldUpdateAndNewOrientation_shouldNotRemount() {
    context.resources.configuration.orientation = Configuration.ORIENTATION_PORTRAIT
    val firstComponent = TestDrawableComponent.create(context).build()
    val lithoView =
        ComponentTestHelper.mountComponent(
            context, Column.create(context).child(firstComponent).build())
    assertThat(firstComponent.wasOnMountCalled()).isTrue
    assertThat(firstComponent.wasOnBindCalled()).isTrue
    assertThat(firstComponent.wasOnUnmountCalled()).isFalse
    context.resources.configuration.orientation = Configuration.ORIENTATION_LANDSCAPE
    val secondComponent = TestDrawableComponent.create(context).build()
    lithoView.componentTree?.root = Column.create(context).child(secondComponent).build()
    assertThat(secondComponent.wasOnMountCalled()).isFalse
    assertThat(secondComponent.wasOnBindCalled()).isTrue
    assertThat(firstComponent.wasOnUnbindCalled()).isTrue
    assertThat(firstComponent.wasOnUnmountCalled()).isFalse
  }

  @Test
  fun mountState_onNoForceShouldUpdateAndNewOrientationAndSameSize_shouldNotRemount() {
    context.resources.configuration.orientation = Configuration.ORIENTATION_PORTRAIT
    val firstComponent =
        TestDrawableComponent.create(
                context, 0, 0, true, true, false, true /*isMountSizeDependent*/)
            .measuredHeight(10)
            .build()
    val lithoView =
        ComponentTestHelper.mountComponent(
            context, Column.create(context).child(firstComponent).build())
    assertThat(firstComponent.wasOnMountCalled()).isTrue
    assertThat(firstComponent.wasOnBindCalled()).isTrue
    assertThat(firstComponent.wasOnUnmountCalled()).isFalse
    context.resources.configuration.orientation = Configuration.ORIENTATION_LANDSCAPE
    val secondComponent =
        TestDrawableComponent.create(
                context, 0, 0, true, true, false, true /*isMountSizeDependent*/)
            .measuredHeight(10)
            .build()
    lithoView.componentTree?.root = Column.create(context).child(secondComponent).build()
    assertThat(secondComponent.wasOnMountCalled()).isFalse
    assertThat(secondComponent.wasOnBindCalled()).isTrue
    assertThat(firstComponent.wasOnUnbindCalled()).isTrue
    assertThat(firstComponent.wasOnUnmountCalled()).isFalse
  }

  @Test
  fun testMountUnmountWithNoShouldUpdateAndDifferentSize() {
    val firstComponent =
        TestDrawableComponent.create(
                context, 0, 0, true, true, false, true /*isMountSizeDependent*/)
            .measuredHeight(10)
            .build()
    val lithoView =
        ComponentTestHelper.mountComponent(
            context, Column.create(context).child(firstComponent).build())
    assertThat(firstComponent.wasOnMountCalled()).isTrue
    assertThat(firstComponent.wasOnBindCalled()).isTrue
    assertThat(firstComponent.wasOnUnmountCalled()).isFalse
    val secondComponent =
        TestDrawableComponent.create(
                context, 0, 0, true, true, false, true /*isMountSizeDependent*/)
            .measuredHeight(11)
            .build()
    lithoView.componentTree?.root = Column.create(context).child(secondComponent).build()
    assertThat(secondComponent.wasOnMountCalled()).isTrue
    assertThat(secondComponent.wasOnBindCalled()).isTrue
    assertThat(firstComponent.wasOnUnmountCalled()).isTrue
  }

  @Test
  fun testMountUnmountWithNoShouldUpdateAndSameSize() {
    val firstComponent =
        TestDrawableComponent.create(
                context, 0, 0, true, true, false, true /*isMountSizeDependent*/)
            .measuredHeight(10)
            .build()
    val lithoView =
        ComponentTestHelper.mountComponent(
            context, Column.create(context).child(firstComponent).build())
    assertThat(firstComponent.wasOnMountCalled()).isTrue
    assertThat(firstComponent.wasOnBindCalled()).isTrue
    assertThat(firstComponent.wasOnUnmountCalled()).isFalse
    val secondComponent =
        TestDrawableComponent.create(
                context, 0, 0, true, true, false, true /*isMountSizeDependent*/)
            .measuredHeight(10)
            .build()
    lithoView.componentTree?.root = Column.create(context).child(secondComponent).build()
    assertThat(secondComponent.wasOnMountCalled()).isFalse
    assertThat(secondComponent.wasOnBindCalled()).isTrue
    assertThat(firstComponent.wasOnUnmountCalled()).isFalse
  }

  @Test
  fun testMountUnmountWithNoShouldUpdateAndDifferentMeasures() {
    val firstComponent = TestDrawableComponent.create(context).build()
    val lithoView =
        ComponentTestHelper.mountComponent(
            LithoView(context),
            ComponentTree.create(context, Column.create(context).child(firstComponent).build())
                .build(),
            atMost(100),
            atMost(100))
    assertThat(firstComponent.wasOnMountCalled()).isTrue
    assertThat(firstComponent.wasOnBindCalled()).isTrue
    assertThat(firstComponent.wasOnUnmountCalled()).isFalse
    val secondComponent = TestDrawableComponent.create(context).build()
    lithoView.componentTree?.root =
        Column.create(context).child(secondComponent).widthPx(10).heightPx(10).build()
    assertThat(lithoView.isLayoutRequested).isTrue
    assertThat(secondComponent.wasOnMountCalled()).isFalse
    assertThat(secondComponent.wasOnBindCalled()).isFalse
    assertThat(firstComponent.wasOnUnmountCalled()).isFalse
  }

  @Test
  fun testMountUnmountWithNoShouldUpdateAndSameMeasures() {
    val firstComponent =
        TestDrawableComponent.create(context, 0, 0, true, true, false, true)
            .color(Color.GRAY)
            .build()
    val lithoView =
        ComponentTestHelper.mountComponent(
            LithoView(context),
            ComponentTree.create(context, Column.create(context).child(firstComponent).build())
                .build(),
            exactly(100),
            exactly(100))
    assertThat(firstComponent.wasOnMountCalled()).isTrue
    assertThat(firstComponent.wasOnBindCalled()).isTrue
    assertThat(firstComponent.wasOnUnmountCalled()).isFalse
    val secondComponent = TestDrawableComponent.create(context).color(Color.RED).build()
    lithoView.componentTree?.root =
        Column.create(context).child(secondComponent).widthPx(10).heightPx(10).build()
    assertThat(lithoView.isLayoutRequested).isFalse
    assertThat(secondComponent.wasOnMountCalled()).isTrue
    assertThat(secondComponent.wasOnBindCalled()).isTrue
    assertThat(firstComponent.wasOnUnmountCalled()).isTrue
  }

  @Test
  fun testRebindWithNoShouldUpdateAndSameMeasures() {
    val firstComponent = TestDrawableComponent.create(context).build()
    val lithoView =
        ComponentTestHelper.mountComponent(
            LithoView(context),
            ComponentTree.create(context, Column.create(context).child(firstComponent).build())
                .build(),
            exactly(100),
            exactly(100))
    assertThat(firstComponent.wasOnMountCalled()).isTrue
    assertThat(firstComponent.wasOnBindCalled()).isTrue
    assertThat(firstComponent.wasOnUnmountCalled()).isFalse
    val secondComponent = TestDrawableComponent.create(context).build()
    lithoView.componentTree?.root =
        Column.create(context).child(secondComponent).widthPx(10).heightPx(10).build()
    assertThat(lithoView.isLayoutRequested).isFalse
    assertThat(secondComponent.wasOnMountCalled()).isFalse
    assertThat(secondComponent.wasOnBindCalled()).isTrue
    assertThat(firstComponent.wasOnUnmountCalled()).isFalse
  }

  @Test
  fun testMountUnmountWithSkipShouldUpdate() {
    val firstComponent = TestDrawableComponent.create(context).color(Color.BLACK).build()
    val lithoView =
        ComponentTestHelper.mountComponent(
            context, Column.create(context).child(firstComponent).build())
    assertThat(firstComponent.wasOnMountCalled()).isTrue
    assertThat(firstComponent.wasOnBindCalled()).isTrue
    assertThat(firstComponent.wasOnUnmountCalled()).isFalse
    val secondComponent = TestDrawableComponent.create(context).color(Color.BLACK).build()
    lithoView.componentTree?.root = Column.create(context).child(secondComponent).build()
    assertThat(secondComponent.wasOnMountCalled()).isFalse
    assertThat(secondComponent.wasOnBindCalled()).isTrue
    assertThat(firstComponent.wasOnUnmountCalled()).isFalse
  }

  @Test
  fun testMountUnmountWithSkipShouldUpdateAndRemount() {
    val firstComponent = TestDrawableComponent.create(context).color(Color.BLACK).build()
    val lithoView =
        ComponentTestHelper.mountComponent(
            context, Column.create(context).child(firstComponent).build())
    assertThat(firstComponent.wasOnMountCalled()).isTrue
    assertThat(firstComponent.wasOnBindCalled()).isTrue
    assertThat(firstComponent.wasOnUnmountCalled()).isFalse
    val secondComponent = TestDrawableComponent.create(context).color(Color.WHITE).build()
    lithoView.componentTree?.root = Column.create(context).child(secondComponent).build()
    assertThat(secondComponent.wasOnMountCalled()).isTrue
    assertThat(secondComponent.wasOnBindCalled()).isTrue
    assertThat(firstComponent.wasOnUnmountCalled()).isTrue
  }

  @Test
  fun testMountUnmountDoesNotSkipShouldUpdateAndRemount() {
    val firstComponent = TestDrawableComponent.create(context).color(Color.RED).build()
    val firstLithoView =
        ComponentTestHelper.mountComponent(
            context, Column.create(context).child(firstComponent).build())
    assertThat(firstComponent.wasOnMountCalled()).isTrue
    assertThat(firstComponent.wasOnBindCalled()).isTrue
    assertThat(firstComponent.wasOnUnmountCalled()).isFalse
    val secondComponent = TestDrawableComponent.create(context).color(Color.BLUE).build()
    val secondTree =
        ComponentTree.create(context, Column.create(context).child(secondComponent).build()).build()
    secondTree.setSizeSpec(100, 100)
    val thirdComponent = spy(TestDrawableComponent.create(context).build())
    doReturn(thirdComponent).`when`(thirdComponent).makeShallowCopy()
    secondTree.root = Column.create(context).child(thirdComponent).build()
    ComponentTestHelper.mountComponent(firstLithoView, secondTree)
    assertThat(thirdComponent.wasOnMountCalled()).isTrue
    assertThat(thirdComponent.wasOnBindCalled()).isTrue
    assertThat(firstComponent.wasOnUnmountCalled()).isTrue
  }
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.facebook.litho

import android.content.Context
import android.graphics.Color
import android.view.ViewGroup
import androidx.test.core.app.ApplicationProvider
import com.facebook.litho.config.ComponentsConfiguration
import com.facebook.litho.drawable.ComparableDrawable
import com.facebook.litho.testing.LegacyLithoViewRule
import com.facebook.litho.testing.TestDrawableComponent
import com.facebook.litho.testing.TestViewComponent
import com.facebook.litho.testing.exactly
import com.facebook.litho.testing.helper.ComponentTestHelper
import com.facebook.litho.testing.testrunner.LithoTestRunner
import com.facebook.litho.widget.EditText
import com.facebook.litho.widget.Text
import java.util.ArrayList
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

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

  private val config =
      ComponentsConfiguration.create().shouldAddHostViewForRootComponent(true).build()
  private lateinit var context: ComponentContext

  @JvmField @Rule val legacyLithoViewRule = LegacyLithoViewRule(config)

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

  @Test
  fun testMountItemsHaveMountData() {
    val component1 = TestDrawableComponent.create(context).build()
    val component2 = TestDrawableComponent.create(context).build()
    val lithoView =
        ComponentTestHelper.mountComponent(
            context, Column.create(context).child(component1).child(component2).build())
    assertThat(component1.isMounted).isTrue
    assertThat(component2.isMounted).isTrue
  }

  @Test
  fun testRemountSameLayoutState() {
    val component1 = TestDrawableComponent.create(context).build()
    val component2 = TestDrawableComponent.create(context).build()
    val component3 = TestDrawableComponent.create(context).build()
    val component4 = TestDrawableComponent.create(context).build()
    legacyLithoViewRule
        .setRoot(
            Column.create(context)
                .widthPx(100)
                .heightPx(100)
                .child(component1)
                .child(component2)
                .build())
        .measure()
        .layout()
        .attachToWindow()
    assertThat(component1.isMounted).isTrue
    assertThat(component2.isMounted).isTrue
    legacyLithoViewRule.setRoot(
        Column.create(context)
            .widthPx(100)
            .heightPx(100)
            .child(component3)
            .child(component4)
            .build())
    assertThat(component1.isMounted).isTrue
    assertThat(component2.isMounted).isTrue
    assertThat(component3.isMounted).isFalse
    assertThat(component4.isMounted).isFalse
    val mountDelegateTarget = legacyLithoViewRule.lithoView.mountDelegateTarget
    val components: MutableList<Component> = ArrayList()
    for (i in 0 until mountDelegateTarget.mountItemCount) {
      mountDelegateTarget.getMountItemAt(i)?.let { mountItem ->
        components.add(LithoRenderUnit.getRenderUnit(mountItem).component)
      }
    }
    assertThat(containsRef(components, component1)).isFalse
    assertThat(containsRef(components, component2)).isFalse
    assertThat(containsRef(components, component3)).isTrue
    assertThat(containsRef(components, component4)).isTrue
  }

  /**
   * There was a crash when mounting a drawing in place of a view. This test is here to make sure
   * this does not regress. To reproduce this crash the pools needed to be in a specific state as
   * view layout outputs and mount items were being re-used for drawables.
   */
  @Test
  fun testRemountDifferentMountType() {
    legacyLithoViewRule
        .setRoot(TestViewComponent.create(context).build())
        .setSizeSpecs(exactly(10), exactly(5))
    legacyLithoViewRule.attachToWindow().measure().layout().setSizeSpecs(10, 10)
    assertThat(legacyLithoViewRule.lithoView.childCount).isEqualTo(1)
    legacyLithoViewRule
        .setRoot(TestDrawableComponent.create(context).build())
        .setSizeSpecs(exactly(10), exactly(5))
    assertThat(legacyLithoViewRule.lithoView.drawables[0]).isNotNull
  }

  @Test
  fun testRemountNewLayoutState() {
    val component1 = TestDrawableComponent.create(context).color(Color.RED).build()
    val component2 = TestDrawableComponent.create(context).color(Color.BLUE).build()
    val component3 = TestDrawableComponent.create(context).color(Color.GREEN).build()
    val component4 = TestDrawableComponent.create(context).color(Color.YELLOW).build()
    val lithoView =
        ComponentTestHelper.mountComponent(
            context, Column.create(context).child(component1).child(component2).build())
    assertThat(component1.isMounted).isTrue
    assertThat(component2.isMounted).isTrue
    ComponentTestHelper.mountComponent(
        context, lithoView, Column.create(context).child(component3).child(component4).build())
    assertThat(component1.isMounted).isFalse
    assertThat(component2.isMounted).isFalse
    assertThat(component3.isMounted).isTrue
    assertThat(component4.isMounted).isTrue
  }

  @Test
  fun testRemountAfterSettingNewRootTwice() {
    val component1 =
        TestDrawableComponent.create(context).color(Color.RED).returnSelfInMakeShallowCopy().build()
    val component2 =
        TestDrawableComponent.create(context)
            .returnSelfInMakeShallowCopy()
            .color(Color.BLUE)
            .build()
    val lithoView = LithoView(context)
    val componentTree =
        ComponentTree.create(context, Column.create(context).child(component1).build()).build()
    ComponentTestHelper.mountComponent(lithoView, componentTree, exactly(100), exactly(100))
    assertThat(component1.isMounted).isTrue
    componentTree.setRootAndSizeSpecSync(
        Column.create(context).child(component2).build(), exactly(50), exactly(50))
    componentTree.setSizeSpec(exactly(100), exactly(100))
    assertThat(component2.isMounted).isTrue
  }

  @Test
  fun testRemountPartiallyDifferentLayoutState() {
    val component1 = TestDrawableComponent.create(context).build()
    val component2 = TestDrawableComponent.create(context).build()
    val component3 = TestDrawableComponent.create(context).build()
    val component4 = TestDrawableComponent.create(context).build()
    legacyLithoViewRule
        .setRoot(
            Column.create(context)
                .widthPx(100)
                .heightPx(100)
                .child(component1)
                .child(component2)
                .build())
        .measure()
        .layout()
        .attachToWindow()
    assertThat(component1.isMounted).isTrue
    assertThat(component2.isMounted).isTrue
    legacyLithoViewRule.setRoot(
        Column.create(context)
            .widthPx(100)
            .heightPx(100)
            .child(component3)
            .child(Column.create(context).wrapInView().child(component4))
            .build())
    assertThat(component1.isMounted).isTrue
    assertThat(component2.isMounted).isFalse
    assertThat(component3.isMounted).isFalse
    assertThat(component4.isMounted).isTrue
  }

  @Test
  fun testRemountOnNoLayoutChanges() {
    val oldComponent =
        Column.create(context)
            .backgroundColor(Color.WHITE)
            .child(
                EditText.create(context)
                    .backgroundColor(Color.RED)
                    .foregroundColor(Color.CYAN)
                    .text("Hello World")
                    .viewTag("Alpha")
                    .contentDescription("some description"))
            .build()
    val lithoView = LithoView(context)
    val componentTree =
        ComponentTree.create(context, oldComponent)
            .componentsConfiguration(config)
            .incrementalMount(false)
            .layoutDiffing(true)
            .build()
    ComponentTestHelper.mountComponent(lithoView, componentTree, exactly(400), exactly(400))
    val oldHost = lithoView.getChildAt(0) as ViewGroup
    val oldView = oldHost.getChildAt(0)
    val oldTag = oldView.tag
    val oldContentDescription = oldView.contentDescription.toString()
    val oldBackground = oldView.background
    val newComponent =
        Column.create(context)
            .backgroundColor(Color.WHITE)
            .child(
                EditText.create(context)
                    .backgroundColor(Color.RED)
                    .foregroundColor(Color.CYAN)
                    .text("Hello World")
                    .viewTag("Alpha")
                    .contentDescription("some description"))
            .build()
    componentTree.setRootAndSizeSpecSync(newComponent, exactly(400), exactly(400))
    componentTree.setSizeSpec(exactly(400), exactly(400))
    val newHost = lithoView.getChildAt(0) as ViewGroup
    val newView = newHost.getChildAt(0)
    assertThat(newView).isSameAs(oldView)
    val newTag = newView.tag
    val newContentDescription = newView.contentDescription.toString()
    val newBackground = newView.background

    // Check that props were not set again
    assertThat(newTag).isSameAs(oldTag)
    assertThat(newContentDescription).isSameAs(oldContentDescription)
    assertThat(oldBackground).isSameAs(newBackground)
  }

  @Test
  fun testRemountOnNodeInfoLayoutChanges() {
    val oldComponent =
        Column.create(context)
            .backgroundColor(Color.WHITE)
            .child(Text.create(context).textSizeSp(12f).text("label:"))
            .child(
                EditText.create(context)
                    .text("Hello World")
                    .textSizeSp(12f)
                    .viewTag("Alpha")
                    .enabled(true))
            .build()
    val lithoView = LithoView(context)
    val componentTree =
        ComponentTree.create(context, oldComponent)
            .componentsConfiguration(config)
            .incrementalMount(false)
            .layoutDiffing(true)
            .build()
    ComponentTestHelper.mountComponent(lithoView, componentTree, exactly(400), exactly(400))
    val oldHost = lithoView.getChildAt(0) as ViewGroup
    val oldView = oldHost.getChildAt(0)
    val oldTag = oldView.tag
    val oldIsEnabled = oldView.isEnabled
    val newComponent =
        Column.create(context)
            .backgroundColor(Color.WHITE)
            .child(Text.create(context).textSizeSp(12f).text("label:"))
            .child(
                EditText.create(context)
                    .text("Hello World")
                    .textSizeSp(12f)
                    .viewTag("Beta")
                    .enabled(false))
            .build()
    componentTree.setRootAndSizeSpecSync(newComponent, exactly(400), exactly(400))
    componentTree.setSizeSpec(exactly(400), exactly(400))
    val newHost = lithoView.getChildAt(0) as ViewGroup
    val newView = newHost.getChildAt(0)
    assertThat(newView).isSameAs(oldView)
    val newTag = newView.tag
    val newIsEnabled = newView.isEnabled
    assertThat(newTag).isNotEqualTo(oldTag)
    assertThat(newIsEnabled).isNotEqualTo(oldIsEnabled)
  }

  @Test
  fun testRemountOnViewNodeInfoLayoutChanges() {
    val oldComponent =
        Column.create(context)
            .backgroundColor(Color.WHITE)
            .child(Text.create(context).textSizeSp(12f).text("label:"))
            .child(
                EditText.create(context)
                    .text("Hello World")
                    .textSizeSp(12f)
                    .backgroundColor(Color.RED))
            .build()
    val lithoView = LithoView(context)
    val componentTree =
        ComponentTree.create(context, oldComponent)
            .componentsConfiguration(config)
            .incrementalMount(false)
            .layoutDiffing(true)
            .build()
    ComponentTestHelper.mountComponent(lithoView, componentTree, exactly(400), exactly(400))
    val oldHost = lithoView.getChildAt(0) as ViewGroup
    val oldView = oldHost.getChildAt(0)
    val oldDrawable = oldView.background as ComparableDrawable
    val newComponent =
        Column.create(context)
            .backgroundColor(Color.WHITE)
            .child(Text.create(context).textSizeSp(12f).text("label:"))
            .child(
                EditText.create(context)
                    .text("Hello World")
                    .textSizeSp(12f)
                    .backgroundColor(Color.CYAN))
            .build()
    componentTree.setRootAndSizeSpecSync(newComponent, exactly(400), exactly(400))
    componentTree.setSizeSpec(exactly(400), exactly(400))
    val newHost = lithoView.getChildAt(0) as ViewGroup
    val newView = newHost.getChildAt(0)
    assertThat(newView).isSameAs(oldView)
    val newDrawable = newView.background as ComparableDrawable
    assertThat(oldDrawable.isEquivalentTo(newDrawable)).isFalse
  }

  private fun containsRef(list: List<*>, obj: Any): Boolean {
    return list.any { o -> o === obj }
  }
}


/*
 * 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.drawable.ColorDrawable
import com.facebook.litho.config.ComponentsConfiguration
import com.facebook.litho.config.TempComponentsConfigurations
import com.facebook.litho.testing.LegacyLithoViewRule
import com.facebook.litho.testing.Whitebox
import com.facebook.litho.testing.exactly
import com.facebook.litho.testing.testrunner.LithoTestRunner
import com.facebook.litho.widget.DynamicPropsComponentTester
import com.facebook.litho.widget.Image
import com.facebook.litho.widget.MountSpecWithMountUnmountAssertion
import com.facebook.litho.widget.MountSpecWithMountUnmountAssertionSpec
import com.facebook.litho.widget.Progress
import com.facebook.litho.widget.SolidColor
import com.facebook.litho.widget.Text
import com.facebook.litho.widget.TextInput
import com.facebook.rendercore.MountState
import com.facebook.yoga.YogaEdge
import org.assertj.core.api.Assertions.assertThat
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith

@RunWith(LithoTestRunner::class)
class MountStateTest {

  @JvmField @Rule val legacyLithoViewRule: LegacyLithoViewRule = LegacyLithoViewRule()
  private lateinit var context: ComponentContext

  @Before
  fun setup() {
    context = legacyLithoViewRule.context
  }

  @Test
  fun testDetachLithoView_unbindComponentFromContent() {
    val child1 = DynamicPropsComponentTester.create(context).dynamicPropValue(1).build()
    val root =
        Column.create(context)
            .child(Wrapper.create(context).delegate(child1).widthPx(10).heightPx(10))
            .build()
    legacyLithoViewRule
        .setRoot(root)
        .attachToWindow()
        .setSizeSpecs(exactly(1_000), exactly(1_000))
        .measure()
        .layout()
    val dynamicPropsManager = legacyLithoViewRule.lithoView.dynamicPropsManager
    assertThat(dynamicPropsManager).isNotNull
    assertThat(dynamicPropsManager?.hasCachedContent(child1)).isTrue
    legacyLithoViewRule.detachFromWindow()
    assertThat(dynamicPropsManager?.hasCachedContent(child1)).isFalse
  }

  @Test
  fun testUnbindMountItem_unbindComponentFromContent() {
    val child1 = DynamicPropsComponentTester.create(context).dynamicPropValue(1).build()
    val root =
        Column.create(context)
            .child(Wrapper.create(context).delegate(child1).widthPx(10).heightPx(10))
            .build()
    legacyLithoViewRule
        .setRoot(root)
        .attachToWindow()
        .setSizeSpecs(exactly(1_000), exactly(1_000))
        .measure()
        .layout()
    val dynamicPropsManager = legacyLithoViewRule.lithoView.dynamicPropsManager
    assertThat(dynamicPropsManager?.hasCachedContent(child1)).isTrue
    legacyLithoViewRule.setRoot(Column.create(context).build())
    assertThat(dynamicPropsManager?.hasCachedContent(child1)).isFalse
  }

  @Test
  fun onSetRootWithNoOutputsWithRenderCore_shouldSuccessfullyCompleteMount() {
    val root =
        Wrapper.create(context)
            .delegate(SolidColor.create(context).color(Color.BLACK).build())
            .build()
    legacyLithoViewRule
        .setRoot(root)
        .attachToWindow()
        .setSizeSpecs(exactly(1_000), exactly(1_000))
        .measure()
        .layout()
    val emptyRoot: Component = Wrapper.create(context).delegate(null).build()
    legacyLithoViewRule.setRoot(emptyRoot)
  }

  @Test
  fun onSetRootWithSimilarComponent_MountContentShouldUsePools() {
    val root =
        Column.create(context)
            .child(TextInput.create(context).widthDip(100f).heightDip(100f))
            .build()
    legacyLithoViewRule
        .setRoot(root)
        .attachToWindow()
        .setSizeSpecs(exactly(1_000), exactly(1_000))
        .measure()
        .layout()
    val view = legacyLithoViewRule.lithoView.getChildAt(0)
    val newRoot =
        Row.create(context)
            .child(TextInput.create(context).initialText("testing").widthDip(120f).heightDip(120f))
            .build()
    legacyLithoViewRule.setRoot(newRoot).setSizeSpecs(exactly(1_000), exactly(1_000))
    val newView = legacyLithoViewRule.lithoView.getChildAt(0)
    assertThat(newView).isSameAs(view)
  }

  @Test
  fun onSetRootWithDifferentComponent_MountContentPoolsShouldNoCollide() {
    val root =
        Column.create(context)
            .child(TextInput.create(context).widthDip(100f).heightDip(100f))
            .build()
    legacyLithoViewRule
        .setRoot(root)
        .attachToWindow()
        .setSizeSpecs(exactly(1_000), exactly(1_000))
        .measure()
        .layout()
    val newRoot =
        Column.create(context)
            .child(Progress.create(context).widthDip(100f).heightDip(100f))
            .build()
    legacyLithoViewRule.setRoot(newRoot).setSizeSpecs(exactly(1_000), exactly(1_000))
  }

  @Test
  fun onSetRootWithNullComponentWithStatelessness_shouldMountWithoutCrashing() {
    legacyLithoViewRule
        .attachToWindow()
        .setRoot(EmptyComponent())
        .setSizeSpecs(exactly(1_000), exactly(1_000))
        .measure()
        .layout()
    assertThat(legacyLithoViewRule.currentRootNode).isNull()
    assertThat(legacyLithoViewRule.lithoView.childCount).isEqualTo(0)
    val tree = legacyLithoViewRule.committedLayoutState?.toRenderTree()
    assertThat(tree?.mountableOutputCount).isEqualTo(1)
    assertThat(tree?.root).isSameAs(tree?.getRenderTreeNodeAtIndex(0))
    assertThat(tree?.getRenderTreeNodeIndex(MountState.ROOT_HOST_ID)).isEqualTo(0)
  }

  @Test
  fun mountingChildForUnmountedParentInRenderCore_shouldMountWithoutCrashing() {
    val root =
        Row.create(context)
            .backgroundColor(Color.BLUE)
            .widthPx(20)
            .heightPx(20)
            .viewTag("root")
            .child(
                Row.create(context) // Parent that will be unmounted
                    .backgroundColor(Color.RED)
                    .widthPx(20)
                    .heightPx(20)
                    .viewTag("parent")
                    .border(
                        Border.create(context) // Drawable to be mounted after parent unmounts
                            .widthPx(YogaEdge.ALL, 2)
                            .color(YogaEdge.ALL, Color.YELLOW)
                            .build()))
            .build()
    legacyLithoViewRule
        .useComponentTree(
            ComponentTree.create(context)
                .componentsConfiguration(
                    ComponentsConfiguration.create()
                        .shouldAddHostViewForRootComponent(true)
                        .build())
                .build())
        .attachToWindow()
        .setRoot(root)
        .setSizeSpecs(exactly(1_000), exactly(1_000))
        .measure()
        .layout()
    val parentOfParent = legacyLithoViewRule.findViewWithTagOrNull("root") as ComponentHost
    val parentNode = parentOfParent.getMountItemAt(0).renderTreeNode
    val parentId = parentNode.renderUnit.id
    val childId = parentNode.getChildAt(0).renderUnit.id

    // Unmount the parent
    legacyLithoViewRule.lithoView.mountDelegateTarget.notifyUnmount(parentId)

    // Attempt to mount the child (border drawable)
    // If there is a problem, a crash will occur here.
    legacyLithoViewRule.lithoView.mountDelegateTarget.notifyMount(childId)
    TempComponentsConfigurations.restoreShouldAddHostViewForRootComponent()
  }

  @Test
  fun shouldUnregisterAllExtensions_whenUnmountAllItems() {
    val root =
        Row.create(context)
            .backgroundColor(Color.BLUE)
            .child(
                Image.create(context).drawable(ColorDrawable(Color.RED)).heightPx(100).widthPx(200))
            .build()
    legacyLithoViewRule
        .attachToWindow()
        .setRoot(root)
        .setSizeSpecs(exactly(1_000), exactly(1_000))
        .measure()
        .layout()
    val lithoView = legacyLithoViewRule.lithoView
    val mountDelegate = lithoView.mountDelegateTarget.mountDelegate
    var coordinator =
        Whitebox.getInternalState<LithoHostListenerCoordinator>(
            lithoView, "mLithoHostListenerCoordinator")
    assertThat(coordinator).isNotNull
    assertThat(coordinator.visibilityExtensionState).isNotNull
    assertThat(coordinator.incrementalMountExtensionState).isNotNull
    assertThat(mountDelegate).isNotNull
    assertThat(mountDelegate?.extensionStates).isNotEmpty

    // Unmount the parent
    legacyLithoViewRule.lithoView.unmountAllItems()
    coordinator =
        Whitebox.getInternalState<LithoHostListenerCoordinator>(
            lithoView, "mLithoHostListenerCoordinator")
    assertThat(coordinator).isNull()
    assertThat(mountDelegate).isNotNull
    assertThat(mountDelegate?.extensionStates).isEmpty()
  }

  /**
   * This test case captures the scenario where unmount gets called on a component which was moved
   * to a different location during prepare mount which causes the wrong item to be unmounted, which
   * can lead to crashes. 1. A layout is mounted. 2. The next layout update cause an item to be
   * moved to a different position, but at that position it gets unmounted because it is outside the
   * visible rect. 3. This causes the wrong item to be unmounted.
   */
  @Test
  fun whenItemsAreMovedThenUnmountedInTheNextMountLoop_shouldUnmountTheCorrectItem() {

    // TODO(T118124771): Test failure because of incorrect visible bounds
    if (ComponentsConfiguration.lithoViewSelfManageViewPortChanges) {
      return
    }
    val c = legacyLithoViewRule.context
    val initialComponent =
        Column.create(c)
            .heightPx(800)
            .child(
                Column.create(c)
                    .wrapInView()
                    .heightPx(800)
                    .child(TextInput.create(c).key("#0").initialText("0").heightPx(100))
                    .child(
                        MountSpecWithMountUnmountAssertion.create(c)
                            .key("test")
                            .container(MountSpecWithMountUnmountAssertionSpec.Container())
                            .heightPx(100))
                    .child(TextInput.create(c).key("#2").initialText("2").heightPx(100))
                    .child(TextInput.create(c).key("#3").initialText("3").heightPx(100))
                    .child(TextInput.create(c).key("#4").initialText("4").heightPx(100))
                    .child(TextInput.create(c).key("#5").initialText("5").heightPx(100))
                    .child(TextInput.create(c).key("#6").initialText("6").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(800))
    lithoView.layout(0, 0, 100, 800)

    // Assert that the view is mounted
    assertThat(lithoView.childCount).isEqualTo(1)
    assertThat((lithoView.getChildAt(0) as ComponentHost).childCount).isEqualTo(7)
    val newComponent =
        Column.create(c)
            .heightPx(800)
            .child(
                Column.create(c)
                    .wrapInView()
                    .heightPx(800)
                    .child(TextInput.create(c).key("#0").initialText("0").heightPx(100))
                    .child(Text.create(c).key("#1").text("1").heightPx(100))
                    .child(TextInput.create(c).key("#2").initialText("2").heightPx(100))
                    .child(TextInput.create(c).key("#3").initialText("3").heightPx(100))
                    .child(TextInput.create(c).key("#4").initialText("4").heightPx(100))
                    .child(
                        MountSpecWithMountUnmountAssertion.create(c)
                            .key("test")
                            .container(MountSpecWithMountUnmountAssertionSpec.Container())
                            .heightPx(100))
                    .child(TextInput.create(c).key("#6").initialText("6").heightPx(100)))
            .build()
    lithoView.setComponent(newComponent)

    // 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(1)
    assertThat((lithoView.getChildAt(0) as ComponentHost).childCount).isEqualTo(1)
    lithoView.unmountAllItems()
  }
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.facebook.litho

import android.content.Context
import androidx.test.core.app.ApplicationProvider
import com.facebook.litho.config.ComponentsConfiguration
import com.facebook.litho.testing.assertj.LithoViewAssert
import com.facebook.litho.testing.assertj.LithoViewAssert.assertThat
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.SimpleMountSpecTester
import com.facebook.litho.widget.Text
import org.assertj.core.api.Assertions.assertThat
import org.junit.After
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.kotlin.mock

@RunWith(LithoTestRunner::class)
class MountStateTestItemTest {

  private lateinit var context: ComponentContext
  private var originalE2ETestRun = false

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

  @After
  fun teardown() {
    ComponentsConfiguration.isEndToEndTestRun = originalE2ETestRun
  }

  @Test
  fun testInnerComponentHostViewTags() {
    val lithoView =
        ComponentTestHelper.mountComponent(
            context,
            object : InlineLayoutSpec() {
              override fun onCreateLayout(c: ComponentContext): Component {
                return Column.create(c)
                    .child(
                        Column.create(c)
                            .child(SimpleMountSpecTester.create(c))
                            .child(SimpleMountSpecTester.create(c))
                            .testKey(TEST_ID_1))
                    .child(SimpleMountSpecTester.create(c))
                    .child(SimpleMountSpecTester.create(c).testKey(TEST_ID_2))
                    .build()
              }
            })
    assertThat(lithoView)
        .containsTestKey(TEST_ID_1)
        .containsTestKey(TEST_ID_2)
        .doesNotContainTestKey(TEST_ID_3)
  }

  @Test
  fun testEndToEndExtensionsWorkWithRenderCoreMountState() {
    val lithoView =
        ComponentTestHelper.mountComponent(
            context,
            object : InlineLayoutSpec() {
              override fun onCreateLayout(c: ComponentContext): Component {
                return Column.create(c).testKey(TEST_ID_1).build()
              }
            })
    assertThat(lithoView).containsTestKey(TEST_ID_1)
  }

  @Test
  fun testMultipleIdenticalInnerComponentHostViewTags() {
    val lithoView =
        ComponentTestHelper.mountComponent(
            context,
            object : InlineLayoutSpec() {
              override fun onCreateLayout(c: ComponentContext): Component {
                return Column.create(c)
                    .child(
                        Column.create(c)
                            .child(SimpleMountSpecTester.create(c))
                            .child(SimpleMountSpecTester.create(c))
                            .testKey(TEST_ID_1))
                    .child(SimpleMountSpecTester.create(c))
                    .child(SimpleMountSpecTester.create(c).testKey(TEST_ID_1))
                    .build()
              }
            })
    assertThat(lithoView)
        .containsTestKey(TEST_ID_1, LithoViewAssert.times(2))
        .doesNotContainTestKey(TEST_ID_2)
  }

  @Test
  fun testSkipInvalidTestKeys() {
    val lithoView =
        ComponentTestHelper.mountComponent(
            context,
            object : InlineLayoutSpec() {
              override fun onCreateLayout(c: ComponentContext): Component {
                return Column.create(c)
                    .child(
                        Column.create(c)
                            .child(SimpleMountSpecTester.create(c))
                            .child(SimpleMountSpecTester.create(c))
                            .testKey(""))
                    .child(SimpleMountSpecTester.create(c))
                    .child(SimpleMountSpecTester.create(c).testKey(null))
                    .child(SimpleMountSpecTester.create(c).testKey(TEST_ID_1))
                    .build()
              }
            })
    assertThat(lithoView)
        .doesNotContainTestKey("")
        .doesNotContainTestKey(null)
        .containsTestKey(TEST_ID_1)
  }

  @Test
  fun testTextItemTextContent() {
    val lithoView =
        ComponentTestHelper.mountComponent(
            context,
            object : InlineLayoutSpec() {
              override fun onCreateLayout(c: ComponentContext): Component {
                return Column.create(c)
                    .child(Text.create(c).text(MY_TEST_STRING_1).testKey(TEST_ID_1))
                    .build()
              }
            })
    assertThat(lithoView).containsTestKey(TEST_ID_1)
    val item1 = LithoViewTestHelper.findTestItem(lithoView, TEST_ID_1)
    assertThat(item1?.textContent).isEqualTo(MY_TEST_STRING_1)
  }

  @Test
  fun testMultipleTextItemsTextContents() {
    val lithoView =
        ComponentTestHelper.mountComponent(
            context,
            object : InlineLayoutSpec() {
              override fun onCreateLayout(c: ComponentContext): Component {
                return Column.create(c)
                    .child(Text.create(c).text(MY_TEST_STRING_1).testKey(TEST_ID_1))
                    .child(Text.create(c).text(MY_TEST_STRING_2).testKey(TEST_ID_2))
                    .build()
              }
            })
    assertThat(lithoView).containsTestKey(TEST_ID_1)
    val item1 = LithoViewTestHelper.findTestItem(lithoView, TEST_ID_1)
    assertThat(item1?.textContent).isEqualTo(MY_TEST_STRING_1)
    val item2 = LithoViewTestHelper.findTestItem(lithoView, TEST_ID_2)
    assertThat(item2?.textContent).isEqualTo(MY_TEST_STRING_2)
  }

  @Test
  fun testTextItemsWithClickHandler() {
    val lithoView =
        ComponentTestHelper.mountComponent(
            context,
            object : InlineLayoutSpec() {
              override fun onCreateLayout(c: ComponentContext): Component {
                return Column.create(c)
                    .child(
                        Text.create(c)
                            .text(MY_TEST_STRING_1)
                            .clickHandler(mock<EventHandler<ClickEvent>>())
                            .testKey(TEST_ID_1))
                    .child(Text.create(c).text(MY_TEST_STRING_2).testKey(TEST_ID_2))
                    .build()
              }
            })
    assertThat(lithoView).containsTestKey(TEST_ID_1)
    val item1 = LithoViewTestHelper.findTestItem(lithoView, TEST_ID_1)
    assertThat(item1?.textContent).isEqualTo(MY_TEST_STRING_1)
    val item2 = LithoViewTestHelper.findTestItem(lithoView, TEST_ID_2)
    assertThat(item2?.textContent).isEqualTo(MY_TEST_STRING_2)
  }

  companion object {
    private const val TEST_ID_1 = "test_id_1"
    private const val TEST_ID_2 = "test_id_2"
    private const val TEST_ID_3 = "test_id_3"
    private const val MY_TEST_STRING_1 = "My test string"
    private const val MY_TEST_STRING_2 = "My second test string"
  }
}