/*
 * 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.os.SystemClock
import android.view.MotionEvent
import android.view.View
import androidx.test.core.app.ApplicationProvider
import com.facebook.litho.testing.LegacyLithoViewRule
import com.facebook.litho.testing.testrunner.LithoTestRunner
import com.facebook.litho.widget.OnClickCallbackComponent
import com.facebook.litho.widget.Text
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
import org.robolectric.annotation.LooperMode

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

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

  private lateinit var context: ComponentContext

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

  @Test
  fun onTouchEventOnEmptyDelegate_shouldNoOp() {
    val host = ComponentHost(ApplicationProvider.getApplicationContext<Context>())
    val delegate = TouchExpansionDelegate(host)
    val handled =
        delegate.onTouchEvent(
            MotionEvent.obtain(
                SystemClock.uptimeMillis(),
                SystemClock.uptimeMillis(),
                MotionEvent.ACTION_DOWN,
                0f,
                0f,
                0))
    assertThat(handled).describedAs("TouchEvent on empty delegate should not handle event").isFalse
  }

  @Test
  fun onTouchEventWithinBounds_shouldBeHandled() {
    val callback = ClickListenerCallback()
    val component =
        Column.create(context)
            .child(
                OnClickCallbackComponent.create(context)
                    .widthPx(10)
                    .heightPx(10)
                    .callback(callback)
                    .touchExpansionPx(YogaEdge.ALL, 5))
            .paddingPx(YogaEdge.ALL, 10)
            .build()
    legacyLithoViewRule.setRoot(component).attachToWindow().measure().layout()
    legacyLithoViewRule.lithoView.emulateClickEvent(7, 7)
    assertThat(callback.handled)
        .describedAs("TouchEvent within bounds bounds should be handled")
        .isTrue
    assertThat(callback.count)
        .describedAs("TouchEvent within bounds bounds should be handled only once")
        .isEqualTo(1)
  }

  @Test
  fun onTouchEventOutsideBounds_shouldNotBeHandled() {
    val callback = ClickListenerCallback()
    val component =
        Column.create(context)
            .child(
                OnClickCallbackComponent.create(context)
                    .widthPx(10)
                    .heightPx(10)
                    .callback(callback)
                    .touchExpansionPx(YogaEdge.ALL, 5))
            .paddingPx(YogaEdge.ALL, 10)
            .build()
    legacyLithoViewRule.setRoot(component).attachToWindow().measure().layout()
    legacyLithoViewRule.lithoView.emulateClickEvent(2, 2)
    assertThat(callback.handled)
        .describedAs("TouchEvent within bounds bounds should not be handled")
        .isFalse
  }

  @Test
  fun onTouchEventOnUnmount_shouldNotBeHandled() {
    val callback = ClickListenerCallback()
    val component =
        Column.create(context)
            .child(
                OnClickCallbackComponent.create(context)
                    .widthPx(10)
                    .heightPx(10)
                    .callback(callback)
                    .touchExpansionPx(YogaEdge.ALL, 5))
            .paddingPx(YogaEdge.ALL, 10)
            .build()
    legacyLithoViewRule.setRoot(component).attachToWindow().measure().layout()
    assertThat(legacyLithoViewRule.lithoView.touchExpansionDelegate.size())
        .describedAs("touch expansion delegates should be present")
        .isEqualTo(1)
    legacyLithoViewRule.lithoView.unmountAllItems()
    legacyLithoViewRule.lithoView.emulateClickEvent(7, 7)
    assertThat(callback.handled)
        .describedAs("TouchEvent within bounds bounds should not be handled")
        .isFalse
    assertThat(legacyLithoViewRule.lithoView.touchExpansionDelegate.size())
        .describedAs("all touch expansion delegates should be released")
        .isEqualTo(0)
  }

  @Test
  fun onTouchEventOnUpdatedComponentWithoutTouchExpansion_shouldNotBeHandled() {
    val callback = ClickListenerCallback()
    val component =
        Column.create(context)
            .child(
                OnClickCallbackComponent.create(context)
                    .widthPx(10)
                    .heightPx(10)
                    .callback(callback)
                    .touchExpansionPx(YogaEdge.ALL, 5))
            .paddingPx(YogaEdge.ALL, 10)
            .build()
    legacyLithoViewRule.setRoot(component).attachToWindow().measure().layout()
    val updated =
        Column.create(context)
            .child(
                OnClickCallbackComponent.create(context)
                    .widthPx(10)
                    .heightPx(10)
                    .callback(callback))
            .paddingPx(YogaEdge.ALL, 10)
            .build()
    legacyLithoViewRule.setRoot(updated)
    legacyLithoViewRule.lithoView.emulateClickEvent(7, 7)
    assertThat(callback.handled)
        .describedAs("TouchEvent within bounds bounds should not be handled")
        .isFalse
    legacyLithoViewRule.lithoView.emulateClickEvent(11, 11)
    assertThat(callback.handled)
        .describedAs("TouchEvent within bounds bounds should be handled")
        .isTrue
    assertThat(callback.count)
        .describedAs("TouchEvent within bounds bounds should be handled only once")
        .isEqualTo(1)
    legacyLithoViewRule.lithoView.unmountAllItems()
    assertThat(legacyLithoViewRule.lithoView.touchExpansionDelegate.size())
        .describedAs("all touch expansion delegates should be released")
        .isEqualTo(0)
  }

  @Test
  fun onTouchEventOnComponentMoved_shouldBeHandled() {
    val callback = ClickListenerCallback()
    val component =
        Column.create(context)
            .child(
                OnClickCallbackComponent.create(context)
                    .widthPx(10)
                    .heightPx(10)
                    .callback(callback)
                    .touchExpansionPx(YogaEdge.ALL, 5))
            .paddingPx(YogaEdge.ALL, 10)
            .build()
    legacyLithoViewRule.setRoot(component).attachToWindow().measure().layout()
    val updated =
        Column.create(context)
            .child(Text.create(context).text("hello world").widthPx(10).heightPx(10))
            .child(
                OnClickCallbackComponent.create(context)
                    .widthPx(10)
                    .heightPx(10)
                    .callback(callback)
                    .touchExpansionPx(YogaEdge.ALL, 5))
            .paddingPx(YogaEdge.ALL, 10)
            .build()
    legacyLithoViewRule.setRoot(updated).measure().layout()
    legacyLithoViewRule.lithoView.emulateClickEvent(7, 7)
    assertThat(callback.handled)
        .describedAs("TouchEvent within bounds bounds should not be handled")
        .isFalse
    legacyLithoViewRule.lithoView.emulateClickEvent(7, 21)
    assertThat(callback.handled)
        .describedAs("TouchEvent within bounds bounds should be handled")
        .isTrue
    assertThat(callback.count)
        .describedAs("TouchEvent within bounds bounds should be handled only once")
        .isEqualTo(1)
  }

  class ClickListenerCallback : View.OnClickListener {
    var handled = false
    var count = 0

    override fun onClick(v: View) {
      handled = true
      count++
    }

    fun reset() {
      handled = false
      count = 0
    }
  }

  companion object {
    fun View.emulateClickEvent(x: Int, y: Int) {
      val down =
          MotionEvent.obtain(
              SystemClock.uptimeMillis(),
              SystemClock.uptimeMillis(),
              MotionEvent.ACTION_DOWN,
              x.toFloat(),
              y.toFloat(),
              0)
      val up =
          MotionEvent.obtain(
              SystemClock.uptimeMillis() + 10,
              SystemClock.uptimeMillis() + 10,
              MotionEvent.ACTION_UP,
              x.toFloat(),
              y.toFloat(),
              0)
      dispatchTouchEvent(down)
      dispatchTouchEvent(up)
    }
  }
}


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

package com.facebook.litho

import android.graphics.Rect
import com.facebook.litho.Transition.TransitionAnimator
import com.facebook.litho.TransitionManager.OnAnimationCompleteListener
import com.facebook.litho.animation.AnimatedProperties
import com.facebook.litho.animation.AnimatedProperty
import com.facebook.litho.animation.PropertyAnimation
import com.facebook.litho.animation.PropertyHandle
import com.facebook.litho.animation.SpringTransition
import com.facebook.litho.testing.testrunner.LithoTestRunner
import java.util.ArrayList
import java.util.LinkedHashMap
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.anyOrNull
import org.mockito.kotlin.mock
import org.mockito.kotlin.whenever

/** Tests for the creation of animations using the targeting API in [Transition]. */
@RunWith(LithoTestRunner::class)
class TransitionManagerAnimationCreationTest {

  private var transitionManager: TransitionManager? = null
  private lateinit var testVerificationAnimator: TransitionAnimator
  private val createdAnimations = ArrayList<PropertyAnimation>()

  @Before
  fun setup() {
    transitionManager =
        TransitionManager(
            object : OnAnimationCompleteListener<Any?> {
              override fun onAnimationComplete(transitionId: TransitionId?) = Unit

              override fun onAnimationUnitComplete(propertyHandle: PropertyHandle, data: Any?) =
                  Unit
            },
            if (AnimationsDebug.ENABLED) AnimationsDebug.TAG else null,
            ComponentsSystrace.systrace)
    testVerificationAnimator = TransitionAnimator { propertyAnimation ->
      createdAnimations.add(propertyAnimation)
      SpringTransition(propertyAnimation)
    }
  }

  @After
  fun tearDown() {
    transitionManager = null
    createdAnimations.clear()
  }

  @Test
  fun testCreateSingleAnimation() {
    val current =
        createMockLayoutState(
            Transition.parallel<Transition>(), createMockLayoutOutput("test", 0, 0))
    val next =
        createMockLayoutState(
            Transition.parallel(
                Transition.create(Transition.TransitionKeyType.GLOBAL, "test")
                    .animate(AnimatedProperties.X)
                    .animator(testVerificationAnimator)),
            createMockLayoutOutput("test", 10, 10))
    transitionManager?.setupTransitions(
        current, next, TransitionManager.getRootTransition(next.transitions))
    assertThat(createdAnimations)
        .containsExactlyInAnyOrder(createPropertyAnimation("test", AnimatedProperties.X, 10f))
  }

