/*
 * 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.animation.StateListAnimator
import android.annotation.TargetApi
import android.content.Context
import android.graphics.Color
import android.os.Build
import android.util.SparseArray
import androidx.test.core.app.ApplicationProvider
import com.facebook.litho.annotations.ImportantForAccessibility
import com.facebook.litho.drawable.ComparableColorDrawable
import com.facebook.litho.it.R.drawable.background_with_padding
import com.facebook.litho.testing.LegacyLithoViewRule
import com.facebook.litho.testing.TestViewComponent
import com.facebook.litho.testing.testrunner.LithoTestRunner
import com.facebook.litho.widget.Text
import com.facebook.yoga.YogaAlign
import com.facebook.yoga.YogaDirection
import com.facebook.yoga.YogaEdge
import com.facebook.yoga.YogaPositionType
import org.assertj.core.api.Assertions.assertThat
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.ArgumentMatchers
import org.mockito.Mockito
import org.mockito.kotlin.any
import org.mockito.kotlin.eq
import org.mockito.kotlin.mock
import org.mockito.kotlin.never
import org.mockito.kotlin.verify

@RunWith(LithoTestRunner::class)
class CommonPropsTest {

  @JvmField @Rule val legacyLithoViewRule: LegacyLithoViewRule = LegacyLithoViewRule()
  private lateinit var node: LithoNode
  private lateinit var commonProps: CommonProps
  private lateinit var componentContext: ComponentContext

  @Before
  fun setup() {
    node = mock()
    commonProps = CommonProps()
    componentContext = ComponentContext(ApplicationProvider.getApplicationContext<Context>())
  }

  @Test
  @TargetApi(Build.VERSION_CODES.LOLLIPOP)
  fun testSetPropsAndBuild() {
    commonProps.layoutDirection(YogaDirection.INHERIT)
    commonProps.alignSelf(YogaAlign.AUTO)
    commonProps.positionType(YogaPositionType.ABSOLUTE)
    commonProps.flex(2f)
    commonProps.flexGrow(3f)
    commonProps.flexShrink(4f)
    commonProps.flexBasisPx(5)
    commonProps.flexBasisPercent(6f)
    commonProps.importantForAccessibility(
        ImportantForAccessibility.IMPORTANT_FOR_ACCESSIBILITY_AUTO)
    commonProps.duplicateParentState(false)
    commonProps.marginPx(YogaEdge.ALL, 5)
    commonProps.marginPx(YogaEdge.RIGHT, 6)
    commonProps.marginPx(YogaEdge.LEFT, 4)
    commonProps.marginPercent(YogaEdge.ALL, 10f)
    commonProps.marginPercent(YogaEdge.VERTICAL, 12f)
    commonProps.marginPercent(YogaEdge.RIGHT, 5f)
    commonProps.marginAuto(YogaEdge.LEFT)
    commonProps.marginAuto(YogaEdge.TOP)
    commonProps.marginAuto(YogaEdge.RIGHT)
    commonProps.marginAuto(YogaEdge.BOTTOM)
    commonProps.paddingPx(YogaEdge.ALL, 1)
    commonProps.paddingPx(YogaEdge.RIGHT, 2)
    commonProps.paddingPx(YogaEdge.LEFT, 3)
    commonProps.paddingPercent(YogaEdge.VERTICAL, 7f)
    commonProps.paddingPercent(YogaEdge.RIGHT, 6f)
    commonProps.paddingPercent(YogaEdge.ALL, 5f)
    commonProps.border(Border.create(componentContext).build())
    commonProps.positionPx(YogaEdge.ALL, 11)
    commonProps.positionPx(YogaEdge.RIGHT, 12)
    commonProps.positionPx(YogaEdge.LEFT, 13)
    commonProps.positionPercent(YogaEdge.VERTICAL, 17f)
    commonProps.positionPercent(YogaEdge.RIGHT, 16f)
    commonProps.positionPercent(YogaEdge.ALL, 15f)
    commonProps.widthPx(5)
    commonProps.widthPercent(50f)
    commonProps.minWidthPx(15)
    commonProps.minWidthPercent(100f)
    commonProps.maxWidthPx(25)
    commonProps.maxWidthPercent(26f)
    commonProps.heightPx(30)
    commonProps.heightPercent(31f)
    commonProps.minHeightPx(32)
    commonProps.minHeightPercent(33f)
    commonProps.maxHeightPx(34)
    commonProps.maxHeightPercent(35f)
    commonProps.aspectRatio(20f)
    commonProps.touchExpansionPx(YogaEdge.RIGHT, 22)
    commonProps.touchExpansionPx(YogaEdge.LEFT, 23)
    commonProps.touchExpansionPx(YogaEdge.ALL, 21)
    val background = ComparableColorDrawable.create(Color.RED)
    commonProps.background(background)
    val foreground = ComparableColorDrawable.create(Color.BLACK)
    commonProps.foreground(foreground)
    commonProps.wrapInView()
    val clickHandler: EventHandler<ClickEvent> = mock()
    val longClickHandler: EventHandler<LongClickEvent> = mock()
    val touchHandler: EventHandler<TouchEvent> = mock()
    val interceptTouchHandler: EventHandler<InterceptTouchEvent> = mock()
    val focusChangedHandler: EventHandler<FocusChangedEvent> = mock()
    commonProps.clickHandler(clickHandler)
    commonProps.focusChangeHandler(focusChangedHandler)
    commonProps.longClickHandler(longClickHandler)
    commonProps.touchHandler(touchHandler)
    commonProps.interceptTouchHandler(interceptTouchHandler)
    commonProps.focusable(true)
    commonProps.clickable(true)
    commonProps.selected(false)
    commonProps.enabled(false)
    commonProps.visibleHeightRatio(55f)
    commonProps.visibleWidthRatio(56f)
    commonProps.accessibilityHeading(false)
    val visibleHandler: EventHandler<VisibleEvent> = mock()
    val focusedHandler: EventHandler<FocusedVisibleEvent> = mock()
    val unfocusedHandler: EventHandler<UnfocusedVisibleEvent> = mock()
    val fullImpressionHandler: EventHandler<FullImpressionVisibleEvent> = mock()
    val invisibleHandler: EventHandler<InvisibleEvent> = mock()
    val visibleRectChangedHandler: EventHandler<VisibilityChangedEvent> = mock()
    commonProps.visibleHandler(visibleHandler)
    commonProps.focusedHandler(focusedHandler)
    commonProps.unfocusedHandler(unfocusedHandler)
    commonProps.fullImpressionHandler(fullImpressionHandler)
    commonProps.invisibleHandler(invisibleHandler)
    commonProps.visibilityChangedHandler(visibleRectChangedHandler)
    commonProps.contentDescription("test")
    val viewTag = Any()
    val viewTags = SparseArray<Any>()
    commonProps.viewTag(viewTag)
    commonProps.viewTags(viewTags)
    commonProps.shadowElevationPx(60f)
    commonProps.clipToOutline(false)
    commonProps.transitionKey("transitionKey", "")
    commonProps.testKey("testKey")
    val dispatchPopulateAccessibilityEventHandler:
        EventHandler<DispatchPopulateAccessibilityEventEvent> =
        mock()
    val onInitializeAccessibilityEventHandler: EventHandler<OnInitializeAccessibilityEventEvent> =
        mock()
    val onInitializeAccessibilityNodeInfoHandler:
        EventHandler<OnInitializeAccessibilityNodeInfoEvent> =
        mock()
    val onPopulateAccessibilityEventHandler: EventHandler<OnPopulateAccessibilityEventEvent> =
        mock()
    val onPopulateAccessibiliNodeHandler: EventHandler<OnPopulateAccessibilityNodeEvent> = mock()
    val onRequestSendAccessibilityEventHandler: EventHandler<OnRequestSendAccessibilityEventEvent> =
        mock()
    val performAccessibilityActionHandler: EventHandler<PerformAccessibilityActionEvent> = mock()
    val sendAccessibilityEventHandler: EventHandler<SendAccessibilityEventEvent> = mock()
    val sendAccessibilityEventUncheckedHandler: EventHandler<SendAccessibilityEventUncheckedEvent> =
        mock()
    commonProps.accessibilityRole(AccessibilityRole.BUTTON)
    commonProps.accessibilityRoleDescription("Test Role Description")
    commonProps.dispatchPopulateAccessibilityEventHandler(dispatchPopulateAccessibilityEventHandler)
    commonProps.onInitializeAccessibilityEventHandler(onInitializeAccessibilityEventHandler)
    commonProps.onInitializeAccessibilityNodeInfoHandler(onInitializeAccessibilityNodeInfoHandler)
    commonProps.onPopulateAccessibilityEventHandler(onPopulateAccessibilityEventHandler)
    commonProps.onPopulateAccessibilityNodeHandler(onPopulateAccessibiliNodeHandler)
    commonProps.onRequestSendAccessibilityEventHandler(onRequestSendAccessibilityEventHandler)
    commonProps.performAccessibilityActionHandler(performAccessibilityActionHandler)
    commonProps.sendAccessibilityEventHandler(sendAccessibilityEventHandler)
    commonProps.sendAccessibilityEventUncheckedHandler(sendAccessibilityEventUncheckedHandler)
    val stateListAnimator: StateListAnimator = mock()
    commonProps.stateListAnimator(stateListAnimator)
    commonProps.copyInto(componentContext, node)
    val output: LayoutProps = Mockito.spy<LayoutProps>(LayoutProps::class.java)
    commonProps.copyLayoutProps(output)
    verify(output).layoutDirection(YogaDirection.INHERIT)
    verify(output).alignSelf(YogaAlign.AUTO)
    verify(output).positionType(YogaPositionType.ABSOLUTE)
    verify(output).flex(2f)
    verify(output).flexGrow(3f)
    verify(output).flexShrink(4f)
    verify(output).flexBasisPx(5)
    verify(output).flexBasisPercent(6f)
    verify(node)
        .importantForAccessibility(ImportantForAccessibility.IMPORTANT_FOR_ACCESSIBILITY_AUTO)
    verify(node).duplicateParentState(false)
    verify(output).marginPx(YogaEdge.ALL, 5)
    verify(output).marginPx(YogaEdge.RIGHT, 6)
    verify(output).marginPx(YogaEdge.LEFT, 4)
    verify(output).marginPercent(YogaEdge.ALL, 10f)
    verify(output).marginPercent(YogaEdge.VERTICAL, 12f)
    verify(output).marginPercent(YogaEdge.RIGHT, 5f)
    verify(output).marginAuto(YogaEdge.LEFT)
    verify(output).marginAuto(YogaEdge.TOP)
    verify(output).marginAuto(YogaEdge.RIGHT)
    verify(output).marginAuto(YogaEdge.BOTTOM)
    verify(output).paddingPx(YogaEdge.ALL, 1)
    verify(output).paddingPx(YogaEdge.RIGHT, 2)
    verify(output).paddingPx(YogaEdge.LEFT, 3)
    verify(output).paddingPercent(YogaEdge.VERTICAL, 7f)
    verify(output).paddingPercent(YogaEdge.RIGHT, 6f)
    verify(output).paddingPercent(YogaEdge.ALL, 5f)
    verify(node).border(any<Border>())
    verify(output).positionPx(YogaEdge.ALL, 11)
    verify(output).positionPx(YogaEdge.RIGHT, 12)
    verify(output).positionPx(YogaEdge.LEFT, 13)
    verify(output).positionPercent(YogaEdge.VERTICAL, 17f)
    verify(output).positionPercent(YogaEdge.RIGHT, 16f)
    verify(output).positionPercent(YogaEdge.ALL, 15f)
    verify(output).widthPx(5)
    verify(output).widthPercent(50f)
    verify(output).minWidthPx(15)
    verify(output).minWidthPercent(100f)
    verify(output).maxWidthPx(25)
    verify(output).maxWidthPercent(26f)
    verify(output).heightPx(30)
    verify(output).heightPercent(31f)
    verify(output).minHeightPx(32)
    verify(output).minHeightPercent(33f)
    verify(output).maxHeightPx(34)
    verify(output).maxHeightPercent(35f)
    verify(output).aspectRatio(20f)
    verify(node).touchExpansionPx(YogaEdge.RIGHT, 22)
    verify(node).touchExpansionPx(YogaEdge.LEFT, 23)
    verify(node).touchExpansionPx(YogaEdge.ALL, 21)
    verify(node).background(background)
    verify(node).foreground(foreground)
    verify(node).wrapInView()
    verify(node).applyNodeInfo(any<NodeInfo>())
    verify(node).visibleHeightRatio(55f)
    verify(node).visibleWidthRatio(56f)
    verify(node).visibleHandler(visibleHandler)
    verify(node).focusedHandler(focusedHandler)
    verify(node).unfocusedHandler(unfocusedHandler)
    verify(node).fullImpressionHandler(fullImpressionHandler)
    verify(node).invisibleHandler(invisibleHandler)
    verify(node).visibilityChangedHandler(visibleRectChangedHandler)
    verify(node).transitionKey(eq("transitionKey"), ArgumentMatchers.anyString())
    verify(node).testKey("testKey")
    verify(node).stateListAnimator(stateListAnimator)
  }

  @Test
  fun testSetScalePropsWrapsInView() {
    commonProps.scale(5f)
    commonProps.copyInto(componentContext, node)
    verify(node).applyNodeInfo(any<NodeInfo>())
    verify(node).wrapInView()
  }

  @Test
  fun testSetFullScalePropsDoesNotWrapInView() {
    commonProps.scale(0.5f)
    commonProps.scale(1f)
    commonProps.copyInto(componentContext, node)
    verify(node).applyNodeInfo(any<NodeInfo>())
    verify(node, never()).wrapInView()
  }

  @Test
  fun testSetAlphaPropsWrapsInView() {
    commonProps.alpha(5f)
    commonProps.copyInto(componentContext, node)
    verify(node).applyNodeInfo(any<NodeInfo>())
    verify(node).wrapInView()
  }

  @Test
  fun testSetFullAlphaPropsDoesNotWrapInView() {
    commonProps.alpha(5f)
    commonProps.alpha(1f)
    commonProps.copyInto(componentContext, node)
    verify(node).applyNodeInfo(any<NodeInfo>())
    verify(node, never()).wrapInView()
  }

  @Test
  fun testSetRotationPropsWrapsInView() {
    commonProps.rotation(5f)
    commonProps.copyInto(componentContext, node)
    verify(node).applyNodeInfo(any<NodeInfo>())
    verify(node).wrapInView()
  }

  @Test
  fun testSetZeroRotationPropsDoesNotWrapInView() {
    commonProps.rotation(1f)
    commonProps.rotation(0f)
    commonProps.copyInto(componentContext, node)
    verify(node).applyNodeInfo(any<NodeInfo>())
    verify(node, never()).wrapInView()
  }

  @Test
  fun testPaddingFromDrawable() {
    val c = legacyLithoViewRule.context
    val component =
        Column.create(c)
            .child(Text.create(c).text("Hello World").backgroundRes(background_with_padding))
            .build()
    legacyLithoViewRule.attachToWindow().setRoot(component).measure().layout()
    val result = legacyLithoViewRule.currentRootNode!!.getChildAt(0)
    assertThat(result.paddingLeft).isEqualTo(48)
    assertThat(result.paddingTop).isEqualTo(0)
    assertThat(result.paddingRight).isEqualTo(0)
    assertThat(result.paddingBottom).isEqualTo(0)
  }

  @Test
  fun testPaddingFromDrawableIsOverwritten() {
    val c = legacyLithoViewRule.context
    val component =
        Column.create(c)
            .child(
                Text.create(c)
                    .text("Hello World")
                    .backgroundRes(background_with_padding)
                    .paddingPx(YogaEdge.LEFT, 8)
                    .paddingPx(YogaEdge.RIGHT, 8)
                    .paddingPx(YogaEdge.TOP, 8)
                    .paddingPx(YogaEdge.BOTTOM, 8))
            .build()
    legacyLithoViewRule.attachToWindow().setRoot(component).measure().layout()
    val result = legacyLithoViewRule.currentRootNode!!.getChildAt(0)
    assertThat(result.paddingLeft).isEqualTo(8)
    assertThat(result.paddingTop).isEqualTo(8)
    assertThat(result.paddingRight).isEqualTo(8)
    assertThat(result.paddingBottom).isEqualTo(8)
  }

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

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

  @Test
  fun testDifferentObjectWithSameContentEquivalentTo() {
    commonProps = CommonProps()
    setCommonProps(commonProps)
    val mCommonProps2 = CommonProps()
    setCommonProps(mCommonProps2)
    assertThat(commonProps.isEquivalentTo(mCommonProps2)).isEqualTo(true)
  }

  @Test
  fun testDifferentObjectWithDifferentContentEquivalentTo() {
    commonProps = CommonProps()
    setCommonProps(commonProps)
    commonProps.duplicateParentState(false)
    val mCommonProps2 = CommonProps()
    setCommonProps(mCommonProps2)
    mCommonProps2.duplicateParentState(true)
    assertThat(commonProps.isEquivalentTo(mCommonProps2)).isEqualTo(false)
  }

  private fun setCommonProps(commonProps: CommonProps) {
    commonProps.layoutDirection(YogaDirection.INHERIT)
    commonProps.alignSelf(YogaAlign.AUTO)
    commonProps.positionType(YogaPositionType.ABSOLUTE)
    commonProps.flex(2f)
    commonProps.flexGrow(3f)
    commonProps.flexShrink(4f)
    commonProps.flexBasisPx(5)
    commonProps.flexBasisPercent(6f)
    commonProps.importantForAccessibility(
        ImportantForAccessibility.IMPORTANT_FOR_ACCESSIBILITY_AUTO)
    commonProps.duplicateParentState(false)
    commonProps.marginPx(YogaEdge.ALL, 5)
    commonProps.marginPx(YogaEdge.RIGHT, 6)
    commonProps.marginPx(YogaEdge.LEFT, 4)
    commonProps.marginPercent(YogaEdge.ALL, 10f)
    commonProps.marginPercent(YogaEdge.VERTICAL, 12f)
    commonProps.marginPercent(YogaEdge.RIGHT, 5f)
    commonProps.marginAuto(YogaEdge.LEFT)
    commonProps.marginAuto(YogaEdge.TOP)
    commonProps.marginAuto(YogaEdge.RIGHT)
    commonProps.marginAuto(YogaEdge.BOTTOM)
    commonProps.paddingPx(YogaEdge.ALL, 1)
    commonProps.paddingPx(YogaEdge.RIGHT, 2)
    commonProps.paddingPx(YogaEdge.LEFT, 3)
    commonProps.paddingPercent(YogaEdge.VERTICAL, 7f)
    commonProps.paddingPercent(YogaEdge.RIGHT, 6f)
    commonProps.paddingPercent(YogaEdge.ALL, 5f)
    commonProps.border(Border.create(componentContext).build())
    commonProps.positionPx(YogaEdge.ALL, 11)
    commonProps.positionPx(YogaEdge.RIGHT, 12)
    commonProps.positionPx(YogaEdge.LEFT, 13)
    commonProps.positionPercent(YogaEdge.VERTICAL, 17f)
    commonProps.positionPercent(YogaEdge.RIGHT, 16f)
    commonProps.positionPercent(YogaEdge.ALL, 15f)
    commonProps.widthPx(5)
    commonProps.widthPercent(50f)
    commonProps.minWidthPx(15)
    commonProps.minWidthPercent(100f)
    commonProps.maxWidthPx(25)
    commonProps.maxWidthPercent(26f)
    commonProps.heightPx(30)
    commonProps.heightPercent(31f)
    commonProps.minHeightPx(32)
    commonProps.minHeightPercent(33f)
    commonProps.maxHeightPx(34)
    commonProps.maxHeightPercent(35f)
    commonProps.aspectRatio(20f)
    commonProps.touchExpansionPx(YogaEdge.RIGHT, 22)
    commonProps.touchExpansionPx(YogaEdge.LEFT, 23)
    commonProps.touchExpansionPx(YogaEdge.ALL, 21)
    val background = ComparableColorDrawable.create(Color.RED)
    commonProps.background(background)
    val foreground = ComparableColorDrawable.create(Color.BLACK)
    commonProps.foreground(foreground)
    commonProps.wrapInView()
    val content = TestViewComponent.create(componentContext).build()
    val clickHandler = EventHandler<ClickEvent>(content, 3)
    val longClickHandler = EventHandler<LongClickEvent>(content, 3)
    val touchHandler = EventHandler<TouchEvent>(content, 3)
    val interceptTouchHandler = EventHandler<InterceptTouchEvent>(content, 3)
    val focusChangedHandler = EventHandler<FocusChangedEvent>(content, 3)
    commonProps.clickHandler(clickHandler)
    commonProps.focusChangeHandler(focusChangedHandler)
    commonProps.longClickHandler(longClickHandler)
    commonProps.touchHandler(touchHandler)
    commonProps.interceptTouchHandler(interceptTouchHandler)
    commonProps.focusable(true)
    commonProps.clickable(true)
    commonProps.selected(false)
    commonProps.enabled(false)
    commonProps.visibleHeightRatio(55f)
    commonProps.visibleWidthRatio(56f)
    commonProps.accessibilityHeading(false)
    val visibleHandler = EventHandler<VisibleEvent>(content, 3)
    val focusedHandler = EventHandler<FocusedVisibleEvent>(content, 3)
    val unfocusedHandler = EventHandler<UnfocusedVisibleEvent>(content, 3)
    val fullImpressionHandler = EventHandler<FullImpressionVisibleEvent>(content, 3)
    val invisibleHandler = EventHandler<InvisibleEvent>(content, 3)
    val visibleRectChangedHandler = EventHandler<VisibilityChangedEvent>(content, 3)
    commonProps.visibleHandler(visibleHandler)
    commonProps.focusedHandler(focusedHandler)
    commonProps.unfocusedHandler(unfocusedHandler)
    commonProps.fullImpressionHandler(fullImpressionHandler)
    commonProps.invisibleHandler(invisibleHandler)
    commonProps.visibilityChangedHandler(visibleRectChangedHandler)
    commonProps.contentDescription("test")
    commonProps.viewTag("Hello World")
    commonProps.shadowElevationPx(60f)
    commonProps.clipToOutline(false)
    commonProps.transitionKey("transitionKey", "")
    commonProps.testKey("testKey")
    val dispatchPopulateAccessibilityEventHandler =
        EventHandler<DispatchPopulateAccessibilityEventEvent>(content, 3)
    val onInitializeAccessibilityEventHandler =
        EventHandler<OnInitializeAccessibilityEventEvent>(content, 3)
    val onInitializeAccessibilityNodeInfoHandler =
        EventHandler<OnInitializeAccessibilityNodeInfoEvent>(content, 3)
    val onPopulateAccessibilityEventHandler =
        EventHandler<OnPopulateAccessibilityEventEvent>(content, 3)
    val onPopulateAccessibilityNodeHandler =
        EventHandler<OnPopulateAccessibilityNodeEvent>(content, 3)
    val onRequestSendAccessibilityEventHandler =
        EventHandler<OnRequestSendAccessibilityEventEvent>(content, 3)
    val performAccessibilityActionHandler =
        EventHandler<PerformAccessibilityActionEvent>(content, 3)
    val sendAccessibilityEventHandler = EventHandler<SendAccessibilityEventEvent>(content, 3)
    val sendAccessibilityEventUncheckedHandler =
        EventHandler<SendAccessibilityEventUncheckedEvent>(content, 3)
    commonProps.accessibilityRole(AccessibilityRole.BUTTON)
    commonProps.accessibilityRoleDescription("Test Role Description")
    commonProps.dispatchPopulateAccessibilityEventHandler(dispatchPopulateAccessibilityEventHandler)
    commonProps.onInitializeAccessibilityEventHandler(onInitializeAccessibilityEventHandler)
    commonProps.onInitializeAccessibilityNodeInfoHandler(onInitializeAccessibilityNodeInfoHandler)
    commonProps.onPopulateAccessibilityEventHandler(onPopulateAccessibilityEventHandler)
    commonProps.onPopulateAccessibilityNodeHandler(onPopulateAccessibilityNodeHandler)
    commonProps.onRequestSendAccessibilityEventHandler(onRequestSendAccessibilityEventHandler)
    commonProps.performAccessibilityActionHandler(performAccessibilityActionHandler)
    commonProps.sendAccessibilityEventHandler(sendAccessibilityEventHandler)
    commonProps.sendAccessibilityEventUncheckedHandler(sendAccessibilityEventUncheckedHandler)

    /*
         Copied from ViewNodeInfo class TODO: (T33421916) We need compare StateListAnimators more accurately
    */
    //    final StateListAnimator stateListAnimator = mock(StateListAnimator.class);
    //    commonProps.stateListAnimator(stateListAnimator);
    commonProps.copyInto(componentContext, node)
  }
}


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

