/*
 * 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.widget

import android.content.Context
import androidx.recyclerview.widget.RecyclerView
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout.OnRefreshListener

/** A test [SectionsRecyclerView] class used for unit testing. */
class TestSectionsRecyclerView(context: Context, view: RecyclerView?) :
    SectionsRecyclerView(context, view) {

  private var listener: OnRefreshListener? = null
  var removeCallbackCount: Int = 0
    private set

  var postCount: Int = 0
    private set

  private var lastRefreshingValue: Boolean = false
  var setRefreshingValuesCount: Int = 0
    private set

  override fun setRefreshing(refreshing: Boolean) {
    super.setRefreshing(refreshing)
    setRefreshingValuesCount++
    lastRefreshingValue = refreshing
  }

  override fun removeCallbacks(action: Runnable): Boolean {
    removeCallbackCount++
    return true
  }

  override fun post(action: Runnable): Boolean {
    postCount++
    return true
  }

  override fun isRefreshing(): Boolean = lastRefreshingValue

  override fun setOnRefreshListener(listener: OnRefreshListener?) {
    this.listener = listener
    super.setOnRefreshListener(listener)
  }

  fun getOnRefreshListener(): OnRefreshListener? = listener

  /** Used for resetting the fields of [TestSectionsRecyclerView] */
  fun reset() {
    setRefreshingValuesCount = 0
    removeCallbackCount = 0
    postCount = 0
  }
}