  @Test
  fun testCreateMultiPropAnimation() {
    val current =
        createMockLayoutState(
            Transition.parallel<Transition>(), createMockLayoutOutput("test", 0, 0))
    val next =
        createMockLayoutState(
            Transition.parallel(
                Transition.create(Transition.TransitionKeyType.GLOBAL, "test")
                    .animate(AnimatedProperties.X, AnimatedProperties.Y)
                    .animator(testVerificationAnimator)),
            createMockLayoutOutput("test", 10, 10))
    transitionManager?.setupTransitions(
        current, next, TransitionManager.getRootTransition(next.transitions))
    assertThat(createdAnimations)
        .containsExactlyInAnyOrder(
            createPropertyAnimation("test", AnimatedProperties.X, 10f),
            createPropertyAnimation("test", AnimatedProperties.Y, 10f))
  }

  @Test
  fun testCreateMultiPropAnimationWithNonChangingProp() {
    val current =
        createMockLayoutState(
            Transition.parallel<Transition>(), createMockLayoutOutput("test", 0, 0))
    val next =
        createMockLayoutState(
            Transition.parallel(
                Transition.create(Transition.TransitionKeyType.GLOBAL, "test")
                    .animate(AnimatedProperties.X, AnimatedProperties.Y)
                    .animator(testVerificationAnimator)),
            createMockLayoutOutput("test", 10, 0))
    transitionManager?.setupTransitions(
        current, next, TransitionManager.getRootTransition(next.transitions))
    assertThat(createdAnimations)
        .containsExactlyInAnyOrder(createPropertyAnimation("test", AnimatedProperties.X, 10f))
  }

  @Test
  fun testCreateMultiComponentAnimation() {
    val current =
        createMockLayoutState(
            Transition.parallel<Transition>(),
            createMockLayoutOutput("test1", 0, 0),
            createMockLayoutOutput("test2", 0, 0))
    val next =
        createMockLayoutState(
            Transition.parallel(
                Transition.create(Transition.TransitionKeyType.GLOBAL, "test1", "test2")
                    .animate(AnimatedProperties.X)
                    .animator(testVerificationAnimator)),
            createMockLayoutOutput("test1", 10, 10),
            createMockLayoutOutput("test2", -10, -10))
    transitionManager?.setupTransitions(
        current, next, TransitionManager.getRootTransition(next.transitions))
    assertThat(createdAnimations)
        .containsExactlyInAnyOrder(
            createPropertyAnimation("test1", AnimatedProperties.X, 10f),
            createPropertyAnimation("test2", AnimatedProperties.X, -10f))
  }

  @Test
  fun testSetsOfComponentsAndPropertiesAnimation() {
    val current =
        createMockLayoutState(
            Transition.parallel<Transition>(),
            createMockLayoutOutput("test1", 0, 0),
            createMockLayoutOutput("test2", 0, 0))
    val next =
        createMockLayoutState(
            Transition.parallel(
                Transition.create(Transition.TransitionKeyType.GLOBAL, "test1", "test2")
                    .animate(AnimatedProperties.X, AnimatedProperties.Y)
                    .animator(testVerificationAnimator)),
            createMockLayoutOutput("test1", 10, 10),
            createMockLayoutOutput("test2", -10, -10))
    transitionManager?.setupTransitions(
        current, next, TransitionManager.getRootTransition(next.transitions))
    assertThat(createdAnimations)
        .containsExactlyInAnyOrder(
            createPropertyAnimation("test1", AnimatedProperties.X, 10f),
            createPropertyAnimation("test1", AnimatedProperties.Y, 10f),
            createPropertyAnimation("test2", AnimatedProperties.X, -10f),
            createPropertyAnimation("test2", AnimatedProperties.Y, -10f))
  }

  @Test
  fun testAutoLayoutAnimation() {
    val current =
        createMockLayoutState(
            Transition.parallel<Transition>(),
            createMockLayoutOutput("test1", 0, 0),
            createMockLayoutOutput("test2", 0, 0))
    val next =
        createMockLayoutState(
            Transition.parallel(Transition.allLayout().animator(testVerificationAnimator)),
            createMockLayoutOutput("test1", 10, 20, 200, 150),
            createMockLayoutOutput("test2", -10, -20, 50, 80))
    transitionManager?.setupTransitions(
        current, next, TransitionManager.getRootTransition(next.transitions))
    assertThat(createdAnimations)
        .containsExactlyInAnyOrder(
            createPropertyAnimation("test1", AnimatedProperties.X, 10f),
            createPropertyAnimation("test1", AnimatedProperties.Y, 20f),
            createPropertyAnimation("test1", AnimatedProperties.WIDTH, 200f),
            createPropertyAnimation("test1", AnimatedProperties.HEIGHT, 150f),
            createPropertyAnimation("test2", AnimatedProperties.X, -10f),
            createPropertyAnimation("test2", AnimatedProperties.Y, -20f),
            createPropertyAnimation("test2", AnimatedProperties.WIDTH, 50f),
            createPropertyAnimation("test2", AnimatedProperties.HEIGHT, 80f))
  }

  @Test
  fun testKeyDoesntExist() {
    val current =
        createMockLayoutState(
            Transition.parallel<Transition>(), createMockLayoutOutput("test", 0, 0))
    val next =
        createMockLayoutState(
            Transition.parallel(
                Transition.create(Transition.TransitionKeyType.GLOBAL, "test", "keydoesntexist")
                    .animate(AnimatedProperties.X, AnimatedProperties.Y)
                    .animator(testVerificationAnimator)),
            createMockLayoutOutput("test", 10, 0))
    transitionManager?.setupTransitions(
        current, next, TransitionManager.getRootTransition(next.transitions))
    assertThat(createdAnimations)
        .containsExactlyInAnyOrder(createPropertyAnimation("test", AnimatedProperties.X, 10f))
  }

  @Test
  fun testNoPreviousLayoutState() {
    val next =
        createMockLayoutState(
            Transition.parallel(
                Transition.create(Transition.TransitionKeyType.GLOBAL, "test", "keydoesntexist")
                    .animate(AnimatedProperties.X, AnimatedProperties.Y)
                    .animator(testVerificationAnimator),
                Transition.create(Transition.TransitionKeyType.GLOBAL, "appearing")
                    .animate(AnimatedProperties.X)
                    .appearFrom(0f)
                    .animator(testVerificationAnimator)),
            createMockLayoutOutput("test", 10, 0),
            createMockLayoutOutput("appearing", 20, 0))
    transitionManager?.setupTransitions(
        null, next, TransitionManager.getRootTransition(next.transitions))
    assertThat(createdAnimations)
        .containsExactlyInAnyOrder(createPropertyAnimation("appearing", AnimatedProperties.X, 20f))
  }

  @Test
  fun testWithMountTimeAnimations() {
    val current =
        createMockLayoutState(
            Transition.parallel<Transition>(),
            createMockLayoutOutput("test", 0, 0),
            createMockLayoutOutput("test2", 0, 0))
    val next =
        createMockLayoutState(
            Transition.parallel(
                Transition.create(Transition.TransitionKeyType.GLOBAL, "test", "keydoesntexist")
                    .animate(AnimatedProperties.X, AnimatedProperties.Y)
                    .animator(testVerificationAnimator)),
            createMockLayoutOutput("test", 10, 0),
            createMockLayoutOutput("test2", 20, 0))
    val mountTimeTransitions = ArrayList<Transition>()
    mountTimeTransitions.add(
        Transition.create(Transition.TransitionKeyType.GLOBAL, "test2")
            .animate(AnimatedProperties.X)
            .appearFrom(0f)
            .animator(testVerificationAnimator))
    val allTransitions: MutableList<Transition> = ArrayList()
    next.transitions?.let { allTransitions.addAll(it) }
    allTransitions.addAll(mountTimeTransitions)
    transitionManager?.setupTransitions(
        current, next, TransitionManager.getRootTransition(allTransitions))
    assertThat(createdAnimations)
        .containsExactlyInAnyOrder(
            createPropertyAnimation("test", AnimatedProperties.X, 10f),
            createPropertyAnimation("test2", AnimatedProperties.X, 20f))
  }

  @Test
  fun testWithOnlyMountTimeAnimations() {
    val current =
        createMockLayoutState(
            Transition.parallel<Transition>(),
            createMockLayoutOutput("test", 0, 0),
            createMockLayoutOutput("test2", 0, 0))
    val next =
        createMockLayoutState(
            null, createMockLayoutOutput("test", 10, 0), createMockLayoutOutput("test2", 20, 0))
    val mountTimeTransitions = ArrayList<Transition>()
    mountTimeTransitions.add(
        Transition.create(Transition.TransitionKeyType.GLOBAL, "test2")
            .animate(AnimatedProperties.X)
            .appearFrom(0f)
            .animator(testVerificationAnimator))
    mountTimeTransitions.add(
        Transition.create(Transition.TransitionKeyType.GLOBAL, "test", "keydoesntexist")
            .animate(AnimatedProperties.X, AnimatedProperties.Y)
            .animator(testVerificationAnimator))
    transitionManager?.setupTransitions(
        current, next, TransitionManager.getRootTransition(mountTimeTransitions))
    assertThat(createdAnimations)
        .containsExactlyInAnyOrder(
            createPropertyAnimation("test", AnimatedProperties.X, 10f),
            createPropertyAnimation("test2", AnimatedProperties.X, 20f))
  }

  @Test
  fun testAnimationFromStateUpdate() {
    val current =
        createMockLayoutState(
            Transition.parallel<Transition>(),
            createMockLayoutOutput("test1", 0, 0),
            createMockLayoutOutput("test2", 0, 0))
    val animationsCreatedFromStateUpdate = ArrayList<PropertyAnimation>()
    val animatorForStateUpdate = TransitionAnimator { propertyAnimation ->
      animationsCreatedFromStateUpdate.add(propertyAnimation)
      SpringTransition(propertyAnimation)
    }
    val transitionsFromStateUpdate = ArrayList<Transition>()
    transitionsFromStateUpdate.add(
        Transition.create(Transition.TransitionKeyType.GLOBAL, "test1", "test2")
            .animate(AnimatedProperties.Y)
            .animator(animatorForStateUpdate))
    val next =
        createMockLayoutState(
            Transition.parallel(
                Transition.create(Transition.TransitionKeyType.GLOBAL, "test1", "test2")
                    .animate(AnimatedProperties.X)
                    .animator(testVerificationAnimator)),
            createMockLayoutOutput("test1", 10, 20),
            createMockLayoutOutput("test2", -10, -20))
    val allTransitions: MutableList<Transition> = ArrayList()
    next.transitions?.let { allTransitions.addAll(it) }
    allTransitions.addAll(transitionsFromStateUpdate)
    transitionManager?.setupTransitions(
        current, next, TransitionManager.getRootTransition(allTransitions))
    assertThat(createdAnimations.size).isEqualTo(2)
    assertThat(animationsCreatedFromStateUpdate)
        .containsExactlyInAnyOrder(
            createPropertyAnimation("test1", AnimatedProperties.Y, 20f),
            createPropertyAnimation("test2", AnimatedProperties.Y, -20f))
  }