package com.facebook.litho

import android.graphics.Rect
import com.facebook.litho.config.ComponentsConfiguration
import com.facebook.litho.kotlin.widget.Text
import com.facebook.litho.sections.SectionContext
import com.facebook.litho.sections.common.SingleComponentSection
import com.facebook.litho.sections.widget.ListRecyclerConfiguration
import com.facebook.litho.sections.widget.RecyclerCollectionComponent
import com.facebook.litho.testing.BackgroundLayoutLooperRule
import com.facebook.litho.testing.LegacyLithoViewRule
import com.facebook.litho.testing.error.TestCrasherOnCreateLayout
import com.facebook.litho.testing.error.TestCrasherOnCreateLayoutWithSizeSpec
import com.facebook.litho.testing.error.TestCrasherOnMount
import com.facebook.litho.testing.error.TestErrorBoundary
import com.facebook.litho.testing.exactly
import com.facebook.litho.testing.testrunner.LithoTestRunner
import com.facebook.litho.widget.CrashFromLayoutFromStyle
import com.facebook.litho.widget.CrashKotlinComponent
import com.facebook.litho.widget.CrashingMountable
import com.facebook.litho.widget.CrashingMountableSpec.MountPhaseException
import com.facebook.litho.widget.DynamicPropCrasher
import com.facebook.litho.widget.OnErrorNotPresentChild
import com.facebook.litho.widget.OnErrorPassUpChildTester
import com.facebook.litho.widget.OnErrorPassUpParentTester
import com.facebook.litho.widget.TestCrashFromEachLayoutLifecycleMethod
import com.facebook.litho.widget.TestCrashFromEachLayoutLifecycleMethodParent
import com.facebook.litho.widget.TestCrashFromEachLayoutLifecycleMethodSpec
import com.facebook.litho.widget.ThrowExceptionGrandChildTester
import com.facebook.yoga.YogaEdge
import java.util.ArrayList
import java.util.concurrent.atomic.AtomicReference
import org.assertj.core.api.Assertions.assertThat
import org.hamcrest.core.Is
import org.junit.After
import org.junit.Assume.assumeThat
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.rules.ExpectedException
import org.junit.runner.RunWith
import org.mockito.ArgumentMatchers
import org.mockito.Mockito
import org.robolectric.annotation.LooperMode

