/*
 * 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.ON_MOUNT
import com.facebook.litho.LifecycleStep.ON_UNMOUNT
import com.facebook.litho.sections.SectionContext
import com.facebook.litho.sections.common.DynamicComponentGroupSection
import com.facebook.litho.sections.widget.ListRecyclerConfiguration
import com.facebook.litho.sections.widget.RecyclerCollectionComponent
import com.facebook.litho.sections.widget.RecyclerConfiguration
import com.facebook.litho.testing.LegacyLithoViewRule
import com.facebook.litho.testing.testrunner.LithoTestRunner
import com.facebook.litho.widget.MountSpecLifecycleTester
import com.facebook.litho.widget.SectionsRecyclerView
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 IncrementalMountTest {

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

  @Before
  fun setup() {
    context = lithoViewRule.context
    lithoViewRule.useLithoView(LithoView(context))
  }

  private fun buildRecyclerCollectionComponent(
      lifecycleTracker1: LifecycleTracker,
      lifecycleTracker2: LifecycleTracker,
      lifecycleTracker3: LifecycleTracker,
      childWidth: Int,
      childHeight: Int
  ): RecyclerCollectionComponent {
    val child1: Component =
        MountSpecLifecycleTester.create(context).lifecycleTracker(lifecycleTracker1).build()
    val child2: Component =
        MountSpecLifecycleTester.create(context).lifecycleTracker(lifecycleTracker2).build()
    val child3: Component =
        MountSpecLifecycleTester.create(context).lifecycleTracker(lifecycleTracker3).build()

    // Item is composed of 3 children of equal size (10x10), making 1 item of height 30.
    val item: Component =
        Column.create(context)
            .child(
                Wrapper.create(context).delegate(child1).widthPx(childWidth).heightPx(childHeight))
            .child(
                Wrapper.create(context).delegate(child2).widthPx(childWidth).heightPx(childHeight))
            .child(
                Wrapper.create(context).delegate(child3).widthPx(childWidth).heightPx(childHeight))
            .build()
    val config: RecyclerConfiguration = ListRecyclerConfiguration.create().build()
    val sectionContext = SectionContext(context)
    return RecyclerCollectionComponent.create(context)
        .recyclerConfiguration(config)
        .section(
            DynamicComponentGroupSection.create(sectionContext)
                .component(item)
                .totalItems(5)
                .build())
        .build()
  }

  @Test
  fun `incrementalMount recycler view vertical orientation all items visible`() {
    val lifecycleTracker1: LifecycleTracker = LifecycleTracker()
    val lifecycleTracker2: LifecycleTracker = LifecycleTracker()
    val lifecycleTracker3: LifecycleTracker = LifecycleTracker()

    val CHILD_HEIGHT = 10

    val rcc: Component =
        buildRecyclerCollectionComponent(
            lifecycleTracker1, lifecycleTracker2, lifecycleTracker3, 10, CHILD_HEIGHT)

    // Set LithoView with height so that it can fully show all the items
    lithoViewRule
        .setRoot(rcc)
        .attachToWindow()
        .setSizeSpecs(
            SizeSpec.makeSizeSpec(100, SizeSpec.EXACTLY),
            SizeSpec.makeSizeSpec(CHILD_HEIGHT * 15, SizeSpec.EXACTLY))
        .measure()
        .layout()

    // All 3 children are visible 5 times, so we should see ON_MOUNT being called 5 times
    // for each child
    assertThat(getCountOfLifecycleSteps(lifecycleTracker1.steps, ON_MOUNT)).isEqualTo(5)
    assertThat(getCountOfLifecycleSteps(lifecycleTracker2.steps, ON_MOUNT)).isEqualTo(5)
    assertThat(getCountOfLifecycleSteps(lifecycleTracker3.steps, ON_MOUNT)).isEqualTo(5)
  }

  @Test
  fun `incrementalMount recycler view vertical orientation nothing in visible bounds`() {
    val lifecycleTracker1: LifecycleTracker = LifecycleTracker()
    val lifecycleTracker2: LifecycleTracker = LifecycleTracker()
    val lifecycleTracker3: LifecycleTracker = LifecycleTracker()

    val CHILD_HEIGHT = 10

    val rcc: Component =
        buildRecyclerCollectionComponent(
            lifecycleTracker1, lifecycleTracker2, lifecycleTracker3, 10, CHILD_HEIGHT)

    // Set LithoView with height so that it can fully show all the items
    lithoViewRule
        .setRoot(rcc)
        .attachToWindow()
        .setSizeSpecs(
            SizeSpec.makeSizeSpec(100, SizeSpec.EXACTLY),
            SizeSpec.makeSizeSpec(0, SizeSpec.EXACTLY))
        .measure()
        .layout()

    // SizeSpec height is 0, so nothing is visible and we should see that ON_MOUNT is not called.
    assertThat(getCountOfLifecycleSteps(lifecycleTracker1.steps, ON_MOUNT)).isEqualTo(0)
    assertThat(getCountOfLifecycleSteps(lifecycleTracker2.steps, ON_MOUNT)).isEqualTo(0)
    assertThat(getCountOfLifecycleSteps(lifecycleTracker3.steps, ON_MOUNT)).isEqualTo(0)
  }

  @Test
  fun `incrementalMount recycler view vertical orientation some items completely in visible bounds`() {
    val lifecycleTracker1: LifecycleTracker = LifecycleTracker()
    val lifecycleTracker2: LifecycleTracker = LifecycleTracker()
    val lifecycleTracker3: LifecycleTracker = LifecycleTracker()

    val CHILD_HEIGHT = 10

    val rcc: Component =
        buildRecyclerCollectionComponent(
            lifecycleTracker1, lifecycleTracker2, lifecycleTracker3, 10, CHILD_HEIGHT)

    // Set LithoView with height so that it can fully show all the items
    lithoViewRule
        .setRoot(rcc)
        .attachToWindow()
        .setSizeSpecs(
            SizeSpec.makeSizeSpec(100, SizeSpec.EXACTLY),
            SizeSpec.makeSizeSpec(CHILD_HEIGHT * 9, SizeSpec.EXACTLY))
        .measure()
        .layout()

    // All 3 children are visible 3 times, so we should see ON_MOUNT being called 3 times
    // for each child
    assertThat(getCountOfLifecycleSteps(lifecycleTracker1.steps, ON_MOUNT)).isEqualTo(3)
    assertThat(getCountOfLifecycleSteps(lifecycleTracker2.steps, ON_MOUNT)).isEqualTo(3)
    assertThat(getCountOfLifecycleSteps(lifecycleTracker3.steps, ON_MOUNT)).isEqualTo(3)
  }

  @Test
  fun `incrementalMount recycler view vertical orientation some items partially in visible bounds`() {
    val lifecycleTracker1: LifecycleTracker = LifecycleTracker()
    val lifecycleTracker2: LifecycleTracker = LifecycleTracker()
    val lifecycleTracker3: LifecycleTracker = LifecycleTracker()

    val CHILD_HEIGHT = 10

    val rcc: Component =
        buildRecyclerCollectionComponent(
            lifecycleTracker1, lifecycleTracker2, lifecycleTracker3, 10, CHILD_HEIGHT)

    // Set LithoView with height so that it can fully show all the items
    lithoViewRule
        .setRoot(rcc)
        .attachToWindow()
        .setSizeSpecs(
            SizeSpec.makeSizeSpec(100, SizeSpec.EXACTLY),
            SizeSpec.makeSizeSpec(CHILD_HEIGHT * 8 + (CHILD_HEIGHT / 2), SizeSpec.EXACTLY))
        .measure()
        .layout()

    // All 3 children are visible 3 times, so we should see ON_MOUNT being called 3 times
    // for each child
    assertThat(getCountOfLifecycleSteps(lifecycleTracker1.steps, ON_MOUNT)).isEqualTo(3)
    assertThat(getCountOfLifecycleSteps(lifecycleTracker2.steps, ON_MOUNT)).isEqualTo(3)
    assertThat(getCountOfLifecycleSteps(lifecycleTracker3.steps, ON_MOUNT)).isEqualTo(3)
  }

  @Test
  fun `incrementalMount recycler view vertical orientation some items in visible bounds scroll up and down`() {
    val lifecycleTracker1: LifecycleTracker = LifecycleTracker()
    val lifecycleTracker2: LifecycleTracker = LifecycleTracker()
    val lifecycleTracker3: LifecycleTracker = LifecycleTracker()
    val CHILD_HEIGHT = 10

    val rcc: Component =
        buildRecyclerCollectionComponent(
            lifecycleTracker1, lifecycleTracker2, lifecycleTracker3, 10, CHILD_HEIGHT)

    // Set LithoView with height so that it can fully show exactly 3 items (3 children per item).
    lithoViewRule
        .setRoot(rcc)
        .attachToWindow()
        .setSizeSpecs(
            SizeSpec.makeSizeSpec(100, SizeSpec.EXACTLY),
            SizeSpec.makeSizeSpec(CHILD_HEIGHT * 9, SizeSpec.EXACTLY))
        .measure()
        .layout()

    // Obtain the RV for scrolling later
    val recyclerView = (lithoViewRule.lithoView.getChildAt(0) as SectionsRecyclerView).recyclerView

    // All 3 children are visible 3 times, so we should see ON_MOUNT being called 3 times
    // for each child
    assertThat(getCountOfLifecycleSteps(lifecycleTracker1.steps, ON_MOUNT)).isEqualTo(3)
    assertThat(getCountOfLifecycleSteps(lifecycleTracker2.steps, ON_MOUNT)).isEqualTo(3)
    assertThat(getCountOfLifecycleSteps(lifecycleTracker3.steps, ON_MOUNT)).isEqualTo(3)

    // Clear the lifecycle steps
    lifecycleTracker1.reset()
    lifecycleTracker2.reset()
    lifecycleTracker3.reset()

    // Scroll down by the size of 1 child. We are expecting to top item's child1 to be
    // unmounted, and a new bottom item's child1 to be mounted.
    recyclerView.scrollBy(0, CHILD_HEIGHT)

    // Ensure unmount is called once
    assertThat(getCountOfLifecycleSteps(lifecycleTracker1.steps, ON_UNMOUNT)).isEqualTo(1)

    // Ensure mount is called once
    // When using Litho's inc-mount, the exiting item will be mounted twice due to an issue with
    // the calculation there. Inc-mount-ext does not have this issue.
    assertThat(getCountOfLifecycleSteps(lifecycleTracker1.steps, ON_MOUNT)).isEqualTo(1)

    // child2 & 3 of all items should not change.
    assertThat(getCountOfLifecycleSteps(lifecycleTracker2.steps, ON_UNMOUNT)).isEqualTo(0)
    assertThat(getCountOfLifecycleSteps(lifecycleTracker2.steps, ON_MOUNT)).isEqualTo(0)
    assertThat(getCountOfLifecycleSteps(lifecycleTracker3.steps, ON_UNMOUNT)).isEqualTo(0)
    assertThat(getCountOfLifecycleSteps(lifecycleTracker3.steps, ON_MOUNT)).isEqualTo(0)

    // Clear the lifecycle steps
    lifecycleTracker1.reset()
    lifecycleTracker2.reset()
    lifecycleTracker3.reset()

    // Scroll up by the size of 1 component. We are expecting to top item's child1 to be mounted,
    // and the bottom item to be unmounted
    recyclerView.scrollBy(0, -CHILD_HEIGHT)

    // Ensure unmount is called once
    assertThat(getCountOfLifecycleSteps(lifecycleTracker1.steps, ON_UNMOUNT)).isEqualTo(1)

    // Ensure mount is called once
    // When using Litho's inc-mount, the item we previously expected to exit is still there, so
    // we don't expect a mount to occur.
    assertThat(getCountOfLifecycleSteps(lifecycleTracker1.steps, ON_MOUNT)).isEqualTo(1)

    // child2 & 3 of all items should not change.
    assertThat(getCountOfLifecycleSteps(lifecycleTracker2.steps, ON_UNMOUNT)).isEqualTo(0)
    assertThat(getCountOfLifecycleSteps(lifecycleTracker2.steps, ON_MOUNT)).isEqualTo(0)
    assertThat(getCountOfLifecycleSteps(lifecycleTracker3.steps, ON_UNMOUNT)).isEqualTo(0)
    assertThat(getCountOfLifecycleSteps(lifecycleTracker3.steps, ON_MOUNT)).isEqualTo(0)
  }
}

/** Returns the amount of steps that match the given step in the given list of steps */
fun getCountOfLifecycleSteps(steps: List<LifecycleStep>, step: LifecycleStep): Int {
  var count = 0
  for (i in steps.indices) {
    if (steps[i] == step) {
      count++
    }
  }
  return count
}