  private fun createPropertyAnimation(
      key: String,
      property: AnimatedProperty,
      endValue: Float
  ): PropertyAnimation {
    val transitionId = TransitionId(TransitionId.Type.GLOBAL, key, null)
    return PropertyAnimation(PropertyHandle(transitionId, property), endValue)
  }

  /** @return a mock LayoutState that only has a transition key -> LayoutOutput mapping. */
  private fun createMockLayoutState(
      transitions: TransitionSet?,
      vararg animatableItems: AnimatableItem
  ): LayoutState {
    val transitionIdMapping: MutableMap<TransitionId, OutputUnitsAffinityGroup<AnimatableItem>> =
        LinkedHashMap()
    animatableItems.forEach { animatableItem ->
      val transitionId = animatableItem.transitionId ?: return@forEach
      var group = transitionIdMapping[transitionId]
      if (group == null) {
        group = OutputUnitsAffinityGroup()
        transitionIdMapping[transitionId] = group
      }
      @OutputUnitType val type = LayoutState.getTypeFromId(animatableItem.id)
      group.add(type, animatableItem)
    }
    val layoutState = mock<LayoutState>()
    whenever(layoutState.transitions).thenReturn(transitions?.children)
    whenever(layoutState.transitionIdMapping).thenReturn(transitionIdMapping)
    whenever(layoutState.getAnimatableItemForTransitionId(anyOrNull())).then { invocation ->
      val transitionId = invocation.arguments[0] as TransitionId
      requireNotNull(transitionIdMapping[transitionId])
    }
    return layoutState
  }

  /** @return a mock LayoutOutput with a transition key and dummy bounds. */
  private fun createMockLayoutOutput(
      transitionKey: String,
      x: Int,
      y: Int,
      width: Int = 100,
      height: Int = 100
  ): AnimatableItem {
    val transitionId = TransitionId(TransitionId.Type.GLOBAL, transitionKey, null)
    val bounds = Rect(x, y, x + width, y + height)
    return LithoAnimtableItem(0, bounds, OutputUnitType.CONTENT, null, transitionId)
  }
}


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

package com.facebook.litho

import android.graphics.Rect
import com.facebook.litho.Transition.RootBoundsTransition
import com.facebook.litho.animation.AnimatedProperties
import com.facebook.litho.animation.DimensionValue
import com.facebook.litho.testing.inlinelayoutspec.InlineLayoutSpec
import com.facebook.litho.testing.testrunner.LithoTestRunner
import com.facebook.litho.widget.SimpleMountSpecTester
import com.facebook.rendercore.transitions.TransitionUtils
import org.assertj.core.api.Assertions.assertThat
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.kotlin.mock
import org.mockito.kotlin.whenever

@RunWith(LithoTestRunner::class)
class TransitionTest {

  @Test
  fun testCollectRootBoundsTransitions() {
    val transition =
        Transition.create(Transition.TransitionKeyType.GLOBAL, "rootKey")
            .animate(AnimatedProperties.WIDTH)
    val rootTransitionId = TransitionId(TransitionId.Type.GLOBAL, "rootKey", null)
    val rootWidthTransition = RootBoundsTransition()
    TransitionUtils.collectRootBoundsTransitions(
        rootTransitionId, transition, AnimatedProperties.WIDTH, rootWidthTransition)
    assertThat(rootWidthTransition.hasTransition).isTrue
    assertThat(rootWidthTransition.appearTransition).isNull()
    val rootHeightTransition = RootBoundsTransition()
    TransitionUtils.collectRootBoundsTransitions(
        rootTransitionId, transition, AnimatedProperties.HEIGHT, rootHeightTransition)
    assertThat(rootHeightTransition.hasTransition).isFalse
  }

  @Test
  fun testCollectRootBoundsTransitionsAppearComesAfterAllLayout() {
    val transition =
        Transition.parallel(
            Transition.allLayout(),
            Transition.create(Transition.TransitionKeyType.GLOBAL, "rootKey")
                .animate(AnimatedProperties.HEIGHT)
                .appearFrom(10f),
            Transition.create(Transition.TransitionKeyType.GLOBAL, "otherkey")
                .animate(AnimatedProperties.ALPHA))
    val rootTransitionId = TransitionId(TransitionId.Type.GLOBAL, "rootKey", null)
    val rootHeightTransition = RootBoundsTransition()
    TransitionUtils.collectRootBoundsTransitions(
        rootTransitionId, transition, AnimatedProperties.HEIGHT, rootHeightTransition)
    assertThat(rootHeightTransition.hasTransition).isTrue
    assertThat(rootHeightTransition.appearTransition).isNotNull
  }

  @Test
  fun testCollectRootBoundsTransitionsAppearComesBeforeAllLayout() {
    val transition =
        Transition.parallel(
            Transition.create(Transition.TransitionKeyType.GLOBAL, "rootKey")
                .animate(AnimatedProperties.HEIGHT)
                .appearFrom(10f),
            Transition.allLayout(),
            Transition.create(Transition.TransitionKeyType.GLOBAL, "otherkey")
                .animate(AnimatedProperties.ALPHA))
    val rootTransitionId = TransitionId(TransitionId.Type.GLOBAL, "rootKey", null)
    val rootHeightTransition = RootBoundsTransition()
    TransitionUtils.collectRootBoundsTransitions(
        rootTransitionId, transition, AnimatedProperties.HEIGHT, rootHeightTransition)
    assertThat(rootHeightTransition.hasTransition).isTrue
    assertThat(rootHeightTransition.appearTransition).isNotNull
  }

  @Test
  fun testCollectRootBoundsTransitionsExtractAppearFrom() {
    val transition =
        Transition.create(Transition.TransitionKeyType.GLOBAL, "rootKey")
            .animate(AnimatedProperties.HEIGHT)
            .appearFrom(10f)
    val rootTransitionId = TransitionId(TransitionId.Type.GLOBAL, "rootKey", null)
    val rootHeightTransition = RootBoundsTransition()
    TransitionUtils.collectRootBoundsTransitions(
        rootTransitionId, transition, AnimatedProperties.HEIGHT, rootHeightTransition)
    assertThat(rootHeightTransition.hasTransition).isTrue
    assertThat(rootHeightTransition.appearTransition).isNotNull
    val layoutState = mock<LayoutState>()
    val component: Component =
        object : InlineLayoutSpec() {
          override fun onCreateLayout(c: ComponentContext): Component =
              SimpleMountSpecTester.create(c).build()
        }
    val rootUnit: LithoRenderUnit =
        MountSpecLithoRenderUnit.create(
            0, component, null, null, null, 0, 0, MountSpecLithoRenderUnit.STATE_UNKNOWN)
    whenever(layoutState.getMountableOutputAt(0))
        .thenReturn(create(rootUnit, Rect(0, 0, 300, 100), null, null))
    val animateFrom =
        Transition.getRootAppearFromValue(
                rootHeightTransition.appearTransition, layoutState, AnimatedProperties.HEIGHT)
            .toInt()
    assertThat(animateFrom).isEqualTo(10)
  }

  @Test
  fun testCollectRootBoundsTransitionsExtractAppearFromDimensionValue() {
    val transition =
        Transition.create(Transition.TransitionKeyType.GLOBAL, "rootKey")
            .animate(AnimatedProperties.HEIGHT)
            .appearFrom(DimensionValue.heightPercentageOffset(50f))
    val rootTransitionId = TransitionId(TransitionId.Type.GLOBAL, "rootKey", null)
    val rootHeightTransition = RootBoundsTransition()
    TransitionUtils.collectRootBoundsTransitions(
        rootTransitionId, transition, AnimatedProperties.HEIGHT, rootHeightTransition)
    assertThat(rootHeightTransition.hasTransition).isTrue
    assertThat(rootHeightTransition.appearTransition).isNotNull
    val layoutState = mock<LayoutState>()
    val bounds = Rect(0, 0, 300, 100)
    whenever(layoutState.animatableRootItem)
        .thenReturn(LithoAnimtableItem(0, Rect(0, 0, 300, 100), OutputUnitType.CONTENT, null, null))
    val animateFrom =
        Transition.getRootAppearFromValue(
                rootHeightTransition.appearTransition, layoutState, AnimatedProperties.HEIGHT)
            .toInt()
    val expectedAppearFrom = bounds.height() * 1.5f
    assertThat(animateFrom).isEqualTo(expectedAppearFrom.toInt())
  }
}


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

package com.facebook.litho

import com.facebook.litho.testing.testrunner.LithoTestRunner
import com.facebook.rendercore.transitions.TransitionUtils
import java.lang.IllegalArgumentException
import org.assertj.core.api.Assertions.assertThat
import org.junit.Test
import org.junit.runner.RunWith

@RunWith(LithoTestRunner::class)
class TransitionUtilsTest {

  private val globalKey = "global-key"
  private val transitionKey = "transition-key"
  private val transitionOwnerKey = "transition-owner-key"

  @Test
  fun noTransitionKeyTest() {
    val id =
        TransitionUtils.createTransitionId(
            null, Transition.DEFAULT_TRANSITION_KEY_TYPE, transitionOwnerKey, globalKey)
    assertThat(id).isNotNull
    assertThat(id?.mType).isEqualTo(TransitionId.Type.AUTOGENERATED)
  }

  @Test
  fun defaultTransitionKeyTest() {
    val id =
        TransitionUtils.createTransitionId(
            transitionKey, Transition.DEFAULT_TRANSITION_KEY_TYPE, transitionOwnerKey, globalKey)
    assertThat(id).isNotNull
    assertThat(id?.mType).isEqualTo(TransitionId.Type.SCOPED)
    assertThat(id?.mReference).isEqualTo(transitionKey)
    assertThat(id?.mExtraData).isEqualTo(transitionOwnerKey)
  }