/**
 * Tests error handling in [Component] using the [TestErrorBoundarySpec] against components crashing
 * in various different lifecycle methods.
 */
@LooperMode(LooperMode.Mode.LEGACY)
@RunWith(LithoTestRunner::class)
class ComponentErrorBoundaryTest {

  @Rule @JvmField val lithoViewRule = LegacyLithoViewRule()

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

  @Rule @JvmField val backgroundLayoutLooperRule = BackgroundLayoutLooperRule()
  private var currentReconciliationValue = false

  @Before
  fun assumeDebugAndChangeConfig() {
    currentReconciliationValue = ComponentsConfiguration.isReconciliationEnabled
    // Disable reconciliation so that the onCreateLayout is called for layout.
    ComponentsConfiguration.isReconciliationEnabled = false
    ComponentsConfiguration.isAnimationDisabled = false
    assumeThat(
        "These tests can only be run in debug mode.",
        ComponentsConfiguration.IS_INTERNAL_BUILD,
        Is.`is`(true))
  }

  @After
  fun adjustConfigs() {
    // Reset the the values of the config.
    ComponentsConfiguration.isReconciliationEnabled = currentReconciliationValue
    ComponentsConfiguration.isAnimationDisabled = true
  }

  @Test
  fun testOnCreateLayoutErrorWithoutBoundaryWhenEnabled() {
    var exception: Exception? = null
    try {
      val component = TestCrasherOnCreateLayout.create(lithoViewRule.context).build()
      lithoViewRule.setRoot(component)
      lithoViewRule.attachToWindow().measure().layout()
    } catch (error: Exception) {
      exception = error
    }
    assertThat(exception).isNotNull
    assertThat(exception).hasStackTraceContaining("onCreateLayout crash")
  }

  @Test
  fun testOnCreateLayoutWithSizeSpecErrorWithoutBoundaryWhenEnabled() {
    var exception: Exception? = null
    try {
      val component = TestCrasherOnCreateLayoutWithSizeSpec.create(lithoViewRule.context).build()
      lithoViewRule.setRoot(component)
      lithoViewRule.attachToWindow().measure().layout()
    } catch (error: Exception) {
      exception = error
    }
    assertThat(exception).isNotNull
    assertThat(exception).hasStackTraceContaining("onCreateLayoutWithSizeSpec crash")
  }

  @Test
  fun testOnMountErrorWithoutBoundaryWhenEnabled() {
    var exception: Exception? = null
    try {
      val component = TestCrasherOnMount.create(lithoViewRule.context).build()
      lithoViewRule.setRoot(component)
      lithoViewRule.attachToWindow().setSizePx(100, 100)
      lithoViewRule.measure().layout()
    } catch (error: Exception) {
      exception = error
    }
    assertThat(exception).isNotNull
    assertThat(exception).hasStackTraceContaining("onMount crash")
  }

  @Test
  fun lifecycleOnErrorIndirectlyPassError() {
    val info: List<String> = ArrayList()
    var exception: Exception? = null
    try {
      val component =
          OnErrorPassUpParentTester.create(lithoViewRule.context)
              .child(OnErrorPassUpChildTester.create(lithoViewRule.context).info(info).build())
              .info(info)
              .build()
      lithoViewRule.setRoot(component)
      lithoViewRule.attachToWindow().measure().layout()
    } catch (error: Exception) {
      exception = error
    }
    assertThat(exception).isNotNull
    assertThat(info)
        .containsExactly(
            "OnErrorPassUpChildTesterSpec->onError", "OnErrorPassUpParentTesterSpec->onError")
  }

  @Test
  fun lifecycleOnErrorDirectlyPassError() {
    val info: List<String> = ArrayList()
    var exception: Exception? = null
    try {
      val component =
          OnErrorPassUpParentTester.create(lithoViewRule.context)
              .child(OnErrorNotPresentChild.create(lithoViewRule.context).build())
              .info(info)
              .build()
      lithoViewRule.setRoot(component)
      lithoViewRule.attachToWindow().measure().layout()
    } catch (error: Exception) {
      exception = error
    }
    assertThat(exception).isNotNull
    assertThat(info).containsExactly("OnErrorPassUpParentTesterSpec->onError")
  }

  @Test
  fun testOwnErrorHandler() {
    val errorEventHandler = Mockito.mock(ErrorEventHandler::class.java)
    val component = ThrowExceptionGrandChildTester.create(lithoViewRule.context).build()
    val componentTreeBuilder = ComponentTree.create(lithoViewRule.context)
    val componentTree = componentTreeBuilder.errorHandler(errorEventHandler).build()
    componentTree.root = component
    lithoViewRule.useComponentTree(componentTree)
    lithoViewRule.attachToWindow().measure().layout()
    Mockito.verify(errorEventHandler).onError(ArgumentMatchers.any(), ArgumentMatchers.any())
  }

  @Test
  fun testKotlinComponentCrashWithTestErrorBoundary() {
    val errorOutput: List<Exception> = ArrayList()
    val component =
        TestErrorBoundary.create(lithoViewRule.context)
            .errorOutput(errorOutput)
            .child(CrashKotlinComponent())
            .build()
    lithoViewRule.setRoot(component).attachToWindow().measure().layout()
    assertThat(errorOutput).hasSize(1)
    assertThat(errorOutput[0])
        .isInstanceOf(RuntimeException::class.java)
        .hasMessage("crash from kotlin component")
  }

  @Test
  fun testOnCreateLayoutCrashWithTestErrorBoundary() {
    crashingScenarioLayoutHelper(LifecycleStep.ON_CREATE_LAYOUT, "onCreateLayout crash", false)
  }

  @Test
  fun testOnCreateTreePropCrashWithTestErrorBoundary() {
    crashingScenarioLayoutHelper(LifecycleStep.ON_CREATE_TREE_PROP, "onCreateTreeProp crash", false)
  }

  @Test
  fun testOnCreateInitialStateCrashWithTestErrorBoundary() {
    crashingScenarioLayoutHelper(
        LifecycleStep.ON_CREATE_INITIAL_STATE, "onCreateInitialState crash", false)
  }

  @Test
  fun testOnCalculateCachedValueCrashWithTestErrorBoundary() {
    crashingScenarioLayoutHelper(
        LifecycleStep.ON_CALCULATE_CACHED_VALUE, "onCalculateCachedValue crash", false)
  }

  @Test
  fun testOnCreateTransitionCrashWithTestErrorBoundary() {
    crashingScenarioLayoutHelper(
        LifecycleStep.ON_CREATE_TRANSITION, "onCreateTransition crash", false)
  }

  @Test
  fun testOnAttachedCrashWithTestErrorBoundary() {
    crashingScenarioLayoutHelper(LifecycleStep.ON_ATTACHED, "onAttached crash", true)
  }

  @Test
  fun testOnRegisterRangesCrashWithTestErrorBoundary() {
    crashingScenarioLayoutSectionHelper(
        LifecycleStep.ON_REGISTER_RANGES, "onRegisterRanges crash", false, false)
  }

  @Test
  fun testOnEnteredRangeCrashWithTestErrorBoundary() {
    crashingScenarioLayoutSectionHelper(
        LifecycleStep.ON_ENTERED_RANGE, "onEnteredRange crash", false, true)
  }

  @Test
  fun testOnExitedRangeCrashWithTestErrorBoundary() {
    crashingScenarioLayoutSectionHelper(
        LifecycleStep.ON_EXITED_RANGE, "onExitedRange crash", true, true)
  }

  @Test
  fun testOnUpdateStateCrashWithTestErrorBoundary() {
    val caller = TestCrashFromEachLayoutLifecycleMethodSpec.Caller()
    val crashingComponent =
        TestCrashFromEachLayoutLifecycleMethod.create(lithoViewRule.context)
            .crashFromStep(LifecycleStep.ON_UPDATE_STATE)
            .caller(caller)
            .build()
    val errorOutput: List<Exception> = ArrayList()
    val component =
        TestErrorBoundary.create(lithoViewRule.context)
            .errorOutput(errorOutput)
            .child(crashingComponent)
            .build()
    lithoViewRule.setRoot(component).attachToWindow().measure().layout()
    caller.updateStateSync()
    assertThat(errorOutput).hasSize(1)
    assertThat(errorOutput[0])
        .isInstanceOf(RuntimeException::class.java)
        .hasMessage("onUpdateState crash")
  }

  @Test
  fun testOnUpdateStateCrashWithTestErrorBoundaryInDeepHierarchy() {
    val caller = TestCrashFromEachLayoutLifecycleMethodSpec.Caller()
    val crashingComponent =
        TestCrashFromEachLayoutLifecycleMethod.create(lithoViewRule.context)
            .crashFromStep(LifecycleStep.ON_UPDATE_STATE)
            .caller(caller)
            .build()
    val errorOutput: List<Exception> = ArrayList()
    val component =
        TestErrorBoundary.create(lithoViewRule.context)
            .errorOutput(errorOutput)
            .child(Column.create(lithoViewRule.context).child(crashingComponent).build())
            .build()
    lithoViewRule.setRoot(component).attachToWindow().measure().layout()
    caller.updateStateSync()
    assertThat(errorOutput).hasSize(1)
    assertThat(errorOutput[0])
        .isInstanceOf(RuntimeException::class.java)
        .hasMessage("onUpdateState crash")
  }

  @Test
  fun testOnUpdateStateCrashWithTestErrorBoundaryInDeepHierarchyOther() {
    val caller = TestCrashFromEachLayoutLifecycleMethodSpec.Caller()
    val errorOutput: List<Exception> = ArrayList()
    val component =
        TestErrorBoundary.create(lithoViewRule.context)
            .errorOutput(errorOutput)
            .child(
                Column.create(lithoViewRule.context)
                    .child(
                        TestCrashFromEachLayoutLifecycleMethodParent.create(lithoViewRule.context)
                            .caller(caller))
                    .build())
            .build()
    lithoViewRule.setRoot(component).attachToWindow().measure().layout()
    caller.updateStateSync()
    assertThat(errorOutput).hasSize(1)
    assertThat(errorOutput[0])
        .isInstanceOf(RuntimeException::class.java)
        .hasMessage("onUpdateState crash")
  }

  @Test
  fun testOnUpdateStateWithTransitionCrashWithTestErrorBoundary() {
    val caller = TestCrashFromEachLayoutLifecycleMethodSpec.Caller()
    val crashingComponent =
        TestCrashFromEachLayoutLifecycleMethod.create(lithoViewRule.context)
            .crashFromStep(LifecycleStep.ON_UPDATE_STATE_WITH_TRANSITION)
            .caller(caller)
            .build()
    val errorOutput: List<Exception> = ArrayList()
    val component =
        TestErrorBoundary.create(lithoViewRule.context)
            .errorOutput(errorOutput)
            .child(crashingComponent)
            .build()
    lithoViewRule.setRoot(component).attachToWindow().measure().layout()
    caller.updateStateWithTransition()
    backgroundLayoutLooperRule.runToEndOfTasksSync()
    assertThat(errorOutput).hasSize(1)
    assertThat(errorOutput[0])
        .isInstanceOf(RuntimeException::class.java)
        .hasMessage("onUpdateStateWithTransition crash")
  }

  @Test
  fun testOnUpdateStateWithTransitionCrashWithTestErrorBoundaryInDeepHierarchy() {
    val caller = TestCrashFromEachLayoutLifecycleMethodSpec.Caller()
    val crashingComponent =
        TestCrashFromEachLayoutLifecycleMethod.create(lithoViewRule.context)
            .crashFromStep(LifecycleStep.ON_UPDATE_STATE_WITH_TRANSITION)
            .caller(caller)
            .build()
    val errorOutput: List<Exception> = ArrayList()
    val component =
        TestErrorBoundary.create(lithoViewRule.context)
            .errorOutput(errorOutput)
            .child(Column.create(lithoViewRule.context).child(crashingComponent).build())
            .build()
    lithoViewRule.setRoot(component).attachToWindow().measure().layout()
    caller.updateStateWithTransition()
    backgroundLayoutLooperRule.runToEndOfTasksSync()
    assertThat(errorOutput).hasSize(1)
    assertThat(errorOutput[0])
        .isInstanceOf(RuntimeException::class.java)
        .hasMessage("onUpdateStateWithTransition crash")
  }