/*
 * 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.widget

import com.facebook.litho.Component
import com.facebook.litho.ComponentContext
import com.facebook.litho.annotations.LayoutSpec
import com.facebook.litho.annotations.OnCreateLayout
import com.facebook.litho.annotations.Prop

@LayoutSpec
internal object TestWrapperComponentSpec {
  @JvmStatic
  @OnCreateLayout
  fun onCreateLayout(c: ComponentContext, @Prop delegate: Component): Component = delegate
}


/*
 * 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.widget

import android.content.Context
import android.widget.TextView
import androidx.annotation.UiThread
import com.facebook.litho.ComponentContext
import com.facebook.litho.ComponentLayout
import com.facebook.litho.Size
import com.facebook.litho.StateValue
import com.facebook.litho.annotations.MountSpec
import com.facebook.litho.annotations.OnCreateInitialState
import com.facebook.litho.annotations.OnCreateMountContent
import com.facebook.litho.annotations.OnMeasure
import com.facebook.litho.annotations.OnMount
import com.facebook.litho.annotations.OnUnmount
import com.facebook.litho.annotations.OnUpdateState
import com.facebook.litho.annotations.Prop
import com.facebook.litho.annotations.State

@MountSpec(isPureRender = true)
internal object TextViewCounterSpec {

  @JvmStatic
  @OnCreateInitialState
  fun onCreateInitialState(c: ComponentContext, count: StateValue<Int>) {
    count.set(0)
  }

  @JvmStatic
  @OnMeasure
  fun onMeasure(
      c: ComponentContext,
      layout: ComponentLayout,
      widthSpec: Int,
      heightSpec: Int,
      size: Size,
      @Prop viewWidth: Int,
      @Prop viewHeight: Int
  ) {
    size.width = viewWidth
    size.height = viewHeight
  }

  @JvmStatic
  @UiThread
  @OnCreateMountContent
  fun onCreateMountContent(c: Context): TextView = TextView(c)

  @JvmStatic
  @OnMount
  fun onMount(c: ComponentContext, view: TextView, @State count: Int) {
    view.text = count.toString()
    view.setOnClickListener { TextViewCounter.incrementSync(c) }
  }

  @JvmStatic
  @OnUnmount
  fun onUnmount(c: ComponentContext, view: TextView) {
    view.text = 0.toString()
    view.setOnClickListener(null)
  }

  @JvmStatic
  @OnUpdateState
  fun increment(count: StateValue<Int>) {
    count.set(count.get()!! + 1)
  }
}


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

package com.facebook.litho.widget

import com.facebook.litho.Component
import com.facebook.litho.ComponentContext
import com.facebook.litho.annotations.LayoutSpec
import com.facebook.litho.annotations.OnCreateLayout

@LayoutSpec
internal object ThrowExceptionGrandChildTesterSpec {
  @JvmStatic
  @OnCreateLayout
  fun onCreateLayout(c: ComponentContext): Component {
    throw RuntimeException("Exception from the ThrowExceptionGrandChildTesterSpec")
  }
}


/*
 * 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.widget

import android.graphics.Color
import com.facebook.litho.Column
import com.facebook.litho.Component
import com.facebook.litho.ComponentContext
import com.facebook.litho.Diff
import com.facebook.litho.Row
import com.facebook.litho.StateValue
import com.facebook.litho.Transition
import com.facebook.litho.TransitionEndEvent
import com.facebook.litho.animation.AnimatedProperties
import com.facebook.litho.animation.AnimatedProperty
import com.facebook.litho.annotations.FromEvent
import com.facebook.litho.annotations.LayoutSpec
import com.facebook.litho.annotations.OnCreateLayout
import com.facebook.litho.annotations.OnCreateTransition
import com.facebook.litho.annotations.OnEvent
import com.facebook.litho.annotations.OnUpdateState
import com.facebook.litho.annotations.Prop
import com.facebook.litho.annotations.PropDefault
import com.facebook.litho.annotations.State
import com.facebook.yoga.YogaAlign
import java.lang.IllegalStateException

@LayoutSpec
object TransitionEndCallbackTestComponentSpec {

  private const val TRANSITION_KEY = "TRANSITION_KEY"
  const val ANIM_X = "ANIM_X"
  const val ANIM_ALPHA = "ANIM_ALPHA"
  const val ANIM_DISAPPEAR = "ANIM_DISAPPEAR"

  @PropDefault const val testPropForRenderData: String = "default"

  @JvmStatic
  @OnCreateLayout
  fun onCreateLayout(
      c: ComponentContext,
      @Prop caller: Caller,
      @Prop(optional = true) testPropForRenderData: String,
      @State state: Boolean
  ): Component? {
    caller.set(c)
    return when (caller.testType) {
      TestType.SAME_KEY -> getSameKeyTestComponent(c, state)
      TestType.DISAPPEAR -> getDisappearComponent(c, state)
      else -> null
    }
  }

  private fun getDisappearComponent(c: ComponentContext, state: Boolean): Component =
      Column.create(c)
          .child(Row.create(c).heightDip(50f).widthDip(50f).backgroundColor(Color.YELLOW))
          .child(
              if (!state)
                  Row.create(c)
                      .heightDip(50f)
                      .widthDip(50f)
                      .backgroundColor(Color.RED)
                      .transitionKey(TRANSITION_KEY)
                      .key(TRANSITION_KEY)
              else null)
          .build()

  private fun getSameKeyTestComponent(c: ComponentContext, state: Boolean): Component =
      Column.create(c)
          .child(
              Column.create(c)
                  .alignItems(if (!state) YogaAlign.FLEX_START else YogaAlign.FLEX_END)
                  .child(
                      Row.create(c)
                          .heightDip(50f)
                          .widthDip(50f)
                          .backgroundColor(Color.RED)
                          .alpha(if (!state) 1.0f else 0.2f)
                          .transitionKey(TRANSITION_KEY)))
          .build()

  @JvmStatic
  @OnEvent(TransitionEndEvent::class)
  fun onTransitionEnd(
      c: ComponentContext,
      @FromEvent transitionKey: String,
      @FromEvent property: AnimatedProperty,
      @State state: Boolean,
      @Prop caller: Caller
  ) {
    when (caller.testType) {
      TestType.SAME_KEY ->
          if (property === AnimatedProperties.X) {
            caller.transitionEndMessage = ANIM_X
          } else {
            caller.transitionEndMessage = ANIM_ALPHA
          }
      TestType.DISAPPEAR -> caller.transitionEndMessage = if (!state) ANIM_DISAPPEAR else ""
      null -> Unit
    }
  }

  @JvmStatic
  @OnUpdateState
  fun updateState(state: StateValue<Boolean>) {
    state.set(!state.get()!!)
  }

  @JvmStatic
  @OnCreateTransition
  fun onCreateTransition(
      c: ComponentContext,
      @Prop caller: Caller,
      @Prop(optional = true) testPropForRenderData: Diff<String>,
      @State state: Boolean,
  ): Transition? {
    if (state && testPropForRenderData.previous == null) {
      throw IllegalStateException("previous render must be set for subsequent layouts but was null")
    }
    val transition =
        when (caller.testType) {
          TestType.SAME_KEY ->
              Transition.parallel(
                  Transition.create(TRANSITION_KEY)
                      .animate(AnimatedProperties.ALPHA)
                      .animator(Transition.timing(100))
                      .transitionEndHandler(TransitionEndCallbackTestComponent.onTransitionEnd(c)),
                  Transition.create(TRANSITION_KEY)
                      .animate(AnimatedProperties.X)
                      .animator(Transition.timing(200))
                      .transitionEndHandler(TransitionEndCallbackTestComponent.onTransitionEnd(c)))
          TestType.DISAPPEAR ->
              Transition.create(Transition.TransitionKeyType.LOCAL, TRANSITION_KEY)
                  .animate(AnimatedProperties.SCALE)
                  .appearFrom(0f)
                  .disappearTo(0f)
                  .transitionEndHandler(TransitionEndCallbackTestComponent.onTransitionEnd(c))
          else -> null
        }
    return transition
  }

  enum class TestType {
    SAME_KEY,
    DISAPPEAR
  }

  class Caller {
    var c: ComponentContext? = null
    var transitionEndMessage: String = ""
    var testType: TestType? = null

    fun set(c: ComponentContext?) {
      this.c = c
    }

    fun toggle() {
      TransitionEndCallbackTestComponent.updateStateSync(c)
    }
  }
}


/*
 * 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.widget

import com.facebook.litho.Component
import com.facebook.litho.ComponentContext
import com.facebook.litho.annotations.LayoutSpec
import com.facebook.litho.annotations.OnCreateLayout

@LayoutSpec
object TreePropTestContainerComponentSpec {

  const val KEY = "TreePropTestContainerComponent"
  const val EXPECTED_GLOBAL_KEY =
      ("$${KEY},\$RootComponentWithTreeProps,\$Row,\$NestedTreeParentComponent,\$NestedTreeComponent,\$Row,\$NestedTreeChildComponent")

  @JvmStatic
  @OnCreateLayout
  fun onCreateLayout(c: ComponentContext): Component =
      RootComponentWithTreeProps.create(c).key("RootComponentWithTreeProps").build()

  @JvmStatic
  fun create(c: ComponentContext): TreePropTestContainerComponent =
      TreePropTestContainerComponent.create(c).key(KEY).build()
}


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

package com.facebook.litho.widget

import com.facebook.litho.Column
import com.facebook.litho.Component
import com.facebook.litho.ComponentContext
import com.facebook.litho.TestTriggerEvent
import com.facebook.litho.annotations.LayoutSpec
import com.facebook.litho.annotations.OnCreateLayout
import com.facebook.litho.annotations.OnTrigger
import com.facebook.litho.annotations.Prop

@LayoutSpec
object TriggerCallbackComponentSpec {

  @JvmStatic
  @OnCreateLayout
  fun onCreateLayout(c: ComponentContext, @Prop callback: TriggerListener): Component =
      Column.create(c).child(Text.create(c).text("hello world")).build()

  @JvmStatic
  @OnTrigger(TestTriggerEvent::class)
  fun doTrigger(c: ComponentContext, @Prop callback: TriggerListener) {
    callback.onTriggerCalled()
  }

  fun interface TriggerListener {
    fun onTriggerCalled()
  }
}


/*
 * 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.widget.events

import kotlin.jvm.JvmField

class EventWithGenericTypes<O, N : Number?, S : CharSequence?, UNUSED> {

  @JvmField var string: String? = null

  @JvmField var `object`: O? = null

  @JvmField var another: O? = null

  @JvmField var number: N? = null

  @JvmField var chars: S? = null
}


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

package com.facebook.litho.widget.events

import kotlin.jvm.JvmField

class EventWithoutAnnotation {

  @JvmField var count: Int = 0
  @JvmField var isDirty: Boolean = false
  @JvmField var message: String? = null

  constructor() {}

  constructor(count: Int, isDirty: Boolean, message: String?) {
    this.count = count
    this.isDirty = isDirty
    this.message = message
  }
}


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

package com.facebook.litho.widget.layoutstate.withoutdrawableoutput

import android.graphics.Color
import com.facebook.litho.Column
import com.facebook.litho.Component
import com.facebook.litho.ComponentContext
import com.facebook.litho.Row
import com.facebook.litho.annotations.LayoutSpec
import com.facebook.litho.annotations.OnCreateLayout
import com.facebook.litho.annotations.Prop
import com.facebook.litho.widget.Text

@LayoutSpec
internal object RootComponentSpec {
  @JvmStatic
  @OnCreateLayout
  fun onCreateLayout(c: ComponentContext, @Prop shouldWrapInView: Boolean): Component =
      Row.create(c)
          .child(
              (if (shouldWrapInView) Column.create(c).wrapInView() else Column.create(c))
                  .backgroundColor(Color.RED)
                  .foregroundColor(Color.GREEN)
                  .child(Text.create(c).text("Hello")))
          .child(
              (if (shouldWrapInView) Column.create(c).wrapInView() else Column.create(c))
                  .backgroundColor(Color.RED)
                  .foregroundColor(Color.GREEN)
                  .child(Text.create(c).text("world")))
          .build()
}


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

package com.facebook.litho.widget.treeprops

class SimpleTreeProp(val name: String)


/*
 * 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.Animator
import android.view.animation.Interpolator
import com.facebook.litho.testing.testrunner.LithoTestRunner
import java.util.concurrent.atomic.AtomicReference
import org.assertj.core.api.Assertions.assertThat
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.ArgumentMatchers
import org.mockito.kotlin.eq
import org.mockito.kotlin.mock
import org.mockito.kotlin.verify
import org.mockito.kotlin.whenever

@RunWith(LithoTestRunner::class)
class AnimationsTest {

  @Test
  fun animations_bind_changeInSourcePropagatesToBinding() {
    val source = DynamicValue(5f)
    val target = StateValue<DynamicValue<Float>>()
    Animations.bind(source).to(target)
    assertThat(target.get()!!.get()).isEqualTo(5f)
    source.set(2f)
    assertThat(target.get()!!.get()).isEqualTo(2f)
  }

  @Test
  fun animations_bindWithFromRange_rangeIsApplied() {
    val source = DynamicValue(0f)
    val target = StateValue<DynamicValue<Float>>()
    Animations.bind(source).inputRange(45f, 55f).to(target)
    assertThat(target.get()!!.get()).isEqualTo(0f)
    source.set(45f)
    assertThat(target.get()!!.get()).isEqualTo(0f)
    source.set(55f)
    assertThat(target.get()!!.get()).isEqualTo(1f)
    source.set(100f)
    assertThat(target.get()!!.get()).isEqualTo(1f)
  }

  @Test
  fun animations_bindWithToRange_rangeIsApplied() {
    val source = DynamicValue(0f)
    val target = StateValue<DynamicValue<Float>>()
    Animations.bind(source).outputRange(45f, 55f).to(target)
    assertThat(target.get()!!.get()).isEqualTo(45f)
    source.set(1f)
    assertThat(target.get()!!.get()).isEqualTo(55f)
    source.set(.5f)
    assertThat(target.get()!!.get()).isEqualTo(50f)
  }

  @Test
  fun animations_bindWithInterpolator_interpolatorIsApplied() {
    val mockInterpolator = mock<Interpolator>()
    whenever(mockInterpolator.getInterpolation(ArgumentMatchers.anyFloat())).thenReturn(1f)
    val source = DynamicValue(0f)
    val target = StateValue<DynamicValue<Float>>()
    Animations.bind(source).with(mockInterpolator).to(target)
    assertThat(target.get()!!.get()).isEqualTo(1f)
    verify(mockInterpolator).getInterpolation(eq(0f))
  }

  @Test
  fun animations_startAnimator_animatorStarted() {
    val `in` = DynamicValue(0f)
    val outState = StateValue<DynamicValue<Float>>()
    Animations.bind(`in`).outputRange(0f, 5f).to(outState)
    val animator = Animations.animate(`in`).to(1f).duration(2_000).start()
    assertThat(animator.duration).isEqualTo(2_000)
    animator.end()
    assertThat(outState.get()!!.get()).isEqualTo(5f)
  }

  @Test
  fun animations_startAnimatorAncCancelPrevious_previousIsCancelled() {
    val `in` = DynamicValue(0f)
    val outState = StateValue<DynamicValue<Float>>()
    Animations.bind(`in`).outputRange(0f, 5f).to(outState)
    val oldAnimator = mock<Animator>()
    val animatorRef = AtomicReference(oldAnimator)
    Animations.animate(`in`).to(1f).duration(2_000).startAndCancelPrevious(animatorRef)
    verify(oldAnimator).cancel()
    val newAnimator = animatorRef.get()
    assertThat(oldAnimator).isNotSameAs(newAnimator)
    assertThat(newAnimator.duration).isEqualTo(2_000)
    newAnimator.end()
    assertThat(outState.get()!!.get()).isEqualTo(5f)
  }

  @Test
  fun animations_bindFloatToInteger_propagates() {
    val source = DynamicValue(5f)
    val target = StateValue<DynamicValue<Int>>()
    Animations.bind(source).toInteger(target)
    assertThat(target.get()!!.get()).isEqualTo(5)
    source.set(2f)
    assertThat(target.get()!!.get()).isEqualTo(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.graphics.drawable.ColorDrawable
import android.view.View
import com.facebook.litho.config.ComponentsConfiguration
import com.facebook.litho.it.R
import com.facebook.litho.testing.LegacyLithoViewRule
import com.facebook.litho.testing.exactly
import com.facebook.litho.testing.testrunner.LithoTestRunner
import com.facebook.litho.testing.unspecified
import com.facebook.rendercore.testing.ViewAssertions
import com.facebook.rendercore.testing.match.MatchNode
import com.facebook.rendercore.testing.match.ViewMatchNode
import org.assertj.core.api.Assertions.assertThat
import org.junit.Ignore
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith

@RunWith(LithoTestRunner::class)
class ApplyStylesTest {

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

  @Test
  fun styles_withWidthHeightStyle_appliesWidthHeight() {
    legacyLithoViewRule
        .setSizeSpecs(unspecified(), unspecified())
        .setRoot(
            Column.create(legacyLithoViewRule.context, 0, R.style.ApplyStylesTest_WidthHeight)
                .build())
        .measure()
        .layout()
        .attachToWindow()
    assertThat(legacyLithoViewRule.lithoView.width).isEqualTo(37)
    assertThat(legacyLithoViewRule.lithoView.height).isEqualTo(100)
  }

  @Test
  fun styles_withMinWidthHeightStyle_appliesMinWidthHeight() {
    legacyLithoViewRule
        .setSizeSpecs(unspecified(), unspecified())
        .setRoot(
            Column.create(legacyLithoViewRule.context, 0, R.style.ApplyStylesTest_MinWidthHeight)
                .build())
        .measure()
        .layout()
        .attachToWindow()
    assertThat(legacyLithoViewRule.lithoView.width).isEqualTo(50)
    assertThat(legacyLithoViewRule.lithoView.height).isEqualTo(75)
  }

  @Test
  fun styles_withPaddingLeftTopRightBottomStyle_appliesPadding() {
    legacyLithoViewRule
        .setSizeSpecs(exactly(100), exactly(100))
        .setRoot(
            Row.create(
                    legacyLithoViewRule.context,
                    0,
                    R.style.ApplyStylesTest_PaddingLeftTopRightBottom)
                .child(Column.create(legacyLithoViewRule.context).flexGrow(1f).wrapInView())
                .build())
        .measure()
        .layout()
        .attachToWindow()
    ViewAssertions.assertThat(legacyLithoViewRule.lithoView)
        .matches(
            ViewMatchNode.forType(LithoView::class.java)
                .child(
                    ViewMatchNode.forType(View::class.java)
                        .bounds(10, 20, 100 - 10 - 30, 100 - 20 - 40)))
  }

  @Test
  fun styles_withPaddingAllStyle_appliesPadding() {
    legacyLithoViewRule
        .setSizeSpecs(exactly(100), exactly(100))
        .setRoot(
            Row.create(legacyLithoViewRule.context, 0, R.style.ApplyStylesTest_PaddingAll)
                .child(Column.create(legacyLithoViewRule.context).flexGrow(1f).wrapInView())
                .build())
        .measure()
        .layout()
        .attachToWindow()
    ViewAssertions.assertThat(legacyLithoViewRule.lithoView)
        .matches(
            ViewMatchNode.forType(LithoView::class.java)
                .child(
                    ViewMatchNode.forType(View::class.java)
                        .bounds(15, 15, 100 - 15 - 15, 100 - 15 - 15)))
  }

  @Ignore("T66670905")
  @Test
  fun styles_withPaddingStartEndStyle_appliesPadding() {
    legacyLithoViewRule
        .setSizeSpecs(exactly(100), exactly(100))
        .setRoot(
            Row.create(legacyLithoViewRule.context, 0, R.style.ApplyStylesTest_PaddingStartEnd)
                .child(Column.create(legacyLithoViewRule.context).flexGrow(1f).wrapInView())
                .build())
        .measure()
        .layout()
        .attachToWindow()
    ViewAssertions.assertThat(legacyLithoViewRule.lithoView)
        .matches(
            ViewMatchNode.forType(LithoView::class.java)
                .child(ViewMatchNode.forType(View::class.java).bounds(20, 0, 100 - 20 - 40, 100)))
  }

  @Test
  fun styles_withMarginLeftTopRightBottomStyle_appliesMargin() {
    legacyLithoViewRule
        .setSizeSpecs(exactly(100), exactly(100))
        .setRoot(
            Row.create(legacyLithoViewRule.context)
                .child(
                    Column.create(
                            legacyLithoViewRule.context,
                            0,
                            R.style.ApplyStylesTest_MarginLeftTopRightBottom)
                        .flexGrow(1f)
                        .wrapInView())
                .build())
        .measure()
        .layout()
        .attachToWindow()
    ViewAssertions.assertThat(legacyLithoViewRule.lithoView)
        .matches(
            ViewMatchNode.forType(LithoView::class.java)
                .child(
                    ViewMatchNode.forType(View::class.java)
                        .bounds(10, 20, 100 - 10 - 30, 100 - 20 - 40)))
  }

  @Test
  fun styles_withMarginAllStyle_appliesMargin() {
    legacyLithoViewRule
        .setSizeSpecs(exactly(100), exactly(100))
        .setRoot(
            Row.create(legacyLithoViewRule.context)
                .child(
                    Column.create(legacyLithoViewRule.context, 0, R.style.ApplyStylesTest_MarginAll)
                        .flexGrow(1f)
                        .wrapInView())
                .build())
        .measure()
        .layout()
        .attachToWindow()
    ViewAssertions.assertThat(legacyLithoViewRule.lithoView)
        .matches(
            ViewMatchNode.forType(LithoView::class.java)
                .child(
                    ViewMatchNode.forType(View::class.java)
                        .bounds(15, 15, 100 - 15 - 15, 100 - 15 - 15)))
  }

  @Ignore("T66670905")
  @Test
  fun styles_withMarginStartEndStyle_appliesMargin() {
    legacyLithoViewRule
        .setSizeSpecs(exactly(100), exactly(100))
        .setRoot(
            Row.create(legacyLithoViewRule.context)
                .child(
                    Column.create(
                            legacyLithoViewRule.context, 0, R.style.ApplyStylesTest_MarginStartEnd)
                        .flexGrow(1f)
                        .wrapInView())
                .build())
        .measure()
        .layout()
        .attachToWindow()
    ViewAssertions.assertThat(legacyLithoViewRule.lithoView)
        .matches(
            ViewMatchNode.forType(LithoView::class.java)
                .child(ViewMatchNode.forType(View::class.java).bounds(10, 0, 100 - 10 - 30, 100)))
  }

  @Test
  fun styles_withBackgroundForegroundStyle_appliesBackgroundAndForeground() {
    legacyLithoViewRule
        .setSizeSpecs(exactly(100), exactly(100))
        .setRoot(
            Row.create(legacyLithoViewRule.context)
                .wrapInView()
                .child(
                    Column.create(
                            legacyLithoViewRule.context,
                            0,
                            R.style.ApplyStylesTest_BackgroundForeground)
                        .flexGrow(1f)
                        .wrapInView())
                .build())
        .measure()
        .layout()
        .attachToWindow()
    val innerHost = legacyLithoViewRule.lithoView.getChildAt(0) as ComponentHost
    ViewAssertions.assertThat(innerHost)
        .matches(
            ViewMatchNode.forType(ComponentHost::class.java)
                .child(
                    ViewMatchNode.forType(ComponentHost::class.java)
                        .prop<MatchNode>(
                            "background", MatchNode.forType(ColorDrawable::class.java))))
  }

  @Test
  fun styles_withAccessibilityAndContentDescriptionStyle_appliesAccessibilityAndContentDescription() {
    legacyLithoViewRule
        .setSizeSpecs(exactly(100), exactly(100))
        .setRoot(
            Row.create(legacyLithoViewRule.context)
                .wrapInView()
                .child(
                    Column.create(
                            legacyLithoViewRule.context,
                            0,
                            R.style.ApplyStylesTest_AccessibilityAndContentDescription)
                        .flexGrow(1f)
                        .wrapInView())
                .build())
        .measure()
        .layout()
        .attachToWindow()
    val innerHost = legacyLithoViewRule.lithoView.getChildAt(0) as ComponentHost
    ViewAssertions.assertThat(innerHost)
        .matches(
            ViewMatchNode.forType(ComponentHost::class.java)
                .child(
                    ViewMatchNode.forType(ComponentHost::class.java)
                        .prop<String>("contentDescription", "Test Content Description")
                        .prop<Boolean>("isImportantForAccessibility", true)))
  }

  @Test
  fun styles_withDuplicateParentStateStyle_appliesDuplicateParentState() {
    legacyLithoViewRule
        .setSizeSpecs(exactly(100), exactly(100))
        .setRoot(
            Row.create(legacyLithoViewRule.context)
                .wrapInView()
                .child(
                    Column.create(
                            legacyLithoViewRule.context,
                            0,
                            R.style.ApplyStylesTest_DuplicateParentState)
                        .flexGrow(1f)
                        .wrapInView())
                .build())
        .measure()
        .layout()
        .attachToWindow()
    val innerHost = legacyLithoViewRule.lithoView.getChildAt(0) as ComponentHost
    ViewAssertions.assertThat(innerHost)
        .matches(
            ViewMatchNode.forType(ComponentHost::class.java)
                .child(
                    ViewMatchNode.forType(ComponentHost::class.java)
                        .prop<Boolean>("isDuplicateParentStateEnabled", true)))
  }
}


/*
 * 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.os.Looper
import android.os.Looper.getMainLooper
import com.facebook.litho.SizeSpec.EXACTLY
import com.facebook.litho.SizeSpec.makeSizeSpec
import com.facebook.litho.testing.BackgroundLayoutLooperRule
import com.facebook.litho.testing.LegacyLithoViewRule
import com.facebook.litho.testing.testrunner.LithoTestRunner
import com.facebook.litho.widget.AttachDetachTester
import com.facebook.litho.widget.AttachDetachTesterSpec
import java.util.ArrayList
import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.CountDownLatch
import java.util.concurrent.TimeUnit
import org.assertj.core.api.Assertions.assertThat
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.Shadows
import org.robolectric.annotation.LooperMode

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

  @JvmField @Rule val legacyLithoViewRule = LegacyLithoViewRule()

  @JvmField @Rule var backgroundLayoutLooperRule = BackgroundLayoutLooperRule()

  @Test
  fun component_setRootWithLayout_onAttachedIsCalled() {
    val steps: List<String> = ArrayList()
    val root =
        AttachDetachTester.create(legacyLithoViewRule.context).name("root").steps(steps).build()
    legacyLithoViewRule.setRoot(root)
    legacyLithoViewRule.attachToWindow().measure().layout()
    assertThat(steps)
        .describedAs("Should call @OnAttached method")
        .containsExactly("root:${AttachDetachTesterSpec.ON_ATTACHED}")
    val attachDetachHandler = legacyLithoViewRule.componentTree.attachDetachHandler
    val currentAttached = attachDetachHandler?.attached ?: error("Should not be null")

    assertThat(currentAttached.size).isEqualTo(1)
  }

  @Test
  fun component_setEmptyRootAfterAttach_onDetachedIsCalled() {
    val steps: MutableList<String> = ArrayList()
    val root =
        AttachDetachTester.create(legacyLithoViewRule.context).name("root").steps(steps).build()
    legacyLithoViewRule.setRoot(root)
    legacyLithoViewRule.attachToWindow().measure().layout()
    assertThat(steps)
        .describedAs("Should call @OnAttached method")
        .containsExactly("root:${AttachDetachTesterSpec.ON_ATTACHED}")
    steps.clear()
    legacyLithoViewRule.setRoot(Column.create(legacyLithoViewRule.context).build())
    assertThat(steps)
        .describedAs("Should call @OnDetached method")
        .containsExactly("root:${AttachDetachTesterSpec.ON_DETACHED}")
  }

  @Test
  fun component_releaseLithoView_onDetachedIsCalled() {
    val steps: MutableList<String> = ArrayList()
    val root =
        AttachDetachTester.create(legacyLithoViewRule.context).name("root").steps(steps).build()
    legacyLithoViewRule.setRoot(root)
    legacyLithoViewRule.attachToWindow().measure().layout()
    val attachDetachHandler = legacyLithoViewRule.componentTree.attachDetachHandler
    steps.clear()
    legacyLithoViewRule.release()
    assertThat(steps)
        .describedAs("Should call @OnDetached method")
        .containsExactly("root:${AttachDetachTesterSpec.ON_DETACHED}")
    assertThat(attachDetachHandler?.attached).isNullOrEmpty()
  }

  @Test
  fun component_replaceRootWithSameComponent_onDetachedIsNotCalled() {
    val c = legacyLithoViewRule.context
    val c3 = AttachDetachTester.create(c).name("c3")
    val c4 = AttachDetachTester.create(c).name("c4")
    val c1 = AttachDetachTester.create(c).name("c1").children(listOf(c3, c4))
    val c2 = AttachDetachTester.create(c).name("c2")
    val steps: MutableList<String> = ArrayList()
    val r1 = AttachDetachTester.create(c).children(listOf(c1, c2)).name("r1").steps(steps).build()
    legacyLithoViewRule.setRoot(r1)
    legacyLithoViewRule.attachToWindow().measure().layout()
    assertThat(steps)
        .describedAs("Should call @OnAttached methods in expected order")
        .containsExactly(
            "c3:${AttachDetachTesterSpec.ON_ATTACHED}",
            "c4:${AttachDetachTesterSpec.ON_ATTACHED}",
            "c1:${AttachDetachTesterSpec.ON_ATTACHED}",
            "c2:${AttachDetachTesterSpec.ON_ATTACHED}",
            "r1:${AttachDetachTesterSpec.ON_ATTACHED}")
    val attachDetachHandler = legacyLithoViewRule.componentTree.attachDetachHandler
    var currentAttached = attachDetachHandler?.attached ?: error("Should not be null")
    assertThat(currentAttached.size).isEqualTo(5)
    steps.clear()

    /*
             r1                r2
           /    \            /    \
         c1      c2   =>   c5      c6
        /  \                         \
      c3    c4                        c7
    */
    val c5 = AttachDetachTester.create(c).name("c5")
    val c7 = AttachDetachTester.create(c).name("c7")
    val c6 = AttachDetachTester.create(c).name("c6").children(listOf(c7))
    val r2 = AttachDetachTester.create(c).children(listOf(c5, c6)).name("r2").steps(steps).build()
    legacyLithoViewRule.setRoot(r2)
    assertThat(steps)
        .describedAs("Should call @OnDetached and @OnAttached methods in expect order")
        .containsExactly(
            "c3:${AttachDetachTesterSpec.ON_DETACHED}",
            "c4:${AttachDetachTesterSpec.ON_DETACHED}",
            "c7:${AttachDetachTesterSpec.ON_ATTACHED}")

    currentAttached = attachDetachHandler.attached ?: error("Should not be null")
    assertThat(currentAttached.size).isEqualTo(4)
    steps.clear()

    /*
            r2                  r3
          /    \              /
        c5      c6     =>   c8
                  \
                   c7
    */
    val c8 = AttachDetachTester.create(c).name("c8")
    val r3 = AttachDetachTester.create(c).children(listOf(c8)).name("r3").steps(steps).build()
    legacyLithoViewRule.setRoot(r3)
    assertThat(steps)
        .describedAs("Should call @OnDetached methods in expect order")
        .containsExactly(
            "c7:${AttachDetachTesterSpec.ON_DETACHED}", "c6:${AttachDetachTesterSpec.ON_DETACHED}")

    currentAttached = attachDetachHandler.attached ?: error("Should not be null")
    assertThat(currentAttached.size).isEqualTo(2)
  }

  @Test
  fun component_replaceRootWithDifferentComponent_onDetachedIsNotCalled() {
    val c = legacyLithoViewRule.context
    val c3 = AttachDetachTester.create(c).name("c3")
    val c4 = AttachDetachTester.create(c).name("c4")
    val c1 = AttachDetachTester.create(c).name("c1").children(listOf(c3, c4))
    val c2 = AttachDetachTester.create(c).name("c2")
    val steps: MutableList<String> = ArrayList()
    val r1 = AttachDetachTester.create(c).children(listOf(c1, c2)).name("r1").steps(steps).build()
    legacyLithoViewRule.setRoot(r1)
    legacyLithoViewRule.attachToWindow().measure().layout()
    assertThat(steps)
        .describedAs("Should call @OnAttached methods in expected order")
        .containsExactly(
            "c3:${AttachDetachTesterSpec.ON_ATTACHED}",
            "c4:${AttachDetachTesterSpec.ON_ATTACHED}",
            "c1:${AttachDetachTesterSpec.ON_ATTACHED}",
            "c2:${AttachDetachTesterSpec.ON_ATTACHED}",
            "r1:${AttachDetachTesterSpec.ON_ATTACHED}")
    val attachDetachHandler = legacyLithoViewRule.componentTree.attachDetachHandler
    var currentAttached = attachDetachHandler?.attached ?: error("Should not be null")

    assertThat(currentAttached.size).isEqualTo(5)
    steps.clear()

    /*
             r1                r2 (TestWrappedComponentProp)
           /    \            /    \
         c1      c2   =>   c5      c6
        /  \                         \
      c3    c4                        c7
    */
    val c5 = AttachDetachTester.create(c).name("c5")
    val c7 = AttachDetachTester.create(c).name("c7")
    val c6 = AttachDetachTester.create(c).name("c6").children(listOf(c7))
    val r2 =
        AttachDetachTester.create(c)
            .children(listOf(c5, c6))
            .name("r2")
            .key("newKey")
            .steps(steps)
            .build()
    legacyLithoViewRule.setRoot(r2)
    assertThat(steps)
        .describedAs("Should call @OnDetached and @OnAttached methods in expect order")
        .containsExactly(
            "c3:${AttachDetachTesterSpec.ON_DETACHED}",
            "c4:${AttachDetachTesterSpec.ON_DETACHED}",
            "c1:${AttachDetachTesterSpec.ON_DETACHED}",
            "c2:${AttachDetachTesterSpec.ON_DETACHED}",
            "r1:${AttachDetachTesterSpec.ON_DETACHED}",
            "c5:${AttachDetachTesterSpec.ON_ATTACHED}",
            "c7:${AttachDetachTesterSpec.ON_ATTACHED}",
            "c6:${AttachDetachTesterSpec.ON_ATTACHED}",
            "r2:${AttachDetachTesterSpec.ON_ATTACHED}")

    currentAttached = attachDetachHandler?.attached ?: error("Should not be null")
    assertThat(currentAttached.size).isEqualTo(4)
  }

  @Test
  fun component_setRootAndSizeSpecTwice_onAttachAndOnDetachedAreCalledOnlyOnce() {
    val c = legacyLithoViewRule.context
    val steps: MutableList<String> = ArrayList()
    val c1 = AttachDetachTester.create(c).name("c1")
    val component =
        AttachDetachTester.create(c).name("root").steps(steps).children(listOf(c1)).build()
    legacyLithoViewRule.setRootAndSizeSpecSync(
        component,
        SizeSpec.makeSizeSpec(100, SizeSpec.EXACTLY),
        SizeSpec.makeSizeSpec(100, SizeSpec.EXACTLY))
    legacyLithoViewRule.attachToWindow().measure().layout()
    legacyLithoViewRule.setRootAndSizeSpecSync(
        component,
        SizeSpec.makeSizeSpec(200, SizeSpec.EXACTLY),
        SizeSpec.makeSizeSpec(200, SizeSpec.EXACTLY))
    assertThat(steps)
        .describedAs("Should call @OnAttached only once for each component")
        .containsExactly(
            "c1:${AttachDetachTesterSpec.ON_ATTACHED}",
            "root:${AttachDetachTesterSpec.ON_ATTACHED}")
    steps.clear()
    legacyLithoViewRule.release()
    assertThat(steps)
        .describedAs("Should call @OnDetached only once for each component")
        .containsExactly(
            "c1:${AttachDetachTesterSpec.ON_DETACHED}",
            "root:${AttachDetachTesterSpec.ON_DETACHED}")
  }

  @Test
  fun component_setRootAndSizeSpecConcurrently_onAttachAndOnDetachedAreCalledOnlyOnce() {
    val c = legacyLithoViewRule.context
    val steps: List<String> = ArrayList()
    val c1 = AttachDetachTester.create(c).name("c1")
    val component =
        AttachDetachTester.create(c).name("root").steps(steps).children(listOf(c1)).build()
    val componentTree = legacyLithoViewRule.componentTree
    val latch1 = CountDownLatch(1)
    val thread1 = Thread {
      for (i in 0..9) {
        componentTree.setRootAndSizeSpecSync(
            component,
            SizeSpec.makeSizeSpec(100 + 10 * i, SizeSpec.EXACTLY),
            SizeSpec.makeSizeSpec(100 + 10 * i, SizeSpec.EXACTLY))
      }
      latch1.countDown()
    }
    val latch2 = CountDownLatch(1)
    val thread2 = Thread {
      for (i in 0..9) {
        componentTree.setRootAndSizeSpecSync(
            component,
            SizeSpec.makeSizeSpec(200 + 10 * i, SizeSpec.EXACTLY),
            SizeSpec.makeSizeSpec(200 + 10 * i, SizeSpec.EXACTLY))
      }
      latch2.countDown()
    }
    thread1.start()
    thread2.start()
    assertThat(latch1.await(5_000, TimeUnit.MILLISECONDS)).isTrue
    assertThat(latch2.await(5_000, TimeUnit.MILLISECONDS)).isTrue
    Shadows.shadowOf(Looper.getMainLooper()).idle()
    assertThat(steps)
        .describedAs("Should call @OnAttached only once for each component")
        .containsExactly(
            "c1:${AttachDetachTesterSpec.ON_ATTACHED}",
            "root:${AttachDetachTesterSpec.ON_ATTACHED}")
  }

  @Test
  fun component_setRoot_onAttachedIsCalledOnUIThread() {
    val steps: List<String> = ArrayList()
    val extraThreadInfo = ConcurrentHashMap<String, Any>()
    val root =
        AttachDetachTester.create(legacyLithoViewRule.context)
            .name("root")
            .steps(steps)
            .extraThreadInfo(extraThreadInfo)
            .build()
    legacyLithoViewRule.attachToWindow().measure().layout()
    legacyLithoViewRule.setRoot(root)
    assertThat(steps)
        .describedAs("Should call @OnAttached method")
        .containsExactly("root:${AttachDetachTesterSpec.ON_ATTACHED}")
    val attachDetachHandler = legacyLithoViewRule.componentTree.attachDetachHandler
    val currentAttached = attachDetachHandler?.attached ?: error("Should not be null")
    assertThat(currentAttached.size).isEqualTo(1)
    val isMainThreadLayout =
        extraThreadInfo[AttachDetachTesterSpec.IS_MAIN_THREAD_LAYOUT] as Boolean?
    assertThat(isMainThreadLayout).isTrue
    val isMainThreadAttached =
        extraThreadInfo[AttachDetachTesterSpec.IS_MAIN_THREAD_ON_ATTACHED] as Boolean?
    assertThat(isMainThreadAttached).isTrue
  }

  @Test
  fun component_setRootAsync_onAttachedIsCalledOnUIThread() {
    val steps: List<String> = ArrayList()
    val extraThreadInfo = ConcurrentHashMap<String, Any>()
    val root =
        AttachDetachTester.create(legacyLithoViewRule.context)
            .name("root")
            .steps(steps)
            .extraThreadInfo(extraThreadInfo)
            .build()
    legacyLithoViewRule.attachToWindow().measure().layout()
    legacyLithoViewRule.setRootAsync(root)
    backgroundLayoutLooperRule.runToEndOfTasksSync()
    Shadows.shadowOf(Looper.getMainLooper()).idle()
    assertThat(steps)
        .describedAs("Should call @OnAttached method")
        .containsExactly("root:${AttachDetachTesterSpec.ON_ATTACHED}")
    val attachDetachHandler = legacyLithoViewRule.componentTree.attachDetachHandler
    val currentAttached = attachDetachHandler?.attached ?: error("Should not be null")
    assertThat(currentAttached.size).isEqualTo(1)
    val isMainThreadLayout =
        extraThreadInfo[AttachDetachTesterSpec.IS_MAIN_THREAD_LAYOUT] as Boolean?
    assertThat(isMainThreadLayout).isFalse
    val isMainThreadAttached =
        extraThreadInfo[AttachDetachTesterSpec.IS_MAIN_THREAD_ON_ATTACHED] as Boolean?
    assertThat(isMainThreadAttached).isTrue
  }
}


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