/*
 * 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.MountSpecInterStagePropsTester
import com.facebook.litho.widget.SimpleStateUpdateEmulator
import com.facebook.litho.widget.SimpleStateUpdateEmulatorSpec
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 InterStagePropsTest {

  private lateinit var context: ComponentContext

  @JvmField @Rule val legacyLithoViewRule = LegacyLithoViewRule()

  @Before
  fun setUp() {
    context = legacyLithoViewRule.context
    legacyLithoViewRule.useLithoView(LithoView(context))
  }

  @Test
  fun interStageProp_FromPrepare_usedIn_OnBind() {
    val lifecycleTracker = LifecycleTracker()
    val stateUpdater = SimpleStateUpdateEmulatorSpec.Caller()
    val root = createComponent(lifecycleTracker, stateUpdater)
    legacyLithoViewRule.setRoot(root).attachToWindow().measure().layout()
    lifecycleTracker.reset()
    stateUpdater.increment()
    assertThat(lifecycleTracker.steps)
        .describedAs("On Bind should be called")
        .contains(LifecycleStep.ON_BIND)
  }

  @Test
  fun interStageProp_FromBind_usedIn_OnUnbind() {
    val lifecycleTracker = LifecycleTracker()
    val stateUpdater = SimpleStateUpdateEmulatorSpec.Caller()
    val root = createComponent(lifecycleTracker, stateUpdater)
    legacyLithoViewRule.setRoot(root).attachToWindow().measure().layout()
    lifecycleTracker.reset()
    legacyLithoViewRule.detachFromWindow()
    assertThat(lifecycleTracker.steps)
        .describedAs("On Unbind should be called")
        .contains(LifecycleStep.ON_UNBIND)
  }

  @Test
  fun interStageProp_FromMeasure_usedIn_OnMount() {
    val lifecycleTracker = LifecycleTracker()
    val stateUpdater = SimpleStateUpdateEmulatorSpec.Caller()
    val root = createComponent(lifecycleTracker, stateUpdater)
    legacyLithoViewRule.setRoot(root).attachToWindow().measure().layout()
    assertThat(lifecycleTracker.steps)
        .describedAs("On Mount should be called")
        .contains(LifecycleStep.ON_MOUNT)
  }

  @Test
  fun interStageProp_FromBoundsDefined_usedIn_OnUnMount() {
    val lifecycleTracker = LifecycleTracker()
    val stateUpdater = SimpleStateUpdateEmulatorSpec.Caller()
    val root = createComponent(lifecycleTracker, stateUpdater)
    legacyLithoViewRule.setRoot(root).attachToWindow().measure().layout()
    lifecycleTracker.reset()
    legacyLithoViewRule.lithoView.unmountAllItems()
    assertThat(lifecycleTracker.steps)
        .describedAs("On Unmount should be called")
        .contains(LifecycleStep.ON_UNMOUNT)
  }

  private fun createComponent(
      lifecycleTracker: LifecycleTracker,
      stateUpdater: SimpleStateUpdateEmulatorSpec.Caller
  ): Component =
      Column.create(legacyLithoViewRule.context)
          .child(
              MountSpecInterStagePropsTester.create(legacyLithoViewRule.context)
                  .lifecycleTracker(lifecycleTracker))
          .child(SimpleStateUpdateEmulator.create(legacyLithoViewRule.context).caller(stateUpdater))
          .build()
}


/*
 * 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.KotlinClass
import com.facebook.litho.widget.KotlinClassSpec
import com.facebook.litho.widget.KotlinObject
import com.facebook.litho.widget.KotlinObjectSpec
import java.util.concurrent.atomic.AtomicBoolean
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 KotlinSpecPropDefaultsTest {

  @JvmField @Rule var lithoViewRule = LegacyLithoViewRule()

  private lateinit var ensureAssertion: AtomicBoolean

  @Before
  fun setup() {
    ensureAssertion = AtomicBoolean(false)
  }

  /** Annotation: `@get:PropDefault val fieldName = ...` */
  @Test
  fun `Kotlin object spec with PropDefault get annotation should initialise default value`() {
    lithoViewRule.render {
      KotlinObject.create(lithoViewRule.context)
          .getPropDefaultAssertion {
            assertThat(it).isEqualTo(KotlinObjectSpec.getPropDefault)
            ensureAssertion.set(true)
          }
          .build()
    }

    assertThat(ensureAssertion.get()).isTrue
  }

  /** Annotation: `@PropDefault const val fieldName = ...` */
  @Test
  fun `Kotlin object spec with const PropDefault should initialise default value`() {
    lithoViewRule.render {
      KotlinObject.create(lithoViewRule.context)
          .constPropDefaultAssertion {
            assertThat(it).isEqualTo(KotlinObjectSpec.constPropDefault)
            ensureAssertion.set(true)
          }
          .build()
    }

    assertThat(ensureAssertion.get()).isTrue
  }

  /** Annotation: `@PropDefault @JvmField val fieldName = ...` */
  @Test
  fun `Kotlin object spec with JvmField PropDefault should initialise default value`() {
    lithoViewRule.render {
      KotlinObject.create(lithoViewRule.context)
          .jvmFieldPropDefaultAssertion {
            assertThat(it).isEqualTo(KotlinObjectSpec.jvmFieldPropDefault)
            ensureAssertion.set(true)
          }
          .build()
    }

    assertThat(ensureAssertion.get()).isTrue
  }

  /** Annotation: `@PropDefault val fieldName = ...` */
  @Test
  fun `Kotlin object spec with just PropDefault should initialise default value`() {
    lithoViewRule.render {
      KotlinObject.create(lithoViewRule.context)
          .justPropDefaultAssertion {
            assertThat(it).isEqualTo(KotlinObjectSpec.justPropDefault)
            ensureAssertion.set(true)
          }
          .build()
    }

    assertThat(ensureAssertion.get()).isTrue
  }

  /** Annotation: `@get:PropDefault val fieldName = ...` */
  @Test
  fun `Kotlin class spec with PropDefault get annotation should initialise default value`() {
    lithoViewRule.render {
      KotlinClass.create(lithoViewRule.context)
          .getPropDefaultAssertion {
            assertThat(it).isEqualTo(KotlinClassSpec.getPropDefault)
            ensureAssertion.set(true)
          }
          .build()
    }

    assertThat(ensureAssertion.get()).isTrue
  }

  /** Annotation: `@PropDefault const val fieldName = ...` */
  @Test
  fun `Kotlin class spec with const PropDefault should initialise default value`() {
    lithoViewRule.render {
      KotlinClass.create(lithoViewRule.context)
          .constPropDefaultAssertion {
            assertThat(it).isEqualTo(KotlinClassSpec.constPropDefault)
            ensureAssertion.set(true)
          }
          .build()
    }

    assertThat(ensureAssertion.get()).isTrue
  }

  /** Annotation: `@PropDefault @JvmField val fieldName = ...` */
  @Test
  fun `Kotlin class spec with JvmField PropDefault should initialise default value`() {
    lithoViewRule.render {
      KotlinClass.create(lithoViewRule.context)
          .jvmFieldPropDefaultAssertion {
            assertThat(it).isEqualTo(KotlinClassSpec.jvmFieldPropDefault)
            ensureAssertion.set(true)
          }
          .build()
    }

    assertThat(ensureAssertion.get()).isTrue
  }

  /** Annotation: `@PropDefault val fieldName = ...` */
  @Test
  fun `Kotlin class spec with just PropDefault should initialise default value`() {
    lithoViewRule.render {
      KotlinClass.create(lithoViewRule.context)
          .justPropDefaultAssertion {
            assertThat(it).isEqualTo(KotlinClassSpec.justPropDefault)
            ensureAssertion.set(true)
          }
          .build()
    }

    assertThat(ensureAssertion.get()).isTrue
  }
}


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