  @Test
  fun globalTransitionKeyTest() {
    val id =
        TransitionUtils.createTransitionId(
            transitionKey, Transition.TransitionKeyType.GLOBAL, transitionOwnerKey, globalKey)
    assertThat(id).isNotNull
    assertThat(id?.mType).isEqualTo(TransitionId.Type.GLOBAL)
    assertThat(id?.mReference).isEqualTo(transitionKey)
  }

  @Test
  fun localTransitionKeyTest() {
    val id =
        TransitionUtils.createTransitionId(
            transitionKey, Transition.TransitionKeyType.LOCAL, transitionOwnerKey, globalKey)
    assertThat(id).isNotNull
    assertThat(id?.mType).isEqualTo(TransitionId.Type.SCOPED)
    assertThat(id?.mReference).isEqualTo(transitionKey)
    assertThat(id?.mExtraData).isEqualTo(transitionOwnerKey)
  }

  @Test(expected = IllegalArgumentException::class)
  fun unhandledTransitionKeyTypeTest() {
    TransitionUtils.createTransitionId(transitionKey, null, transitionOwnerKey, globalKey)
  }
}


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

package com.facebook.litho

import android.R
import android.graphics.Color
import android.graphics.Rect
import android.graphics.drawable.Drawable
import androidx.collection.SparseArrayCompat
import com.facebook.litho.MountSpecLithoRenderUnit.UpdateState
import com.facebook.litho.config.ComponentsConfiguration
import com.facebook.litho.drawable.ComparableColorDrawable
import com.facebook.litho.testing.LegacyLithoViewRule
import com.facebook.litho.testing.TestComponent
import com.facebook.litho.testing.TestDrawableComponent
import com.facebook.litho.testing.TestSizeDependentComponent
import com.facebook.litho.testing.Whitebox
import com.facebook.litho.testing.exactly
import com.facebook.litho.testing.inlinelayoutspec.InlineLayoutSpec
import com.facebook.litho.testing.testrunner.LithoTestRunner
import com.facebook.litho.widget.Text
import com.facebook.rendercore.MountItem
import com.facebook.rendercore.RenderTreeNode
import com.facebook.yoga.YogaAlign
import com.facebook.yoga.YogaEdge
import java.lang.Exception
import org.assertj.core.api.Assertions.assertThat
import org.junit.Assert
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.kotlin.mock
import org.mockito.kotlin.whenever
import org.robolectric.annotation.LooperMode

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

  @JvmField
  @Rule
  val legacyLithoViewRule =
      LegacyLithoViewRule(
          ComponentsConfiguration.create().shouldAddHostViewForRootComponent(true).build())

  @Before
  @Throws(Exception::class)
  fun setup() {
    RedDrawable = ComparableColorDrawable.create(Color.RED)
    BlackDrawable = ComparableColorDrawable.create(Color.BLACK)
    TransparentDrawable = ComparableColorDrawable.create(Color.TRANSPARENT)
  }

  @Test
  fun testDiffTreeDisabled() {
    val component: Component =
        object : InlineLayoutSpec() {
          override fun onCreateLayout(c: ComponentContext): Component {
            return Column.create(c)
                .child(TestDrawableComponent.create(c))
                .child(Column.create(c).child(TestDrawableComponent.create(c)))
                .build()
          }
        }
    val layoutState =
        calculateLayoutState(
            legacyLithoViewRule.componentTree.context, component, exactly(350), exactly(200))

    // Check diff tree is null.
    assertThat(layoutState.diffTree).isNull()
  }

  @Test
  fun testDiffTreeEnabled() {
    val component: Component =
        object : InlineLayoutSpec() {
          override fun onCreateLayout(c: ComponentContext): Component {
            return Column.create(c)
                .child(TestDrawableComponent.create(c))
                .child(Column.create(c).child(TestDrawableComponent.create(c)))
                .build()
          }
        }
    val layoutState =
        calculateLayoutStateWithDiffing(
            legacyLithoViewRule.componentTree.context, component, exactly(350), exactly(200), null)

    // Check diff tree is not null and consistent.
    val node = layoutState.diffTree
    assertThat(node).isNotNull
    assertThat(4).isEqualTo(countNodes(requireNotNull(node)))
  }

  @Test
  fun testCachedMeasureFunction() {
    val c = legacyLithoViewRule.context
    val component0: Component = Text.create(c).text("hello-world").build()
    legacyLithoViewRule.attachToWindow().setRoot(component0).layout().measure()
    val diffNode = legacyLithoViewRule.committedLayoutState?.diffTree
    val component1: Component = Text.create(c).text("hello-world").build()
    legacyLithoViewRule.setRoot(component1).layout().measure()
    val result = legacyLithoViewRule.currentRootNode
    assertThat(result?.width).isEqualTo(diffNode?.lastMeasuredWidth?.toInt())
    assertThat(result?.height).isEqualTo(diffNode?.lastMeasuredHeight?.toInt())
  }

  @Test
  fun tesLastConstraints() {
    val c = legacyLithoViewRule.context
    val component0: Component = Text.create(c).text("hello-world").build()
    legacyLithoViewRule.attachToWindow().setRoot(component0).layout().measure()
    val diffNode = legacyLithoViewRule.committedLayoutState?.diffTree
    val component1: Component = Text.create(c).text("hello-world").build()
    legacyLithoViewRule.setRoot(component1).layout().measure()
    val result = legacyLithoViewRule.currentRootNode
    assertThat(diffNode?.lastWidthSpec).isEqualTo(result?.widthSpec)
    assertThat(diffNode?.lastHeightSpec).isEqualTo(result?.heightSpec)
  }

  @Test
  fun testCachedMeasures() {
    val c = legacyLithoViewRule.context
    val component0: Component =
        Column.create(c)
            .child(Text.create(c).text("hello-world").build())
            .child(Text.create(c).text("hello-world").build())
            .build()
    legacyLithoViewRule.attachToWindow().setRoot(component0).layout().measure()
    val component1: Component =
        Column.create(c)
            .child(Text.create(c).text("hello-world").build())
            .child(Text.create(c).text("hello-world").build())
            .build()
    legacyLithoViewRule.setRoot(component1).layout().measure()
    val result = legacyLithoViewRule.currentRootNode
    assertThat(result?.getChildAt(0)?.areCachedMeasuresValid()).isTrue
    assertThat(result?.getChildAt(1)?.areCachedMeasuresValid()).isTrue
  }

  @Test
  fun testPartiallyCachedMeasures() {
    val c = legacyLithoViewRule.context
    val component0: Component =
        Column.create(c)
            .child(Text.create(c).text("hello-world-1").build())
            .child(Text.create(c).text("hello-world-2").build())
            .build()
    legacyLithoViewRule.attachToWindow().setRoot(component0).layout().measure()
    val component1: Component =
        Column.create(c)
            .child(Text.create(c).text("hello-world-1").build())
            .child(Text.create(c).text("hello-world-3").build())
            .build()
    legacyLithoViewRule.setRoot(component1).layout().measure()
    val result = legacyLithoViewRule.currentRootNode
    assertThat(result?.getChildAt(0)?.areCachedMeasuresValid()).isTrue
    assertThat(result?.getChildAt(1)?.areCachedMeasuresValid()).isFalse
  }

  @Test
  fun testLayoutOutputReuse() {
    // Needed because of (legacy) usage two different InlineLayoutSpec that the test wants to treat
    // as the same component type.
    val COMPONENT_IDENTITY = 12345
    val component1: Component =
        object : InlineLayoutSpec(COMPONENT_IDENTITY) {
          override fun onCreateLayout(c: ComponentContext): Component {
            return Column.create(c)
                .child(TestDrawableComponent.create(c))
                .child(Column.create(c).child(TestDrawableComponent.create(c)))
                .build()
          }
        }
    val component2: Component =
        object : InlineLayoutSpec(COMPONENT_IDENTITY) {
          override fun onCreateLayout(c: ComponentContext): Component {
            return Column.create(c)
                .child(TestDrawableComponent.create(c))
                .child(Column.create(c).child(TestDrawableComponent.create(c)))
                .build()
          }
        }
    val prevLayoutState =
        calculateLayoutStateWithDiffing(
            legacyLithoViewRule.componentTree.context, component1, exactly(350), exactly(200), null)
    val layoutState =
        calculateLayoutStateWithDiffing(
            legacyLithoViewRule.componentTree.context,
            component2,
            exactly(350),
            exactly(200),
            prevLayoutState)
    assertThat(layoutState.mountableOutputCount).isEqualTo(prevLayoutState.mountableOutputCount)
    var i = 0
    val count = prevLayoutState.mountableOutputCount
    while (i < count) {
      assertThat(layoutState.getMountableOutputAt(i).renderUnit.id)
          .isEqualTo(prevLayoutState.getMountableOutputAt(i).renderUnit.id)
      i++
    }
  }

  @Test
  fun testLayoutOutputPartialReuse() {
    // Needed because of (legacy) usage two different InlineLayoutSpec that the test wants to treat
    // as the same component type.
    val COMPONENT_IDENTITY = 12345
    val component1: Component =
        object : InlineLayoutSpec(COMPONENT_IDENTITY) {
          override fun onCreateLayout(c: ComponentContext): Component {
            return Column.create(c)
                .child(TestDrawableComponent.create(c))
                .child(Column.create(c).child(TestDrawableComponent.create(c)))
                .build()
          }
        }
    val component2: Component =
        object : InlineLayoutSpec(COMPONENT_IDENTITY) {
          override fun onCreateLayout(c: ComponentContext): Component {
            return Column.create(c)
                .child(TestDrawableComponent.create(c))
                .child(Column.create(c).child(TestDrawableComponent.create(c)))
                .child(TestDrawableComponent.create(c))
                .build()
          }
        }
    val prevLayoutState =
        calculateLayoutStateWithDiffing(
            legacyLithoViewRule.componentTree.context, component1, exactly(350), exactly(200), null)
    val layoutState =
        calculateLayoutStateWithDiffing(
            legacyLithoViewRule.componentTree.context,
            component2,
            exactly(350),
            exactly(200),
            prevLayoutState)
    Assert.assertNotEquals(
        prevLayoutState.mountableOutputCount.toLong(), layoutState.mountableOutputCount.toLong())
    var i = 0
    val count = prevLayoutState.mountableOutputCount
    while (i < count) {
      assertThat(layoutState.getMountableOutputAt(i).renderUnit.id)
          .describedAs("Output $i")
          .isEqualTo(prevLayoutState.getMountableOutputAt(i).renderUnit.id)
      i++
    }
  }

  private fun assertCachedMeasurementsNotDefined(node: LithoLayoutResult) {
    assertThat(node.areCachedMeasuresValid()).isFalse
  }

  private fun checkAllComponentsHaveMeasureCache(node: LithoLayoutResult) {
    if (node.node.tailComponent != null && node.node.tailComponent.canMeasure()) {
      assertCachedMeasurementsDefined(node)
    }
    val numChildren = node.childCount
    for (i in 0 until numChildren) {
      checkAllComponentsHaveMeasureCache(node.getChildAt(i))
    }
  }

  @Test
  fun testComponentHostMoveItem() {
    val c = legacyLithoViewRule.componentTree.context
    val hostHolder = ComponentHost(c)
    val mountItem: MountItem = mock()
    whenever(mountItem.renderTreeNode).thenReturn(createNode(Column.create(c).build()))
    val mountItem1: MountItem = mock()
    whenever(mountItem1.renderTreeNode).thenReturn(createNode(Column.create(c).build()))
    val mountItem2: MountItem = mock()
    whenever(mountItem2.renderTreeNode).thenReturn(createNode(Column.create(c).build()))
    hostHolder.mount(0, mountItem, Rect())
    hostHolder.mount(1, mountItem1, Rect())
    hostHolder.mount(2, mountItem2, Rect())
    assertThat(mountItem).isEqualTo(hostHolder.getMountItemAt(0))
    assertThat(mountItem1).isEqualTo(hostHolder.getMountItemAt(1))
    assertThat(mountItem2).isEqualTo(hostHolder.getMountItemAt(2))
    hostHolder.moveItem(mountItem, 0, 2)
    hostHolder.moveItem(mountItem2, 2, 0)
    assertThat(mountItem2).isEqualTo(hostHolder.getMountItemAt(0))
    assertThat(mountItem1).isEqualTo(hostHolder.getMountItemAt(1))
    assertThat(mountItem).isEqualTo(hostHolder.getMountItemAt(2))
  }

  @Test
  fun testComponentHostMoveItemPartial() {
    val c = legacyLithoViewRule.componentTree.context
    val hostHolder = ComponentHost(c)
    val mountItem: MountItem = mock()
    whenever(mountItem.renderTreeNode).thenReturn(createNode(Column.create(c).build()))
    val mountItem1: MountItem = mock()
    whenever(mountItem1.renderTreeNode).thenReturn(createNode(Column.create(c).build()))
    val mountItem2: MountItem = mock()
    whenever(mountItem2.renderTreeNode).thenReturn(createNode(Column.create(c).build()))
    hostHolder.mount(0, mountItem, Rect())
    hostHolder.mount(1, mountItem1, Rect())
    hostHolder.mount(2, mountItem2, Rect())
    assertThat(mountItem).isEqualTo(hostHolder.getMountItemAt(0))
    assertThat(mountItem1).isEqualTo(hostHolder.getMountItemAt(1))
    assertThat(mountItem2).isEqualTo(hostHolder.getMountItemAt(2))
    hostHolder.moveItem(mountItem2, 2, 0)
    assertThat(mountItem2).isEqualTo(hostHolder.getMountItemAt(0))
    assertThat(mountItem1).isEqualTo(hostHolder.getMountItemAt(1))
    assertThat(1)
        .isEqualTo(
            (Whitebox.getInternalState<Any>(hostHolder, "mScrapMountItemsArray")
                    as? SparseArrayCompat<MountItem?>)
                ?.size())
    hostHolder.unmount(0, mountItem)
    assertThat(2)
        .isEqualTo(
            (Whitebox.getInternalState<Any>(hostHolder, "mMountItems")
                    as? SparseArrayCompat<MountItem?>)
                ?.size())
    assertThat(Whitebox.getInternalState<Any?>(hostHolder, "mScrapMountItemsArray")).isNull()
  }

  @Test
  fun testLayoutOutputUpdateState() {
    val c = legacyLithoViewRule.context
    val firstComponent: Component = TestDrawableComponent.create(c).color(Color.BLACK).build()
    val secondComponent: Component = TestDrawableComponent.create(c).color(Color.BLACK).build()
    val thirdComponent: Component = TestDrawableComponent.create(c).color(Color.WHITE).build()
    legacyLithoViewRule.setRoot(firstComponent).attachToWindow()
    legacyLithoViewRule.setRootAndSizeSpecSync(firstComponent, exactly(10), exactly(10))
    val state = legacyLithoViewRule.componentTree.mainThreadLayoutState
    assertOutputsState(state, MountSpecLithoRenderUnit.STATE_UNKNOWN)
    legacyLithoViewRule.setRoot(secondComponent)
    val secondState = legacyLithoViewRule.componentTree.mainThreadLayoutState
    assertOutputsState(secondState, MountSpecLithoRenderUnit.STATE_UPDATED)
    legacyLithoViewRule.setRoot(thirdComponent)
    val thirdState = legacyLithoViewRule.componentTree.mainThreadLayoutState
    assertOutputsState(thirdState, MountSpecLithoRenderUnit.STATE_DIRTY)
  }

  @Test
  fun testLayoutOutputUpdateStateWithBackground() {
    val component1: Component = TestLayoutSpecBgState(false)
    val component2: Component = TestLayoutSpecBgState(false)
    val component3: Component = TestLayoutSpecBgState(true)
    legacyLithoViewRule.setRoot(component1).attachToWindow()
    legacyLithoViewRule.setRootAndSizeSpecSync(component1, exactly(10), exactly(10))
    val state = legacyLithoViewRule.componentTree.mainThreadLayoutState
    assertOutputsState(state, MountSpecLithoRenderUnit.STATE_UNKNOWN)
    legacyLithoViewRule.setRoot(component2)
    val secondState = legacyLithoViewRule.componentTree.mainThreadLayoutState
    assertThat(secondState?.mountableOutputCount).isEqualTo(4)
    legacyLithoViewRule.setRoot(component3)
    val thirdState = legacyLithoViewRule.componentTree.mainThreadLayoutState
    assertThat(thirdState?.mountableOutputCount).isEqualTo(4)
    assertThat(
            MountSpecLithoRenderUnit.getUpdateState(
                requireNotNull(thirdState?.getMountableOutputAt(2))))
        .isEqualTo(MountSpecLithoRenderUnit.STATE_UPDATED)
    assertThat(
            MountSpecLithoRenderUnit.getUpdateState(
                requireNotNull(thirdState?.getMountableOutputAt(3))))
        .isEqualTo(MountSpecLithoRenderUnit.STATE_UPDATED)
  }

  // This test covers the same case with the foreground since the code path is the same!
  @Test
  fun testLayoutOutputUpdateStateWithBackgroundInWithLayout() {
    val component1: Component = TestLayoutSpecInnerState(false)
    val component2: Component = TestLayoutSpecInnerState(false)
    val component3: Component = TestLayoutSpecInnerState(true)
    legacyLithoViewRule.setRoot(component1).attachToWindow()
    legacyLithoViewRule.setRootAndSizeSpecSync(component1, exactly(10), exactly(10))
    val state = legacyLithoViewRule.componentTree.mainThreadLayoutState
    assertThat(
            MountSpecLithoRenderUnit.getUpdateState(requireNotNull(state?.getMountableOutputAt(2))))
        .isEqualTo(MountSpecLithoRenderUnit.STATE_UNKNOWN)
    legacyLithoViewRule.setRoot(component2)
    val secondState = legacyLithoViewRule.componentTree.mainThreadLayoutState
    assertThat(
            MountSpecLithoRenderUnit.getUpdateState(
                requireNotNull(secondState?.getMountableOutputAt(2))))
        .isEqualTo(MountSpecLithoRenderUnit.STATE_UNKNOWN)
    assertThat(
            MountSpecLithoRenderUnit.getUpdateState(
                requireNotNull(secondState?.getMountableOutputAt(3))))
        .isEqualTo(MountSpecLithoRenderUnit.STATE_UPDATED)
    legacyLithoViewRule.setRoot(component3)
    val thirdState = legacyLithoViewRule.componentTree.mainThreadLayoutState
    assertThat(
            MountSpecLithoRenderUnit.getUpdateState(
                requireNotNull(thirdState?.getMountableOutputAt(2))))
        .isEqualTo(MountSpecLithoRenderUnit.STATE_UNKNOWN)
    assertThat(
            MountSpecLithoRenderUnit.getUpdateState(
                requireNotNull(thirdState?.getMountableOutputAt(3))))
        .isEqualTo(MountSpecLithoRenderUnit.STATE_UPDATED)
  }

  @Test
  fun testLayoutOutputUpdateStateIdClash() {
    val component1: Component = TestLayoutWithStateIdClash(false)
    val component2: Component = TestLayoutWithStateIdClash(true)
    legacyLithoViewRule.setRoot(component1).attachToWindow()
    legacyLithoViewRule.setRootAndSizeSpecSync(component1, exactly(10), exactly(10))
    val state = legacyLithoViewRule.componentTree.mainThreadLayoutState
    assertOutputsState(state, MountSpecLithoRenderUnit.STATE_UNKNOWN)
    legacyLithoViewRule.setRoot(component2)
    val secondState = legacyLithoViewRule.componentTree.mainThreadLayoutState
    assertThat(6).isEqualTo(secondState?.mountableOutputCount)
    assertThat(MountSpecLithoRenderUnit.STATE_DIRTY)
        .isEqualTo(
            MountSpecLithoRenderUnit.getUpdateState(
                requireNotNull(secondState?.getMountableOutputAt(0))))
    assertThat(MountSpecLithoRenderUnit.STATE_UNKNOWN)
        .isEqualTo(
            MountSpecLithoRenderUnit.getUpdateState(
                requireNotNull(secondState?.getMountableOutputAt(1))))
    assertThat(MountSpecLithoRenderUnit.STATE_UPDATED)
        .isEqualTo(
            MountSpecLithoRenderUnit.getUpdateState(
                requireNotNull(secondState?.getMountableOutputAt(2))))
    assertThat(MountSpecLithoRenderUnit.STATE_UNKNOWN)
        .isEqualTo(
            MountSpecLithoRenderUnit.getUpdateState(
                requireNotNull(secondState?.getMountableOutputAt(3))))
    assertThat(MountSpecLithoRenderUnit.STATE_UNKNOWN)
        .isEqualTo(
            MountSpecLithoRenderUnit.getUpdateState(
                requireNotNull(secondState?.getMountableOutputAt(4))))
    assertThat(MountSpecLithoRenderUnit.STATE_UPDATED)
        .isEqualTo(
            MountSpecLithoRenderUnit.getUpdateState(
                requireNotNull(secondState?.getMountableOutputAt(5))))
  }

  @Test
  fun testDiffTreeUsedIfRootMeasureSpecsAreDifferentButChildHasSame() {
    val c = legacyLithoViewRule.componentTree.context
    val component: TestComponent = TestDrawableComponent.create(c).color(Color.BLACK).build()
    val layoutComponent: Component = TestSimpleContainerLayout2(component)
    val firstLayoutState =
        calculateLayoutStateWithDiffing(c, layoutComponent, exactly(100), exactly(100), null)
    assertThat(component.wasMeasureCalled()).isTrue
    val secondComponent: TestComponent = TestDrawableComponent.create(c).color(Color.BLACK).build()
    val secondLayoutComponent: Component = TestSimpleContainerLayout2(secondComponent)
    calculateLayoutStateWithDiffing(
        c, secondLayoutComponent, exactly(100), exactly(90), firstLayoutState)
    assertThat(secondComponent.wasMeasureCalled()).isFalse
  }

  @Test
  fun testDiffTreeUsedIfMeasureSpecsAreSame() {
    val c = legacyLithoViewRule.componentTree.context
    val component: TestComponent = TestDrawableComponent.create(c).color(Color.BLACK).build()
    val layoutComponent: Component = TestSimpleContainerLayout(component, 0)
    val firstLayoutState =
        calculateLayoutStateWithDiffing(c, layoutComponent, exactly(100), exactly(100), null)
    assertThat(component.wasMeasureCalled()).isTrue
    val secondComponent: TestComponent = TestDrawableComponent.create(c).color(Color.BLACK).build()
    val secondLayoutComponent: Component = TestSimpleContainerLayout(secondComponent, 0)
    calculateLayoutStateWithDiffing(
        c, secondLayoutComponent, exactly(100), exactly(100), firstLayoutState)
    assertThat(secondComponent.wasMeasureCalled()).isFalse
  }

  @Test
  fun testCachedMeasuresForNestedTreeComponentDelegateWithUndefinedSize() {
    val component1: Component = TestNestedTreeDelegateWithUndefinedSizeLayout()
    val component2: Component = TestNestedTreeDelegateWithUndefinedSizeLayout()
    val prevLayoutState =
        calculateLayoutStateWithDiffing(
            legacyLithoViewRule.componentTree.context, component1, exactly(350), exactly(200), null)
    val layoutState =
        calculateLayoutStateWithDiffing(
            legacyLithoViewRule.componentTree.context,
            component2,
            exactly(350),
            exactly(200),
            prevLayoutState)

    // The nested root measure() was called in the first layout calculation.
    val prevNestedRoot =
        LithoRenderUnit.getRenderUnit(prevLayoutState.getMountableOutputAt(2)).component
            as TestComponent
    assertThat(prevNestedRoot.wasMeasureCalled()).isTrue
    val nestedRoot =
        LithoRenderUnit.getRenderUnit(layoutState.getMountableOutputAt(2)).component
            as TestComponent
    assertThat(nestedRoot.wasMeasureCalled()).isFalse
  }

  @Test
  fun testCachedMeasuresForNestedTreeComponentWithUndefinedSize() {
    val component1: Component = TestUndefinedSizeLayout()
    val component2: Component = TestUndefinedSizeLayout()
    val prevLayoutState =
        calculateLayoutStateWithDiffing(
            legacyLithoViewRule.componentTree.context, component1, exactly(350), exactly(200), null)
    val layoutState =
        calculateLayoutStateWithDiffing(
            legacyLithoViewRule.componentTree.context,
            component2,
            exactly(350),
            exactly(200),
            prevLayoutState)

    // The nested root measure() was called in the first layout calculation.
    val prevMainTreeLeaf =
        LithoRenderUnit.getRenderUnit(prevLayoutState.getMountableOutputAt(1)).component
            as TestComponent
    assertThat(prevMainTreeLeaf.wasMeasureCalled()).isTrue
    val prevNestedLeaf1 =
        LithoRenderUnit.getRenderUnit(prevLayoutState.getMountableOutputAt(3)).component
            as TestComponent
    assertThat(prevNestedLeaf1.wasMeasureCalled()).isTrue
    val prevNestedLeaf2 =
        LithoRenderUnit.getRenderUnit(prevLayoutState.getMountableOutputAt(4)).component
            as TestComponent
    assertThat(prevNestedLeaf2.wasMeasureCalled()).isTrue
    val mainTreeLeaf =
        LithoRenderUnit.getRenderUnit(layoutState.getMountableOutputAt(1)).component
            as TestComponent
    assertThat(mainTreeLeaf.wasMeasureCalled()).isFalse
    val nestedLeaf1 =
        LithoRenderUnit.getRenderUnit(layoutState.getMountableOutputAt(3)).component
            as TestComponent
    assertThat(nestedLeaf1.wasMeasureCalled()).isFalse
    val nestedLeaf2 =
        LithoRenderUnit.getRenderUnit(layoutState.getMountableOutputAt(4)).component
            as TestComponent
    assertThat(nestedLeaf2.wasMeasureCalled()).isFalse
  }

  private class TestLayoutSpec internal constructor(private val addThirdChild: Boolean) :
      InlineLayoutSpec() {
    override fun onCreateLayout(c: ComponentContext): Component {
      return Column.create(c)
          .child(TestDrawableComponent.create(c))
          .child(Column.create(c).child(TestDrawableComponent.create(c)))
          .child(if (addThirdChild) TestDrawableComponent.create(c) else null)
          .build()
    }
  }

  private class TestSimpleContainerLayout
  internal constructor(private val component: Component, private val horizontalPadding: Int) :
      InlineLayoutSpec() {
    override fun onCreateLayout(c: ComponentContext): Component {
      return Column.create(c)
          .paddingPx(YogaEdge.HORIZONTAL, horizontalPadding)
          .child(component)
          .build()
    }
  }

  private class TestSimpleContainerLayout2 internal constructor(private val component: Component) :
      InlineLayoutSpec() {
    override fun onCreateLayout(c: ComponentContext): Component {
      return Column.create(c)
          .alignItems(YogaAlign.FLEX_START)
          .child(Wrapper.create(c).delegate(component).heightPx(50))
          .build()
    }
  }

  private class TestLayoutSpecInnerState
  internal constructor(private val changeChildDrawable: Boolean) : InlineLayoutSpec() {
    override fun onCreateLayout(c: ComponentContext): Component {
      return Column.create(c)
          .background(RedDrawable)
          .foregroundRes(R.drawable.btn_default)
          .child(
              TestDrawableComponent.create(c)
                  .background(if (changeChildDrawable) RedDrawable else BlackDrawable))
          .child(Column.create(c).child(TestDrawableComponent.create(c)))
          .build()
    }
  }

  private class TestLayoutSpecBgState internal constructor(private val changeBg: Boolean) :
      InlineLayoutSpec() {
    override fun onCreateLayout(c: ComponentContext): Component {
      return Column.create(c)
          .background(if (changeBg) BlackDrawable else RedDrawable)
          .foreground(TransparentDrawable)
          .child(TestDrawableComponent.create(c))
          .child(Column.create(c).child(TestDrawableComponent.create(c)))
          .build()
    }
  }

  private class TestUndefinedSizeLayout : InlineLayoutSpec() {
    override fun onCreateLayout(c: ComponentContext): Component {
      return Column.create(c)
          .paddingPx(YogaEdge.ALL, 2)
          .child(TestDrawableComponent.create(c, true, true, false))
          .child(
              TestSizeDependentComponent.create(c)
                  .setDelegate(false)
                  .flexShrink(0f)
                  .marginPx(YogaEdge.ALL, 11))
          .build()
    }
  }

  private class TestNestedTreeDelegateWithUndefinedSizeLayout : InlineLayoutSpec() {
    override fun onCreateLayout(c: ComponentContext): Component {
      return Column.create(c)
          .paddingPx(YogaEdge.ALL, 2)
          .child(TestSizeDependentComponent.create(c).setDelegate(true).marginPx(YogaEdge.ALL, 11))
          .build()
    }
  }

  private class TestLayoutWithStateIdClash internal constructor(private val addChild: Boolean) :
      InlineLayoutSpec() {
    override fun onCreateLayout(c: ComponentContext): Component {
      return Column.create(c)
          .child(
              Column.create(c)
                  .wrapInView()
                  .child(TestDrawableComponent.create(c))
                  .child(if (addChild) TestDrawableComponent.create(c) else null))
          .child(Column.create(c).wrapInView().child(TestDrawableComponent.create(c)))
          .build()
    }
  }

  companion object {

    private lateinit var RedDrawable: Drawable
    private lateinit var BlackDrawable: Drawable
    private lateinit var TransparentDrawable: Drawable

    private fun countNodes(node: DiffNode): Int {
      var sum = 1
      for (i in 0 until node.childCount) {
        val child = node.getChildAt(i)
        if (child != null) {
          sum += countNodes(child)
        }
      }
      return sum
    }

    private fun calculateLayoutState(
        context: ComponentContext,
        component: Component,
        widthSpec: Int,
        heightSpec: Int
    ): LayoutState {
      val result =
          ResolveTreeFuture.resolve(context, component, TreeState(), -1, -1, null, null, null, null)
      return LayoutTreeFuture.layout(
          result, widthSpec, heightSpec, -1, -1, false, null, null, null, null)
    }

    private fun calculateLayoutStateWithDiffing(
        context: ComponentContext,
        component: Component,
        widthSpec: Int,
        heightSpec: Int,
        previousLayoutState: LayoutState?
    ): LayoutState {
      val result =
          ResolveTreeFuture.resolve(
              context, component, TreeState(), -1, -1, previousLayoutState?.mRoot, null, null, null)
      return LayoutTreeFuture.layout(
          result,
          widthSpec,
          heightSpec,
          -1,
          -1,
          true,
          previousLayoutState,
          previousLayoutState?.diffTree,
          null,
          null)
    }

    private fun assertOutputsState(layoutState: LayoutState?, @UpdateState state: Int) {
      assertThat(MountSpecLithoRenderUnit.STATE_DIRTY)
          .isEqualTo(
              MountSpecLithoRenderUnit.getUpdateState(
                  requireNotNull(layoutState?.getMountableOutputAt(0))))
      for (i in 1 until (layoutState?.mountableOutputCount ?: 0)) {
        assertThat(state)
            .isEqualTo(
                MountSpecLithoRenderUnit.getUpdateState(
                    requireNotNull(layoutState?.getMountableOutputAt(i))))
      }
    }

    private fun assertCachedMeasurementsDefined(node: LithoLayoutResult) {
      val diffHeight: Float = node.diffNode?.lastMeasuredHeight ?: -1f
      val diffWidth: Float = node.diffNode?.lastMeasuredWidth ?: -1f
      assertThat(diffHeight != -1f).isTrue
      assertThat(diffWidth != -1f).isTrue
      assertThat(node.areCachedMeasuresValid()).isTrue
    }

    private fun createNode(component: Component): RenderTreeNode {
      val unit: LithoRenderUnit =
          MountSpecLithoRenderUnit.create(
              0, component, null, null, null, 0, 0, MountSpecLithoRenderUnit.STATE_UNKNOWN)
      return create(unit, Rect(), LithoLayoutData(0, 0, 0, 0, null, null), null)
    }
  }
}


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