  @Test
  fun testOnCreateLayoutWithSizeSpecCrashWithTestErrorBoundary() {
    val crashingComponent =
        TestCrasherOnCreateLayoutWithSizeSpec.create(lithoViewRule.context)
            .widthPx(1)
            .heightPx(1)
            .build()
    val errorOutput: List<Exception> = ArrayList()
    val component =
        TestErrorBoundary.create(lithoViewRule.context)
            .errorOutput(errorOutput)
            .child(crashingComponent)
            .build()
    lithoViewRule.setRoot(component).attachToWindow().measure().layout()
    assertThat(errorOutput).hasSize(1)
    assertThat(errorOutput[0])
        .isInstanceOf(RuntimeException::class.java)
        .hasMessage("onCreateLayoutWithSizeSpec crash")
  }

  @Test
  fun testOnEventVisibleCrashWithTestErrorBoundary() {
    val caller = TestCrashFromEachLayoutLifecycleMethodSpec.Caller()
    val crashingComponent =
        TestCrashFromEachLayoutLifecycleMethod.create(lithoViewRule.context)
            .crashFromStep(LifecycleStep.ON_EVENT_VISIBLE)
            .caller(caller)
            .widthPx(10)
            .heightPx(5)
            .build()
    val errorOutput: List<Exception> = ArrayList()
    val component =
        TestErrorBoundary.create(lithoViewRule.context)
            .errorOutput(errorOutput)
            .child(crashingComponent)
            .build()
    lithoViewRule.setRoot(component).attachToWindow().measure().layout()
    lithoViewRule.lithoView.notifyVisibleBoundsChanged(Rect(0, 0, 20, 20), true)
    assertThat(errorOutput).hasSize(1)
    assertThat(errorOutput[0]).isInstanceOf(LithoMetadataExceptionWrapper::class.java)
    assertThat(errorOutput[0].message).contains("onEventVisible crash")
    assertThat(errorOutput[0].message)
        .contains(
            "layout_stack:", "TestCrashFromEachLayoutLifecycleMethod", "->", "TestErrorBoundary")
  }

  @Test
  fun testOnEventInvisibleCrashWithTestErrorBoundary() {
    val caller = TestCrashFromEachLayoutLifecycleMethodSpec.Caller()
    val crashingComponent =
        TestCrashFromEachLayoutLifecycleMethod.create(lithoViewRule.context)
            .crashFromStep(LifecycleStep.ON_EVENT_INVISIBLE)
            .caller(caller)
            .widthPx(10)
            .heightPx(5)
            .marginPx(YogaEdge.TOP, 5)
            .build()
    val errorOutput: List<Exception> = ArrayList()
    val component =
        TestErrorBoundary.create(lithoViewRule.context)
            .errorOutput(errorOutput)
            .child(crashingComponent)
            .build()
    lithoViewRule
        .setRoot(component)
        .attachToWindow()
        .setSizeSpecs(
            SizeSpec.makeSizeSpec(10, SizeSpec.EXACTLY),
            SizeSpec.makeSizeSpec(10, SizeSpec.EXACTLY))
        .measure()
        .layout()
    lithoViewRule.lithoView.notifyVisibleBoundsChanged(Rect(0, 0, 10, 5), true)
    assertThat(errorOutput).hasSize(1)
    assertThat(errorOutput[0]).isInstanceOf(LithoMetadataExceptionWrapper::class.java)
    assertThat(errorOutput[0].message).contains("onEventInvisible crash")
    assertThat(errorOutput[0].message)
        .contains(
            "layout_stack:", "TestCrashFromEachLayoutLifecycleMethod", "->", "TestErrorBoundary")
  }

  @Test
  fun testOnFocusedEventVisibleCrashWithTestErrorBoundary() {
    crashingScenarioLayoutSectionHelper(
        LifecycleStep.ON_FOCUSED_EVENT_VISIBLE, "onFocusedEventVisible crash", false, true)
  }

  @Test
  fun testOnFullImpressionVisibleEventCrashWithTestErrorBoundary() {
    crashingScenarioLayoutSectionHelper(
        LifecycleStep.ON_FULL_IMPRESSION_VISIBLE_EVENT,
        "onFullImpressionVisible crash",
        false,
        true)
  }

  @Test
  fun testOnVisibilityChangedCrashWithTestErrorBoundary() {
    crashingScenarioLayoutSectionHelper(
        LifecycleStep.ON_VISIBILITY_CHANGED, "onVisibilityChanged crash", false, true)
  }

  @Test
  fun testOnDetachedCrashWithTestErrorBoundary() {
    val crashingComponent =
        TestCrashFromEachLayoutLifecycleMethod.create(lithoViewRule.context)
            .crashFromStep(LifecycleStep.ON_DETACHED)
            .build()
    val errorOutput: List<Exception> = ArrayList()
    val component =
        TestErrorBoundary.create(lithoViewRule.context)
            .errorOutput(errorOutput)
            .child(crashingComponent)
            .build()
    lithoViewRule.setRoot(component).attachToWindow().measure().layout()
    lithoViewRule.release()
    assertThat(errorOutput).hasSize(1)
    assertThat(errorOutput[0]).isInstanceOf(RuntimeException::class.java)
    assertThat(errorOutput[0].message).contains("onDetached crash")
    assertThat(errorOutput[0].message)
        .contains(
            "layout_stack:", "TestCrashFromEachLayoutLifecycleMethod", "->", "TestErrorBoundary")
  }

  @Test
  fun testOnTriggerCrashWithTestErrorBoundary() {
    val triggerHandle = Handle()
    val crashingComponent =
        TestCrashFromEachLayoutLifecycleMethod.create(lithoViewRule.context)
            .crashFromStep(LifecycleStep.ON_TRIGGER)
            .handle(triggerHandle)
            .build()
    val errorOutput: List<Exception> = ArrayList()
    val component =
        TestErrorBoundary.create(lithoViewRule.context)
            .errorOutput(errorOutput)
            .child(crashingComponent)
            .build()
    lithoViewRule.setRoot(component).attachToWindow().measure().layout()
    val bazObject = Any()

    // We need to use a ComponentContext with a ComponentTree on it
    TestCrashFromEachLayoutLifecycleMethod.triggerTestEvent(
        lithoViewRule.componentTree.context, triggerHandle, bazObject)
    assertThat(errorOutput).hasSize(1)
    assertThat(errorOutput[0]).isInstanceOf(RuntimeException::class.java)
    assertThat(errorOutput[0].message).contains("onTrigger crash")
    assertThat(errorOutput[0].message)
        .contains(
            "layout_stack:", "TestCrashFromEachLayoutLifecycleMethod", "->", "TestErrorBoundary")
  }

  @Test
  fun testOnLoadStyleCrashWithTestErrorBoundary() {
    // note: this test has a slightly different flow than the rest, since
    // TestCrashFromEachLayoutLifecycleMethod.create(c, 0, android.R.style.Animation) crashes at
    // component initialization and not at layout, so we need to wrap it in another spec that will
    // rethrow from onCreateLayout()
    val crashingComponent = CrashFromLayoutFromStyle.create(lithoViewRule.context).build()
    val errorOutput: List<Exception> = ArrayList()
    val component =
        TestErrorBoundary.create(lithoViewRule.context)
            .errorOutput(errorOutput)
            .child(crashingComponent)
            .build()
    lithoViewRule.setRoot(component).attachToWindow().measure().layout()
    assertThat(errorOutput).hasSize(1)
    assertThat(errorOutput[0])
        .isInstanceOf(RuntimeException::class.java)
        .hasMessage("onLoadStyle crash")
  }

  @Test
  fun testOnBindDynamicValueCrashWithTestErrorBoundary() {
    val dynamicStringProp = DynamicValue("dynamic_prop_test")
    val crashingComponent =
        DynamicPropCrasher.create(lithoViewRule.context).someStringProp(dynamicStringProp).build()
    val errorOutput: List<Exception> = ArrayList()
    val component =
        TestErrorBoundary.create(lithoViewRule.context)
            .errorOutput(errorOutput)
            .child(crashingComponent)
            .build()
    lithoViewRule.setRoot(component).attachToWindow().measure().layout()
    dynamicStringProp.set("change_dynamic_prop_test")
    assertThat(errorOutput).hasSize(1)
    assertThat(errorOutput[0]).isInstanceOf(LithoMetadataExceptionWrapper::class.java)
    assertThat(errorOutput[0].message).contains("onBindDynamicValue crash")
    assertThat(errorOutput[0].message)
        .contains("layout_stack:", "DynamicPropCrasher", "->", "TestErrorBoundary")
  }

  @Test
  fun testOnMountCrashWithTestErrorBoundary() {
    crashingScenarioMountHelper(LifecycleStep.ON_MOUNT, "Crashed on ON_MOUNT", false, true)
  }

  @Test
  fun testOnUnMountCrashWithTestErrorBoundary() {
    crashingScenarioMountHelper(LifecycleStep.ON_UNMOUNT, "Crashed on ON_UNMOUNT", true, true)
  }

  @Test
  fun testOnBindCrashWithTestErrorBoundary() {
    crashingScenarioMountHelper(LifecycleStep.ON_BIND, "Crashed on ON_BIND", false, true)
  }

  @Test
  fun testOnUnBindCrashWithTestErrorBoundary() {
    crashingScenarioMountHelper(LifecycleStep.ON_UNBIND, "Crashed on ON_UNBIND", true, true)
  }

  @Test
  fun testOnPrepareCrashWithTestErrorBoundary() {
    crashingScenarioMountHelper(LifecycleStep.ON_PREPARE, "Crashed on ON_PREPARE", false, false)
  }

  @Test
  fun testOnMeasureCrashWithTestErrorBoundary() {
    crashingScenarioMountHelper(LifecycleStep.ON_MEASURE, "Crashed on ON_MEASURE", false, true)
  }

  @Test
  fun testOnBoundsDefinedCrashWithTestErrorBoundary() {
    crashingScenarioMountHelper(
        LifecycleStep.ON_BOUNDS_DEFINED, "Crashed on ON_BOUNDS_DEFINED", false, false)
  }

  @Test
  fun testOnCreateMountContentCrashWithTestErrorBoundary() {
    // TODO(T85975360): add onError coverage and remove expected exception
    // RuntimeException we throw is wrapped, so we need to expect that one
    expectedException.expect(LithoMetadataExceptionWrapper::class.java)
    expectedException.expectMessage("Crashed on ON_CREATE_MOUNT_CONTENT")
    crashingScenarioMountHelper(
        LifecycleStep.ON_CREATE_MOUNT_CONTENT, "Crashed on ON_CREATE_MOUNT_CONTENT", false, true)
  }

  @Test
  fun testOnCreateMountContentPoolCrashWithTestErrorBoundary() {
    crashingScenarioMountHelper(
        LifecycleStep.ON_CREATE_MOUNT_CONTENT_POOL,
        "Crashed on ON_CREATE_MOUNT_CONTENT_POOL",
        false,
        true)
  }

  @Test
  fun testShouldUpdateCrashWithTestErrorBoundary() {
    val crashingComponent =
        CrashingMountable.create(lithoViewRule.context)
            .someStringProp("someString")
            .lifecycle(LifecycleStep.SHOULD_UPDATE)
            .build()
    val errorOutput: List<Exception> = ArrayList()
    val component =
        TestErrorBoundary.create(lithoViewRule.context)
            .errorOutput(errorOutput)
            .child(crashingComponent)
            .build()
    lithoViewRule.attachToWindow().setRoot(component).measure().layout()
    val crashingComponent2: Component =
        CrashingMountable.create(lithoViewRule.context)
            .someStringProp("someString2")
            .lifecycle(LifecycleStep.SHOULD_UPDATE)
            .build()
    val component2: Component =
        TestErrorBoundary.create(lithoViewRule.context)
            .errorOutput(errorOutput)
            .child(crashingComponent2)
            .build()
    lithoViewRule.setRoot(component2).measure().layout()
    assertThat(errorOutput).hasSize(1)
    assertThat(errorOutput[0])
        .isInstanceOf(MountPhaseException::class.java)
        .hasMessage("Crashed on SHOULD_UPDATE")
  }