package com.facebook.litho

import com.facebook.litho.config.ComponentsConfiguration
import com.facebook.litho.sections.SectionContext
import com.facebook.litho.sections.common.DynamicComponentGroupSection
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.sections.widget.RecyclerConfiguration
import com.facebook.litho.testing.LegacyLithoViewRule
import com.facebook.litho.testing.exactly
import com.facebook.litho.testing.testrunner.LithoTestRunner
import com.facebook.litho.testing.unspecified
import com.facebook.litho.widget.MountSpecInterStagePropsTester
import com.facebook.litho.widget.MountSpecLifecycleTester
import com.facebook.litho.widget.MountSpecPureRenderLifecycleTester
import com.facebook.litho.widget.SimpleStateUpdateEmulator
import com.facebook.litho.widget.SimpleStateUpdateEmulatorSpec
import org.assertj.core.api.Assertions
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 LayoutCachingTest {

  @JvmField @Rule var legacyLithoViewRule = LegacyLithoViewRule()

  fun `unchanged node with inter stage prop should not be remeasured when state updates`() {
    if (!ComponentsConfiguration.enableLayoutCaching) {
      return
    }

    val c = legacyLithoViewRule.context
    val lifecycleTracker = LifecycleTracker()
    val caller = SimpleStateUpdateEmulatorSpec.Caller()
    val component =
        Column.create(c)
            .child(SimpleStateUpdateEmulator.create(c).caller(caller).build())
            .child(
                MountSpecInterStagePropsTester.create(c).lifecycleTracker(lifecycleTracker).build())
            .build()

    legacyLithoViewRule.setRoot(component).attachToWindow().measure().layout()
    Assertions.assertThat(lifecycleTracker.steps)
        .containsExactly(
            LifecycleStep.ON_CREATE_INITIAL_STATE,
            LifecycleStep.ON_PREPARE,
            LifecycleStep.ON_MEASURE,
            LifecycleStep.ON_MOUNT,
            LifecycleStep.ON_BIND)

    lifecycleTracker.reset()
    caller.increment()
    Assertions.assertThat(lifecycleTracker.steps)
        .describedAs(
            "Node with inter stage props doesn't need remeasurement but rebinding if layout caching is turned on")
        .containsExactly(
            LifecycleStep.ON_UNBIND,
            LifecycleStep.ON_UNMOUNT,
            LifecycleStep.ON_MOUNT,
            LifecycleStep.ON_BIND)
  }

  @Test
  fun `unchanged node without inter stage prop should not get rebinding when state updates`() {
    if (!ComponentsConfiguration.enableLayoutCaching) {
      return
    }

    val c = legacyLithoViewRule.context
    val lifecycleTracker = LifecycleTracker()
    val caller = SimpleStateUpdateEmulatorSpec.Caller()
    val component =
        Column.create(c)
            .child(SimpleStateUpdateEmulator.create(c).caller(caller).build())
            .child(
                MountSpecLifecycleTester.create(c)
                    .intrinsicSize(Size(100, 100))
                    .lifecycleTracker(lifecycleTracker)
                    .build())
            .build()

    legacyLithoViewRule.setRoot(component).attachToWindow().measure().layout()
    Assertions.assertThat(lifecycleTracker.steps)
        .containsExactly(
            LifecycleStep.ON_CREATE_INITIAL_STATE,
            LifecycleStep.ON_CREATE_TREE_PROP,
            LifecycleStep.ON_CALCULATE_CACHED_VALUE,
            LifecycleStep.ON_PREPARE,
            LifecycleStep.ON_MEASURE,
            LifecycleStep.ON_BOUNDS_DEFINED,
            LifecycleStep.ON_ATTACHED,
            LifecycleStep.ON_CREATE_MOUNT_CONTENT,
            LifecycleStep.ON_MOUNT,
            LifecycleStep.ON_BIND)

    lifecycleTracker.reset()
    caller.increment()
    Assertions.assertThat(lifecycleTracker.steps)
        .describedAs(
            "Node without inter stage props doesn't need rebinding if layout caching is turned on")
        .containsExactly(LifecycleStep.ON_BOUNDS_DEFINED)
  }

  @Test
  fun `unchanged subtree should not get rebinding when state updates`() {
    if (!ComponentsConfiguration.enableLayoutCaching) {
      return
    }

    val c = legacyLithoViewRule.context
    val lifecycleTracker1 = LifecycleTracker()
    val lifecycleTracker2 = LifecycleTracker()
    val caller: SimpleStateUpdateEmulatorSpec.Caller = SimpleStateUpdateEmulatorSpec.Caller()
    val component: Column =
        Column.create(c)
            .child(SimpleStateUpdateEmulator.create(c).caller(caller))
            .child(
                Column.create(c)
                    .child(
                        MountSpecPureRenderLifecycleTester.create(c)
                            .intrinsicSize(Size(100, 100))
                            .lifecycleTracker(lifecycleTracker1))
                    .child(
                        Column.create(c)
                            .child(
                                MountSpecPureRenderLifecycleTester.create(c)
                                    .intrinsicSize(Size(200, 200))
                                    .lifecycleTracker(lifecycleTracker2))))
            .build()

    legacyLithoViewRule.setRoot(component).attachToWindow().measure().layout()
    Assertions.assertThat(lifecycleTracker1.steps)
        .containsExactly(
            LifecycleStep.ON_CREATE_INITIAL_STATE,
            LifecycleStep.ON_CREATE_TREE_PROP,
            LifecycleStep.ON_CALCULATE_CACHED_VALUE,
            LifecycleStep.ON_PREPARE,
            LifecycleStep.ON_MEASURE,
            LifecycleStep.ON_BOUNDS_DEFINED,
            LifecycleStep.ON_ATTACHED,
            LifecycleStep.ON_CREATE_MOUNT_CONTENT,
            LifecycleStep.ON_MOUNT,
            LifecycleStep.ON_BIND)
    Assertions.assertThat(lifecycleTracker2.steps)
        .containsExactly(
            LifecycleStep.ON_CREATE_INITIAL_STATE,
            LifecycleStep.ON_CREATE_TREE_PROP,
            LifecycleStep.ON_CALCULATE_CACHED_VALUE,
            LifecycleStep.ON_PREPARE,
            LifecycleStep.ON_MEASURE,
            LifecycleStep.ON_BOUNDS_DEFINED,
            LifecycleStep.ON_ATTACHED,
            LifecycleStep.ON_CREATE_MOUNT_CONTENT,
            LifecycleStep.ON_MOUNT,
            LifecycleStep.ON_BIND)

    lifecycleTracker1.reset()
    lifecycleTracker2.reset()
    caller.increment()
    Assertions.assertThat(lifecycleTracker1.steps).containsExactly(LifecycleStep.ON_BOUNDS_DEFINED)
    Assertions.assertThat(lifecycleTracker2.steps).containsExactly(LifecycleStep.ON_BOUNDS_DEFINED)
  }

  @Test
  fun `changing size spec should trigger re-measurement`() {
    if (!ComponentsConfiguration.enableLayoutCaching) {
      return
    }

    val c = legacyLithoViewRule.context
    val lifecycleTracker = LifecycleTracker()
    val component =
        MountSpecPureRenderLifecycleTester.create(c)
            .lifecycleTracker(lifecycleTracker)
            .intrinsicSize(Size(100, 100))
            .build()

    // Make the target component to be the root component and change the size spec
    legacyLithoViewRule
        .setRoot(component)
        .attachToWindow()
        .setSizeSpecs(exactly(100), unspecified())
        .measure()
        .layout()
    Assertions.assertThat(lifecycleTracker.steps)
        .containsExactly(
            LifecycleStep.ON_CREATE_INITIAL_STATE,
            LifecycleStep.ON_CREATE_TREE_PROP,
            LifecycleStep.ON_CALCULATE_CACHED_VALUE,
            LifecycleStep.ON_PREPARE,
            LifecycleStep.ON_MEASURE,
            LifecycleStep.ON_BOUNDS_DEFINED,
            LifecycleStep.ON_ATTACHED,
            LifecycleStep.ON_CREATE_MOUNT_CONTENT,
            LifecycleStep.ON_MOUNT,
            LifecycleStep.ON_BIND)

    lifecycleTracker.reset()
    legacyLithoViewRule.setSizeSpecs(exactly(200), unspecified()).measure().layout()
    Assertions.assertThat(lifecycleTracker.steps)
        .containsExactly(
            LifecycleStep.ON_MEASURE,
            LifecycleStep.ON_BOUNDS_DEFINED,
            LifecycleStep.SHOULD_UPDATE,
            LifecycleStep.ON_UNBIND,
            LifecycleStep.ON_UNMOUNT,
            LifecycleStep.ON_MOUNT,
            LifecycleStep.ON_BIND)
  }

  @Test
  fun `changing size spec but end up with same measured size should reuse render unit`() {
    if (!ComponentsConfiguration.enableLayoutCaching) {
      return
    }

    val c = legacyLithoViewRule.context
    val lifecycleTracker = LifecycleTracker()
    val component =
        MountSpecPureRenderLifecycleTester.create(c)
            .lifecycleTracker(lifecycleTracker)
            .intrinsicSize(Size(100, 100))
            .build()

    // Make the target component to be the root component and change the size spec
    legacyLithoViewRule
        .setRoot(component)
        .attachToWindow()
        .setSizeSpecs(exactly(100), unspecified())
        .measure()
        .layout()
    Assertions.assertThat(lifecycleTracker.steps)
        .containsExactly(
            LifecycleStep.ON_CREATE_INITIAL_STATE,
            LifecycleStep.ON_CREATE_TREE_PROP,
            LifecycleStep.ON_CALCULATE_CACHED_VALUE,
            LifecycleStep.ON_PREPARE,
            LifecycleStep.ON_MEASURE,
            LifecycleStep.ON_BOUNDS_DEFINED,
            LifecycleStep.ON_ATTACHED,
            LifecycleStep.ON_CREATE_MOUNT_CONTENT,
            LifecycleStep.ON_MOUNT,
            LifecycleStep.ON_BIND)

    lifecycleTracker.reset()
    legacyLithoViewRule.setSizeSpecs(unspecified(), unspecified()).measure().layout()
    Assertions.assertThat(lifecycleTracker.steps)
        .containsExactly(LifecycleStep.ON_MEASURE, LifecycleStep.ON_BOUNDS_DEFINED)
  }

  @Test
  fun `inter stage data should be copied for cached nodes`() {
    if (!ComponentsConfiguration.enableLayoutCaching) {
      return
    }

    val c = legacyLithoViewRule.context
    val lifecycleTracker = LifecycleTracker()
    val caller = SimpleStateUpdateEmulatorSpec.Caller()
    val component =
        Column.create(c)
            .child(SimpleStateUpdateEmulator.create(c).caller(caller))
            .child(MountSpecInterStagePropsTester.create(c).lifecycleTracker(lifecycleTracker))
            .build()

    legacyLithoViewRule.setRoot(component).attachToWindow().measure().layout()
    Assertions.assertThat(lifecycleTracker.steps)
        .containsExactly(
            LifecycleStep.ON_CREATE_INITIAL_STATE,
            LifecycleStep.ON_PREPARE,
            LifecycleStep.ON_MEASURE,
            LifecycleStep.ON_MOUNT,
            LifecycleStep.ON_BIND)

    lifecycleTracker.reset()
    caller.increment()
    Assertions.assertThat(lifecycleTracker.steps)
        .describedAs("prepare and measure should not be called for cached node")
        .containsExactly(
            LifecycleStep.ON_UNBIND,
            LifecycleStep.ON_UNMOUNT,
            LifecycleStep.ON_MOUNT,
            LifecycleStep.ON_BIND)
  }

  @Test
  fun `unchanged node should not get rebinding when the size of root node changes`() {
    if (!ComponentsConfiguration.enableLayoutCaching) {
      return
    }

    val c = legacyLithoViewRule.context
    val lifecycleTracker = LifecycleTracker()
    val component =
        Column.create(c)
            .child(
                MountSpecPureRenderLifecycleTester.create(c)
                    .lifecycleTracker(lifecycleTracker)
                    .maxWidthPx(200)
                    .maxHeightPx(200))
            .build()

    legacyLithoViewRule
        .setRoot(component)
        .setSizeSpecs(exactly(300), exactly(300))
        .attachToWindow()
        .measure()
        .layout()
    Assertions.assertThat(lifecycleTracker.steps)
        .containsExactly(
            LifecycleStep.ON_CREATE_INITIAL_STATE,
            LifecycleStep.ON_CREATE_TREE_PROP,
            LifecycleStep.ON_CALCULATE_CACHED_VALUE,
            LifecycleStep.ON_PREPARE,
            LifecycleStep.ON_MEASURE,
            LifecycleStep.ON_BOUNDS_DEFINED,
            LifecycleStep.ON_ATTACHED,
            LifecycleStep.ON_CREATE_MOUNT_CONTENT,
            LifecycleStep.ON_MOUNT,
            LifecycleStep.ON_BIND)

    lifecycleTracker.reset()
    legacyLithoViewRule.setSizeSpecs(exactly(200), exactly(200)).measure().layout()
    Assertions.assertThat(lifecycleTracker.steps).containsExactly(LifecycleStep.ON_BOUNDS_DEFINED)
  }

  /**
   * Context: With layout caching we need to reset the size spec of the root component if measured
   * size is different from specified one, to re-measure the whole tree. This case is to ensure that
   * the SizeSpec of AT_MOST is also being reset correctly.
   */
  @Test
  fun `RecyclerCollectionComponent with wrapContent should be re-measured with the latest size specs when it changes`() {
    if (!ComponentsConfiguration.enableLayoutCaching) {
      return
    }

    val context = legacyLithoViewRule.context
    val lifecycleTracker1 = LifecycleTracker()
    val lifecycleTracker2 = LifecycleTracker()
    val lifecycleTracker3 = LifecycleTracker()

    val component =
        buildRecyclerCollectionComponent(
            context, lifecycleTracker1, lifecycleTracker2, lifecycleTracker3)

    // Set LithoView with height so that it can fully show all the items
    legacyLithoViewRule
        .setRoot(component)
        .attachToWindow()
        .setSizeSpecs(exactly(100), unspecified())
        .measure()
        .layout()

    Assertions.assertThat(
            getCountOfLifecycleSteps(lifecycleTracker1.steps, LifecycleStep.ON_MEASURE))
        .isEqualTo(5)
    Assertions.assertThat(
            getCountOfLifecycleSteps(lifecycleTracker2.steps, LifecycleStep.ON_MEASURE))
        .isEqualTo(5)
    Assertions.assertThat(
            getCountOfLifecycleSteps(lifecycleTracker3.steps, LifecycleStep.ON_MEASURE))
        .isEqualTo(5)

    val height = legacyLithoViewRule.lithoView.height

    lifecycleTracker1.reset()
    lifecycleTracker2.reset()
    lifecycleTracker3.reset()

    val measuredSize = Size()
    legacyLithoViewRule.lithoView.componentTree?.setSizeSpec(
        exactly(200), unspecified(), measuredSize)
    Assertions.assertThat(measuredSize.height).isEqualTo(height)
    Assertions.assertThat(
            getCountOfLifecycleSteps(lifecycleTracker1.steps, LifecycleStep.ON_MEASURE))
        .describedAs("Ensure that all children are re-measured")
        .isEqualTo(5)
    Assertions.assertThat(
            getCountOfLifecycleSteps(lifecycleTracker2.steps, LifecycleStep.ON_MEASURE))
        .describedAs("Ensure that all children are re-measured")
        .isEqualTo(5)
    Assertions.assertThat(
            getCountOfLifecycleSteps(lifecycleTracker3.steps, LifecycleStep.ON_MEASURE))
        .describedAs("Ensure that all children are re-measured")
        .isEqualTo(5)
  }

  private fun buildRecyclerCollectionComponent(
      context: ComponentContext,
      lifecycleTracker1: LifecycleTracker,
      lifecycleTracker2: LifecycleTracker,
      lifecycleTracker3: LifecycleTracker,
  ): RecyclerCollectionComponent {

    val child1: Component =
        MountSpecLifecycleTester.create(context)
            .intrinsicSize(Size(10, 10))
            .lifecycleTracker(lifecycleTracker1)
            .build()
    val child2: Component =
        MountSpecLifecycleTester.create(context)
            .intrinsicSize(Size(10, 15))
            .lifecycleTracker(lifecycleTracker2)
            .build()
    val child3: Component =
        MountSpecLifecycleTester.create(context)
            .intrinsicSize(Size(10, 20))
            .lifecycleTracker(lifecycleTracker3)
            .build()

    val item: Component =
        Column.create(context)
            .child(Wrapper.create(context).delegate(child1))
            .child(Wrapper.create(context).delegate(child2))
            .child(Wrapper.create(context).delegate(child3))
            .build()
    val config: RecyclerConfiguration =
        ListRecyclerConfiguration.create()
            .recyclerBinderConfiguration(
                RecyclerBinderConfiguration.create().wrapContent(true).build())
            .build()
    val sectionContext = SectionContext(context)
    return RecyclerCollectionComponent.create(context)
        .recyclerConfiguration(config)
        .section(
            DynamicComponentGroupSection.create(sectionContext)
                .component(item)
                .totalItems(5)
                .build())
        .maxHeightDip(300f)
        .build()
  }
}