package com.facebook.litho

import com.facebook.litho.testing.LegacyLithoViewRule
import com.facebook.litho.testing.testrunner.LithoTestRunner
import com.facebook.litho.widget.HorizontalScroll
import com.facebook.litho.widget.MountSpecLifecycleTester
import org.assertj.core.api.Assertions.assertThat
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith

@RunWith(LithoTestRunner::class)
class AttachDetachTest {
  @JvmField @Rule val legacyLithoViewRule = LegacyLithoViewRule()

  @Test
  fun horizontalScroll_releaseTree_childComponentShouldCallOnDetached() {
    val c = legacyLithoViewRule.context
    val lifecycleTracker = LifecycleTracker()
    legacyLithoViewRule.setRoot(
        HorizontalScroll.create(c)
            .contentProps(
                MountSpecLifecycleTester.create(c)
                    .intrinsicSize(Size(800, 600))
                    .lifecycleTracker(lifecycleTracker)
                    .build())
            .build())
    legacyLithoViewRule.attachToWindow().measure().layout()
    legacyLithoViewRule.release()
    assertThat(lifecycleTracker.steps)
        .describedAs(
            "Child component should call @OnDetached when parent HorizontalScroll is detached")
        .contains(LifecycleStep.ON_DETACHED)
  }
}