  private fun crashingScenarioMountHelper(
      crashFromStep: LifecycleStep,
      expectedMessage: String,
      unmountAfter: Boolean,
      expectHierarchy: Boolean
  ) {
    val crashingComponent =
        CrashingMountable.create(lithoViewRule.context)
            .someStringProp("someString")
            .lifecycle(crashFromStep)
            .build()
    val errorOutput: List<Exception> = ArrayList()
    val component =
        TestErrorBoundary.create(lithoViewRule.context)
            .errorOutput(errorOutput)
            .child(crashingComponent)
            .build()
    lithoViewRule.attachToWindow().setRoot(component).measure().layout()

    // The crashing mountable component should not get added to
    // the RenderTree if the error is thrown before mount.
    if (crashFromStep == LifecycleStep.ON_PREPARE ||
        crashFromStep == LifecycleStep.ON_MEASURE ||
        crashFromStep == LifecycleStep.ON_BOUNDS_DEFINED) {
      val count = lithoViewRule.lithoView.mountDelegateTarget.renderUnitCount
      for (i in 0 until count) {
        val item = lithoViewRule.lithoView.mountDelegateTarget.getMountItemAt(i)
        item?.renderTreeNode?.let {
          assertThat(LithoRenderUnit.getRenderUnit(it).component)
              .isNotInstanceOf(CrashingMountable::class.java)
        }
      }
    }

    if (unmountAfter) {
      lithoViewRule.lithoView.unmountAllItems()
    }
    assertThat(errorOutput).hasSize(1)
    if (expectHierarchy) {
      assertThat(errorOutput[0]).isInstanceOf(LithoMetadataExceptionWrapper::class.java)
      assertThat(errorOutput[0].message).contains(expectedMessage)
      assertThat(errorOutput[0].message)
          .contains("layout_stack:", "CrashingMountable", "->", "TestErrorBoundary")
    } else {
      assertThat(errorOutput[0])
          .isInstanceOf(MountPhaseException::class.java)
          .hasMessage(expectedMessage)
    }
  }

  private fun crashingScenarioLayoutHelper(
      crashFromStep: LifecycleStep,
      expectedMessage: String,
      expectHierarchy: Boolean
  ) {
    val crashingComponent =
        TestCrashFromEachLayoutLifecycleMethod.create(lithoViewRule.context)
            .crashFromStep(crashFromStep)
            .build()
    val errorOutput: List<Exception> = ArrayList()
    val component =
        TestErrorBoundary.create(lithoViewRule.context)
            .errorOutput(errorOutput)
            .child(crashingComponent)
            .build()
    lithoViewRule.setRoot(component).attachToWindow().measure().layout()
    assertThat(errorOutput).hasSize(1)
    if (expectHierarchy) {
      assertThat(errorOutput[0]).isInstanceOf(LithoMetadataExceptionWrapper::class.java)
      assertThat(errorOutput[0].message).contains(expectedMessage)
      assertThat(errorOutput[0].message)
          .contains(
              "layout_stack:", "TestCrashFromEachLayoutLifecycleMethod", "->", "TestErrorBoundary")
    } else {
      assertThat(errorOutput[0])
          .isInstanceOf(RuntimeException::class.java)
          .hasMessage(expectedMessage)
    }
  }

  private fun crashingScenarioLayoutSectionHelper(
      crashFromStep: LifecycleStep,
      expectedMessage: String,
      releaseAfter: Boolean,
      expectHierarchy: Boolean
  ) {
    val crashingComponent =
        TestCrashFromEachLayoutLifecycleMethod.create(lithoViewRule.context)
            .crashFromStep(crashFromStep)
            .build()
    val errorOutput: List<Exception> = ArrayList()
    val component =
        TestErrorBoundary.create(lithoViewRule.context)
            .heightPx(100)
            .errorOutput(errorOutput)
            .child(crashingComponent)
            .build()
    val rcc =
        RecyclerCollectionComponent.create(lithoViewRule.context)
            .recyclerConfiguration(ListRecyclerConfiguration.create().build())
            .section(
                SingleComponentSection.create(SectionContext(lithoViewRule.context))
                    .component(component)
                    .build())
            .build()
    lithoViewRule.setRoot(rcc).setSizeSpecs(exactly(100), exactly(100))
    lithoViewRule.attachToWindow().measure().layout()
    if (releaseAfter) {
      lithoViewRule.release()
    }
    assertThat(errorOutput).hasSize(1)
    if (expectHierarchy) {
      assertThat(errorOutput[0]).isInstanceOf(LithoMetadataExceptionWrapper::class.java)
      assertThat(errorOutput[0].message).contains(expectedMessage)
      assertThat(errorOutput[0].message)
          .contains(
              "layout_stack:", "TestCrashFromEachLayoutLifecycleMethod", "->", "TestErrorBoundary")
    } else {
      assertThat(errorOutput[0]).isInstanceOf(RuntimeException::class.java)
      assertThat(errorOutput[0].message).contains(expectedMessage)
    }
  }

  @Test
  fun testOnCreateLayoutCrashWithKotlinErrorBoundary() {
    lateinit var stateRef: AtomicReference<List<Exception>>

    val crashingComponent =
        TestCrashFromEachLayoutLifecycleMethod.create(lithoViewRule.context)
            .crashFromStep(LifecycleStep.ON_CREATE_LAYOUT)
            .build()

    class UseErrorComponent : KComponent() {
      override fun ComponentScope.render(): Component? {
        val errorState = useState { listOf<Exception>() }
        stateRef = AtomicReference(errorState.value)
        useErrorBoundary { exception: Exception ->
          errorState.update { prevErrors -> prevErrors + listOf(exception) }
          assertThat(exception.message).contains("onCreateLayout crash")
        }
        return if (errorState.value.isEmpty()) crashingComponent else Text("error caught")
      }
    }

    lithoViewRule.render { UseErrorComponent() }
    lithoViewRule.idle()

    val errorList = stateRef.get()
    assertThat(errorList.size).isEqualTo(1)
    assertThat(errorList[0].message).isEqualTo("onCreateLayout crash")
  }

  @Test
  fun testOnCreateLayoutWithSizeSpecCrashWithKotlinErrorBoundary() {
    lateinit var stateRef: AtomicReference<List<Exception>>

    val crashingComponent =
        TestCrasherOnCreateLayoutWithSizeSpec.create(lithoViewRule.context).build()

    class UseErrorComponent : KComponent() {
      override fun ComponentScope.render(): Component? {
        val errorState = useState { listOf<Exception>() }
        stateRef = AtomicReference(errorState.value)
        useErrorBoundary { exception: Exception ->
          errorState.update { prevErrors -> prevErrors + listOf(exception) }
          assertThat(exception.message).contains("onCreateLayoutWithSizeSpec crash")
        }
        return if (errorState.value.isEmpty()) crashingComponent else Text("error caught")
      }
    }

    lithoViewRule.render { UseErrorComponent() }
    lithoViewRule.idle()

    val errorList = stateRef.get()
    assertThat(errorList.size).isEqualTo(2)
    errorList.forEach { error ->
      assertThat(error.message).isEqualTo("onCreateLayoutWithSizeSpec crash")
    }
  }
}


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

package com.facebook.litho

import android.content.Context
import android.graphics.Color
import android.graphics.Rect
import android.graphics.drawable.ColorDrawable
import android.graphics.drawable.Drawable
import android.util.SparseArray
import android.view.MotionEvent
import android.view.View
import androidx.collection.SparseArrayCompat
import androidx.test.core.app.ApplicationProvider
import com.facebook.litho.testing.LegacyLithoViewRule
import com.facebook.litho.testing.TestViewComponent
import com.facebook.litho.testing.Whitebox
import com.facebook.litho.testing.helper.ComponentTestHelper
import com.facebook.litho.testing.testrunner.LithoTestRunner
import com.facebook.litho.widget.SimpleMountSpecTester
import com.facebook.litho.widget.Text
import com.facebook.litho.widget.TextInput
import com.facebook.rendercore.MountItem
import com.facebook.yoga.YogaEdge
import org.assertj.core.api.Assertions.assertThat
import org.junit.Before
import org.junit.Ignore
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.ArgumentMatchers
import org.mockito.Mockito
import org.mockito.kotlin.any
import org.mockito.kotlin.eq
import org.mockito.kotlin.mock
import org.mockito.kotlin.never
import org.mockito.kotlin.reset
import org.mockito.kotlin.times
import org.mockito.kotlin.verify
import org.mockito.kotlin.whenever

/** Tests [ComponentHost] */
@RunWith(LithoTestRunner::class)
class ComponentHostTest {

  private lateinit var viewGroupHost: Component
  private lateinit var host: TestableComponentHost
  private lateinit var drawableComponent: Component
  private lateinit var viewComponent: Component
  private lateinit var context: ComponentContext

  @JvmField @Rule val legacyLithoViewRule = LegacyLithoViewRule()

  @Before
  fun setup() {
    context = legacyLithoViewRule.context
    viewComponent = TestViewComponent.create(context).key(viewComponentKey).build()
    drawableComponent = SimpleMountSpecTester.create(context).key(drawableComponentKey).build()
    host = TestableComponentHost(context)
    viewGroupHost = HostComponent.create()
  }

  private val dummyMotionEvent =
      MotionEvent.obtain(100L, 100L, MotionEvent.ACTION_DOWN, 50f, 50f, 0)

  @Test
  fun testInvalidations() {
    assertThat(host.invalidationCount).isEqualTo(0)
    assertThat(host.invalidationRect).isNull()
    val d1 = ColorDrawable()
    d1.setBounds(0, 0, 1, 1)
    val mountItem1 = mount(0, d1)
    assertThat(host.invalidationCount).isEqualTo(1)
    assertThat(host.invalidationRect).isEqualTo(d1.bounds)
    val d2 = ColorDrawable()
    d2.setBounds(0, 0, 2, 2)
    val mountItem2 = mount(1, d2)
    assertThat(host.invalidationCount).isEqualTo(2)
    assertThat(host.invalidationRect).isEqualTo(d2.bounds)
    val v1 = View(context.androidContext)
    val v1Bounds = Rect(0, 0, 10, 10)
    v1.measure(
        View.MeasureSpec.makeMeasureSpec(v1Bounds.width(), View.MeasureSpec.EXACTLY),
        View.MeasureSpec.makeMeasureSpec(v1Bounds.height(), View.MeasureSpec.EXACTLY))
    v1.layout(v1Bounds.left, v1Bounds.top, v1Bounds.right, v1Bounds.bottom)
    val mountItem3 = mount(2, v1)
    assertThat(host.invalidationCount).isEqualTo(3)
    assertThat(host.invalidationRect).isEqualTo(v1Bounds)
    unmount(0, mountItem1)
    assertThat(host.invalidationCount).isEqualTo(4)
    assertThat(host.invalidationRect).isEqualTo(d1.bounds)
    unmount(1, mountItem2)
    assertThat(host.invalidationCount).isEqualTo(5)
    assertThat(host.invalidationRect).isEqualTo(d2.bounds)
    unmount(2, mountItem3)
    assertThat(host.invalidationCount).isEqualTo(6)
    assertThat(host.invalidationRect).isEqualTo(v1Bounds)
  }

  @Test
  fun testCallbacks() {
    val d = ColorDrawable()
    assertThat(d.callback).isNull()
    val mountItem = mount(0, d)
    assertThat(d.callback).isEqualTo(host)
    unmount(0, mountItem)
    assertThat(d.callback).isNull()
  }

  @Test
  fun testGetMountItemCount() {
    assertThat(host.mountItemCount).isEqualTo(0)
    val mountItem1 = mount(0, ColorDrawable())
    assertThat(host.mountItemCount).isEqualTo(1)
    mount(1, ColorDrawable())
    assertThat(host.mountItemCount).isEqualTo(2)
    val mountItem3 = mount(2, View(context.androidContext))
    assertThat(host.mountItemCount).isEqualTo(3)
    unmount(0, mountItem1)
    assertThat(host.mountItemCount).isEqualTo(2)
    val mountItem4 = mount(1, ColorDrawable())
    assertThat(host.mountItemCount).isEqualTo(2)
    unmount(2, mountItem3)
    assertThat(host.mountItemCount).isEqualTo(1)
    unmount(1, mountItem4)
    assertThat(host.mountItemCount).isEqualTo(0)
  }

  @Test
  fun testGetMountItemAt() {
    assertThat(host.getMountItemAt(0)).isNull()
    assertThat(host.getMountItemAt(1)).isNull()
    assertThat(host.getMountItemAt(2)).isNull()
    val mountItem1 = mount(0, ColorDrawable())
    val mountItem2 = mount(1, View(context.androidContext))
    val mountItem3 = mount(5, ColorDrawable())
    assertThat(host.getMountItemAt(0)).isEqualTo(mountItem1)
    assertThat(host.getMountItemAt(1)).isEqualTo(mountItem2)
    assertThat(host.getMountItemAt(2)).isEqualTo(mountItem3)
    unmount(1, mountItem2)
    assertThat(host.getMountItemAt(0)).isEqualTo(mountItem1)
    assertThat(host.getMountItemAt(1)).isEqualTo(mountItem3)
    unmount(0, mountItem1)
    assertThat(host.getMountItemAt(0)).isEqualTo(mountItem3)
  }