package com.facebook.litho

import android.content.Context
import androidx.test.core.app.ApplicationProvider
import com.facebook.litho.testing.testrunner.LithoTestRunner
import org.assertj.core.api.Assertions.assertThat
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.kotlin.mock
import org.mockito.kotlin.whenever
import org.robolectric.annotation.LooperMode

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

  private lateinit var context: ComponentContext
  private lateinit var testComponent: Component
  private lateinit var testComponentKey: String

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

  @Test
  fun testQueueStateUpdate() {
    val treeState = TreeState()
    val stateContainer = StateUpdateTestComponent.TestStateContainer()

    // Add state container
    treeState.addStateContainer(testComponentKey, stateContainer, false)

    // Queue state update
    treeState.queueStateUpdate(
        testComponentKey, StateUpdateTestComponent.createIncrementStateUpdate(), false, false)

    // Apply state updates
    treeState.applyStateUpdatesEarly(context, testComponent, null, false)
    val previousStateContainer =
        treeState.getStateContainer(testComponentKey, false)
            as StateUpdateTestComponent.TestStateContainer?
    assertThat(previousStateContainer).isNotNull
    assertThat(previousStateContainer?.count).isEqualTo(1)
  }

  private fun testRegisterUnregisterInitialStateContainer(isNestedTree: Boolean) {
    val componentContext: ComponentContext = mock()
    whenever(componentContext.isNestedTreeContext).thenReturn(isNestedTree)
    whenever(componentContext.scopedComponentInfo)
        .thenReturn(ScopedComponentInfo(testComponent, context, null))
    val treeState = TreeState()
    val localTreeState = TreeState(treeState)

    // Register local tree state to ISC
    localTreeState.registerResolveState()
    localTreeState.registerLayoutState()

    // Add state containers in ISC
    localTreeState.createOrGetStateContainerForComponent(
        componentContext, testComponent, testComponentKey)
    val resolveState = treeState.resolveState
    val layoutStateHandler = treeState.layoutState
    if (isNestedTree) {
      assertThat(resolveState.initialStateContainer.initialStates).isEmpty()
      assertThat(layoutStateHandler.initialStateContainer.initialStates).isNotEmpty
    } else {
      assertThat(resolveState.initialStateContainer.initialStates).isNotEmpty
      assertThat(layoutStateHandler.initialStateContainer.initialStates).isEmpty()
    }

    // Unregister local tree state from ISC
    localTreeState.unregisterResolveInitialState()
    localTreeState.unregisterLayoutInitialState()

    // State containers in ISC should be cleared
    assertThat(resolveState.initialStateContainer).isNotNull
    assertThat(resolveState.initialStateContainer.initialStates).isEmpty()
    assertThat(layoutStateHandler.initialStateContainer).isNotNull
    assertThat(layoutStateHandler.initialStateContainer.initialStates).isEmpty()
  }

  @Test
  fun testRegisterUnregisterInitialStateContainerRenderState() {
    testRegisterUnregisterInitialStateContainer(false)
  }

  @Test
  fun testRegisterUnregisterInitialStateContainerLayoutState() {
    testRegisterUnregisterInitialStateContainer(true)
  }

  @Test
  fun testCommitRenderState() {
    val previousTreeState = TreeState()
    previousTreeState.addStateContainer(
        testComponentKey, StateUpdateTestComponent.TestStateContainer(), false)
    val treeState = TreeState()
    treeState.commitResolveState(previousTreeState)
    val previousStateContainer =
        treeState.getStateContainer(testComponentKey, false)
            as StateUpdateTestComponent.TestStateContainer?
    assertThat(previousStateContainer).isNotNull
    assertThat(previousStateContainer?.count).isEqualTo(0)
  }

  @Test
  fun testCommitLayoutState() {
    val previousTreeState = TreeState()
    previousTreeState.addStateContainer(
        testComponentKey, StateUpdateTestComponent.TestStateContainer(), true)
    val treeState = TreeState()
    treeState.commitLayoutState(previousTreeState)
    val previousStateContainer =
        treeState.getStateContainer(testComponentKey, true)
            as StateUpdateTestComponent.TestStateContainer?
    assertThat(previousStateContainer).isNotNull
    assertThat(previousStateContainer?.count).isEqualTo(0)
  }

  @Test
  fun testCopyPreviousState() {
    val previousTreeState = TreeState()
    previousTreeState.addStateContainer(
        testComponentKey, StateUpdateTestComponent.TestStateContainer(), false)
    val treeState = TreeState(previousTreeState)
    val previousStateContainer =
        treeState.getStateContainer(testComponentKey, false)
            as StateUpdateTestComponent.TestStateContainer?
    assertThat(previousStateContainer).isNotNull
    assertThat(previousStateContainer?.count).isEqualTo(0)
  }

  @Test
  fun testKeysForPendingUpdates() {
    val treeState = TreeState()
    val anotherTestComponentKey = "anotherTestComponentKey"

    // State Update on main tree component
    treeState.queueStateUpdate(
        testComponentKey, StateUpdateTestComponent.createIncrementStateUpdate(), false, false)

    // State Update on nested tree component
    treeState.queueStateUpdate(
        anotherTestComponentKey, StateUpdateTestComponent.createIncrementStateUpdate(), false, true)
    val renderKeysForPendingUpdates = treeState.keysForPendingResolveStateUpdates
    assertThat(renderKeysForPendingUpdates).contains(testComponentKey)
    val layoutKeysForPendingUpdates = treeState.keysForPendingLayoutStateUpdates
    assertThat(layoutKeysForPendingUpdates).contains(anotherTestComponentKey)
  }
}