/*
 * 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.subcomponents.InspectableComponent
import com.facebook.litho.testing.testrunner.LithoTestRunner
import org.assertj.core.api.Assertions.assertThat
import org.hamcrest.core.IsNull
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.Mock
import org.mockito.MockitoAnnotations
import org.mockito.kotlin.whenever

@RunWith(LithoTestRunner::class)
class BaseMatcherTest {

  @Mock var inspectableComponent: InspectableComponent? = null

  @Before
  fun setUp() {
    MockitoAnnotations.initMocks(this)
    whenever(inspectableComponent!!.component)
        .thenReturn(
            Row.create(ComponentContext(ApplicationProvider.getApplicationContext<Context>()))
                .wrapInView()
                .build())
  }

  @Test
  fun testMatcherCreation() {
    val matcher = TestBaseMatcher().clickHandler(IsNull.nullValue(null))
    val condition = BaseMatcherBuilder.buildCommonMatcher(matcher)
    assertThat(condition.matches(inspectableComponent)).isTrue
  }

  @Test
  fun testMatcherFailureMessage() {
    val matcher = TestBaseMatcher().clickHandler(IsNull.notNullValue(null))
    val condition = BaseMatcherBuilder.buildCommonMatcher(matcher)
    condition.matches(inspectableComponent)
    assertThat(condition.description().toString())
        .isEqualTo("Click handler <not null> (doesn't match <null>)")
  }

  internal class TestBaseMatcher : BaseMatcher<TestBaseMatcher?>() {
    override fun getThis(): TestBaseMatcher = this
  }
}


/*
 * 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.ComposePathEffect
import android.graphics.DashPathEffect
import android.graphics.DiscretePathEffect
import android.graphics.Path
import android.graphics.PathDashPathEffect
import androidx.test.core.app.ApplicationProvider
import com.facebook.litho.testing.testrunner.LithoTestRunner
import com.facebook.yoga.YogaEdge
import org.assertj.core.api.Assertions.assertThat
import org.junit.Test
import org.junit.runner.RunWith

@RunWith(LithoTestRunner::class)
class BorderTest {

  @Test
  fun testIndividualColorSetting() {
    val c = ComponentContext(ApplicationProvider.getApplicationContext<Context>())
    val border =
        Border.create(c)
            .color(YogaEdge.LEFT, 0xFFFF0000.toInt())
            .color(YogaEdge.TOP, 0xFFFFFF00.toInt())
            .color(YogaEdge.RIGHT, 0xFFFFFFFF.toInt())
            .color(YogaEdge.BOTTOM, 0xFFFF00FF.toInt())
            .build()
    assertThat(border.mEdgeColors[Border.EDGE_LEFT]).isEqualTo(0xFFFF0000.toInt())
    assertThat(border.mEdgeColors[Border.EDGE_TOP]).isEqualTo(0xFFFFFF00.toInt())
    assertThat(border.mEdgeColors[Border.EDGE_RIGHT]).isEqualTo(0xFFFFFFFF.toInt())
    assertThat(border.mEdgeColors[Border.EDGE_BOTTOM]).isEqualTo(0xFFFF00FF.toInt())
  }

  @Test
  fun testAllColorSetting() {
    val c = ComponentContext(ApplicationProvider.getApplicationContext<Context>())
    val border = Border.create(c).color(YogaEdge.ALL, 0xFFFF0000.toInt()).build()
    assertThat(border.mEdgeColors[Border.EDGE_LEFT]).isEqualTo(0xFFFF0000.toInt())
    assertThat(border.mEdgeColors[Border.EDGE_TOP]).isEqualTo(0xFFFF0000.toInt())
    assertThat(border.mEdgeColors[Border.EDGE_RIGHT]).isEqualTo(0xFFFF0000.toInt())
    assertThat(border.mEdgeColors[Border.EDGE_BOTTOM]).isEqualTo(0xFFFF0000.toInt())
  }

  @Test
  fun testHorizontalColorSetting() {
    val c = ComponentContext(ApplicationProvider.getApplicationContext<Context>())
    val border =
        Border.create(c)
            .color(YogaEdge.ALL, 0xFFFF0000.toInt())
            .color(YogaEdge.HORIZONTAL, 0xFF00FF00.toInt())
            .build()
    assertThat(border.mEdgeColors[Border.EDGE_LEFT]).isEqualTo(0xFF00FF00.toInt())
    assertThat(border.mEdgeColors[Border.EDGE_TOP]).isEqualTo(0xFFFF0000.toInt())
    assertThat(border.mEdgeColors[Border.EDGE_RIGHT]).isEqualTo(0xFF00FF00.toInt())
    assertThat(border.mEdgeColors[Border.EDGE_BOTTOM]).isEqualTo(0xFFFF0000.toInt())
  }

  @Test
  fun testVerticalColorSetting() {
    val c = ComponentContext(ApplicationProvider.getApplicationContext<Context>())
    val border =
        Border.create(c)
            .color(YogaEdge.ALL, 0xFFFF0000.toInt())
            .color(YogaEdge.VERTICAL, 0xFF00FF00.toInt())
            .build()
    assertThat(border.mEdgeColors[Border.EDGE_LEFT]).isEqualTo(0xFFFF0000.toInt())
    assertThat(border.mEdgeColors[Border.EDGE_TOP]).isEqualTo(0xFF00FF00.toInt())
    assertThat(border.mEdgeColors[Border.EDGE_RIGHT]).isEqualTo(0xFFFF0000.toInt())
    assertThat(border.mEdgeColors[Border.EDGE_BOTTOM]).isEqualTo(0xFF00FF00.toInt())
  }

  @Test
  fun testStartEndResolving() {
    val c = ComponentContext(ApplicationProvider.getApplicationContext<Context>())
    val border =
        Border.create(c)
            .color(YogaEdge.START, 0xFFFF0000.toInt())
            .color(YogaEdge.END, 0x0000FFFF)
            .widthPx(YogaEdge.START, 100)
            .widthPx(YogaEdge.END, 200)
            .build()
    assertThat(border.mEdgeColors[Border.EDGE_LEFT]).isEqualTo(0xFFFF0000.toInt())
    assertThat(border.mEdgeColors[Border.EDGE_RIGHT]).isEqualTo(0x0000FFFF)
    assertThat(border.mEdgeWidths[Border.EDGE_LEFT]).isEqualTo(100)
    assertThat(border.mEdgeWidths[Border.EDGE_RIGHT]).isEqualTo(200)
  }

  @Test
  fun testIndividualWidthSetting() {
    val c = ComponentContext(ApplicationProvider.getApplicationContext<Context>())
    val border =
        Border.create(c)
            .widthPx(YogaEdge.LEFT, 1)
            .widthPx(YogaEdge.TOP, 2)
            .widthPx(YogaEdge.RIGHT, 3)
            .widthPx(YogaEdge.BOTTOM, 4)
            .build()
    assertThat(border.mEdgeWidths[Border.EDGE_LEFT]).isEqualTo(1)
    assertThat(border.mEdgeWidths[Border.EDGE_TOP]).isEqualTo(2)
    assertThat(border.mEdgeWidths[Border.EDGE_RIGHT]).isEqualTo(3)
    assertThat(border.mEdgeWidths[Border.EDGE_BOTTOM]).isEqualTo(4)
  }

  @Test
  fun testAllWidthSetting() {
    val c = ComponentContext(ApplicationProvider.getApplicationContext<Context>())
    val border = Border.create(c).widthPx(YogaEdge.ALL, 5).build()
    assertThat(border.mEdgeWidths[Border.EDGE_LEFT]).isEqualTo(5)
    assertThat(border.mEdgeWidths[Border.EDGE_TOP]).isEqualTo(5)
    assertThat(border.mEdgeWidths[Border.EDGE_RIGHT]).isEqualTo(5)
    assertThat(border.mEdgeWidths[Border.EDGE_BOTTOM]).isEqualTo(5)
  }

  @Test
  fun testHorizontalWidthSetting() {
    val c = ComponentContext(ApplicationProvider.getApplicationContext<Context>())
    val border = Border.create(c).widthPx(YogaEdge.ALL, 1).widthPx(YogaEdge.HORIZONTAL, 5).build()
    assertThat(border.mEdgeWidths[Border.EDGE_LEFT]).isEqualTo(5)
    assertThat(border.mEdgeWidths[Border.EDGE_TOP]).isEqualTo(1)
    assertThat(border.mEdgeWidths[Border.EDGE_RIGHT]).isEqualTo(5)
    assertThat(border.mEdgeWidths[Border.EDGE_BOTTOM]).isEqualTo(1)
  }

  @Test
  fun testVerticalWidthSetting() {
    val c = ComponentContext(ApplicationProvider.getApplicationContext<Context>())
    val border = Border.create(c).widthPx(YogaEdge.ALL, 1).widthPx(YogaEdge.VERTICAL, 5).build()
    assertThat(border.mEdgeWidths[Border.EDGE_LEFT]).isEqualTo(1)
    assertThat(border.mEdgeWidths[Border.EDGE_TOP]).isEqualTo(5)
    assertThat(border.mEdgeWidths[Border.EDGE_RIGHT]).isEqualTo(1)
    assertThat(border.mEdgeWidths[Border.EDGE_BOTTOM]).isEqualTo(5)
  }

  @Test
  fun testAllColorWidthSetting() {
    val c = ComponentContext(ApplicationProvider.getApplicationContext<Context>())
    val border =
        Border.create(c)
            .color(YogaEdge.LEFT, 0xFFFF0000.toInt())
            .color(YogaEdge.TOP, 0xFFFFFF00.toInt())
            .color(YogaEdge.RIGHT, 0xFFFFFFFF.toInt())
            .color(YogaEdge.BOTTOM, 0xFFFF00FF.toInt())
            .widthPx(YogaEdge.LEFT, 1)
            .widthPx(YogaEdge.TOP, 2)
            .widthPx(YogaEdge.RIGHT, 3)
            .widthPx(YogaEdge.BOTTOM, 4)
            .build()
    assertThat(border.mEdgeColors[Border.EDGE_LEFT]).isEqualTo(0xFFFF0000.toInt())
    assertThat(border.mEdgeColors[Border.EDGE_TOP]).isEqualTo(0xFFFFFF00.toInt())
    assertThat(border.mEdgeColors[Border.EDGE_RIGHT]).isEqualTo(0xFFFFFFFF.toInt())
    assertThat(border.mEdgeColors[Border.EDGE_BOTTOM]).isEqualTo(0xFFFF00FF.toInt())
    assertThat(border.mEdgeWidths[Border.EDGE_LEFT]).isEqualTo(1)
    assertThat(border.mEdgeWidths[Border.EDGE_TOP]).isEqualTo(2)
    assertThat(border.mEdgeWidths[Border.EDGE_RIGHT]).isEqualTo(3)
    assertThat(border.mEdgeWidths[Border.EDGE_BOTTOM]).isEqualTo(4)
  }

  @Test
  fun testBothRadiiSetting() {
    val c = ComponentContext(ApplicationProvider.getApplicationContext<Context>())
    val border = Border.create(c).radiusPx(1_337).build()
    assertThat(border.mRadius[Border.Corner.BOTTOM_LEFT]).isEqualTo(1_337f)
    assertThat(border.mRadius[Border.Corner.BOTTOM_RIGHT]).isEqualTo(1_337f)
    assertThat(border.mRadius[Border.Corner.TOP_LEFT]).isEqualTo(1_337f)
    assertThat(border.mRadius[Border.Corner.TOP_RIGHT]).isEqualTo(1_337f)
  }

  @Test
  fun testIndividualRadiiSetting() {
    val c = ComponentContext(ApplicationProvider.getApplicationContext<Context>())
    val border =
        Border.create(c)
            .radiusPx(Border.Corner.TOP_LEFT, 1)
            .radiusPx(Border.Corner.TOP_RIGHT, 2)
            .radiusPx(Border.Corner.BOTTOM_RIGHT, 3)
            .radiusPx(Border.Corner.BOTTOM_LEFT, 4)
            .build()
    assertThat(border.mRadius[Border.Corner.TOP_LEFT]).isEqualTo(1f)
    assertThat(border.mRadius[Border.Corner.TOP_RIGHT]).isEqualTo(2f)
    assertThat(border.mRadius[Border.Corner.BOTTOM_RIGHT]).isEqualTo(3f)
    assertThat(border.mRadius[Border.Corner.BOTTOM_LEFT]).isEqualTo(4f)
  }

  @Test
  fun testEffectSetting() {
    val c = ComponentContext(ApplicationProvider.getApplicationContext<Context>())
    var border = Border.create(c).dashEffect(floatArrayOf(1f, 1f), 0f).build()
    assertThat(border.mPathEffect).isInstanceOf(DashPathEffect::class.java)
    border = Border.create(c).discreteEffect(1f, 0f).build()
    assertThat(border.mPathEffect).isInstanceOf(DiscretePathEffect::class.java)
    border =
        Border.create(c).pathDashEffect(Path(), 0f, 0f, PathDashPathEffect.Style.ROTATE).build()
    assertThat(border.mPathEffect).isInstanceOf(PathDashPathEffect::class.java)
    border = Border.create(c).discreteEffect(1f, 1f).dashEffect(floatArrayOf(1f, 2f), 1f).build()
    assertThat(border.mPathEffect).isInstanceOf(ComposePathEffect::class.java)
  }

  @Test(expected = IllegalArgumentException::class)
  fun testTooManyEffectsThrows() {
    val c = ComponentContext(ApplicationProvider.getApplicationContext<Context>())
    Border.create(c)
        .pathDashEffect(Path(), 1f, 1f, PathDashPathEffect.Style.MORPH)
        .dashEffect(floatArrayOf(1f, 2f), 1f)
        .discreteEffect(1f, 2f)
        .build()
  }

  @Test(expected = IllegalArgumentException::class)
  fun testDifferentWidthWithEffectThrows() {
    val c = ComponentContext(ApplicationProvider.getApplicationContext<Context>())
    Border.create(c)
        .widthPx(YogaEdge.ALL, 10)
        .widthPx(YogaEdge.LEFT, 5)
        .discreteEffect(1f, 1f)
        .build()
  }

  @Test
  fun testSameObjectEquivalentTo() {
    val c = ComponentContext(ApplicationProvider.getApplicationContext<Context>())
    val border = Border.create(c).build()
    assertThat(border.isEquivalentTo(border)).isEqualTo(true)
  }

  @Test
  fun testNullObjectEquivalentTo() {
    val c = ComponentContext(ApplicationProvider.getApplicationContext<Context>())
    val border = Border.create(c).build()
    assertThat(border.isEquivalentTo(null)).isEqualTo(false)
  }

  @Test
  fun testDifferentObjectWithSameContentEquivalentTo() {
    val c = ComponentContext(ApplicationProvider.getApplicationContext<Context>())
    val border1 =
        Border.create(c)
            .color(YogaEdge.LEFT, 0xFFFF0000.toInt())
            .color(YogaEdge.TOP, 0xFFFFFF00.toInt())
            .color(YogaEdge.RIGHT, 0xFFFFFFFF.toInt())
            .color(YogaEdge.BOTTOM, 0xFFFF00FF.toInt())
            .widthPx(YogaEdge.LEFT, 1)
            .widthPx(YogaEdge.TOP, 2)
            .widthPx(YogaEdge.RIGHT, 3)
            .widthPx(YogaEdge.BOTTOM, 4)
            .radiusPx(Border.Corner.TOP_LEFT, 1)
            .radiusPx(Border.Corner.TOP_RIGHT, 2)
            .radiusPx(Border.Corner.BOTTOM_RIGHT, 3)
            .radiusPx(Border.Corner.BOTTOM_LEFT, 4)
            .build()
    val border2 =
        Border.create(c)
            .color(YogaEdge.LEFT, 0xFFFF0000.toInt())
            .color(YogaEdge.TOP, 0xFFFFFF00.toInt())
            .color(YogaEdge.RIGHT, 0xFFFFFFFF.toInt())
            .color(YogaEdge.BOTTOM, 0xFFFF00FF.toInt())
            .widthPx(YogaEdge.LEFT, 1)
            .widthPx(YogaEdge.TOP, 2)
            .widthPx(YogaEdge.RIGHT, 3)
            .widthPx(YogaEdge.BOTTOM, 4)
            .radiusPx(Border.Corner.TOP_LEFT, 1)
            .radiusPx(Border.Corner.TOP_RIGHT, 2)
            .radiusPx(Border.Corner.BOTTOM_RIGHT, 3)
            .radiusPx(Border.Corner.BOTTOM_LEFT, 4)
            .build()
    assertThat(border1.isEquivalentTo(border2)).isEqualTo(true)
  }

  @Test
  fun testDifferentObjectWithDifferentContentEquivalentTo() {
    val c = ComponentContext(ApplicationProvider.getApplicationContext<Context>())
    val border1 =
        Border.create(c)
            .color(YogaEdge.LEFT, 0xFFFF0000.toInt())
            .color(YogaEdge.TOP, 0xFF29FF00.toInt())
            .color(YogaEdge.RIGHT, 0xFFFFFFFF.toInt())
            .color(YogaEdge.BOTTOM, 0xFFFF00FF.toInt())
            .widthPx(YogaEdge.LEFT, 1)
            .widthPx(YogaEdge.TOP, 2)
            .widthPx(YogaEdge.RIGHT, 3)
            .widthPx(YogaEdge.BOTTOM, 3)
            .radiusPx(Border.Corner.TOP_LEFT, 1)
            .radiusPx(Border.Corner.TOP_RIGHT, 2)
            .radiusPx(Border.Corner.BOTTOM_RIGHT, 6)
            .radiusPx(Border.Corner.BOTTOM_LEFT, 4)
            .build()
    val border2 =
        Border.create(c)
            .color(YogaEdge.LEFT, 0xFFFF0000.toInt())
            .color(YogaEdge.TOP, 0xFFFFFF00.toInt())
            .color(YogaEdge.RIGHT, 0xFFFFFFFF.toInt())
            .color(YogaEdge.BOTTOM, 0xFFFF00FF.toInt())
            .widthPx(YogaEdge.LEFT, 1)
            .widthPx(YogaEdge.TOP, 2)
            .widthPx(YogaEdge.RIGHT, 3)
            .widthPx(YogaEdge.BOTTOM, 4)
            .radiusPx(Border.Corner.TOP_LEFT, 1)
            .radiusPx(Border.Corner.TOP_RIGHT, 2)
            .radiusPx(Border.Corner.BOTTOM_RIGHT, 3)
            .radiusPx(Border.Corner.BOTTOM_LEFT, 4)
            .build()
    assertThat(border1.isEquivalentTo(border2)).isEqualTo(false)
  }
}


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

package com.facebook.litho

import com.facebook.litho.testing.testrunner.LithoTestRunner
import org.assertj.core.api.Assertions.assertThat
import org.junit.Test
import org.junit.runner.RunWith

@RunWith(LithoTestRunner::class)
class BoundaryWorkingRangeTest {

  @Test
  fun tearShouldEnterRange() {
    val workingRange = BoundaryWorkingRange(1)
    assertThat(workingRange.shouldEnterRange(0, 0, 2, 1, 3)).isEqualTo(true)
    assertThat(workingRange.shouldEnterRange(4, 0, 2, 1, 3)).isEqualTo(false)
  }

  @Test
  fun testShouldExitRange() {
    val workingRange = BoundaryWorkingRange(1)
    assertThat(workingRange.shouldExitRange(4, 0, 2, 1, 3)).isEqualTo(true)
    assertThat(workingRange.shouldExitRange(0, 0, 2, 1, 3)).isEqualTo(false)
  }
}


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

package com.facebook.litho

import com.facebook.litho.LifecycleStep.StepInfo
import com.facebook.litho.testing.LegacyLithoViewRule
import com.facebook.litho.testing.assertj.Conditions.exactly
import com.facebook.litho.testing.testrunner.LithoTestRunner
import com.facebook.litho.widget.LayoutSpecLifecycleTester
import com.facebook.litho.widget.LayoutWithSizeSpecLifecycleTester
import com.facebook.litho.widget.MountSpecLifecycleTester
import com.facebook.litho.widget.Text
import com.facebook.rendercore.utils.MeasureSpecUtils.exactly
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 CachedLayoutTest {

  @Rule @JvmField val lithoViewRule = LegacyLithoViewRule()

  val commonAssertion =
      fun(steps: List<StepInfo>) {
        assertThat(LifecycleStep.getSteps(steps))
            .describedAs("cached layout spec should be resolved only once")
            .containsExactly(
                LifecycleStep.ON_CREATE_INITIAL_STATE,
                LifecycleStep.ON_CREATE_TREE_PROP,
                LifecycleStep.ON_CALCULATE_CACHED_VALUE,
                LifecycleStep.ON_CREATE_LAYOUT)
      }

  @Test
  fun `when component has compatible cached layout it should not be recreated or remeasured`() {

    val tracker = LifecycleTracker()
    val steps = mutableListOf<StepInfo>()
    val sizeSpec = Size(exactly(1080), 0)
    val root = TrackingComponent(tracker, steps, sizeSpec, commonAssertion)

    lithoViewRule.render { root }

    assertThat(LifecycleStep.getSteps(steps))
        .describedAs("cached layout spec should be resolved only once")
        .containsOnlyOnce(LifecycleStep.ON_CREATE_LAYOUT)

    assertThat(tracker.steps)
        .describedAs("cached mount spec should be measured only once")
        .containsOnlyOnce(LifecycleStep.ON_MEASURE)
  }

  @Test
  fun `when component has incompatible cached layout it should only be remeasured`() {
    val tracker = LifecycleTracker()
    val steps = mutableListOf<StepInfo>()
    val size = Size(exactly(200), 200)
    val root = TrackingComponent(tracker, steps, size, commonAssertion)

    lithoViewRule.render { root }

    assertThat(LifecycleStep.getSteps(steps))
        .describedAs("cached layout spec should be resolved only once")
        .containsOnlyOnce(LifecycleStep.ON_CREATE_LAYOUT)
  }

  @Test
  fun `when component with flex has incompatible cached layout it should only be remeasured`() {
    val c: ComponentContext = lithoViewRule.context
    val tracker = LifecycleTracker()
    val steps = mutableListOf<StepInfo>()
    val size = Size(exactly(200), 200)
    val root =
        Row.create(c)
            .child(
                LayoutWithSizeSpecLifecycleTester.create(c)
                    .steps(mutableListOf<StepInfo>())
                    .causeYogaRemeasure(true)
                    .body(TrackingComponent(tracker, steps, size, commonAssertion)))
            .child(Text.create(c).text("Hello World"))
            .build()

    lithoViewRule.render { root }

    assertThat(LifecycleStep.getSteps(steps))
        .describedAs("cached layout spec should be resolved only once")
        .containsOnlyOnce(LifecycleStep.ON_CREATE_LAYOUT)
  }

  class TrackingComponent(
      private val tracker: LifecycleTracker,
      private val steps: List<StepInfo>,
      private val sizeSpec: Size,
      private val assertion: ((List<StepInfo>) -> Unit)?
  ) : KComponent() {
    override fun ComponentScope.render(): Component {
      val mountSpec =
          MountSpecLifecycleTester.create(context)
              .intrinsicSize(Size(100, 100))
              .lifecycleTracker(tracker)
              .build()

      val layoutSpec = LayoutSpecLifecycleTester.create(context).steps(steps).build()

      // measure the components
      mountSpec.measure(context, sizeSpec.width, sizeSpec.height, Size())

      // resolve and measure the layout spec
      layoutSpec.measure(context, sizeSpec.width, sizeSpec.height, Size())

      assertion?.let { it(steps) }

      return Column {
        child(layoutSpec)
        child(mountSpec)
      }
    }
  }
}


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

package com.facebook.litho

import com.facebook.litho.core.height
import com.facebook.litho.core.width
import com.facebook.litho.kotlin.widget.Text
import com.facebook.litho.testing.LithoViewRule
import com.facebook.litho.testing.testrunner.LithoTestRunner
import com.facebook.litho.testing.viewtree.ViewPredicates
import com.facebook.litho.widget.LayoutWithSizeSpecWithCachedValue
import com.facebook.litho.widget.LayoutWithSizeSpecWithCachedValueSpec
import java.util.concurrent.atomic.AtomicInteger
import java.util.concurrent.atomic.AtomicReference
import org.assertj.core.api.Assertions
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.annotation.LooperMode

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

  @JvmField @Rule val lithoViewRule = LithoViewRule()

  @Test
  fun `cached value is not calculated when input is same for main and nested tree`() {
    val initCounter = AtomicInteger(0)
    val nestedTreeInitCounter = LayoutWithSizeSpecWithCachedValueSpec.CalculateCachedValueCounter()

    val repeatNum = AtomicInteger(100)
    class TestComponent : KComponent() {
      override fun ComponentScope.render(): Component? {
        val expensiveString =
            useCached("count" + repeatNum.get()) {
              initCounter.incrementAndGet()
              expensiveRepeatFunc("count" + repeatNum.get())
            }
        return Column(style = Style.width(200.px).height(200.px)) {
          child(Text(text = expensiveString))
          child(
              LayoutWithSizeSpecWithCachedValue.create(context)
                  .number(repeatNum.get())
                  .counter(nestedTreeInitCounter)
                  .build())
        }
      }
    }

    val testLithoView = lithoViewRule.render { TestComponent() }

    // Cached value was calculated for main tree
    Assertions.assertThat(initCounter.get()).isEqualTo(1)
    Assertions.assertThat(nestedTreeInitCounter.get()).isEqualTo(1)

    // Check cached value updated in nested tree as well
    lithoViewRule.act(testLithoView) { ViewPredicates.hasVisibleText("ExpensiveCachedValue: 100") }

    // Clear root component from ComponentTree.
    lithoViewRule.render(testLithoView.lithoView) { EmptyComponent() }

    // Re-set root component with same inputs
    lithoViewRule.render(testLithoView.lithoView) { TestComponent() }

    Assertions.assertThat(initCounter.get()).isEqualTo(1)
    Assertions.assertThat(nestedTreeInitCounter.get()).isEqualTo(1)
    lithoViewRule.act(testLithoView) { ViewPredicates.hasVisibleText("ExpensiveCachedValue: 100") }
  }

  @Test
  fun `cached value is recalculated when input is changed for main and nested tree`() {
    val initCounter = AtomicInteger(0)
    val nestedTreeInitCounter = LayoutWithSizeSpecWithCachedValueSpec.CalculateCachedValueCounter()

    val repeatNum = AtomicInteger(100)
    class TestComponent : KComponent() {
      override fun ComponentScope.render(): Component? {
        val expensiveString =
            useCached("count" + repeatNum.get()) {
              initCounter.incrementAndGet()
              expensiveRepeatFunc("count" + repeatNum.get())
            }
        return Column(style = Style.width(200.px).height(200.px)) {
          child(Text(text = expensiveString))
          child(
              LayoutWithSizeSpecWithCachedValue.create(context)
                  .number(repeatNum.get())
                  .counter(nestedTreeInitCounter)
                  .build())
        }
      }
    }

    val testLithoView = lithoViewRule.render { TestComponent() }

    // Cached value was calculated for main tree
    Assertions.assertThat(initCounter.get()).isEqualTo(1)
    Assertions.assertThat(nestedTreeInitCounter.get()).isEqualTo(1)

    // Check cached value updated in nested tree as well
    lithoViewRule.act(testLithoView) { ViewPredicates.hasVisibleText("ExpensiveCachedValue: 100") }

    // Clear root component from ComponentTree.
    lithoViewRule.render(testLithoView.lithoView) { EmptyComponent() }

    // Increase repeat number.
    repeatNum.incrementAndGet()
    // Re-set root component and cache value is re-created.
    lithoViewRule.render(testLithoView.lithoView) { TestComponent() }

    Assertions.assertThat(initCounter.get()).isEqualTo(2)
    Assertions.assertThat(nestedTreeInitCounter.get()).isEqualTo(2)
    lithoViewRule.act(testLithoView) { ViewPredicates.hasVisibleText("ExpensiveCachedValue: 101") }
  }

  @Test
  fun `cached value is recalculated when input is changed from nullable to non null value for nested tree component`() {
    val nestedTreeInitCounter = LayoutWithSizeSpecWithCachedValueSpec.CalculateCachedValueCounter()

    val nullableRepeatNum = AtomicReference<Integer>(null)
    class TestComponent : KComponent() {
      override fun ComponentScope.render(): Component? {
        return Column(style = Style.width(200.px).height(200.px)) {
          child(
              LayoutWithSizeSpecWithCachedValue.create(context)
                  .number(nullableRepeatNum.get()?.toInt())
                  .counter(nestedTreeInitCounter)
                  .build())
        }
      }
    }

    val testLithoView = lithoViewRule.render { TestComponent() }

    // Check cached value updated in nested tree as well
    Assertions.assertThat(nestedTreeInitCounter.get()).isEqualTo(1)
    lithoViewRule.act(testLithoView) { ViewPredicates.hasVisibleText("ExpensiveCachedValue: -1") }

    // Clear root component from ComponentTree.
    lithoViewRule.render(testLithoView.lithoView) { EmptyComponent() }

    // Increase repeat number.
    nullableRepeatNum.set(Integer(100))
    // Re-set root component and cache value is re-created.
    lithoViewRule.render(testLithoView.lithoView) { TestComponent() }

    Assertions.assertThat(nestedTreeInitCounter.get()).isEqualTo(2)
    lithoViewRule.act(testLithoView) { ViewPredicates.hasVisibleText("ExpensiveCachedValue: 100") }
  }

  @Test
  fun `cached value is not reused between different nested trees components`() {
    val firstNestedTreeInitCounter =
        LayoutWithSizeSpecWithCachedValueSpec.CalculateCachedValueCounter()
    val secondNestedTreeInitCounter =
        LayoutWithSizeSpecWithCachedValueSpec.CalculateCachedValueCounter()

    val repeatNumFirst = AtomicInteger(100)
    val repeatNumSecond = AtomicInteger(200)

    class TestComponent : KComponent() {
      override fun ComponentScope.render(): Component? {
        return Column(style = Style.width(200.px).height(200.px)) {
          child(
              LayoutWithSizeSpecWithCachedValue.create(context)
                  .number(repeatNumFirst.get())
                  .counter(firstNestedTreeInitCounter)
                  .build())
          child(
              LayoutWithSizeSpecWithCachedValue.create(context)
                  .number(repeatNumSecond.get())
                  .counter(secondNestedTreeInitCounter)
                  .build())
        }
      }
    }

    val testLithoView = lithoViewRule.render { TestComponent() }

    // Check cached value updated in nested tree as well
    Assertions.assertThat(firstNestedTreeInitCounter.get()).isEqualTo(1)
    lithoViewRule.act(testLithoView) { ViewPredicates.hasVisibleText("ExpensiveCachedValue: 100") }

    Assertions.assertThat(secondNestedTreeInitCounter.get()).isEqualTo(1)
    lithoViewRule.act(testLithoView) { ViewPredicates.hasVisibleText("ExpensiveCachedValue: 200") }

    // Clear root component from ComponentTree.
    lithoViewRule.render(testLithoView.lithoView) { EmptyComponent() }

    // Increase repeat number.
    repeatNumFirst.incrementAndGet()
    // Re-set root component and cache value is re-created.
    lithoViewRule.render(testLithoView.lithoView) { TestComponent() }

    Assertions.assertThat(firstNestedTreeInitCounter.get()).isEqualTo(2)
    lithoViewRule.act(testLithoView) { ViewPredicates.hasVisibleText("ExpensiveCachedValue: 101") }

    Assertions.assertThat(secondNestedTreeInitCounter.get()).isEqualTo(1)
    lithoViewRule.act(testLithoView) { ViewPredicates.hasVisibleText("ExpensiveCachedValue: 200") }
  }

  companion object {
    private fun expensiveRepeatFunc(prefix: String, num: Int = 20, suffix: String? = null): String {
      return StringBuilder()
          .apply {
            repeat(num) {
              append(prefix)
              suffix?.let { append(it) }
            }
          }
          .toString()
    }
  }
}


/*
 * 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.SparseArray
import androidx.collection.SparseArrayCompat
import com.facebook.litho.testing.testrunner.LithoTestRunner
import java.util.ArrayList
import java.util.HashMap
import org.assertj.core.api.Assertions.assertThat
import org.junit.Test
import org.junit.runner.RunWith

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

  @Test
  fun verifyList() {
    var list: MutableList<String>? = null
    assertThat(CollectionsUtils.isNullOrEmpty(list)).isTrue
    assertThat(CollectionsUtils.isNotNullOrEmpty(list)).isFalse
    assertThat(CollectionsUtils.isEmpty(list)).isFalse
    list = ArrayList()
    assertThat(CollectionsUtils.isNullOrEmpty(list)).isTrue
    assertThat(CollectionsUtils.isNotNullOrEmpty(list)).isFalse
    assertThat(CollectionsUtils.isEmpty(list)).isTrue
    list.add("1")
    assertThat(CollectionsUtils.isNullOrEmpty(list)).isFalse
    assertThat(CollectionsUtils.isNotNullOrEmpty(list)).isTrue
    assertThat(CollectionsUtils.isEmpty(list)).isFalse
  }

  @Test
  fun verifyMap() {
    var map: MutableMap<String, String>? = null
    assertThat(CollectionsUtils.isNullOrEmpty(map)).isTrue
    assertThat(CollectionsUtils.isNotNullOrEmpty(map)).isFalse
    assertThat(CollectionsUtils.isEmpty(map)).isFalse
    map = HashMap()
    assertThat(CollectionsUtils.isNullOrEmpty(map)).isTrue
    assertThat(CollectionsUtils.isNotNullOrEmpty(map)).isFalse
    assertThat(CollectionsUtils.isEmpty(map)).isTrue
    map["key"] = "value"
    assertThat(CollectionsUtils.isNullOrEmpty(map)).isFalse
    assertThat(CollectionsUtils.isNotNullOrEmpty(map)).isTrue
    assertThat(CollectionsUtils.isEmpty(map)).isFalse
  }

  @Test
  fun verifySparseArrayCompat() {
    var sparseArrayCompat: SparseArrayCompat<String>? = null
    assertThat(CollectionsUtils.isNullOrEmpty(sparseArrayCompat)).isTrue
    assertThat(CollectionsUtils.isNotNullOrEmpty(sparseArrayCompat)).isFalse
    assertThat(CollectionsUtils.isEmpty(sparseArrayCompat)).isFalse
    sparseArrayCompat = SparseArrayCompat()
    assertThat(CollectionsUtils.isNullOrEmpty(sparseArrayCompat)).isTrue
    assertThat(CollectionsUtils.isNotNullOrEmpty(sparseArrayCompat)).isFalse
    assertThat(CollectionsUtils.isEmpty(sparseArrayCompat)).isTrue
    sparseArrayCompat.put(1, "value")
    assertThat(CollectionsUtils.isNullOrEmpty(sparseArrayCompat)).isFalse
    assertThat(CollectionsUtils.isNotNullOrEmpty(sparseArrayCompat)).isTrue
    assertThat(CollectionsUtils.isEmpty(sparseArrayCompat)).isFalse
  }

  @Test
  fun verifySparseArray() {
    var sparseArray: SparseArray<String>? = null
    assertThat(CollectionsUtils.isNullOrEmpty(sparseArray)).isTrue
    assertThat(CollectionsUtils.isNotNullOrEmpty(sparseArray)).isFalse
    assertThat(CollectionsUtils.isEmpty(sparseArray)).isFalse
    sparseArray = SparseArray()
    assertThat(CollectionsUtils.isNullOrEmpty(sparseArray)).isTrue
    assertThat(CollectionsUtils.isNotNullOrEmpty(sparseArray)).isFalse
    assertThat(CollectionsUtils.isEmpty(sparseArray)).isTrue
    sparseArray.put(1, "value")
    assertThat(CollectionsUtils.isNullOrEmpty(sparseArray)).isFalse
    assertThat(CollectionsUtils.isNotNullOrEmpty(sparseArray)).isTrue
    assertThat(CollectionsUtils.isEmpty(sparseArray)).isFalse
  }
}


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

package com.facebook.litho

import com.facebook.litho.animation.AnimatedProperties
import com.facebook.litho.config.ComponentsConfiguration
import com.facebook.litho.testing.LithoViewRule
import com.facebook.litho.testing.inlinelayoutspec.InlineLayoutSpec
import com.facebook.litho.testing.testrunner.LithoTestRunner
import org.assertj.core.api.Assertions
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith

@RunWith(LithoTestRunner::class)
class CollectTransitionsTest {
  @JvmField @Rule val lithoViewRule: LithoViewRule = LithoViewRule()
  private val wrappingContentWithTransition: InlineLayoutSpec =
      object : InlineLayoutSpec() {
        protected override fun onCreateLayout(c: ComponentContext): Component {
          return Wrapper.create(c).delegate(componentWithTransition).build()
        }

        protected override fun onCreateTransition(c: ComponentContext): Transition? {
          return Transition.create(Transition.TransitionKeyType.GLOBAL, "test")
              .animate(AnimatedProperties.Y)
        }
      }
  private val componentWithTransition: InlineLayoutSpec =
      object : InlineLayoutSpec() {
        protected override fun onCreateLayout(c: ComponentContext): Component {
          return Row.create(c)
              .child(Row.create(c).transitionKey("test"))
              .transitionKeyType(Transition.TransitionKeyType.GLOBAL)
              .child(Row.create(c).transitionKey("test2"))
              .transitionKeyType(Transition.TransitionKeyType.GLOBAL)
              .build()
        }

        protected override fun onCreateTransition(c: ComponentContext): Transition? {
          return Transition.create(Transition.TransitionKeyType.GLOBAL, "test")
              .animate(AnimatedProperties.X)
        }
      }

  @Test
  fun testCollectsWrappingTransitions() {
    val originalValue: Boolean = ComponentsConfiguration.isAnimationDisabled
    try {
      ComponentsConfiguration.isAnimationDisabled = false
      val testLithoView =
          lithoViewRule.render(widthPx = 100, heightPx = 100) { wrappingContentWithTransition }
      Assertions.assertThat(testLithoView.componentTree.mainThreadLayoutState!!.transitions)
          .hasSize(2)
    } finally {
      ComponentsConfiguration.isAnimationDisabled = originalValue
    }
  }
}