/*
 * 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 android.widget.TextView
import com.facebook.litho.testing.BackgroundLayoutLooperRule
import com.facebook.litho.testing.LegacyLithoViewRule
import com.facebook.litho.testing.testrunner.LithoTestRunner
import com.facebook.litho.widget.MountSpecWithShouldUpdate
import com.facebook.litho.widget.SimpleStateUpdateEmulator
import com.facebook.litho.widget.SimpleStateUpdateEmulatorSpec
import com.facebook.litho.widget.TextViewCounter
import java.util.ArrayList
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
import org.robolectric.shadows.ShadowLooper

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

  @JvmField @Rule var legacyLithoViewRule = LegacyLithoViewRule()

  @JvmField @Rule var backgroundLayoutLooperRule = BackgroundLayoutLooperRule()

  /**
   * In this scenario, we make sure that if a state update happens in the background followed by a
   * second state update in the background before the first can commit on the main thread, that we
   * still properly diff at mount time and don't unmount and remount and MountSpecs
   * with @ShouldUpdate(onMount = true).
   */
  @Test
  fun layoutDiffing_multipleStateUpdatesInParallelWithShouldUpdateFalse_mountContentIsNotRemounted() {
    val stateUpdater = SimpleStateUpdateEmulatorSpec.Caller()
    val operations = ArrayList<LifecycleStep>()
    val firstObjectForShouldUpdate = Any()
    legacyLithoViewRule
        .setRoot(
            createRootComponentWithStateUpdater(
                legacyLithoViewRule.context, firstObjectForShouldUpdate, operations, stateUpdater))
        .setSizePx(100, 100)
        .measure()
        .layout()
        .attachToWindow()
    assertThat(operations)
        .describedAs("Test setup, there should be an initial mount")
        .containsExactly(LifecycleStep.ON_MOUNT)
    operations.clear()

    // Do two state updates sequentially without draining the main thread queue
    stateUpdater.incrementAsync()
    backgroundLayoutLooperRule.runToEndOfTasksSync()
    stateUpdater.incrementAsync()
    backgroundLayoutLooperRule.runToEndOfTasksSync()

    // Now drain the main thread queue and mount the result
    ShadowLooper.idleMainLooper()
    legacyLithoViewRule.layout()
    assertThat(operations).isEmpty()
  }

  @Test
  fun layoutDiffing_multipleSetRootsInParallelWithShouldUpdateFalse_mountContentIsNotRemounted() {
    val operations = ArrayList<LifecycleStep>()
    val firstObjectForShouldUpdate = Any()
    legacyLithoViewRule
        .setRoot(
            createRootComponent(
                legacyLithoViewRule.context, firstObjectForShouldUpdate, operations))
        .setSizePx(100, 100)
        .measure()
        .layout()
        .attachToWindow()
    assertThat(operations)
        .describedAs("Test setup, there should be an initial mount")
        .containsExactly(LifecycleStep.ON_MOUNT)
    operations.clear()

    // Do two prop updates sequentially without draining the main thread queue
    legacyLithoViewRule.setRootAsync(
        createRootComponent(legacyLithoViewRule.context, firstObjectForShouldUpdate, operations))
    backgroundLayoutLooperRule.runToEndOfTasksSync()
    legacyLithoViewRule.setRootAsync(
        createRootComponent(legacyLithoViewRule.context, firstObjectForShouldUpdate, operations))
    backgroundLayoutLooperRule.runToEndOfTasksSync()

    // Now drain the main thread queue and mount the result
    ShadowLooper.idleMainLooper()
    legacyLithoViewRule.layout()
    assertThat(operations).isEmpty()
  }

  /**
   * In this scenario, we make sure that if a setRoot happens in the background followed by a second
   * setRoot in the background before the first can commit on the main thread, that we still
   * properly diff at mount time and don't unmount and remount and MountSpecs
   * with @ShouldUpdate(onMount = true).
   */
  @Test
  fun layoutDiffing_multipleSetRootsInParallelWithShouldUpdateTrueForFirstLayout_mountContentIsRemounted() {
    val operations = ArrayList<LifecycleStep>()
    val firstObjectForShouldUpdate = Any()
    legacyLithoViewRule
        .setRoot(
            createRootComponent(
                legacyLithoViewRule.context, firstObjectForShouldUpdate, operations))
        .setSizePx(100, 100)
        .measure()
        .layout()
        .attachToWindow()
    assertThat(operations).containsExactly(LifecycleStep.ON_MOUNT)
    operations.clear()
    val secondObjectForShouldUpdate = Any()

    // Do two prop updates sequentially without draining the main thread queue
    legacyLithoViewRule.setRootAsync(
        createRootComponent(legacyLithoViewRule.context, secondObjectForShouldUpdate, operations))
    backgroundLayoutLooperRule.runToEndOfTasksSync()
    legacyLithoViewRule.setRootAsync(
        createRootComponent(legacyLithoViewRule.context, secondObjectForShouldUpdate, operations))
    backgroundLayoutLooperRule.runToEndOfTasksSync()

    // Now drain the main thread queue and mount the result
    ShadowLooper.idleMainLooper()
    legacyLithoViewRule.layout()

    // In this case, we did change the object for shouldUpdate in layout 1 even though
    // it was the same for layouts 2. We expect to see unmount and mount.
    assertThat(operations).containsExactly(LifecycleStep.ON_UNMOUNT, LifecycleStep.ON_MOUNT)
    operations.clear()
  }

  @Test
  fun layoutDiffing_multipleSetRootsInParallelWithShouldUpdateTrueForSecondLayout_mountContentIsRemounted() {
    val operations = ArrayList<LifecycleStep>()
    val firstObjectForShouldUpdate = Any()
    legacyLithoViewRule
        .setRoot(
            createRootComponent(
                legacyLithoViewRule.context, firstObjectForShouldUpdate, operations))
        .setSizePx(100, 100)
        .measure()
        .layout()
        .attachToWindow()
    assertThat(operations).containsExactly(LifecycleStep.ON_MOUNT)
    operations.clear()
    val secondObjectForShouldUpdate = Any()

    // Do two prop updates sequentially without draining the main thread queue
    legacyLithoViewRule.setRootAsync(
        createRootComponent(legacyLithoViewRule.context, firstObjectForShouldUpdate, operations))
    backgroundLayoutLooperRule.runToEndOfTasksSync()
    legacyLithoViewRule.setRootAsync(
        createRootComponent(legacyLithoViewRule.context, secondObjectForShouldUpdate, operations))
    backgroundLayoutLooperRule.runToEndOfTasksSync()

    // Now drain the main thread queue and mount the result
    ShadowLooper.idleMainLooper()
    legacyLithoViewRule.layout()

    // Similar to the previous test, but the object changes on the second layout instead.
    assertThat(operations).containsExactly(LifecycleStep.ON_UNMOUNT, LifecycleStep.ON_MOUNT)
  }

  @Test
  fun whenStateUpdateOnPureRenderMountSpec_shouldRemountItem() {
    val c = legacyLithoViewRule.context
    val component =
        Column.create(c)
            .child(TextViewCounter.create(c).viewWidth(200).viewHeight(200).build())
            .build()
    legacyLithoViewRule.attachToWindow().setRoot(component).measure().layout()
    val view = legacyLithoViewRule.lithoView.getChildAt(0)
    assertThat(view).isNotNull
    assertThat(view).isInstanceOf(TextView::class.java)
    assertThat((view as TextView).text).isEqualTo("0")
    view.callOnClick()
    assertThat(view.text).isEqualTo("1")
  }

  @Test
  fun onSetRootWithSameComponent_thenShouldNotRemeasureMountSpec() {
    val c = legacyLithoViewRule.context
    val operations = ArrayList<LifecycleStep>()
    val objectForShouldUpdate = Any()
    val component =
        Row.create(c)
            .child(
                Column.create(c)
                    .child(
                        Row.create(c)
                            .widthPx(100)
                            .heightPx(100)
                            .background(ColorDrawable(Color.RED)))
                    .child(
                        MountSpecWithShouldUpdate.create(c)
                            .objectForShouldUpdate(objectForShouldUpdate)
                            .operationsOutput(operations)))
            .build()
    legacyLithoViewRule.attachToWindow().setRoot(component).measure().layout()
    assertThat(operations).containsExactly(LifecycleStep.ON_MEASURE, LifecycleStep.ON_MOUNT)
    operations.clear()
    legacyLithoViewRule.attachToWindow().setRoot(component).measure().layout()
    assertThat(operations).isEmpty()
  }

  @Test
  fun onSetRootWithSimilarComponent_thenShouldNotRemeasureMountSpec() {
    val c = legacyLithoViewRule.context
    val operations = ArrayList<LifecycleStep>()
    val objectForShouldUpdate = Any()
    val component =
        Row.create(c)
            .child(
                Column.create(c)
                    .child(
                        Row.create(c)
                            .widthPx(100)
                            .heightPx(100)
                            .background(ColorDrawable(Color.RED)))
                    .child(
                        MountSpecWithShouldUpdate.create(c)
                            .objectForShouldUpdate(objectForShouldUpdate)
                            .operationsOutput(operations)))
            .build()
    legacyLithoViewRule.attachToWindow().setRoot(component).measure().layout()
    assertThat(operations).containsExactly(LifecycleStep.ON_MEASURE, LifecycleStep.ON_MOUNT)
    operations.clear()
    val next =
        Row.create(c)
            .child(
                Column.create(c)
                    .child(
                        Row.create(c)
                            .widthPx(100)
                            .heightPx(100)
                            .background(ColorDrawable(Color.RED)))
                    .child(
                        MountSpecWithShouldUpdate.create(c)
                            .objectForShouldUpdate(objectForShouldUpdate)
                            .operationsOutput(operations)))
            .build()
    legacyLithoViewRule.attachToWindow().setRoot(next).measure().layout()
    assertThat(operations).isEmpty()
  }

  @Test
  fun onSetRootWithSimilarComponentWithShouldUpdateTrue_thenShouldRemeasureMountSpec() {
    val c = legacyLithoViewRule.context
    val operations = ArrayList<LifecycleStep>()
    val objectForShouldUpdate = Any()
    val component =
        Row.create(c)
            .child(
                Column.create(c)
                    .child(
                        Row.create(c)
                            .widthPx(100)
                            .heightPx(100)
                            .background(ColorDrawable(Color.RED)))
                    .child(
                        MountSpecWithShouldUpdate.create(c)
                            .objectForShouldUpdate(objectForShouldUpdate)
                            .operationsOutput(operations)))
            .build()
    legacyLithoViewRule.attachToWindow().setRoot(component).measure().layout()
    assertThat(operations).containsExactly(LifecycleStep.ON_MEASURE, LifecycleStep.ON_MOUNT)
    operations.clear()
    val next =
        Row.create(c)
            .child(
                Column.create(c)
                    .child(
                        Row.create(c)
                            .widthPx(100)
                            .heightPx(100)
                            .background(ColorDrawable(Color.RED)))
                    .child(
                        MountSpecWithShouldUpdate.create(c)
                            .objectForShouldUpdate(Any())
                            .operationsOutput(operations)))
            .build()
    legacyLithoViewRule.attachToWindow().setRoot(next).measure().layout()
    assertThat(operations)
        .containsExactly(LifecycleStep.ON_MEASURE, LifecycleStep.ON_UNMOUNT, LifecycleStep.ON_MOUNT)
  }

  companion object {
    private fun createRootComponentWithStateUpdater(
        c: ComponentContext,
        objectForShouldUpdate: Any,
        operationsOutput: List<LifecycleStep>,
        stateUpdateCaller: SimpleStateUpdateEmulatorSpec.Caller
    ): Component =
        Row.create(c)
            .child(
                Column.create(c)
                    .child(
                        SimpleStateUpdateEmulator.create(c)
                            .caller(stateUpdateCaller)
                            .widthPx(100)
                            .heightPx(100)
                            .background(ColorDrawable(Color.RED)))
                    .child(
                        MountSpecWithShouldUpdate.create(c)
                            .objectForShouldUpdate(objectForShouldUpdate)
                            .operationsOutput(operationsOutput)
                            .widthPx(10)
                            .heightPx(10)))
            .build()

    private fun createRootComponent(
        c: ComponentContext,
        objectForShouldUpdate: Any,
        operationsOutput: List<LifecycleStep>
    ): Component =
        Row.create(c)
            .child(
                Column.create(c)
                    .child(
                        Row.create(c)
                            .widthPx(100)
                            .heightPx(100)
                            .background(ColorDrawable(Color.RED)))
                    .child(
                        MountSpecWithShouldUpdate.create(c)
                            .objectForShouldUpdate(objectForShouldUpdate)
                            .operationsOutput(operationsOutput)
                            .widthPx(10)
                            .heightPx(10)))
            .build()
  }
}