/*
 * 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.LithoViewRule
import com.facebook.litho.testing.testrunner.LithoTestRunner
import com.facebook.litho.transition.transitionKey
import java.lang.RuntimeException
import org.assertj.core.api.Assertions
import org.junit.Rule
import org.junit.Test
import org.junit.rules.ExpectedException
import org.junit.runner.RunWith

@RunWith(LithoTestRunner::class)
class UniqueTransitionKeysTest {
  @JvmField @Rule val lithoViewRule: LithoViewRule = LithoViewRule()
  @JvmField @Rule var expectedException: ExpectedException = ExpectedException.none()

  @Test
  fun testGetTransitionKeyMapping() {
    class HasUniqueTransitionKeys : KComponent() {
      override fun ComponentScope.render(): Component {
        return Row {
          child(
              Row(
                  style =
                      Style.transitionKey(context, "test", Transition.TransitionKeyType.GLOBAL)))
          child(
              Row(
                  style =
                      Style.transitionKey(context, "test2", Transition.TransitionKeyType.GLOBAL)))
        }
      }
    }

    val testLithoView =
        lithoViewRule.render(widthPx = 100, heightPx = 100) { HasUniqueTransitionKeys() }
    testLithoView.componentTree.mainThreadLayoutState!!.transitionIdMapping
  }

  @Test
  fun testThrowIfSameTransitionKeyAppearsMultipleTimes() {
    expectedException.expect(RuntimeException::class.java)
    expectedException.expectMessage(
        "The transitionId 'TransitionId{\"test\", GLOBAL}' is defined multiple times in the same layout.")
    class HasNonUniqueTransitionKeys : KComponent() {
      override fun ComponentScope.render(): Component {
        return Row {
          child(
              Row(
                  style =
                      Style.transitionKey(context, "test", Transition.TransitionKeyType.GLOBAL)))
          child(
              Row(
                  style =
                      Style.transitionKey(context, "test", Transition.TransitionKeyType.GLOBAL)))
        }
      }
    }

    val testLithoView =
        lithoViewRule.render(widthPx = 100, heightPx = 100) { HasNonUniqueTransitionKeys() }
    Assertions.assertThat(testLithoView.componentTree.mainThreadLayoutState!!.transitionIdMapping)
        .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.Context
import androidx.test.core.app.ApplicationProvider.getApplicationContext
import com.facebook.litho.kotlin.widget.Text
import com.facebook.litho.testing.BackgroundLayoutLooperRule
import com.facebook.litho.testing.LithoViewRule
import com.facebook.litho.testing.TestLithoView
import com.facebook.litho.testing.assertj.LithoAssertions
import com.facebook.litho.testing.testrunner.LithoTestRunner
import com.facebook.litho.view.onClick
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith

@RunWith(LithoTestRunner::class)
class UnregisterLocalTreeStateInResolveTest {

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

  @get:Rule
  val backgroundLayoutLooperRule: BackgroundLayoutLooperRule = BackgroundLayoutLooperRule()

  /**
   * This test describes a bug we have found that led to a new [Component] to not re-calculate its
   * initial state and to use a previous redundant one.
   *
   * This happened because we were not unregistering pending state handlers whenever a "resolve" or
   * "layout" state handler was not completed.
   */
  @Test
  fun `the initial state container is cleaned on non-commited resolves`() {
    val lithoView = createEmptyLithoView()
    lithoViewRule.idle()

    val componentTree = lithoView.componentTree
    componentTree.setFutureExecutionListener(
        object : TreeFuture.FutureExecutionListener {
          override fun onPreExecution(
              version: Int,
              futureExecutionType: TreeFuture.FutureExecutionType?,
              attribution: String?
          ) {
            if (version == 1 && attribution == "resolve") {
              componentTree.setRootSync(TestCounterComponent(initialCounter = 2))
            }
          }

          override fun onPostExecution(version: Int, released: Boolean, attribution: String?) {}
        })

    componentTree.setRootAsync(TestCounterComponent(initialCounter = 1))
    lithoViewRule.idle()
    LithoAssertions.assertThat(lithoView).hasVisibleText("Counter: 2")

    componentTree.setRootSync(EmptyComponent())
    LithoAssertions.assertThat(lithoView).doesNotHaveVisibleText("Counter: 2")

    componentTree.setRootSync(TestCounterComponent(initialCounter = 3))
    LithoAssertions.assertThat(lithoView).hasVisibleText("Counter: 3")
  }

  private fun createEmptyLithoView(): TestLithoView {
    val context = getApplicationContext<Context>()
    val componentContext = ComponentContext(context)

    val componentTree = ComponentTree.create(componentContext).build()

    val lithoView =
        lithoViewRule.createTestLithoView(
            componentTree = componentTree, widthPx = 1080, heightPx = 840) {
              EmptyComponent()
            }
    lithoView.attachToWindow().measure().layout()
    return lithoView
  }
}