  @Test
  fun testOnTouchWithTouchables() {
    assertThat(host.getMountItemAt(0)).isNull()
    assertThat(host.getMountItemAt(1)).isNull()
    assertThat(host.getMountItemAt(2)).isNull()

    // Touchables are traversed backwards as drawing order.
    // The n.4 is the first parsed, and returning false means the n.2 will be parsed too.
    val touchableDrawableOnItem2 = Mockito.spy(TouchableDrawable())
    val touchableDrawableOnItem4 = Mockito.spy(TouchableDrawable())
    whenever(touchableDrawableOnItem2.shouldHandleTouchEvent(dummyMotionEvent)).thenReturn(true)
    whenever(touchableDrawableOnItem4.shouldHandleTouchEvent(dummyMotionEvent)).thenReturn(false)
    val mountItem1 = mount(0, ColorDrawable())
    val mountItem2 = mount(1, touchableDrawableOnItem2)
    val mountItem3 = mount(2, View(context.androidContext))
    val mountItem4 = mount(5, touchableDrawableOnItem4)
    assertThat(host.getMountItemAt(0)).isEqualTo(mountItem1)
    assertThat(host.getMountItemAt(1)).isEqualTo(mountItem2)
    assertThat(host.getMountItemAt(2)).isEqualTo(mountItem3)
    assertThat(host.getMountItemAt(3)).isEqualTo(mountItem4)
    host.onTouchEvent(dummyMotionEvent)
    verify(touchableDrawableOnItem4, times(1)).shouldHandleTouchEvent(any())
    verify(touchableDrawableOnItem4, never()).onTouchEvent(any(), any())
    verify(touchableDrawableOnItem2, times(1)).shouldHandleTouchEvent(any())
    verify(touchableDrawableOnItem2, times(1)).onTouchEvent(any(), any())
  }

  @Test
  fun testOnTouchWithDisableTouchables() {
    assertThat(host.getMountItemAt(0)).isNull()
    assertThat(host.getMountItemAt(1)).isNull()
    assertThat(host.getMountItemAt(2)).isNull()
    val mountItem1 = mount(0, ColorDrawable())
    val mountItem2 = mount(1, TouchableDrawable(), LithoRenderUnit.LAYOUT_FLAG_DISABLE_TOUCHABLE)
    val mountItem3 = mount(2, View(context.androidContext))
    val mountItem4 = mount(4, Mockito.spy(TouchableDrawable()))
    val mountItem5 = mount(5, TouchableDrawable(), LithoRenderUnit.LAYOUT_FLAG_DISABLE_TOUCHABLE)
    val mountItem6 = mount(7, View(context.androidContext))
    val mountItem7 = mount(8, TouchableDrawable(), LithoRenderUnit.LAYOUT_FLAG_DISABLE_TOUCHABLE)
    assertThat(host.getMountItemAt(0)).isEqualTo(mountItem1)
    assertThat(host.getMountItemAt(1)).isEqualTo(mountItem2)
    assertThat(host.getMountItemAt(2)).isEqualTo(mountItem3)
    assertThat(host.getMountItemAt(3)).isEqualTo(mountItem4)
    assertThat(host.getMountItemAt(4)).isEqualTo(mountItem5)
    assertThat(host.getMountItemAt(5)).isEqualTo(mountItem6)
    assertThat(host.getMountItemAt(6)).isEqualTo(mountItem7)
    host.onTouchEvent(dummyMotionEvent)
    val touchableDrawable = mountItem4.content as TouchableDrawable
    verify(touchableDrawable, times(1)).shouldHandleTouchEvent(any())
    verify(touchableDrawable, times(1)).onTouchEvent(any(), any())
  }

  @Test
  fun testMoveItem() {
    val mountItem1 = mount(1, ColorDrawable())
    val mountItem2 = mount(2, View(context.androidContext))
    assertThat(host.mountItemCount).isEqualTo(2)
    assertThat(host.getMountItemAt(0)).isEqualTo(mountItem1)
    assertThat(host.getMountItemAt(1)).isEqualTo(mountItem2)
    host.moveItem(mountItem2, 2, 0)
    assertThat(host.mountItemCount).isEqualTo(2)
    assertThat(host.getMountItemAt(0)).isEqualTo(mountItem2)
    assertThat(host.getMountItemAt(1)).isEqualTo(mountItem1)
    host.moveItem(mountItem2, 0, 1)
    assertThat(host.mountItemCount).isEqualTo(1)
    assertThat(host.getMountItemAt(0)).isEqualTo(mountItem2)
    host.moveItem(mountItem2, 1, 0)
    assertThat(host.mountItemCount).isEqualTo(2)
    assertThat(host.getMountItemAt(0)).isEqualTo(mountItem1)
    assertThat(host.getMountItemAt(1)).isEqualTo(mountItem2)
  }

  @Test
  fun testMoveItemWithoutTouchables() {
    val d1 = ColorDrawable(Color.BLACK)
    val mountItem1 = mount(1, d1)
    val d2 = ColorDrawable(Color.BLACK)
    val mountItem2 = mount(2, d2)
    assertThat(drawableItemsSize).isEqualTo(2)
    assertThat(getDrawableMountItemAt(0)).isEqualTo(mountItem1)
    assertThat(getDrawableMountItemAt(1)).isEqualTo(mountItem2)
    host.moveItem(mountItem2, 2, 0)

    // There are no Touchable Drawables so this call should return false and not crash.
    assertThat(host.onTouchEvent(MotionEvent.obtain(0, 0, 0, 0f, 0f, 0))).isFalse
  }

  @Test
  fun testDrawableStateChangedOnDrawables() {
    val d1 = mock<ColorDrawable>()
    whenever(d1.bounds).thenReturn(Rect())
    whenever(d1.isStateful).thenReturn(false)
    val mountItem1 = mount(0, d1)
    verify(d1, never()).state = (any<IntArray>())
    unmount(0, mountItem1)
    val d2 = mock<ColorDrawable>()
    whenever(d2.bounds).thenReturn(Rect())
    whenever(d2.isStateful).thenReturn(true)
    mount(0, d2, LithoRenderUnit.LAYOUT_FLAG_DUPLICATE_PARENT_STATE)
    verify(d2, times(1)).state = eq(host.drawableState)
    host.isSelected = true
    verify(d2, times(1)).state = eq(host.drawableState)
  }

  @Test
  fun testMoveTouchExpansionItem() {
    val view = mock<View>()
    whenever(view.context).thenReturn(ApplicationProvider.getApplicationContext())
    val mountItem = mountTouchExpansionItem(0, view)
    host.moveItem(mountItem, 0, 1)
    unmount(1, mountItem)
  }

  @Test
  @Ignore("This test is failing since we stopped running tests on API 16")
  fun testRecursiveTouchExpansionItemShouldNotAddTouchDelegate() {
    val mountItem = mountTouchExpansionItem(0, host)
    assertThat(host.touchExpansionDelegate).isNull()
    unmount(0, mountItem)
  }

  @Test
  fun testDuplicateParentStateOnViews() {
    val v1 = mock<View>()
    mount(0, v1)
    verify(v1, never()).isDuplicateParentStateEnabled = ArgumentMatchers.anyBoolean()
    val v2 = mock<View>()
    mount(1, v2, LithoRenderUnit.LAYOUT_FLAG_DUPLICATE_PARENT_STATE)
    verify(v2, times(1)).isDuplicateParentStateEnabled = eq(true)
    reset(v2)
    unmount(1, v2, LithoRenderUnit.LAYOUT_FLAG_DUPLICATE_PARENT_STATE)
    verify(v2, times(1)).isDuplicateParentStateEnabled = eq(false)
  }

  @Test
  fun testJumpDrawablesToCurrentState() {
    host.jumpDrawablesToCurrentState()
    val d1 = mock<ColorDrawable>()
    whenever(d1.bounds).thenReturn(Rect())
    mount(0, d1)
    val d2 = mock<ColorDrawable>()
    whenever(d2.bounds).thenReturn(Rect())
    mount(1, d2)
    val v1 = mock<View>()
    mount(2, v1)
    host.jumpDrawablesToCurrentState()
    verify(d1, times(1)).jumpToCurrentState()
    verify(d2, times(1)).jumpToCurrentState()
  }

  @Test
  fun testSetVisibility() {
    val d1 = mock<ColorDrawable>()
    whenever(d1.bounds).thenReturn(Rect())
    mount(0, d1)
    val d2 = mock<ColorDrawable>()
    whenever(d2.bounds).thenReturn(Rect())
    mount(1, d2)
    val v1 = mock<View>()
    mount(2, v1)
    host.visibility = View.GONE
    host.visibility = View.INVISIBLE
    host.visibility = View.VISIBLE
    verify(d1, times(2)).setVisible(eq(true), eq(false))
    verify(d1, times(2)).setVisible(eq(false), eq(false))
    verify(d2, times(2)).setVisible(eq(true), eq(false))
    verify(d2, times(2)).setVisible(eq(false), eq(false))
    verify(v1, never()).visibility = ArgumentMatchers.anyInt()
  }

  @Test
  fun testGetDrawables() {
    val d1 = ColorDrawable()
    val mountItem1 = mount(0, d1)
    val d2 = ColorDrawable()
    mount(1, d2)
    val mountItem3 = mount(2, View(context.androidContext))
    var drawables = host.drawables
    assertThat(drawables).hasSize(2)
    assertThat(drawables[0]).isEqualTo(d1)
    assertThat(drawables[1]).isEqualTo(d2)
    unmount(0, mountItem1)
    drawables = host.drawables
    assertThat(drawables).hasSize(1)
    assertThat(drawables[0]).isEqualTo(d2)
    unmount(2, mountItem3)
    drawables = host.drawables
    assertThat(drawables).hasSize(1)
    assertThat(drawables[0]).isEqualTo(d2)
  }

  @Test
  fun testViewTag() {
    val rootComponent =
        Column.create(context)
            .child(SimpleMountSpecTester.create(context).viewTag("test_tag"))
            .build()
    val lithoView = ComponentTestHelper.mountComponent(context, rootComponent, false, false)
    assertThat(lithoView.findViewWithTag("test_tag") as View).isNotNull
  }

  @Test
  fun testViewTags() {
    assertThat(host.getTag(1)).isNull()
    assertThat(host.getTag(2)).isNull()
    val value1 = Any()
    val value2 = Any()
    val viewTags = SparseArray<Any>()
    viewTags.put(1, value1)
    viewTags.put(2, value2)
    host.setViewTags(viewTags)
    assertThat(host.getTag(1)).isEqualTo(value1)
    assertThat(host.getTag(2)).isEqualTo(value2)
    host.setViewTags(null)
    assertThat(host.getTag(1)).isNull()
    assertThat(host.getTag(2)).isNull()
  }

  @Test
  fun testComponentLongClickListener() {
    assertThat(host.componentLongClickListener).isNull()
    val listener = ComponentLongClickListener()
    host.componentLongClickListener = listener
    assertThat(host.componentLongClickListener).isEqualTo(listener)
    host.componentLongClickListener = null
    assertThat(host.componentLongClickListener).isNull()
  }

  @Test
  fun testComponentFocusChangeListener() {
    assertThat(host.componentFocusChangeListener).isNull()
    val listener = ComponentFocusChangeListener()
    host.componentFocusChangeListener = listener
    assertThat(host.componentFocusChangeListener).isEqualTo(listener)
    host.componentFocusChangeListener = null
    assertThat(host.componentFocusChangeListener).isNull()
  }

  @Test
  fun testComponentTouchListener() {
    assertThat(host.componentTouchListener).isNull()
    val listener = ComponentTouchListener()
    host.componentTouchListener = listener
    assertThat(host.componentTouchListener).isEqualTo(listener)
    host.componentTouchListener = null
    assertThat(host.componentTouchListener).isNull()
  }

  @Test
  fun testGetContentDescriptions() {
    val hostContentDescription = "hostContentDescription"
    host.contentDescription = hostContentDescription
    val drawableContentDescription = "drawableContentDescription"
    val mountItem0 = mount(0, ColorDrawable(), 0, drawableContentDescription)
    val viewContentDescription = "viewContentDescription"
    mount(1, mock<View>(), 0, viewContentDescription)
    assertThat(host.contentDescriptions).contains(hostContentDescription)
    assertThat(host.contentDescriptions).contains(drawableContentDescription)
    assertThat(host.contentDescriptions).doesNotContain(viewContentDescription)
    unmount(0, mountItem0)
    assertThat(host.contentDescriptions).contains(hostContentDescription)
    assertThat(host.contentDescriptions).doesNotContain(drawableContentDescription)
    assertThat(host.contentDescriptions).doesNotContain(viewContentDescription)
  }