/*
 * 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.config.ComponentsConfiguration
import com.facebook.litho.testing.BackgroundLayoutLooperRule
import com.facebook.litho.testing.LegacyLithoViewRule
import com.facebook.litho.testing.testrunner.LithoTestRunner
import com.facebook.litho.widget.LayoutSpecLifecycleTester
import com.facebook.litho.widget.LayoutSpecLifecycleTesterSpec
import com.facebook.litho.widget.events.EventWithoutAnnotation
import java.util.ArrayList
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 LayoutSpecLifecycleStatelessTest {

  @JvmField @Rule val legacyLithoViewRule = LegacyLithoViewRule()

  @JvmField @Rule var backgroundLayoutLooperRule = BackgroundLayoutLooperRule()

  @Test
  fun lifecycle_onSetRootWithoutLayout_shouldNotCallLifecycleMethods() {
    val info: List<StepInfo> = ArrayList()
    val component =
        LayoutSpecLifecycleTester.create(legacyLithoViewRule.context).steps(info).build()
    legacyLithoViewRule.setRoot(component).idle()
    assertThat(LifecycleStep.getSteps(info))
        .describedAs("Only render lifecycle methods should be called")
        .containsExactly(
            LifecycleStep.ON_CREATE_INITIAL_STATE,
            LifecycleStep.ON_CREATE_TREE_PROP,
            LifecycleStep.ON_CALCULATE_CACHED_VALUE,
            LifecycleStep.ON_CREATE_LAYOUT)
  }

  @Test
  fun lifecycle_onSetRootWithLayout_shouldCallLifecycleMethods() {
    ComponentsConfiguration.isAnimationDisabled = false
    val info: List<StepInfo> = ArrayList()
    val component =
        LayoutSpecLifecycleTester.create(legacyLithoViewRule.context).steps(info).build()
    legacyLithoViewRule.setRoot(component)
    legacyLithoViewRule.attachToWindow().measure().layout()
    assertThat(LifecycleStep.getSteps(info))
        .describedAs("Should call the lifecycle methods in expected order")
        .containsExactly(
            LifecycleStep.ON_CREATE_INITIAL_STATE,
            LifecycleStep.ON_CREATE_TREE_PROP,
            LifecycleStep.ON_CALCULATE_CACHED_VALUE,
            LifecycleStep.ON_CREATE_LAYOUT,
            LifecycleStep.ON_CREATE_TRANSITION,
            LifecycleStep.ON_ATTACHED)
    ComponentsConfiguration.isAnimationDisabled = true
  }

  @Test
  fun lifecycle_release_shouldCallLifecycleMethodOnDetach() {
    val info: MutableList<StepInfo> = ArrayList()
    val component =
        LayoutSpecLifecycleTester.create(legacyLithoViewRule.context).steps(info).build()
    legacyLithoViewRule.setRoot(component)
    legacyLithoViewRule.attachToWindow().measure().layout()
    info.clear()
    legacyLithoViewRule.release()
    assertThat(LifecycleStep.getSteps(info))
        .describedAs("Should call onDetached")
        .containsExactly(LifecycleStep.ON_DETACHED)
  }

  @Test
  fun lifecycle_subsequentSetRoot_shouldCallLifecycleMethod() {
    val info: MutableList<StepInfo> = ArrayList()
    val component =
        LayoutSpecLifecycleTester.create(legacyLithoViewRule.context).steps(info).build()
    legacyLithoViewRule.setRoot(component)
    legacyLithoViewRule.attachToWindow().measure().layout()
    info.clear()
    val newComponent =
        LayoutSpecLifecycleTester.create(legacyLithoViewRule.context).steps(info).build()
    legacyLithoViewRule.setRoot(newComponent)
    legacyLithoViewRule.attachToWindow().measure().layout()
    assertThat(LifecycleStep.getSteps(info))
        .describedAs("Should call the lifecycle methods in expected order")
        .containsExactly(
            LifecycleStep.ON_CREATE_TREE_PROP,
            LifecycleStep.ON_CALCULATE_CACHED_VALUE,
            LifecycleStep.ON_CREATE_LAYOUT)
  }

  @Test
  fun lifecycle_updateState_shouldCallLifecycleMethod() {
    // TODO: T66662176 Remove code to disable and enable reconciliation.
    ComponentsConfiguration.isReconciliationEnabled = false
    val info: MutableList<StepInfo> = ArrayList()
    val caller = LayoutSpecLifecycleTesterSpec.Caller()
    val component =
        LayoutSpecLifecycleTester.create(legacyLithoViewRule.context)
            .steps(info)
            .caller(caller)
            .build()
    legacyLithoViewRule.setRoot(component)
    legacyLithoViewRule.attachToWindow().measure().layout()
    info.clear()
    caller.updateStateSync()
    assertThat(LifecycleStep.getSteps(info))
        .describedAs("Should call the lifecycle methods in expected order")
        .containsExactly(
            LifecycleStep.ON_UPDATE_STATE,
            LifecycleStep.ON_CREATE_TREE_PROP,
            LifecycleStep.ON_CALCULATE_CACHED_VALUE,
            LifecycleStep.ON_CREATE_LAYOUT)
    ComponentsConfiguration.isReconciliationEnabled = true
  }

  @Test
  fun lifecycle_updateStateWithTransition_shouldCallLifecycleMethod() {
    // TODO: T66662176 Remove code to disable and enable reconciliation.
    ComponentsConfiguration.isReconciliationEnabled = false
    val info: MutableList<StepInfo> = ArrayList()
    val caller = LayoutSpecLifecycleTesterSpec.Caller()
    val component =
        LayoutSpecLifecycleTester.create(legacyLithoViewRule.context)
            .steps(info)
            .caller(caller)
            .build()
    legacyLithoViewRule.setRoot(component)
    legacyLithoViewRule.attachToWindow().measure().layout()
    info.clear()
    caller.updateStateWithTransition()
    backgroundLayoutLooperRule.runToEndOfTasksSync()
    assertThat(LifecycleStep.getSteps(info))
        .describedAs("Should call the lifecycle methods in expected order")
        .containsExactly(
            LifecycleStep.ON_UPDATE_STATE_WITH_TRANSITION,
            LifecycleStep.ON_CREATE_TREE_PROP,
            LifecycleStep.ON_CALCULATE_CACHED_VALUE,
            LifecycleStep.ON_CREATE_LAYOUT)
    ComponentsConfiguration.isReconciliationEnabled = true
  }

  @Test
  fun lifecycle_dispatchEventWithoutAnnotation_shouldCallOnEventWithoutAnnotation() {
    val info: MutableList<StepInfo> = ArrayList()
    val caller = LayoutSpecLifecycleTesterSpec.Caller()
    val component =
        LayoutSpecLifecycleTester.create(legacyLithoViewRule.context)
            .steps(info)
            .caller(caller)
            .build()
    legacyLithoViewRule.setRoot(component)
    legacyLithoViewRule.attachToWindow().measure().layout()
    info.clear()
    val eventDispatched = EventWithoutAnnotation(1, true, "hello")
    caller.dispatchEventWithoutAnnotation(eventDispatched)
    val eventReceived = caller.eventWithoutAnnotation
    assertThat(eventReceived).isNotNull
    assertThat(eventDispatched.count).isSameAs(eventReceived?.count)
    assertThat(eventDispatched.isDirty).isSameAs(eventReceived?.isDirty)
    assertThat(eventDispatched.message).isSameAs(eventReceived?.message)
  }
}


/*
 * 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.config.ComponentsConfiguration
import com.facebook.litho.testing.BackgroundLayoutLooperRule
import com.facebook.litho.testing.LegacyLithoViewRule
import com.facebook.litho.testing.testrunner.LithoTestRunner
import com.facebook.litho.widget.LayoutSpecLifecycleTester
import com.facebook.litho.widget.LayoutSpecLifecycleTesterSpec
import com.facebook.litho.widget.events.EventWithoutAnnotation
import java.util.ArrayList
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 LayoutSpecLifecycleTest {

  @JvmField @Rule val legacyLithoViewRule = LegacyLithoViewRule()

  @JvmField @Rule var backgroundLayoutLooperRule = BackgroundLayoutLooperRule()

  @Test
  fun lifecycle_onSetRootWithoutLayout_shouldNotCallLifecycleMethods() {
    val info: List<StepInfo> = ArrayList()
    val component =
        LayoutSpecLifecycleTester.create(legacyLithoViewRule.context).steps(info).build()
    legacyLithoViewRule.setRoot(component).idle()
    assertThat(LifecycleStep.getSteps(info))
        .describedAs("Only render lifecycle methods should be called")
        .containsExactly(
            LifecycleStep.ON_CREATE_INITIAL_STATE,
            LifecycleStep.ON_CREATE_TREE_PROP,
            LifecycleStep.ON_CALCULATE_CACHED_VALUE,
            LifecycleStep.ON_CREATE_LAYOUT)
  }

  @Test
  fun lifecycle_onSetRootWithLayout_shouldCallLifecycleMethods() {
    ComponentsConfiguration.isAnimationDisabled = false
    val info: List<StepInfo> = ArrayList()
    val component =
        LayoutSpecLifecycleTester.create(legacyLithoViewRule.context).steps(info).build()
    legacyLithoViewRule.setRoot(component)
    legacyLithoViewRule.attachToWindow().measure().layout()
    assertThat(LifecycleStep.getSteps(info))
        .describedAs("Should call the lifecycle methods in expected order")
        .containsExactly(
            LifecycleStep.ON_CREATE_INITIAL_STATE,
            LifecycleStep.ON_CREATE_TREE_PROP,
            LifecycleStep.ON_CALCULATE_CACHED_VALUE,
            LifecycleStep.ON_CREATE_LAYOUT,
            LifecycleStep.ON_CREATE_TRANSITION,
            LifecycleStep.ON_ATTACHED)
    ComponentsConfiguration.isAnimationDisabled = true
  }

  @Test
  fun lifecycle_release_shouldCallLifecycleMethodOnDetach() {
    val info: MutableList<StepInfo> = ArrayList()
    val component =
        LayoutSpecLifecycleTester.create(legacyLithoViewRule.context).steps(info).build()
    legacyLithoViewRule.setRoot(component)
    legacyLithoViewRule.attachToWindow().measure().layout()
    info.clear()
    legacyLithoViewRule.release()
    assertThat(LifecycleStep.getSteps(info))
        .describedAs("Should call onDetached")
        .containsExactly(LifecycleStep.ON_DETACHED)
  }

  @Test
  fun lifecycle_subsequentSetRoot_shouldCallLifecycleMethod() {
    val info: MutableList<StepInfo> = ArrayList()
    val component =
        LayoutSpecLifecycleTester.create(legacyLithoViewRule.context).steps(info).build()
    legacyLithoViewRule.setRoot(component)
    legacyLithoViewRule.attachToWindow().measure().layout()
    info.clear()
    val newComponent =
        LayoutSpecLifecycleTester.create(legacyLithoViewRule.context).steps(info).build()
    legacyLithoViewRule.setRoot(newComponent)
    legacyLithoViewRule.attachToWindow().measure().layout()
    assertThat(LifecycleStep.getSteps(info))
        .describedAs("Should call the lifecycle methods in expected order")
        .containsExactly(
            LifecycleStep.ON_CREATE_TREE_PROP,
            LifecycleStep.ON_CALCULATE_CACHED_VALUE,
            LifecycleStep.ON_CREATE_LAYOUT)
  }

  @Test
  fun lifecycle_updateState_shouldCallLifecycleMethod() {
    // TODO: T66662176 Remove code to disable and enable reconciliation.
    ComponentsConfiguration.isReconciliationEnabled = false
    val info: MutableList<StepInfo> = ArrayList()
    val caller = LayoutSpecLifecycleTesterSpec.Caller()
    val component =
        LayoutSpecLifecycleTester.create(legacyLithoViewRule.context)
            .steps(info)
            .caller(caller)
            .build()
    legacyLithoViewRule.setRoot(component)
    legacyLithoViewRule.attachToWindow().measure().layout()
    info.clear()
    caller.updateStateSync()
    assertThat(LifecycleStep.getSteps(info))
        .describedAs("Should call the lifecycle methods in expected order")
        .containsExactly(
            LifecycleStep.ON_UPDATE_STATE,
            LifecycleStep.ON_CREATE_TREE_PROP,
            LifecycleStep.ON_CALCULATE_CACHED_VALUE,
            LifecycleStep.ON_CREATE_LAYOUT)
    ComponentsConfiguration.isReconciliationEnabled = true
  }

  @Test
  fun lifecycle_updateStateWithTransition_shouldCallLifecycleMethod() {
    // TODO: T66662176 Remove code to disable and enable reconciliation.
    ComponentsConfiguration.isReconciliationEnabled = false
    val info: MutableList<StepInfo> = ArrayList()
    val caller = LayoutSpecLifecycleTesterSpec.Caller()
    val component =
        LayoutSpecLifecycleTester.create(legacyLithoViewRule.context)
            .steps(info)
            .caller(caller)
            .build()
    legacyLithoViewRule.setRoot(component)
    legacyLithoViewRule.attachToWindow().measure().layout()
    info.clear()
    caller.updateStateWithTransition()
    backgroundLayoutLooperRule.runToEndOfTasksSync()
    assertThat(LifecycleStep.getSteps(info))
        .describedAs("Should call the lifecycle methods in expected order")
        .containsExactly(
            LifecycleStep.ON_UPDATE_STATE_WITH_TRANSITION,
            LifecycleStep.ON_CREATE_TREE_PROP,
            LifecycleStep.ON_CALCULATE_CACHED_VALUE,
            LifecycleStep.ON_CREATE_LAYOUT)
    ComponentsConfiguration.isReconciliationEnabled = true
  }

  @Test
  fun lifecycle_dispatchEventWithoutAnnotation_shouldCallOnEventWithoutAnnotation() {
    val info: MutableList<StepInfo> = ArrayList()
    val caller = LayoutSpecLifecycleTesterSpec.Caller()
    val component =
        LayoutSpecLifecycleTester.create(legacyLithoViewRule.context)
            .steps(info)
            .caller(caller)
            .build()
    legacyLithoViewRule.setRoot(component)
    legacyLithoViewRule.attachToWindow().measure().layout()
    info.clear()
    val eventDispatched = EventWithoutAnnotation(1, true, "hello")
    caller.dispatchEventWithoutAnnotation(eventDispatched)
    val eventReceived = caller.eventWithoutAnnotation
    assertThat(eventReceived).isNotNull
    assertThat(eventDispatched.count).isSameAs(eventReceived?.count)
    assertThat(eventDispatched.isDirty).isSameAs(eventReceived?.isDirty)
    assertThat(eventDispatched.message).isSameAs(eventReceived?.message)
  }
}


/*
 * 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.LayoutSpecTriggerTester
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 LayoutSpecTriggerTest {

  @JvmField @Rule val legacyLithoViewRule = LegacyLithoViewRule()

  @Test
  fun layoutSpec_setRootAndTriggerEvent_eventIsTriggered() {
    val componentContext = legacyLithoViewRule.context
    val triggerObjectRef = AtomicReference<Any>()
    val triggerHandle = Handle()
    val info: List<StepInfo> = ArrayList()
    val component =
        LayoutSpecTriggerTester.create(componentContext)
            .steps(info)
            .triggerObjectRef(triggerObjectRef)
            .handle(triggerHandle)
            .build()
    legacyLithoViewRule.setRoot(component)
    legacyLithoViewRule.attachToWindow().measure().layout()
    val bazObject = Any()

    // We need to use a ComponentContext with a ComponentTree on it
    LayoutSpecTriggerTester.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 layoutSpec_setRootAndTriggerEvent_eventIsTriggered_handle_used_in_child() {
    val componentContext = legacyLithoViewRule.context
    val triggerObjectRef = AtomicReference<Any>()
    val triggerHandle = Handle()
    val info: List<StepInfo> = ArrayList()
    val component =
        Column.create(componentContext)
            .child(Text.create(componentContext).text("Sample"))
            .child(
                LayoutSpecTriggerTester.create(componentContext)
                    .steps(info)
                    .triggerObjectRef(triggerObjectRef)
                    .handle(triggerHandle)
                    .build())
            .build()
    legacyLithoViewRule.setRoot(component)
    legacyLithoViewRule.attachToWindow().measure().layout()
    val bazObject = Any()

    // We need to use a ComponentContext with a ComponentTree on it
    LayoutSpecTriggerTester.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 layoutSpec_setRootAndTriggerEvent_eventIsTriggered_handle_used_in_nested_tree_root() {
    val componentContext = legacyLithoViewRule.context
    val triggerObjectRef = AtomicReference<Any>()
    val triggerHandle = Handle()
    val info: List<StepInfo> = ArrayList()
    val component =
        Column.create(componentContext)
            .child(Text.create(componentContext).text("Sample"))
            .child(
                RecyclerCollectionComponent.create(componentContext)
                    .heightPx(150)
                    .section(
                        SingleComponentSection.create(SectionContext(componentContext))
                            .component(
                                LayoutSpecTriggerTester.create(componentContext)
                                    .steps(info)
                                    .triggerObjectRef(triggerObjectRef)
                                    .handle(triggerHandle)
                                    .build())))
            .build()
    legacyLithoViewRule.setRoot(component)
    legacyLithoViewRule.attachToWindow().measure().layout()
    val bazObject = Any()

    // We need to use a ComponentContext with a ComponentTree on it
    LayoutSpecTriggerTester.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 layoutSpec_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()
    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(
                                        LayoutSpecTriggerTester.create(componentContext)
                                            .steps(info)
                                            .triggerObjectRef(triggerObjectRef)
                                            .handle(triggerHandle)
                                            .build()))))
            .build()
    legacyLithoViewRule.setRoot(component)
    legacyLithoViewRule.attachToWindow().measure().layout()
    val bazObject = Any()

    // We need to use a ComponentContext with a ComponentTree on it
    LayoutSpecTriggerTester.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 androidx.test.core.app.ApplicationProvider
import com.facebook.litho.testing.LegacyLithoViewRule
import com.facebook.litho.testing.TestLayoutComponent
import com.facebook.litho.testing.testrunner.LithoTestRunner
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 LayoutStateAddChildWithInputTest {

  private lateinit var context: ComponentContext

  @JvmField @Rule var legacyLithoViewRule = LegacyLithoViewRule()

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

  @Test
  fun testNewEmptyLayout() {
    val component =
        Column.create(context)
            .child(TestLayoutComponent.create(context))
            .child(TestLayoutComponent.create(context))
            .build()
    val node = LegacyLithoViewRule.getRootLayout(legacyLithoViewRule, component)?.node
    assertThat(node?.childCount).isEqualTo(2)
    assertThat(node?.getChildAt(0)?.childCount).isEqualTo(0)
    assertThat(node?.getChildAt(1)?.childCount).isEqualTo(0)
  }
}