private class TestCounterComponent(val initialCounter: Int) : KComponent() {
  override fun ComponentScope.render(): Component {
    val counter = useState { initialCounter }

    return Row {
      child(Text("Counter: ${counter.value}", style = Style.onClick { counter.update { it + 1 } }))
    }
  }
}


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

package com.facebook.litho

import android.graphics.Rect
import android.view.View
import com.facebook.litho.testing.LegacyLithoViewRule
import com.facebook.litho.testing.testrunner.LithoTestRunner
import com.facebook.litho.widget.MountSpecLifecycleTester
import org.assertj.core.api.Assertions.assertThat
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith

@RunWith(LithoTestRunner::class)
class ViewAttributeExtensionTest {

  @JvmField @Rule var lithoViewRule: LegacyLithoViewRule = LegacyLithoViewRule()

  @Test
  fun `when view attributes is set it should override attribute set by mount spec`() {

    val c: ComponentContext = lithoViewRule.context

    val root: Component =
        MountSpecLifecycleTester.create(c)
            .intrinsicSize(Size(100, 100))
            .lifecycleTracker(LifecycleTracker())
            .defaultAlpha(0.5f)
            .alpha(0.2f)
            .build()

    lithoViewRule.render { root }

    val content: View = lithoViewRule.lithoView.getChildAt(0)

    assertThat(content.alpha)
        .describedAs("alpha should be applied from the common props")
        .isEqualTo(0.2f)

    // unmount everything
    lithoViewRule.useComponentTree(null)

    assertThat(content.alpha)
        .describedAs("alpha should be restored to the initial value")
        .isEqualTo(0.5f)
  }