  @Test
  fun testGetChildDrawingOrder() {
    val v1 = View(context.androidContext)
    mount(2, v1)
    val v2 = View(context.androidContext)
    val mountItem2 = mount(0, v2)
    assertThat(host.getChildDrawingOrder(host.childCount, 0)).isEqualTo(1)
    assertThat(host.getChildDrawingOrder(host.childCount, 1)).isEqualTo(0)
    val v3 = ComponentHost(context)
    val mountItem3 = mount(1, v3)
    assertThat(host.getChildDrawingOrder(host.childCount, 0)).isEqualTo(1)
    assertThat(host.getChildDrawingOrder(host.childCount, 1)).isEqualTo(2)
    assertThat(host.getChildDrawingOrder(host.childCount, 2)).isEqualTo(0)
    host.unmount(1, mountItem3)
    assertThat(host.getChildDrawingOrder(host.childCount, 0)).isEqualTo(1)
    assertThat(host.getChildDrawingOrder(host.childCount, 1)).isEqualTo(0)
    host.mount(1, mountItem3)
    assertThat(host.getChildDrawingOrder(host.childCount, 0)).isEqualTo(1)
    assertThat(host.getChildDrawingOrder(host.childCount, 1)).isEqualTo(2)
    assertThat(host.getChildDrawingOrder(host.childCount, 2)).isEqualTo(0)
    host.unmount(0, mountItem2)
    assertThat(host.getChildDrawingOrder(host.childCount, 0)).isEqualTo(1)
    assertThat(host.getChildDrawingOrder(host.childCount, 1)).isEqualTo(0)
    host.moveItem(mountItem3, 1, 3)
    assertThat(host.getChildDrawingOrder(host.childCount, 0)).isEqualTo(0)
    assertThat(host.getChildDrawingOrder(host.childCount, 1)).isEqualTo(1)
  }

  @Test
  fun testTemporaryChildClippingDisabling() {
    val componentHost = ComponentHost(context)
    assertThat(componentHost.clipChildren).isTrue

    // 1. Testing disable > restore
    componentHost.temporaryDisableChildClipping()
    assertThat(componentHost.clipChildren).isFalse
    componentHost.restoreChildClipping()
    assertThat(componentHost.clipChildren).isTrue

    // 2. Testing disable > set > restore
    componentHost.temporaryDisableChildClipping()
    componentHost.clipChildren = true
    assertThat(componentHost.clipChildren).isFalse
    componentHost.restoreChildClipping()
    assertThat(componentHost.clipChildren).isTrue

    // 3. Same as 1 (disable > restore), but starting with clipping tuned off initially
    componentHost.clipChildren = false
    componentHost.temporaryDisableChildClipping()
    assertThat(componentHost.clipChildren).isFalse
    componentHost.restoreChildClipping()
    assertThat(componentHost.clipChildren).isFalse

    // 4. Same as 2 (disable > set > restore), with reverted values
    componentHost.temporaryDisableChildClipping()
    componentHost.clipChildren = true
    assertThat(componentHost.clipChildren).isFalse
    componentHost.restoreChildClipping()
    assertThat(componentHost.clipChildren).isTrue
  }

  @Test
  fun testDisappearingItems() {
    val v1 = View(context.androidContext)
    mount(0, v1)
    val d1 = ColorDrawable(Color.BLACK)
    val mountItem1 = mount(1, d1)
    val v2 = View(context.androidContext)
    val mountItem2 = mount(2, v2)
    val d2 = ColorDrawable(Color.BLACK)
    mount(3, d2)
    assertThat(host.mountItemCount).isEqualTo(4)
    assertThat(host.childCount).isEqualTo(2)
    assertThat(host.hasDisappearingItems()).isFalse
    host.startUnmountDisappearingItem(1, mountItem1)
    assertThat(host.mountItemCount).isEqualTo(3)
    assertThat(host.childCount).isEqualTo(2)
    assertThat(host.hasDisappearingItems()).isTrue
    var wasRemoved: Boolean = host.finaliseDisappearingItem(mountItem1)
    assertThat(wasRemoved).isTrue
    assertThat(host.mountItemCount).isEqualTo(3)
    assertThat(host.childCount).isEqualTo(2)
    assertThat(host.hasDisappearingItems()).isFalse
    host.startUnmountDisappearingItem(2, mountItem2)
    assertThat(host.mountItemCount).isEqualTo(2)
    assertThat(host.childCount).isEqualTo(2)
    assertThat(host.hasDisappearingItems()).isTrue
    wasRemoved = host.finaliseDisappearingItem(mountItem2)
    assertThat(wasRemoved).isTrue
    assertThat(host.mountItemCount).isEqualTo(2)
    assertThat(host.childCount).isEqualTo(1)
    assertThat(host.hasDisappearingItems()).isFalse
  }

  @Ignore("t19681984")
  @Test
  fun testDisappearingItemDrawingOrder() {
    val v1 = View(context.androidContext)
    mount(5, v1)
    val v2 = View(context.androidContext)
    mount(2, v2)
    val v3 = View(context.androidContext)
    val mountItem3 = mount(4, v3)
    val v4 = View(context.androidContext)
    val mountItem4 = mount(0, v4)
    assertThat(host.getChildDrawingOrder(host.childCount, 0)).isEqualTo(3)
    assertThat(host.getChildDrawingOrder(host.childCount, 1)).isEqualTo(1)
    assertThat(host.getChildDrawingOrder(host.childCount, 2)).isEqualTo(2)
    assertThat(host.getChildDrawingOrder(host.childCount, 3)).isEqualTo(0)
    assertThat(host.mountItemCount).isEqualTo(4)
    assertThat(host.childCount).isEqualTo(4)

    // mountItem3 started disappearing
    host.startUnmountDisappearingItem(4, mountItem3)
    assertThat(host.mountItemCount).isEqualTo(3)
    assertThat(host.childCount).isEqualTo(4)
    assertThat(host.getChildDrawingOrder(host.childCount, 0)).isEqualTo(3)
    assertThat(host.getChildDrawingOrder(host.childCount, 1)).isEqualTo(1)
    assertThat(host.getChildDrawingOrder(host.childCount, 2)).isEqualTo(0)
    assertThat(host.getChildDrawingOrder(host.childCount, 3)).isEqualTo(2)

    // mountItem4 started disappearing
    host.startUnmountDisappearingItem(0, mountItem4)
    assertThat(host.mountItemCount).isEqualTo(2)
    assertThat(host.childCount).isEqualTo(4)
    assertThat(host.getChildDrawingOrder(host.childCount, 0)).isEqualTo(1)
    assertThat(host.getChildDrawingOrder(host.childCount, 1)).isEqualTo(0)
    assertThat(host.getChildDrawingOrder(host.childCount, 2)).isEqualTo(3)
    assertThat(host.getChildDrawingOrder(host.childCount, 3)).isEqualTo(2)

    // mountItem4 finished disappearing
    var wasRemoved: Boolean = host.finaliseDisappearingItem(mountItem4)
    assertThat(wasRemoved).isTrue
    assertThat(host.mountItemCount).isEqualTo(2)
    assertThat(host.childCount).isEqualTo(3)
    assertThat(host.getChildDrawingOrder(host.childCount, 0)).isEqualTo(1)
    assertThat(host.getChildDrawingOrder(host.childCount, 1)).isEqualTo(0)
    assertThat(host.getChildDrawingOrder(host.childCount, 2)).isEqualTo(2)

    // mountItem3 finished disappearing
    wasRemoved = host.finaliseDisappearingItem(mountItem3)
    assertThat(wasRemoved).isTrue
    assertThat(host.mountItemCount).isEqualTo(2)
    assertThat(host.childCount).isEqualTo(2)
    assertThat(host.getChildDrawingOrder(host.childCount, 0)).isEqualTo(1)
    assertThat(host.getChildDrawingOrder(host.childCount, 1)).isEqualTo(0)
  }

  @Test
  fun testDrawableItemsSize() {
    assertThat(drawableItemsSize).isEqualTo(0)
    assertThat(drawableItemsSize).isEqualTo(0)
    val d1 = ColorDrawable(Color.BLACK)
    val m1 = mount(0, d1)
    assertThat(drawableItemsSize).isEqualTo(1)
    val d2 = ColorDrawable(Color.BLACK)
    mount(1, d2)
    assertThat(drawableItemsSize).isEqualTo(2)
    unmount(0, m1)
    assertThat(drawableItemsSize).isEqualTo(1)
    val d3 = ColorDrawable(Color.BLACK)
    val m3 = mount(1, d3)
    assertThat(drawableItemsSize).isEqualTo(1)
    unmount(1, m3)
    assertThat(drawableItemsSize).isEqualTo(0)
  }

  @Test
  fun testGetDrawableMountItem() {
    val d1 = ColorDrawable(Color.BLACK)
    val mountItem1 = mount(0, d1)
    val d2 = ColorDrawable(Color.BLACK)
    val mountItem2 = mount(1, d2)
    val d3 = ColorDrawable(Color.BLACK)
    val mountItem3 = mount(5, d3)
    assertThat(getDrawableMountItemAt(0)).isEqualTo(mountItem1)
    assertThat(getDrawableMountItemAt(1)).isEqualTo(mountItem2)
    assertThat(getDrawableMountItemAt(2)).isEqualTo(mountItem3)
  }

  @Test
  fun testGetLinkedDrawableForAnimation() {
    val d1 = ColorDrawable()
    val mountItem1 = mount(0, d1, LithoRenderUnit.LAYOUT_FLAG_MATCH_HOST_BOUNDS)
    val d2 = ColorDrawable()
    val mountItem2 = mount(1, d2)
    val d3 = ColorDrawable()
    val mountItem3 = mount(2, d3, LithoRenderUnit.LAYOUT_FLAG_MATCH_HOST_BOUNDS)
    var drawables = host.linkedDrawablesForAnimation
    assertThat(drawables).hasSize(2)
    assertThat(drawables).contains(d1, d3)
    unmount(0, mountItem1)
    drawables = host.linkedDrawablesForAnimation
    assertThat(drawables).hasSize(1)
    assertThat(drawables).contains(d3)
    unmount(1, mountItem2)
    drawables = host.drawables
    assertThat(drawables).hasSize(1)
    assertThat(drawables).contains(d3)
  }

  @Test
  fun whenTouchExpansionSetOnComponent_shouldHaveTouchDelegateOnParentOfHostView() {
    val component =
        Column.create(context)
            .paddingPx(YogaEdge.ALL, 15)
            .widthPx(100)
            .heightPx(100)
            .child(
                Text.create(context)
                    .text("hello-world")
                    .touchExpansionPx(YogaEdge.ALL, 5)
                    .clickHandler(NoOpEventHandler.getNoOpEventHandler()))
            .child(
                Row.create(context)
                    .wrapInView()
                    .child(
                        TextInput.create(context)
                            .touchExpansionPx(YogaEdge.ALL, 5)
                            .clickHandler(NoOpEventHandler.getNoOpEventHandler())))
            .build()
    legacyLithoViewRule.setRoot(component).attachToWindow().measure().layout()
    val delegate = legacyLithoViewRule.lithoView.touchExpansionDelegate
    assertThat(delegate).describedAs("Should be not null for the host view of the Text").isNotNull
    val child1 = legacyLithoViewRule.lithoView.getChildAt(0)
    assertThat(child1).isInstanceOf(ComponentHost::class.java)
    assertThat((child1 as ComponentHost).touchExpansionDelegate)
        .describedAs("should be null for the Text")
        .isNull()
    val child2 = legacyLithoViewRule.lithoView.getChildAt(1)
    assertThat(child2).isInstanceOf(ComponentHost::class.java)
    assertThat((child2 as ComponentHost).touchExpansionDelegate)
        .describedAs("Should be not null for the host view of the Text Input")
        .isNotNull
  }

  private val drawableItemsSize: Int
    get() {
      val drawableItems =
          Whitebox.getInternalState<SparseArrayCompat<*>>(host, "mDrawableMountItems")
      return Whitebox.invokeMethod(drawableItems, "size")
    }

  private fun getDrawableMountItemAt(index: Int): MountItem {
    val drawableItems = Whitebox.getInternalState<SparseArrayCompat<*>>(host, "mDrawableMountItems")
    return Whitebox.invokeMethod(drawableItems, "valueAt", index)
  }

  private fun mount(
      index: Int,
      content: Any,
      flags: Int = 0,
      contentDescription: CharSequence? = null
  ): MountItem {
    val nodeInfo = NodeInfo()
    nodeInfo.contentDescription = contentDescription
    val mountItem =
        MountItemTestHelper.create(
            if (content is Drawable) drawableComponent else viewComponent,
            content,
            nodeInfo,
            if (content is Drawable) content.bounds else null,
            flags,
            View.IMPORTANT_FOR_ACCESSIBILITY_AUTO)
    host.mount(index, mountItem, if (content is Drawable) content.bounds else Rect())
    return mountItem
  }

  private fun unmount(index: Int, view: View, flags: Int): MountItem {
    val nodeInfo = NodeInfo()
    val mountItem =
        MountItemTestHelper.create(
            viewComponent, view, nodeInfo, null, flags, View.IMPORTANT_FOR_ACCESSIBILITY_AUTO)
    host.unmount(index, mountItem)
    return mountItem
  }

  private fun mountTouchExpansionItem(index: Int, content: Any): MountItem {
    val result = mock<LithoLayoutResult>()
    val node = mock<LithoNode>()
    whenever(result.node).thenReturn(node)
    whenever(node.hasTouchExpansion()).thenReturn(true)
    val viewMountItem =
        MountItemTestHelper.create(
            viewComponent,
            content,
            null,
            Rect(1, 1, 1, 1),
            0,
            View.IMPORTANT_FOR_ACCESSIBILITY_AUTO)
    host.mount(index, viewMountItem, Rect())
    return viewMountItem
  }