  @Test
  fun `when view attributes is set on LithoView should be unset when root is unmounted`() {

    val c: ComponentContext = lithoViewRule.context

    val root: Component =
        Column.create(c)
            .alpha(0.2f)
            .child(
                MountSpecLifecycleTester.create(c)
                    .intrinsicSize(Size(100, 100))
                    .lifecycleTracker(LifecycleTracker()))
            .build()

    lithoViewRule.render { root }

    assertThat(lithoViewRule.lithoView.alpha)
        .describedAs("alpha should be applied from the common props")
        .isEqualTo(0.2f)

    lithoViewRule.lithoView.mountComponent(Rect(0, -10, 1080, -5), true)

    assertThat(lithoViewRule.lithoView.alpha)
        .describedAs("alpha should be restored to the default value")
        .isEqualTo(1f)
  }
}


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

package com.facebook.litho

import android.content.Context
import android.view.ViewGroup
import android.widget.TextView
import androidx.test.core.app.ApplicationProvider
import com.facebook.litho.testing.helper.ComponentTestHelper
import com.facebook.litho.testing.testrunner.LithoTestRunner
import com.facebook.litho.viewcompat.ViewBinder
import com.facebook.litho.viewcompat.ViewCreator
import org.assertj.core.api.Assertions.assertThat
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith

/** Tests [ViewCompatComponent] */
@RunWith(LithoTestRunner::class)
class ViewCompatComponentTest {

  private lateinit var context: ComponentContext

  private val textViewCreator: ViewCreator<TextView> = ViewCreator { c, _ -> TextView(c) }
  private val textViewCreator2: ViewCreator<TextView> = ViewCreator { c, _ -> TextView(c) }
  private val noOpViewBinder: ViewBinder<TextView> =
      object : ViewBinder<TextView> {
        override fun prepare() = Unit

        override fun bind(view: TextView) = Unit

        override fun unbind(view: TextView) = Unit
      }

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

  @Test
  fun testSimpleRendering() {
    val binder =
        object : ViewBinder<TextView> {
          override fun prepare() = Unit

          override fun bind(view: TextView) {
            view.text = "Hello World!"
          }

          override fun unbind(view: TextView) = Unit
        }
    val lithoView =
        ComponentTestHelper.mountComponent(
            ViewCompatComponent.get(textViewCreator, "TextView").create(context).viewBinder(binder))
    assertThat(lithoView.mountItemCount).isEqualTo(1)
    val view = lithoView.getMountItemAt(0).content as TextView
    assertThat(view.text).isEqualTo("Hello World!")
  }

  @Test
  fun testPrepare() {
    val binder =
        object : ViewBinder<TextView> {
          private var state: String? = null

          override fun prepare() {
            state = "Hello World!"
          }

          override fun bind(view: TextView) {
            view.text = state
          }

          override fun unbind(view: TextView) = Unit
        }
    val lithoView =
        ComponentTestHelper.mountComponent(
            ViewCompatComponent.get(textViewCreator, "TextView").create(context).viewBinder(binder))
    assertThat(lithoView.mountItemCount).isEqualTo(1)
    val view = lithoView.getMountItemAt(0).content as TextView
    assertThat(view.text).isEqualTo("Hello World!")
  }

  @Test
  fun testUnbind() {
    val binder =
        object : ViewBinder<TextView> {
          private lateinit var state: String

          override fun prepare() {
            state = "Hello World!"
          }

          override fun bind(view: TextView) {
            view.text = state
          }

          override fun unbind(view: TextView) {
            view.text = ""
          }
        }
    val lithoView =
        ComponentTestHelper.mountComponent(
            ViewCompatComponent.get(textViewCreator, "TextView").create(context).viewBinder(binder))
    ComponentTestHelper.unbindComponent(lithoView)
    assertThat(lithoView.mountItemCount).isEqualTo(1)
    val view = lithoView.getMountItemAt(0).content as TextView
    assertThat(view.text).isEqualTo("")
  }

  @Test
  fun testTypeIdForDifferentViewCreators() {
    val compatComponent: ViewCompatComponent<*> =
        ViewCompatComponent.get(textViewCreator, "compat")
            .create(context)
            .viewBinder(noOpViewBinder)
            .build()
    val sameCompatComponent: ViewCompatComponent<*> =
        ViewCompatComponent.get(textViewCreator, "sameCompat")
            .create(context)
            .viewBinder(noOpViewBinder)
            .build()
    val differentCompatComponent: ViewCompatComponent<*> =
        ViewCompatComponent.get(textViewCreator2, "differentCompat")
            .create(context)
            .viewBinder(noOpViewBinder)
            .build()
    assertThat(compatComponent.id).isNotEqualTo(sameCompatComponent.id)
    assertThat(compatComponent.id).isNotEqualTo(differentCompatComponent.id)
    assertThat(sameCompatComponent.id).isNotEqualTo(differentCompatComponent.id)
    assertThat(compatComponent.typeId).isEqualTo(sameCompatComponent.typeId)
    assertThat(compatComponent.typeId).isNotEqualTo(differentCompatComponent.typeId)
  }

  @Test
  fun testTypeIdForSameViewCreatorTypeButDifferentInstances() {
    val textViewCreator1 = CustomViewCreator("textviewcreator1")
    val textViewCreator2 = CustomViewCreator("textviewcreator2")
    val compatComponent: ViewCompatComponent<*> =
        ViewCompatComponent.get(textViewCreator1, "compat")
            .create(context)
            .viewBinder(noOpViewBinder)
            .build()
    val sameCompatComponent: ViewCompatComponent<*> =
        ViewCompatComponent.get(textViewCreator1, "sameCompat")
            .create(context)
            .viewBinder(noOpViewBinder)
            .build()
    val differentCompatComponent: ViewCompatComponent<*> =
        ViewCompatComponent.get(textViewCreator2, "differentCompat")
            .create(context)
            .viewBinder(noOpViewBinder)
            .build()
    assertThat(compatComponent.id).isNotEqualTo(sameCompatComponent.id)
    assertThat(compatComponent.id).isNotEqualTo(differentCompatComponent.id)
    assertThat(sameCompatComponent.id).isNotEqualTo(differentCompatComponent.id)
    assertThat(compatComponent.typeId).isEqualTo(sameCompatComponent.typeId)
    assertThat(compatComponent.typeId).isNotEqualTo(differentCompatComponent.typeId)
  }

  private inner class CustomViewCreator constructor(private val text: String) :
      ViewCreator<TextView> {
    override fun createView(c: Context, parent: ViewGroup): TextView {
      return TextView(c).apply { this.text = text }
    }
  }
}