  private fun unmount(index: Int, mountItem: MountItem) {
    host.unmount(index, mountItem)
  }

  private class TestableComponentHost : ComponentHost {
    var invalidationCount = 0
      private set

    var invalidationRect: Rect? = null
      private set

    var focusRequestCount = 0
      private set

    constructor(context: ComponentContext?) : super(context)

    constructor(context: Context?) : super(context)

    override fun invalidate(dirty: Rect) {
      super.invalidate(dirty)
      trackInvalidation(dirty.left, dirty.top, dirty.right, dirty.bottom)
    }

    override fun invalidate(l: Int, t: Int, r: Int, b: Int) {
      super.invalidate(l, t, r, b)
      trackInvalidation(l, t, r, b)
    }

    override fun invalidate() {
      super.invalidate()
      trackInvalidation(0, 0, width, height)
    }

    override fun onViewAdded(child: View) {
      super.onViewAdded(child)
      trackInvalidation(child.left, child.top, child.right, child.bottom)
    }

    override fun onViewRemoved(child: View) {
      super.onViewRemoved(child)
      trackInvalidation(child.left, child.top, child.right, child.bottom)
    }

    override fun getDescendantFocusability(): Int {
      focusRequestCount++
      return super.getDescendantFocusability()
    }

    private fun trackInvalidation(l: Int, t: Int, r: Int, b: Int) {
      invalidationCount++
      invalidationRect = Rect().apply { this[l, t, r] = b }
    }
  }

  /** Open to allow mocking. */
  private open class TouchableDrawable : ColorDrawable(), Touchable {
    override fun onTouchEvent(event: MotionEvent, host: View): Boolean = true

    override fun shouldHandleTouchEvent(event: MotionEvent): Boolean = true
  }

  companion object {
    private const val drawableComponentKey = "drawable_key"
    private const val viewComponentKey = "view_key"
  }
}


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

package com.facebook.litho

import android.content.Context
import android.view.View
import com.facebook.litho.testing.testrunner.LithoTestRunner
import com.facebook.rendercore.MountItemsPool
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.robolectric.RuntimeEnvironment
import org.robolectric.annotation.LooperMode

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

  private val context: Context = RuntimeEnvironment.application

  @Before
  fun setup() {
    MountItemsPool.clear()
  }

  @After
  fun cleanup() {
    MountItemsPool.clear()
  }

  @Test
  fun testMaybePreallocateContent() {
    val component = PooledComponent("PooledComponent")

    // Preallocate content more times than the defined pool size
    for (i in 0 until POOL_SIZE * 2) {
      MountItemsPool.maybePreallocateContent(context, component)
    }

    // Ensure onCreateMountContent was called POOL_SIZE times.
    assertThat(component.onCreateMountContentCount).isEqualTo(POOL_SIZE)

    // Acquire POOL_SIZE contents
    val objects = arrayOfNulls<Any>(POOL_SIZE + 1)
    for (i in 0 until POOL_SIZE) {
      objects[i] = MountItemsPool.acquireMountContent(context, component)
    }

    // Ensure onCreateMountContent wasn't called an additional time.
    assertThat(component.onCreateMountContentCount).isEqualTo(POOL_SIZE)

    // Acquire one more content
    objects[POOL_SIZE] = MountItemsPool.acquireMountContent(context, component)

    // Ensure onCreateMountContent was called an additional time.
    assertThat(component.onCreateMountContentCount).isEqualTo(POOL_SIZE + 1)

    // Release all acquired content
    for (`object` in objects) {
      MountItemsPool.release(context, component, `object`)
    }

    // Reacquire POOL_SIZE contents
    for (i in 0 until POOL_SIZE) {
      MountItemsPool.acquireMountContent(context, component)
    }

    // Ensure onCreateMountContent wasn't called an additional time.
    assertThat(component.onCreateMountContentCount).isEqualTo(POOL_SIZE + 1)
  }

  private class PooledComponent(simpleName: String) : SpecGeneratedComponent(simpleName) {
    var onCreateMountContentCount: Int = 0
      private set

    override fun poolSize(): Int = POOL_SIZE

    public override fun onCreateMountContent(context: Context): Any {
      onCreateMountContentCount++
      return View(context)
    }
  }

  companion object {
    private const val POOL_SIZE = 2
  }
}


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

package com.facebook.litho

import android.content.Context
import androidx.test.core.app.ApplicationProvider
import com.facebook.litho.SizeSpec.makeSizeSpec
import com.facebook.litho.config.ComponentsConfiguration
import com.facebook.litho.sections.SectionContext
import com.facebook.litho.sections.common.SingleComponentSection
import com.facebook.litho.sections.widget.ListRecyclerConfiguration
import com.facebook.litho.sections.widget.RecyclerBinderConfiguration
import com.facebook.litho.sections.widget.RecyclerCollectionComponent
import com.facebook.litho.testing.LegacyLithoViewRule
import com.facebook.litho.testing.testrunner.LithoTestRunner
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 ComponentsConfigurationTest {

  @JvmField @Rule val legacyLithoViewRule = LegacyLithoViewRule()

  private val componentContext =
      ComponentContext(ApplicationProvider.getApplicationContext<Context>())

  private val defaultBuilder = ComponentsConfiguration.getDefaultComponentsConfigurationBuilder()

  @Test
  fun testSetFlagThroughComponentConfigToComponentTree() {
    ComponentsConfiguration.setDefaultComponentsConfigurationBuilder(
        ComponentsConfiguration.create().useCancelableLayoutFutures(true))
    val componentTree =
        ComponentTree.create(componentContext)
            .componentsConfiguration(ComponentsConfiguration.getDefaultComponentsConfiguration())
            .build()
    val componentsConfiguration = componentTree.context.mLithoConfiguration.mComponentsConfiguration

    assertThat(componentsConfiguration.useCancelableLayoutFutures).isTrue
    ComponentsConfiguration.setDefaultComponentsConfigurationBuilder(defaultBuilder)
  }

  @Test
  fun testSetFlagThroughComponentConfigToComponentTreeWithRecyclerCollectionComponent() {
    ComponentsConfiguration.setDefaultComponentsConfigurationBuilder(
        ComponentsConfiguration.create().useCancelableLayoutFutures(true))
    val recyclerBinderConfiguration =
        RecyclerBinderConfiguration.create()
            .componentsConfiguration(ComponentsConfiguration.getDefaultComponentsConfiguration())
            .build()
    legacyLithoViewRule
        .setRoot(
            RecyclerCollectionComponent.create(componentContext)
                .recyclerConfiguration(
                    ListRecyclerConfiguration.create()
                        .recyclerBinderConfiguration(recyclerBinderConfiguration)
                        .build())
                .section(
                    SingleComponentSection.create(SectionContext(componentContext))
                        .component(
                            Row.create(componentContext)
                                .viewTag("rv_row")
                                .heightDip(100f)
                                .widthDip(100f))
                        .build())
                .build())
        .setSizeSpecs(makeSizeSpec(10, SizeSpec.EXACTLY), makeSizeSpec(5, SizeSpec.EXACTLY))
    legacyLithoViewRule.attachToWindow().measure().layout().setSizeSpecs(10, 10)
    val childView = legacyLithoViewRule.lithoView.findViewWithTag("rv_row") as LithoView?
    assertThat(childView).isNotNull
    val componentsConfiguration =
        childView?.componentTree?.context?.mLithoConfiguration?.mComponentsConfiguration
    assertThat(componentsConfiguration?.useCancelableLayoutFutures).isTrue
    ComponentsConfiguration.setDefaultComponentsConfigurationBuilder(defaultBuilder)
  }

  @Test
  fun testOverrideDefaultBuilder() {
    ComponentsConfiguration.setDefaultComponentsConfigurationBuilder(
        ComponentsConfiguration.create().useCancelableLayoutFutures(true))
    assertThat(
            ComponentsConfiguration.getDefaultComponentsConfiguration().useCancelableLayoutFutures)
        .isTrue
    ComponentsConfiguration.setDefaultComponentsConfigurationBuilder(
        ComponentsConfiguration.create().useCancelableLayoutFutures(false))
    assertThat(
            ComponentsConfiguration.getDefaultComponentsConfiguration().useCancelableLayoutFutures)
        .isFalse
    ComponentsConfiguration.setDefaultComponentsConfigurationBuilder(defaultBuilder)
  }
}


/*
 * 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 com.facebook.litho.widget.SimpleMountSpecTester
import com.facebook.litho.widget.Text
import org.assertj.core.api.Assertions.assertThat
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith

@RunWith(LithoTestRunner::class)
class ComponentShallowCopyTest {

  private lateinit var context: ComponentContext

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

  @Test
  fun testShallowCopyCachedLayoutSameLayoutState() {
    context =
        ComponentContextUtils.withComponentTree(context, ComponentTree.create(context).build())
    val c = ComponentContext(context)
    val resolveStateContext = c.setRenderStateContextForTests()
    val resultCache = resolveStateContext.cache
    val component = SimpleMountSpecTester.create(context).build()
    component.measure(c, 100, 100, Size())
    assertThat(resultCache.getCachedResult(component)).isNotNull
    val copyComponent = component.makeShallowCopy()
    assertThat(resultCache.getCachedResult(copyComponent)).isNotNull
    assertThat(resultCache.getCachedResult(component))
        .isEqualTo(resultCache.getCachedResult(copyComponent))
  }

  @Test
  fun testShallowCopyCachedLayoutOtherLayoutStateCacheLayoutState() {
    context =
        ComponentContextUtils.withComponentTree(context, ComponentTree.create(context).build())
    val c1 = ComponentContextUtils.withComponentTree(context, ComponentTree.create(context).build())
    val c2 = ComponentContextUtils.withComponentTree(context, ComponentTree.create(context).build())
    val rsc1 = c1.setRenderStateContextForTests()
    val rsc2 = c2.setRenderStateContextForTests()
    val resultCache1 = rsc1.cache
    val resultCache2 = rsc2.cache
    val component = SimpleMountSpecTester.create(context).build()
    component.measure(c1, 100, 100, Size())
    assertThat(resultCache1.getCachedResult(component)).isNotNull
    val copyComponent = component.makeShallowCopy()
    assertThat(resultCache2.getCachedResult(copyComponent)).isNull()
  }

  @Test
  fun shallowCopy_withManualKey_preservesManualKeyInformation() {
    val component = Text.create(context).text("test").key("manual_key").build() as Component
    assertThat(component.key).isEqualTo("manual_key")
    assertThat(component.hasManualKey()).isTrue
    val shallowCopy = component.makeShallowCopy()
    assertThat(shallowCopy.key).isEqualTo("manual_key")
    assertThat(shallowCopy.hasManualKey()).isTrue
  }
}


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

package com.facebook.litho

import android.util.Pair
import com.facebook.litho.testing.logging.TestComponentsReporter
import com.facebook.litho.testing.testrunner.LithoTestRunner
import com.facebook.rendercore.LogLevel
import java.lang.RuntimeException
import org.assertj.core.api.Assertions.assertThat
import org.assertj.core.api.ThrowableAssert
import org.junit.After
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith

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

  private lateinit var reporter: TestComponentsReporter

  @Before
  fun setup() {
    reporter = TestComponentsReporter()
    ComponentsReporter.provide(reporter)
  }

  @After
  fun tearDown() {
    ComponentsReporter.provide(DefaultComponentsReporter())
  }

  @Test
  fun testEmitFatalMessage() {
    val throwable =
        ThrowableAssert.catchThrowable {
          ComponentsReporter.emitMessage(ComponentsReporter.LogLevel.FATAL, CATEGORY_KEY, FATAL_MSG)
          assertThat(reporter.loggedMessages.size).isEqualTo(1)
          assertThat(reporter.loggedMessages).contains(Pair(LogLevel.FATAL, FATAL_MSG))
        }
    assertThat(throwable).isInstanceOf(RuntimeException::class.java)
    assertThat(throwable.message).isEqualTo(FATAL_MSG)
  }

  @Test
  fun testEmitErrorMessage() {
    ComponentsReporter.emitMessage(ComponentsReporter.LogLevel.ERROR, CATEGORY_KEY, ERROR_MSG)
    assertThat(reporter.loggedMessages.size).isEqualTo(1)
    assertThat(reporter.loggedMessages).contains(Pair(LogLevel.ERROR, ERROR_MSG))
  }

  @Test
  fun testEmitWarningMessage() {
    ComponentsReporter.emitMessage(ComponentsReporter.LogLevel.WARNING, CATEGORY_KEY, WARNING_MSG)
    assertThat(reporter.loggedMessages.size).isEqualTo(1)
    assertThat(reporter.loggedMessages).contains(Pair(LogLevel.WARNING, WARNING_MSG))
  }

  companion object {
    private const val FATAL_MSG = "fatal"
    private const val ERROR_MSG = "error"
    private const val WARNING_MSG = "warning"
    private const val CATEGORY_KEY = "categoryKey"
  }
}