/*
 * 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.drawable.ColorDrawable
import com.facebook.litho.config.ComponentsConfiguration
import com.facebook.litho.kotlin.widget.Text
import com.facebook.litho.testing.LegacyLithoViewRule
import com.facebook.litho.testing.exactly
import com.facebook.litho.testing.setRoot
import com.facebook.litho.testing.testrunner.LithoTestRunner
import com.facebook.rendercore.MeasureResult
import com.facebook.rendercore.primitives.DrawableAllocator
import com.facebook.rendercore.primitives.Primitive
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

/** Unit tests for [useEffect]. */
@LooperMode(LooperMode.Mode.LEGACY)
@RunWith(LithoTestRunner::class)
class KEffectsTest {

  @Rule @JvmField val lithoViewRule = LegacyLithoViewRule()

  @Test
  fun useEffect_createdThenReleased_callbacksAreInvoked() {
    class UseEffectComponent(val useEffectCalls: MutableList<String>) : KComponent() {
      override fun ComponentScope.render(): Component? {
        useEffect(Unit) {
          useEffectCalls.add("attach")
          onCleanup { useEffectCalls.add("detach") }
        }
        return Row()
      }
    }

    val useEffectCalls = mutableListOf<String>()
    lithoViewRule
        .setSizeSpecs(exactly(100), exactly(100))
        .setRoot { UseEffectComponent(useEffectCalls) }
        .attachToWindow()
        .measure()
        .layout()
        .detachFromWindow()
        .release()

    assertThat(useEffectCalls).containsExactly("attach", "detach")
  }

  @Test
  fun useEffect_anyParam_callbacksAreInvokedOnUpdate() {
    class UseEffectComponent(val useEffectCalls: MutableList<String>, val seq: Int) : KComponent() {
      override fun ComponentScope.render(): Component? {
        useEffect(Any()) {
          useEffectCalls.add("attach $seq")
          onCleanup { useEffectCalls.add("detach $seq") }
        }
        return Row()
      }
    }

    val useEffectCalls = mutableListOf<String>()
    lithoViewRule
        .setSizeSpecs(exactly(100), exactly(100))
        .setRoot { UseEffectComponent(useEffectCalls, seq = 0) }
        .attachToWindow()
        .measure()
        .layout()

    assertThat(useEffectCalls).containsExactly("attach 0")
    useEffectCalls.clear()

    lithoViewRule.setRoot(UseEffectComponent(useEffectCalls, seq = 1))

    assertThat(useEffectCalls).containsExactly("detach 0", "attach 1")
    useEffectCalls.clear()

    lithoViewRule.release()

    assertThat(useEffectCalls).containsExactly("detach 1")
  }

  @Test
  fun useEffect_withDeps_callbacksAreInvokedOnlyIfDepsChange() {
    class UseEffectComponent(val useEffectCalls: MutableList<String>, val dep: Int, val seq: Int) :
        KComponent() {
      override fun ComponentScope.render(): Component? {
        useEffect(dep) {
          useEffectCalls.add("attach $seq")
          onCleanup { useEffectCalls.add("detach $seq") }
        }
        return Row()
      }
    }

    val useEffectCalls = mutableListOf<String>()
    lithoViewRule
        .setSizeSpecs(exactly(100), exactly(100))
        .setRoot { UseEffectComponent(useEffectCalls, dep = 0, seq = 0) }
        .attachToWindow()
        .measure()
        .layout()

    assertThat(useEffectCalls).containsExactly("attach 0")
    useEffectCalls.clear()

    lithoViewRule.setRoot(UseEffectComponent(useEffectCalls, dep = 0, seq = 1))

    assertThat(useEffectCalls).isEmpty()
    useEffectCalls.clear()

    lithoViewRule.setRoot(UseEffectComponent(useEffectCalls, dep = 1, seq = 2))

    assertThat(useEffectCalls).containsExactly("detach 0", "attach 2")
    useEffectCalls.clear()

    lithoViewRule.release()

    assertThat(useEffectCalls).containsExactly("detach 2")
  }

  @Test
  fun useEffect_withNullableDeps_callbacksAreInvokedOnlyIfDepsChange() {
    class UseEffectComponent(val useEffectCalls: MutableList<String>, val dep: Int?, val seq: Int) :
        KComponent() {
      override fun ComponentScope.render(): Component? {
        useEffect(dep) {
          useEffectCalls.add("attach $seq")
          onCleanup { useEffectCalls.add("detach $seq") }
        }
        return Row()
      }
    }

    val useEffectCalls = mutableListOf<String>()

    lithoViewRule.render { UseEffectComponent(useEffectCalls, dep = null, seq = 0) }
    assertThat(useEffectCalls).containsExactly("attach 0")
    useEffectCalls.clear()

    lithoViewRule.render { UseEffectComponent(useEffectCalls, dep = null, seq = 1) }
    assertThat(useEffectCalls).isEmpty()
    useEffectCalls.clear()

    lithoViewRule.render { UseEffectComponent(useEffectCalls, dep = 1, seq = 2) }
    assertThat(useEffectCalls).containsExactly("detach 0", "attach 2")
    useEffectCalls.clear()

    lithoViewRule.render { UseEffectComponent(useEffectCalls, dep = null, seq = 3) }
    assertThat(useEffectCalls).containsExactly("detach 2", "attach 3")
    useEffectCalls.clear()

    lithoViewRule.release()
    assertThat(useEffectCalls).containsExactly("detach 3")
  }

  @Test
  fun useEffect_withMultipleDeps_callbacksAreInvokedIfAnyDepsChange() {
    class UseEffectComponent(
        val useEffectCalls: MutableList<String>,
        val dep1: Int,
        val dep2: Int,
        val seq: Int,
    ) : KComponent() {
      override fun ComponentScope.render(): Component? {
        useEffect(dep1, dep2) {
          useEffectCalls.add("attach $dep1:$dep2:$seq")
          onCleanup { useEffectCalls.add("detach $dep1:$dep2:$seq") }
        }
        return Row()
      }
    }

    val useEffectCalls = mutableListOf<String>()
    lithoViewRule
        .setSizeSpecs(exactly(100), exactly(100))
        .setRoot { UseEffectComponent(useEffectCalls, dep1 = 0, dep2 = 0, seq = 0) }
        .attachToWindow()
        .measure()
        .layout()

    assertThat(useEffectCalls).containsExactly("attach 0:0:0")
    useEffectCalls.clear()

    lithoViewRule.setRoot(UseEffectComponent(useEffectCalls, dep1 = 0, dep2 = 0, seq = 1))

    assertThat(useEffectCalls).isEmpty()
    useEffectCalls.clear()

    lithoViewRule.setRoot(UseEffectComponent(useEffectCalls, dep1 = 0, dep2 = 1, seq = 2))

    assertThat(useEffectCalls).containsExactly("detach 0:0:0", "attach 0:1:2")
    useEffectCalls.clear()

    lithoViewRule.setRoot(UseEffectComponent(useEffectCalls, dep1 = 1, dep2 = 1, seq = 3))

    assertThat(useEffectCalls).containsExactly("detach 0:1:2", "attach 1:1:3")
    useEffectCalls.clear()

    lithoViewRule.setRoot(UseEffectComponent(useEffectCalls, dep1 = 2, dep2 = 2, seq = 4))

    assertThat(useEffectCalls).containsExactly("detach 1:1:3", "attach 2:2:4")
    useEffectCalls.clear()

    lithoViewRule.release()

    assertThat(useEffectCalls).containsExactly("detach 2:2:4")
  }

  @Test
  fun useEffect_multipleUseEffectCalls_callbacksAreInvokeIndependently() {
    class UseEffectComponent(
        val useEffectCalls: MutableList<String>,
        val dep1: Int,
        val dep2: Int,
        val dep3: Int,
        val seq: Int,
    ) : KComponent() {
      override fun ComponentScope.render(): Component? {
        useEffect(dep1) {
          useEffectCalls.add("dep1: attach $seq")
          onCleanup { useEffectCalls.add("dep1: detach $seq") }
        }
        useEffect(dep2) {
          useEffectCalls.add("dep2: attach $seq")
          onCleanup { useEffectCalls.add("dep2: detach $seq") }
        }
        useEffect(dep3) {
          useEffectCalls.add("dep3: attach $seq")
          onCleanup { useEffectCalls.add("dep3: detach $seq") }
        }
        return Row()
      }
    }

    val useEffectCalls = mutableListOf<String>()
    lithoViewRule
        .setSizeSpecs(exactly(100), exactly(100))
        .setRoot { UseEffectComponent(useEffectCalls, dep1 = 0, dep2 = 0, dep3 = 0, seq = 0) }
        .attachToWindow()
        .measure()
        .layout()

    assertThat(useEffectCalls).containsExactly("dep1: attach 0", "dep2: attach 0", "dep3: attach 0")
    useEffectCalls.clear()

    lithoViewRule.setRoot(UseEffectComponent(useEffectCalls, dep1 = 0, dep2 = 1, dep3 = 0, seq = 1))

    assertThat(useEffectCalls).containsExactly("dep2: detach 0", "dep2: attach 1")
    useEffectCalls.clear()

    lithoViewRule.setRoot(UseEffectComponent(useEffectCalls, dep1 = 1, dep2 = 1, dep3 = 1, seq = 2))

    assertThat(useEffectCalls)
        .containsExactly("dep1: detach 0", "dep1: attach 2", "dep3: detach 0", "dep3: attach 2")
    useEffectCalls.clear()

    lithoViewRule.setRoot(UseEffectComponent(useEffectCalls, dep1 = 1, dep2 = 1, dep3 = 1, seq = 3))

    assertThat(useEffectCalls).isEmpty()
    useEffectCalls.clear()

    lithoViewRule.release()

    assertThat(useEffectCalls).containsExactly("dep1: detach 2", "dep2: detach 1", "dep3: detach 2")
  }

  @Test
  fun useEffect_unitParam_onUpdate_callbacksOnlyInvokedOnRelease() {
    class UseEffectComponent(
        val useEffectCalls: MutableList<String>,
        val seq: Int,
    ) : KComponent() {
      override fun ComponentScope.render(): Component? {
        useEffect(Unit) {
          useEffectCalls.add("attach $seq")
          onCleanup { useEffectCalls.add("detach $seq") }
        }
        return Row()
      }
    }

    val useEffectCalls = mutableListOf<String>()
    lithoViewRule
        .setSizeSpecs(exactly(100), exactly(100))
        .setRoot { UseEffectComponent(useEffectCalls, seq = 0) }
        .attachToWindow()
        .measure()
        .layout()

    assertThat(useEffectCalls).containsExactly("attach 0")
    useEffectCalls.clear()

    lithoViewRule.setRoot(UseEffectComponent(useEffectCalls, seq = 1))

    assertThat(useEffectCalls).isEmpty()
    useEffectCalls.clear()

    lithoViewRule.release()

    assertThat(useEffectCalls).containsExactly("detach 0")
  }

  @Test
  fun useEffect_createdThenReleased_callbacksAreInvoked_forMountables() {
    class UseEffectComponent(val useEffectCalls: MutableList<String>) : MountableComponent() {
      override fun MountableComponentScope.render(): MountableRenderResult {
        useEffect(Unit) {
          useEffectCalls.add("attach")
          onCleanup { useEffectCalls.add("detach") }
        }
        return MountableRenderResult(TestMountable(), null)
      }
    }

    val useEffectCalls = mutableListOf<String>()
    lithoViewRule
        .setSizeSpecs(exactly(100), exactly(100))
        .setRoot { UseEffectComponent(useEffectCalls) }
        .attachToWindow()
        .measure()
        .layout()
        .detachFromWindow()
        .release()

    assertThat(useEffectCalls).containsExactly("attach", "detach")
  }

  @Test
  fun useEffect_createdThenReleased_callbacksAreInvoked_forPrimitives() {
    class UseEffectComponent(val useEffectCalls: MutableList<String>) : PrimitiveComponent() {
      override fun PrimitiveComponentScope.render(): LithoPrimitive {
        useEffect(Unit) {
          useEffectCalls.add("attach")
          onCleanup { useEffectCalls.add("detach") }
        }
        return LithoPrimitive(TestPrimitive(), null)
      }
    }

    val useEffectCalls = mutableListOf<String>()
    lithoViewRule
        .setSizeSpecs(exactly(100), exactly(100))
        .setRoot { UseEffectComponent(useEffectCalls) }
        .attachToWindow()
        .measure()
        .layout()
        .detachFromWindow()
        .release()

    assertThat(useEffectCalls).containsExactly("attach", "detach")
  }

  @Test
  fun useEffect_anyParams_callbacksAreInvokedOnUpdate_forMountable() {
    class UseEffectComponent(val useEffectCalls: MutableList<String>, val seq: Int) :
        MountableComponent() {
      override fun MountableComponentScope.render(): MountableRenderResult {
        useEffect(Any()) {
          useEffectCalls.add("attach $seq")
          onCleanup { useEffectCalls.add("detach $seq") }
        }
        return MountableRenderResult(TestMountable(), null)
      }
    }

    val useEffectCalls = mutableListOf<String>()
    lithoViewRule
        .setSizeSpecs(exactly(100), exactly(100))
        .setRoot { UseEffectComponent(useEffectCalls, seq = 0) }
        .attachToWindow()
        .measure()
        .layout()

    assertThat(useEffectCalls).containsExactly("attach 0")
    useEffectCalls.clear()

    lithoViewRule.setRoot(UseEffectComponent(useEffectCalls, seq = 1))

    assertThat(useEffectCalls).containsExactly("detach 0", "attach 1")
    useEffectCalls.clear()

    lithoViewRule.release()

    assertThat(useEffectCalls).containsExactly("detach 1")
  }

  @Test
  fun useEffect_noParams_callbacksAreInvokedOnUpdate_forPrimitive() {
    class UseEffectComponent(val useEffectCalls: MutableList<String>, val seq: Int) :
        PrimitiveComponent() {
      override fun PrimitiveComponentScope.render(): LithoPrimitive {
        useEffect(Any()) {
          useEffectCalls.add("attach $seq")
          onCleanup { useEffectCalls.add("detach $seq") }
        }
        return LithoPrimitive(TestPrimitive(), null)
      }
    }

    val useEffectCalls = mutableListOf<String>()
    lithoViewRule
        .setSizeSpecs(exactly(100), exactly(100))
        .setRoot { UseEffectComponent(useEffectCalls, seq = 0) }
        .attachToWindow()
        .measure()
        .layout()

    assertThat(useEffectCalls).containsExactly("attach 0")
    useEffectCalls.clear()

    lithoViewRule.setRoot(UseEffectComponent(useEffectCalls, seq = 1))

    assertThat(useEffectCalls).containsExactly("detach 0", "attach 1")
    useEffectCalls.clear()

    lithoViewRule.release()

    assertThat(useEffectCalls).containsExactly("detach 1")
  }

  @Test
  fun useEffect_withDeps_callbacksAreInvokedOnlyIfDepsChange_forMountable() {
    class UseEffectComponent(val useEffectCalls: MutableList<String>, val dep: Int, val seq: Int) :
        MountableComponent() {
      override fun MountableComponentScope.render(): MountableRenderResult {
        useEffect(dep) {
          useEffectCalls.add("attach $seq")
          onCleanup { useEffectCalls.add("detach $seq") }
        }
        return MountableRenderResult(TestMountable(), null)
      }
    }

    val useEffectCalls = mutableListOf<String>()
    lithoViewRule
        .setSizeSpecs(exactly(100), exactly(100))
        .setRoot { UseEffectComponent(useEffectCalls, dep = 0, seq = 0) }
        .attachToWindow()
        .measure()
        .layout()

    assertThat(useEffectCalls).containsExactly("attach 0")
    useEffectCalls.clear()

    lithoViewRule.setRoot(UseEffectComponent(useEffectCalls, dep = 0, seq = 1))

    assertThat(useEffectCalls).isEmpty()
    useEffectCalls.clear()

    lithoViewRule.setRoot(UseEffectComponent(useEffectCalls, dep = 1, seq = 2))

    assertThat(useEffectCalls).containsExactly("detach 0", "attach 2")
    useEffectCalls.clear()

    lithoViewRule.release()

    assertThat(useEffectCalls).containsExactly("detach 2")
  }

  @Test
  fun useEffect_withDeps_callbacksAreInvokedOnlyIfDepsChange_forPrimitive() {
    class UseEffectComponent(val useEffectCalls: MutableList<String>, val dep: Int, val seq: Int) :
        PrimitiveComponent() {
      override fun PrimitiveComponentScope.render(): LithoPrimitive {
        useEffect(dep) {
          useEffectCalls.add("attach $seq")
          onCleanup { useEffectCalls.add("detach $seq") }
        }
        return LithoPrimitive(TestPrimitive(), null)
      }
    }

    val useEffectCalls = mutableListOf<String>()
    lithoViewRule
        .setSizeSpecs(exactly(100), exactly(100))
        .setRoot { UseEffectComponent(useEffectCalls, dep = 0, seq = 0) }
        .attachToWindow()
        .measure()
        .layout()

    assertThat(useEffectCalls).containsExactly("attach 0")
    useEffectCalls.clear()

    lithoViewRule.setRoot(UseEffectComponent(useEffectCalls, dep = 0, seq = 1))

    assertThat(useEffectCalls).isEmpty()
    useEffectCalls.clear()

    lithoViewRule.setRoot(UseEffectComponent(useEffectCalls, dep = 1, seq = 2))

    assertThat(useEffectCalls).containsExactly("detach 0", "attach 2")
    useEffectCalls.clear()

    lithoViewRule.release()

    assertThat(useEffectCalls).containsExactly("detach 2")
  }

  @Test
  fun useEffect_withNullableDeps_callbacksAreInvokedOnlyIfDepsChange_forMountable() {
    class UseEffectComponent(val useEffectCalls: MutableList<String>, val dep: Int?, val seq: Int) :
        MountableComponent() {
      override fun MountableComponentScope.render(): MountableRenderResult {
        useEffect(dep) {
          useEffectCalls.add("attach $seq")
          onCleanup { useEffectCalls.add("detach $seq") }
        }
        return MountableRenderResult(TestMountable(), null)
      }
    }

    val useEffectCalls = mutableListOf<String>()

    lithoViewRule.render { UseEffectComponent(useEffectCalls, dep = null, seq = 0) }
    assertThat(useEffectCalls).containsExactly("attach 0")
    useEffectCalls.clear()

    lithoViewRule.render { UseEffectComponent(useEffectCalls, dep = null, seq = 1) }
    assertThat(useEffectCalls).isEmpty()
    useEffectCalls.clear()

    lithoViewRule.render { UseEffectComponent(useEffectCalls, dep = 1, seq = 2) }
    assertThat(useEffectCalls).containsExactly("detach 0", "attach 2")
    useEffectCalls.clear()

    lithoViewRule.render { UseEffectComponent(useEffectCalls, dep = null, seq = 3) }
    assertThat(useEffectCalls).containsExactly("detach 2", "attach 3")
    useEffectCalls.clear()

    lithoViewRule.release()
    assertThat(useEffectCalls).containsExactly("detach 3")
  }

  @Test
  fun useEffect_withNullableDeps_callbacksAreInvokedOnlyIfDepsChange_forPrimitive() {
    class UseEffectComponent(val useEffectCalls: MutableList<String>, val dep: Int?, val seq: Int) :
        PrimitiveComponent() {
      override fun PrimitiveComponentScope.render(): LithoPrimitive {
        useEffect(dep) {
          useEffectCalls.add("attach $seq")
          onCleanup { useEffectCalls.add("detach $seq") }
        }
        return LithoPrimitive(TestPrimitive(), null)
      }
    }

    val useEffectCalls = mutableListOf<String>()

    lithoViewRule.render { UseEffectComponent(useEffectCalls, dep = null, seq = 0) }
    assertThat(useEffectCalls).containsExactly("attach 0")
    useEffectCalls.clear()

    lithoViewRule.render { UseEffectComponent(useEffectCalls, dep = null, seq = 1) }
    assertThat(useEffectCalls).isEmpty()
    useEffectCalls.clear()

    lithoViewRule.render { UseEffectComponent(useEffectCalls, dep = 1, seq = 2) }
    assertThat(useEffectCalls).containsExactly("detach 0", "attach 2")
    useEffectCalls.clear()

    lithoViewRule.render { UseEffectComponent(useEffectCalls, dep = null, seq = 3) }
    assertThat(useEffectCalls).containsExactly("detach 2", "attach 3")
    useEffectCalls.clear()

    lithoViewRule.release()
    assertThat(useEffectCalls).containsExactly("detach 3")
  }

  @Test
  fun useEffect_withMultipleDeps_callbacksAreInvokedIfAnyDepsChange_forMountable() {
    class UseEffectComponent(
        val useEffectCalls: MutableList<String>,
        val dep1: Int,
        val dep2: Int,
        val seq: Int,
    ) : MountableComponent() {
      override fun MountableComponentScope.render(): MountableRenderResult {
        useEffect(dep1, dep2) {
          useEffectCalls.add("attach $dep1:$dep2:$seq")
          onCleanup { useEffectCalls.add("detach $dep1:$dep2:$seq") }
        }
        return MountableRenderResult(TestMountable(), null)
      }
    }

    val useEffectCalls = mutableListOf<String>()
    lithoViewRule
        .setSizeSpecs(exactly(100), exactly(100))
        .setRoot { UseEffectComponent(useEffectCalls, dep1 = 0, dep2 = 0, seq = 0) }
        .attachToWindow()
        .measure()
        .layout()

    assertThat(useEffectCalls).containsExactly("attach 0:0:0")
    useEffectCalls.clear()

    lithoViewRule.setRoot(UseEffectComponent(useEffectCalls, dep1 = 0, dep2 = 0, seq = 1))

    assertThat(useEffectCalls).isEmpty()
    useEffectCalls.clear()

    lithoViewRule.setRoot(UseEffectComponent(useEffectCalls, dep1 = 0, dep2 = 1, seq = 2))

    assertThat(useEffectCalls).containsExactly("detach 0:0:0", "attach 0:1:2")
    useEffectCalls.clear()

    lithoViewRule.setRoot(UseEffectComponent(useEffectCalls, dep1 = 1, dep2 = 1, seq = 3))

    assertThat(useEffectCalls).containsExactly("detach 0:1:2", "attach 1:1:3")
    useEffectCalls.clear()

    lithoViewRule.setRoot(UseEffectComponent(useEffectCalls, dep1 = 2, dep2 = 2, seq = 4))

    assertThat(useEffectCalls).containsExactly("detach 1:1:3", "attach 2:2:4")
    useEffectCalls.clear()

    lithoViewRule.release()

    assertThat(useEffectCalls).containsExactly("detach 2:2:4")
  }

  @Test
  fun useEffect_withMultipleDeps_callbacksAreInvokedIfAnyDepsChange_forPrimitive() {
    class UseEffectComponent(
        val useEffectCalls: MutableList<String>,
        val dep1: Int,
        val dep2: Int,
        val seq: Int,
    ) : PrimitiveComponent() {
      override fun PrimitiveComponentScope.render(): LithoPrimitive {
        useEffect(dep1, dep2) {
          useEffectCalls.add("attach $dep1:$dep2:$seq")
          onCleanup { useEffectCalls.add("detach $dep1:$dep2:$seq") }
        }
        return LithoPrimitive(TestPrimitive(), null)
      }
    }

    val useEffectCalls = mutableListOf<String>()
    lithoViewRule
        .setSizeSpecs(exactly(100), exactly(100))
        .setRoot { UseEffectComponent(useEffectCalls, dep1 = 0, dep2 = 0, seq = 0) }
        .attachToWindow()
        .measure()
        .layout()

    assertThat(useEffectCalls).containsExactly("attach 0:0:0")
    useEffectCalls.clear()

    lithoViewRule.setRoot(UseEffectComponent(useEffectCalls, dep1 = 0, dep2 = 0, seq = 1))

    assertThat(useEffectCalls).isEmpty()
    useEffectCalls.clear()

    lithoViewRule.setRoot(UseEffectComponent(useEffectCalls, dep1 = 0, dep2 = 1, seq = 2))

    assertThat(useEffectCalls).containsExactly("detach 0:0:0", "attach 0:1:2")
    useEffectCalls.clear()

    lithoViewRule.setRoot(UseEffectComponent(useEffectCalls, dep1 = 1, dep2 = 1, seq = 3))

    assertThat(useEffectCalls).containsExactly("detach 0:1:2", "attach 1:1:3")
    useEffectCalls.clear()

    lithoViewRule.setRoot(UseEffectComponent(useEffectCalls, dep1 = 2, dep2 = 2, seq = 4))

    assertThat(useEffectCalls).containsExactly("detach 1:1:3", "attach 2:2:4")
    useEffectCalls.clear()

    lithoViewRule.release()

    assertThat(useEffectCalls).containsExactly("detach 2:2:4")
  }

  @Test
  fun useEffect_multipleUseEffectCalls_callbacksAreInvokeIndependently_forMountable() {
    class UseEffectComponent(
        val useEffectCalls: MutableList<String>,
        val dep1: Int,
        val dep2: Int,
        val dep3: Int,
        val seq: Int,
    ) : MountableComponent() {
      override fun MountableComponentScope.render(): MountableRenderResult {
        useEffect(dep1) {
          useEffectCalls.add("dep1: attach $seq")
          onCleanup { useEffectCalls.add("dep1: detach $seq") }
        }
        useEffect(dep2) {
          useEffectCalls.add("dep2: attach $seq")
          onCleanup { useEffectCalls.add("dep2: detach $seq") }
        }
        useEffect(dep3) {
          useEffectCalls.add("dep3: attach $seq")
          onCleanup { useEffectCalls.add("dep3: detach $seq") }
        }
        return MountableRenderResult(TestMountable(), null)
      }
    }

    val useEffectCalls = mutableListOf<String>()
    lithoViewRule
        .setSizeSpecs(exactly(100), exactly(100))
        .setRoot { UseEffectComponent(useEffectCalls, dep1 = 0, dep2 = 0, dep3 = 0, seq = 0) }
        .attachToWindow()
        .measure()
        .layout()

    assertThat(useEffectCalls).containsExactly("dep1: attach 0", "dep2: attach 0", "dep3: attach 0")
    useEffectCalls.clear()

    lithoViewRule.setRoot(UseEffectComponent(useEffectCalls, dep1 = 0, dep2 = 1, dep3 = 0, seq = 1))

    assertThat(useEffectCalls).containsExactly("dep2: detach 0", "dep2: attach 1")
    useEffectCalls.clear()

    lithoViewRule.setRoot(UseEffectComponent(useEffectCalls, dep1 = 1, dep2 = 1, dep3 = 1, seq = 2))

    assertThat(useEffectCalls)
        .containsExactly("dep1: detach 0", "dep1: attach 2", "dep3: detach 0", "dep3: attach 2")
    useEffectCalls.clear()

    lithoViewRule.setRoot(UseEffectComponent(useEffectCalls, dep1 = 1, dep2 = 1, dep3 = 1, seq = 3))

    assertThat(useEffectCalls).isEmpty()
    useEffectCalls.clear()

    lithoViewRule.release()

    assertThat(useEffectCalls).containsExactly("dep1: detach 2", "dep2: detach 1", "dep3: detach 2")
  }

  @Test
  fun useEffect_multipleUseEffectCalls_callbacksAreInvokeIndependently_forPrimitive() {
    class UseEffectComponent(
        val useEffectCalls: MutableList<String>,
        val dep1: Int,
        val dep2: Int,
        val dep3: Int,
        val seq: Int,
    ) : PrimitiveComponent() {
      override fun PrimitiveComponentScope.render(): LithoPrimitive {
        useEffect(dep1) {
          useEffectCalls.add("dep1: attach $seq")
          onCleanup { useEffectCalls.add("dep1: detach $seq") }
        }
        useEffect(dep2) {
          useEffectCalls.add("dep2: attach $seq")
          onCleanup { useEffectCalls.add("dep2: detach $seq") }
        }
        useEffect(dep3) {
          useEffectCalls.add("dep3: attach $seq")
          onCleanup { useEffectCalls.add("dep3: detach $seq") }
        }
        return LithoPrimitive(TestPrimitive(), null)
      }
    }

    val useEffectCalls = mutableListOf<String>()
    lithoViewRule
        .setSizeSpecs(exactly(100), exactly(100))
        .setRoot { UseEffectComponent(useEffectCalls, dep1 = 0, dep2 = 0, dep3 = 0, seq = 0) }
        .attachToWindow()
        .measure()
        .layout()

    assertThat(useEffectCalls).containsExactly("dep1: attach 0", "dep2: attach 0", "dep3: attach 0")
    useEffectCalls.clear()

    lithoViewRule.setRoot(UseEffectComponent(useEffectCalls, dep1 = 0, dep2 = 1, dep3 = 0, seq = 1))

    assertThat(useEffectCalls).containsExactly("dep2: detach 0", "dep2: attach 1")
    useEffectCalls.clear()

    lithoViewRule.setRoot(UseEffectComponent(useEffectCalls, dep1 = 1, dep2 = 1, dep3 = 1, seq = 2))

    assertThat(useEffectCalls)
        .containsExactly("dep1: detach 0", "dep1: attach 2", "dep3: detach 0", "dep3: attach 2")
    useEffectCalls.clear()

    lithoViewRule.setRoot(UseEffectComponent(useEffectCalls, dep1 = 1, dep2 = 1, dep3 = 1, seq = 3))

    assertThat(useEffectCalls).isEmpty()
    useEffectCalls.clear()

    lithoViewRule.release()

    assertThat(useEffectCalls).containsExactly("dep1: detach 2", "dep2: detach 1", "dep3: detach 2")
  }

  @Test
  fun useEffect_unitParam_onUpdate_callbacksOnlyInvokedOnRelease_forMountable() {
    class UseEffectComponent(
        val useEffectCalls: MutableList<String>,
        val seq: Int,
    ) : MountableComponent() {
      override fun MountableComponentScope.render(): MountableRenderResult {
        useEffect(Unit) {
          useEffectCalls.add("attach $seq")
          onCleanup { useEffectCalls.add("detach $seq") }
        }
        return MountableRenderResult(TestMountable(), null)
      }
    }

    val useEffectCalls = mutableListOf<String>()
    lithoViewRule
        .setSizeSpecs(exactly(100), exactly(100))
        .setRoot { UseEffectComponent(useEffectCalls, seq = 0) }
        .attachToWindow()
        .measure()
        .layout()

    assertThat(useEffectCalls).containsExactly("attach 0")
    useEffectCalls.clear()

    lithoViewRule.setRoot(UseEffectComponent(useEffectCalls, seq = 1))

    assertThat(useEffectCalls).isEmpty()
    useEffectCalls.clear()

    lithoViewRule.release()

    assertThat(useEffectCalls).containsExactly("detach 0")
  }

  @Test
  fun useEffect_unitParam_onUpdate_callbacksOnlyInvokedOnRelease_forPrimitive() {
    class UseEffectComponent(
        val useEffectCalls: MutableList<String>,
        val seq: Int,
    ) : PrimitiveComponent() {
      override fun PrimitiveComponentScope.render(): LithoPrimitive {
        useEffect(Unit) {
          useEffectCalls.add("attach $seq")
          onCleanup { useEffectCalls.add("detach $seq") }
        }
        return LithoPrimitive(TestPrimitive(), null)
      }
    }

    val useEffectCalls = mutableListOf<String>()
    lithoViewRule
        .setSizeSpecs(exactly(100), exactly(100))
        .setRoot { UseEffectComponent(useEffectCalls, seq = 0) }
        .attachToWindow()
        .measure()
        .layout()

    assertThat(useEffectCalls).containsExactly("attach 0")
    useEffectCalls.clear()

    lithoViewRule.setRoot(UseEffectComponent(useEffectCalls, seq = 1))

    assertThat(useEffectCalls).isEmpty()
    useEffectCalls.clear()

    lithoViewRule.release()

    assertThat(useEffectCalls).containsExactly("detach 0")
  }

  @Test
  fun `effects should work with components that render to null`() {
    ComponentsConfiguration.isNullNodeEnabled = true

    var renderCount = 0
    var attachCount = 0
    var detachCount = 0
    val controller = TestCounterComponent.Controller()
    val component =
        TestCounterComponent(
            initialCount = 2,
            onRender = { renderCount++ },
            onAttach = { attachCount++ },
            onDetach = { detachCount++ },
            controller = controller,
        )

    lithoViewRule.render { Column { child(component) } }

    // first render
    assertThat(renderCount).isEqualTo(1)
    lithoViewRule.findViewWithText("count: 2")
    assertThat(attachCount).isEqualTo(1)
    assertThat(detachCount).isEqualTo(0)

    // state update
    controller.decrement!!()
    lithoViewRule.findViewWithText("count: 1")
    assertThat(renderCount).isEqualTo(2)
    assertThat(attachCount).isEqualTo(1)
    assertThat(detachCount).isEqualTo(0)

    // state update (renders to null)
    controller.decrement!!.invoke()
    assertThat(renderCount).isEqualTo(3)
    assertThat(attachCount).isEqualTo(1)
    assertThat(detachCount).isEqualTo(0)

    // state update
    controller.increment!!.invoke()
    lithoViewRule.findViewWithText("count: 1")
    assertThat(renderCount).isEqualTo(4)
    assertThat(attachCount).isEqualTo(1)
    assertThat(detachCount).isEqualTo(0)

    // remove component
    lithoViewRule.render { Text(text = "hello") }
    lithoViewRule.findViewWithText("hello")
    assertThat(renderCount).isEqualTo(4)
    assertThat(attachCount).isEqualTo(1)
    assertThat(detachCount).isEqualTo(1)

    ComponentsConfiguration.isNullNodeEnabled = false
  }
}

class TestCounterComponent(
    private val initialCount: Int = 0,
    private val onRender: () -> Unit,
    private val onAttach: () -> Unit,
    private val onDetach: () -> Unit,
    private val controller: Controller,
) : KComponent() {
  override fun ComponentScope.render(): Component? {

    val count = useState { initialCount }

    onRender()

    useEffect(onAttach, onDetach, controller) {
      onAttach()
      controller.increment = { count.updateSync { count -> count + 1 } }
      controller.decrement = { count.updateSync { count -> count - 1 } }
      CleanupFunc {
        onDetach()
        controller.increment = null
        controller.decrement = null
      }
    }

    return if (count.value > 0) {
      Text("count: ${count.value}")
    } else {
      null
    }
  }

  class Controller {
    var increment: (() -> Unit)? = null
    var decrement: (() -> Unit)? = null
  }
}

internal class TestMountable : SimpleMountable<ColorDrawable>(RenderType.DRAWABLE) {

  override fun createContent(context: Context): ColorDrawable = ColorDrawable(Color.RED)

  override fun MeasureScope.measure(widthSpec: Int, heightSpec: Int): MeasureResult =
      MeasureResult(100, 100)

  override fun mount(c: Context, content: ColorDrawable, layoutData: Any?) = Unit

  override fun unmount(c: Context, content: ColorDrawable, layoutData: Any?) = Unit
}

@Suppress("TestFunctionName")
internal fun PrimitiveComponentScope.TestPrimitive(): Primitive {
  return Primitive(
      layoutBehavior = FixedSizeLayoutBehavior(100.px, 100.px),
      mountBehavior = MountBehavior(DrawableAllocator { ColorDrawable(Color.RED) }) {})
}


/*
 * 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.kotlin.widget.Text
import com.facebook.litho.testing.LithoViewRule
import com.facebook.litho.testing.testrunner.LithoTestRunner
import java.lang.Exception
import java.lang.RuntimeException
import java.util.concurrent.atomic.AtomicReference
import org.assertj.core.api.Assertions.assertThat
import org.junit.Rule
import org.junit.Test
import org.junit.rules.ExpectedException
import org.junit.runner.RunWith
import org.robolectric.annotation.LooperMode

/** Unit tests for [useError]. */
@LooperMode(LooperMode.Mode.LEGACY)
@RunWith(LithoTestRunner::class)
class KErrorTest {

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

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

    class CrashingKComponent : KComponent() {
      override fun ComponentScope.render(): Component? {
        throw RuntimeException("crash from kotlin component")
      }
    }

    class UseErrorComponent : KComponent() {
      override fun ComponentScope.render(): Component? {
        val errorState = useState { listOf<Exception>() }
        stateRef = AtomicReference(errorState.value)
        useErrorBoundary { exception: Exception ->
          assertThat(exception.message).contains("crash from kotlin component")
          errorState.update { prevErrors -> prevErrors + listOf(exception) }
        }

        return if (errorState.value.isEmpty()) CrashingKComponent() else Text("error caught")
      }
    }

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

    val errorList = stateRef.get()
    assertThat(errorList.size).isEqualTo(1)
    assertThat(errorList.get(0).message).isEqualTo("crash from kotlin component")
  }

  @Test
  fun useError_noErrorHandlingSoErrorNotHandled() {
    expectedException.expect(RuntimeException::class.java)
    expectedException.expectMessage("crash from kotlin component")

    class CrashingKComponent : KComponent() {
      override fun ComponentScope.render(): Component? {
        throw RuntimeException("crash from kotlin component")
      }
    }

    class UseErrorComponent : KComponent() {
      override fun ComponentScope.render(): Component? {
        // useError not implemented, so no error handling
        return CrashingKComponent()
      }
    }

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

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

    class CrashingKComponent : KComponent() {
      override fun ComponentScope.render(): Component? {
        throw RuntimeException("crash from kotlin component 2 levels down")
      }
    }

    class IntermediaryKComponent : KComponent() {
      override fun ComponentScope.render(): Component? {
        return CrashingKComponent()
      }
    }

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

    lithoViewRule.render { UseErrorComponent() }
    lithoViewRule.idle()
    val errorList = stateRef.get()
    assertThat(errorList.size).isEqualTo(1)
    assertThat(errorList.get(0).message).isEqualTo("crash from kotlin component 2 levels down")
  }

  @Test
  fun useError_noErrorNoHandling() {
    class NoncrashingKComponent : KComponent() {
      override fun ComponentScope.render(): Component? {
        return Row()
      }
    }

    class UseErrorComponent : KComponent() {
      override fun ComponentScope.render(): Component? {
        useErrorBoundary { exception: Exception ->
          throw RuntimeException("There was no error, so why has useError run?!")
        }
        return NoncrashingKComponent()
      }
    }

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

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

    class CrashingKComponent : KComponent() {
      override fun ComponentScope.render(): Component? {
        useState { throw RuntimeException("crash from kotlin component's state") }
        return Row()
      }
    }

    class UseErrorComponent : KComponent() {
      override fun ComponentScope.render(): Component? {
        val errorState = useState { listOf<Exception>() }
        stateRef = AtomicReference(errorState.value)
        useErrorBoundary { exception: Exception ->
          assertThat(exception.message).contains("crash from kotlin component's state")
          errorState.update { prevErrors -> prevErrors + listOf(exception) }
        }

        return if (errorState.value.isEmpty()) CrashingKComponent() else Text("error caught")
      }
    }

    lithoViewRule.render { UseErrorComponent() }
    lithoViewRule.idle()
    val errorList = stateRef.get()
    assertThat(errorList.size).isEqualTo(1)
    assertThat(errorList.get(0).message).isEqualTo("crash from kotlin component's state")
  }
}


/*
 * 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 org.assertj.core.api.Assertions.assertThat
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.JUnit4

/** Unit tests for [KEventHandler]. */
@RunWith(JUnit4::class)
class KEventHandlerTest {

  @Test
  fun isEquivalentTo_handlersWithDifferentLambdas_returnsFalse() {
    val eh1 = eventHandler<Any> { println("A") }
    val eh2 = eventHandler<Any> { println("B") }

    assertThat(eh1.isEquivalentTo(eh2)).isFalse()
  }

  @Test
  fun isEquivalentTo_handlersWithSameLambda_returnsTrue() {
    val onEvent: (Any) -> Unit = { println("A") }

    val eh1 = eventHandler(onEvent)
    val eh2 = eventHandler(onEvent)

    assertThat(eh1.isEquivalentTo(eh2)).isTrue()
  }

  private fun onEventMethod(e: Any) {
    println("A")
  }

  @Test
  fun isEquivalentTo_handlersWithSameMethod_returnsTrue() {
    val eh1 = eventHandler(::onEventMethod)
    val eh2 = eventHandler(::onEventMethod)

    assertThat(eh1.isEquivalentTo(eh2)).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.kotlin.widget.Text
import com.facebook.litho.testing.LithoViewRule
import com.facebook.litho.testing.assertj.LithoViewAssert
import com.facebook.litho.testing.testrunner.LithoTestRunner
import com.facebook.litho.view.onClick
import com.facebook.litho.view.viewTag
import java.util.concurrent.atomic.AtomicInteger
import org.assertj.core.api.Assertions
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.annotation.LooperMode
import org.robolectric.shadows.ShadowLooper

/** Unit tests for skipping state updates if value won't change. */
@LooperMode(LooperMode.Mode.LEGACY)
@RunWith(LithoTestRunner::class)
class KStateSkipSameValueTest {
  @Rule @JvmField val lithoViewRule = LithoViewRule()

  @Test
  fun `skip state update if new value is the same as old value during layout`() {
    ShadowLooper.pauseMainLooper()

    val renderCount = AtomicInteger()

    class RootComponent(private val renderCount: AtomicInteger) : KComponent() {
      override fun ComponentScope.render(): Component {
        renderCount.incrementAndGet()
        val state = useState { false }

        // unconditional state update
        state.updateSync(true)

        return Row { child(Text(text = "hello world")) }
      }
    }

    lithoViewRule.render { RootComponent(renderCount = renderCount) }
    for (i in 0..10) {
      ShadowLooper.runMainLooperOneTask()
    }

    Assertions.assertThat(renderCount.get()).isEqualTo(2)
  }

  @Test
  fun `skip state update if nullable new value is the same as old value during layout`() {
    ShadowLooper.pauseMainLooper()

    val renderCount = AtomicInteger()
    val stateSampleString: String? = "sample string"

    class RootComponent(private val renderCount: AtomicInteger) : KComponent() {
      override fun ComponentScope.render(): Component {
        renderCount.incrementAndGet()
        val state = useState { stateSampleString }

        // unconditional state update
        state.updateSync(null)

        return Row { child(Text(text = "hello world")) }
      }
    }

    lithoViewRule.render { RootComponent(renderCount = renderCount) }
    for (i in 0..10) {
      ShadowLooper.runMainLooperOneTask()
    }

    Assertions.assertThat(renderCount.get()).isEqualTo(2)
  }

  @Test
  fun `skip state update if new value object is the same as old value during layout`() {
    ShadowLooper.pauseMainLooper()

    val renderCount = AtomicInteger()

    class RootComponent(private val renderCount: AtomicInteger) : KComponent() {
      override fun ComponentScope.render(): Component {
        renderCount.incrementAndGet()
        val state = useState { Person("Joe") }

        // unconditional state update
        state.updateSync(Person("Joe"))

        return Row { child(Text(text = "hello world")) }
      }
    }

    lithoViewRule.render { RootComponent(renderCount = renderCount) }
    for (i in 0..10) {
      ShadowLooper.runMainLooperOneTask()
    }

    Assertions.assertThat(renderCount.get()).isEqualTo(2)
  }

  @Test
  fun `skip async state update if new value is the same as old value`() {
    val renderCount = AtomicInteger()

    class RootComponent(private val renderCount: AtomicInteger) : KComponent() {
      override fun ComponentScope.render(): Component {
        renderCount.incrementAndGet()
        val state = useState { false }

        // unconditional state update
        state.update(true)

        return Row { child(Text(text = "hello world")) }
      }
    }

    lithoViewRule.render { RootComponent(renderCount = renderCount) }
    lithoViewRule.idle()

    Assertions.assertThat(renderCount.get()).isEqualTo(2)
  }

  @Test
  fun `skip async state update if nullable new value is the same as old value`() {
    val renderCount = AtomicInteger()

    val stateSampleString: String? = "sample string"

    class RootComponent(private val renderCount: AtomicInteger) : KComponent() {
      override fun ComponentScope.render(): Component {
        renderCount.incrementAndGet()
        val state = useState { stateSampleString }

        // unconditional state update
        state.update(null)

        return Row { child(Text(text = "hello world")) }
      }
    }

    lithoViewRule.render { RootComponent(renderCount = renderCount) }
    lithoViewRule.idle()

    Assertions.assertThat(renderCount.get()).isEqualTo(2)
  }

  @Test
  fun `skip async state update if new value object reference is the same as old value`() {
    val renderCount = AtomicInteger()

    class RootComponent(private val renderCount: AtomicInteger) : KComponent() {
      override fun ComponentScope.render(): Component {
        renderCount.incrementAndGet()
        val state = useState { Person("Joe") }

        // unconditional state update
        state.update(Person("Joe"))

        return Row { child(Text(text = "hello world")) }
      }
    }

    lithoViewRule.render { RootComponent(renderCount = renderCount) }
    lithoViewRule.idle()

    Assertions.assertThat(renderCount.get()).isEqualTo(2)
  }

  @Test
  fun `skip sync state update triggered from outside layout if value is same`() {
    ShadowLooper.pauseMainLooper()
    val renderCount = AtomicInteger()
    val listeners = mutableListOf<Listener>()

    class RootComponent : KComponent() {
      override fun ComponentScope.render(): Component {
        renderCount.incrementAndGet()
        val state = useState { false }

        val listener1 =
            useCached(state) {
              object : Listener {
                override fun onTriggerListener() {
                  state.updateSync(true)
                }
              }
            }

        useEffect(listener1) {
          listeners.add(listener1)
          onCleanup { listeners.remove(listener1) }
        }

        return Row { child(Text(text = "hello world")) }
      }
    }

    lithoViewRule.render { RootComponent() }

    for (i in 0..10) {
      for (listener in listeners) {
        listener.onTriggerListener()
      }
    }

    for (i in 0..10) {
      ShadowLooper.runMainLooperOneTask()
    }

    Assertions.assertThat(renderCount.get()).isEqualTo(2)
  }

  @Test
  fun `do not skip state update if new updates were enqueued during layout`() {
    ShadowLooper.pauseMainLooper()
    val renderCount = AtomicInteger()

    class RootComponent(private val renderCount: AtomicInteger) : KComponent() {
      override fun ComponentScope.render(): Component {
        renderCount.incrementAndGet()
        val state = useState { false }

        // unconditional state update
        if (renderCount.get() == 1) {
          state.updateSync(true)
          state.updateSync(false)
        }

        return Row { child(Text(text = "State is ${state.value}")) }
      }
    }

    val lithoView = lithoViewRule.render { RootComponent(renderCount = renderCount) }
    for (i in 0..10) {
      ShadowLooper.runMainLooperOneTask()
    }

    LithoViewAssert.assertThat(lithoView.lithoView).hasVisibleText("State is false")
  }

  @Test
  fun `do not skip state update if new updates were enqueued outside layout`() {
    ShadowLooper.pauseMainLooper()
    val renderCount = AtomicInteger()

    class RootComponent(private val renderCount: AtomicInteger) : KComponent() {
      override fun ComponentScope.render(): Component {
        renderCount.incrementAndGet()
        val state = useState { false }

        return Row(
            style =
                Style.viewTag("clickTag").onClick {
                  state.updateSync(true)
                  state.updateSync(false)
                }) {
              child(Text(text = "State is ${state.value}"))
            }
      }
    }

    val lithoView = lithoViewRule.render { RootComponent(renderCount = renderCount) }
    lithoViewRule.act(lithoView) { clickOnTag("clickTag") }
    for (i in 0..10) {
      ShadowLooper.runMainLooperOneTask()
    }

    LithoViewAssert.assertThat(lithoView.lithoView).hasVisibleText("State is false")
  }

  // Same tests as above but with lambda state updates.

  @Test
  fun `skip lambda state update if new value is the same as old value during layout`() {
    ShadowLooper.pauseMainLooper()

    val renderCount = AtomicInteger()

    class RootComponent(private val renderCount: AtomicInteger) : KComponent() {
      override fun ComponentScope.render(): Component {
        renderCount.incrementAndGet()
        val state = useState { false }

        // unconditional state update
        state.updateSync { true }

        return Row { child(Text(text = "hello world")) }
      }
    }

    lithoViewRule.render { RootComponent(renderCount = renderCount) }
    for (i in 0..10) {
      ShadowLooper.runMainLooperOneTask()
    }

    Assertions.assertThat(renderCount.get()).isEqualTo(2)
  }

  @Test
  fun `skip lambda state update if new nullable value is the same as old value during layout`() {
    ShadowLooper.pauseMainLooper()

    val renderCount = AtomicInteger()
    val stateSampleString: String? = "sample string"

    class RootComponent(private val renderCount: AtomicInteger) : KComponent() {
      override fun ComponentScope.render(): Component {
        renderCount.incrementAndGet()
        val state = useState { stateSampleString }

        // unconditional state update
        state.updateSync { null }

        return Row { child(Text(text = "hello world")) }
      }
    }

    lithoViewRule.render { RootComponent(renderCount = renderCount) }
    for (i in 0..10) {
      ShadowLooper.runMainLooperOneTask()
    }

    Assertions.assertThat(renderCount.get()).isEqualTo(2)
  }

  @Test
  fun `skip lambda state update if new object is the same as old value during layout`() {
    ShadowLooper.pauseMainLooper()

    val renderCount = AtomicInteger()

    class RootComponent(private val renderCount: AtomicInteger) : KComponent() {
      override fun ComponentScope.render(): Component {
        renderCount.incrementAndGet()
        val state = useState { Person("Joe") }

        // unconditional state update
        state.updateSync { Person("Joe") }

        return Row { child(Text(text = "hello world")) }
      }
    }

    lithoViewRule.render { RootComponent(renderCount = renderCount) }
    for (i in 0..10) {
      ShadowLooper.runMainLooperOneTask()
    }

    Assertions.assertThat(renderCount.get()).isEqualTo(2)
  }

  @Test
  fun `skip lambda async state update if new value is the same as old value`() {
    val renderCount = AtomicInteger()

    class RootComponent(private val renderCount: AtomicInteger) : KComponent() {
      override fun ComponentScope.render(): Component {
        renderCount.incrementAndGet()
        val state = useState { false }

        // unconditional state update
        state.update { true }

        return Row { child(Text(text = "hello world")) }
      }
    }

    lithoViewRule.render { RootComponent(renderCount = renderCount) }
    lithoViewRule.idle()

    Assertions.assertThat(renderCount.get()).isEqualTo(2)
  }

  @Test
  fun `skip lambda async state update if new nullable value is the same as old value`() {
    val renderCount = AtomicInteger()
    val stateSampleString: String? = "sample string"

    class RootComponent(private val renderCount: AtomicInteger) : KComponent() {
      override fun ComponentScope.render(): Component {
        renderCount.incrementAndGet()
        val state = useState { stateSampleString }

        // unconditional state update
        state.update { null }

        return Row { child(Text(text = "hello world")) }
      }
    }

    lithoViewRule.render { RootComponent(renderCount = renderCount) }
    lithoViewRule.idle()

    Assertions.assertThat(renderCount.get()).isEqualTo(2)
  }

  @Test
  fun `skip lambda async state update if new object value is the same as old value`() {
    val renderCount = AtomicInteger()

    class RootComponent(private val renderCount: AtomicInteger) : KComponent() {
      override fun ComponentScope.render(): Component {
        renderCount.incrementAndGet()
        val state = useState { Person("Joe") }

        // unconditional state update
        state.update { Person("Joe") }

        return Row { child(Text(text = "hello world")) }
      }
    }

    lithoViewRule.render { RootComponent(renderCount = renderCount) }
    lithoViewRule.idle()

    Assertions.assertThat(renderCount.get()).isEqualTo(2)
  }

  @Test
  fun `skip lambda sync state update triggered from outside layout if value is same`() {
    ShadowLooper.pauseMainLooper()
    val renderCount = AtomicInteger()
    val listeners = mutableListOf<Listener>()

    class RootComponent : KComponent() {
      override fun ComponentScope.render(): Component {
        renderCount.incrementAndGet()
        val state = useState { false }

        val listener1 =
            useCached(state) {
              object : Listener {
                override fun onTriggerListener() {
                  state.updateSync { true }
                }
              }
            }

        useEffect(listener1) {
          listeners.add(listener1)
          onCleanup { listeners.remove(listener1) }
        }

        return Row { child(Text(text = "hello world")) }
      }
    }

    lithoViewRule.render { RootComponent() }

    for (i in 0..10) {
      for (listener in listeners) {
        listener.onTriggerListener()
      }
    }

    for (i in 0..10) {
      ShadowLooper.runMainLooperOneTask()
    }

    Assertions.assertThat(renderCount.get()).isEqualTo(2)
  }

  @Test
  fun `do not skip lambda state update if new updates were enqueued during layout`() {
    ShadowLooper.pauseMainLooper()
    val renderCount = AtomicInteger()

    class RootComponent(private val renderCount: AtomicInteger) : KComponent() {
      override fun ComponentScope.render(): Component {
        renderCount.incrementAndGet()
        val state = useState { false }

        // unconditional state update
        if (renderCount.get() == 1) {
          state.updateSync { currentVal -> !currentVal }
          state.updateSync { currentVal -> !currentVal }
        }

        return Row { child(Text(text = "State is ${state.value}")) }
      }
    }

    val lithoView = lithoViewRule.render { RootComponent(renderCount = renderCount) }
    for (i in 0..10) {
      ShadowLooper.runMainLooperOneTask()
    }

    LithoViewAssert.assertThat(lithoView.lithoView).hasVisibleText("State is false")
  }

  @Test
  fun `do not skip lambda state update if new updates were enqueued outside layout`() {
    ShadowLooper.pauseMainLooper()

    class RootComponent : KComponent() {
      override fun ComponentScope.render(): Component {
        val state = useState { false }

        return Row(
            style =
                Style.viewTag("clickTag").onClick {
                  state.updateSync { currentState -> !currentState }
                  state.updateSync { currentState -> !currentState }
                }) {
              child(Text(text = "State is ${state.value}"))
            }
      }
    }

    val lithoView = lithoViewRule.render { RootComponent() }
    lithoViewRule.act(lithoView) {
      clickOnTag("clickTag")
      /**
       * We need to perform this assertion here because as soon as we exit this lambda, all pending
       * runnables enqueued on the UI or BG thread will be executed.
       */
      Assertions.assertThat(
              lithoView.componentTree.treeState?.resolveState?.pendingHookUpdatesCount)
          .isEqualTo(2)
    }

    for (i in 0..10) {
      ShadowLooper.runMainLooperOneTask()
    }

    LithoViewAssert.assertThat(lithoView.lithoView).hasVisibleText("State is false")
  }

  private interface Listener {
    fun onTriggerListener()
  }

  class Person(var name: String) {
    override fun equals(that: Any?): Boolean {
      if (that == null) return false
      if (that !is Person) return false
      return this.name == that.name
    }

    override fun hashCode(): Int {
      return name.hashCode()
    }
  }
}


/*
 * 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.view.View
import com.facebook.litho.SizeSpec.EXACTLY
import com.facebook.litho.accessibility.contentDescription
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.exactly
import com.facebook.litho.testing.testrunner.LithoTestRunner
import com.facebook.litho.view.onClick
import com.facebook.litho.view.viewId
import com.facebook.litho.view.viewTag
import com.facebook.litho.view.wrapInView
import java.util.concurrent.CountDownLatch
import java.util.concurrent.atomic.AtomicInteger
import java.util.concurrent.atomic.AtomicReference
import org.assertj.core.api.Assertions.assertThat
import org.junit.Rule
import org.junit.Test
import org.junit.rules.ExpectedException
import org.junit.runner.RunWith
import org.robolectric.annotation.LooperMode

/** Unit tests for [useState]. */
@LooperMode(LooperMode.Mode.LEGACY)
@RunWith(LithoTestRunner::class)
class KStateTest {

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

  private fun <T> ComponentScope.useCustomState(value: T): State<T> {
    val state = useState { value }
    return state
  }

  @Test
  fun useState_updateState_stateIsUpdated() {
    lateinit var stateRef: AtomicReference<String>

    class TestComponent : KComponent() {
      override fun ComponentScope.render(): Component? {
        val state = useState { "hello" }
        stateRef = AtomicReference(state.value)

        return Row(
            style =
                Style.height(100.dp).width(100.dp).viewTag("test_view").onClick {
                  state.update("world")
                })
      }
    }

    val testLithoView = lithoViewRule.render { TestComponent() }

    assertThat(stateRef.get()).isEqualTo("hello")

    lithoViewRule.act(testLithoView) { clickOnTag("test_view") }

    assertThat(stateRef.get()).describedAs("String state is updated").isEqualTo("world")
  }

  @Test
  fun useStateOnHooks_updateTwoStatesWithSamePropertyName_bothStatesAreUpdatedIndependently() {
    lateinit var state1Ref: AtomicReference<State<String>>
    lateinit var state2Ref: AtomicReference<State<Int>>

    class TestComponent : KComponent() {
      override fun ComponentScope.render(): Component? {
        val state1 = useCustomState("hello")
        val state2 = useCustomState(20)

        state1Ref = AtomicReference(state1)
        state2Ref = AtomicReference(state2)

        return Row(
            style =
                Style.height(100.dp).width(100.dp).viewTag("test_view").onClick {
                  // The correct way to do this (at least until we have automatic batching)
                  // would be to store these states in the same obj to trigger only one state
                  // update
                  state1.update("world")
                  state2.update { value -> value + 1 }
                })
      }
    }

    val testLithoView = lithoViewRule.render { TestComponent() }

    assertThat(state1Ref.get().value).isEqualTo("hello")
    assertThat(state2Ref.get().value).isEqualTo(20)
    lithoViewRule.act(testLithoView) { clickOnTag("test_view") }

    assertThat(state1Ref.get().value).describedAs("String state is updated").isEqualTo("world")
    assertThat(state2Ref.get().value).describedAs("Int state is updated").isEqualTo(21)
  }

  @Test
  fun useState_calculateLayoutInTwoThreadsConcurrently_stateIsInitializedOnlyOnce() {
    val initCounter = AtomicInteger(0)
    val countDownLatch = CountDownLatch(2)
    val firstCountDownLatch = CountDownLatch(1)
    val secondCountDownLatch = CountDownLatch(1)

    val view = lithoViewRule.createTestLithoView()

    val thread1 = Thread {
      view.setRootAndSizeSpecSync(
          CountDownLatchComponent(firstCountDownLatch, secondCountDownLatch, initCounter),
          SizeSpec.makeSizeSpec(100, EXACTLY),
          SizeSpec.makeSizeSpec(100, EXACTLY))
      countDownLatch.countDown()
    }
    val thread2 = Thread {
      firstCountDownLatch.await()
      view.setRootAndSizeSpecSync(
          CountDownLatchComponent(secondCountDownLatch, null, initCounter),
          SizeSpec.makeSizeSpec(200, EXACTLY),
          SizeSpec.makeSizeSpec(200, EXACTLY))
      countDownLatch.countDown()
    }

    thread1.start()
    thread2.start()

    countDownLatch.await()
    lithoViewRule.idle()

    assertThat(initCounter.get()).describedAs("initCounter is initialized only once").isEqualTo(1)
    val componentTree = view.componentTree
    assertThat(getStateHandler(componentTree)?.initialStateContainer?.initialStates)
        .describedAs("Initial hook state container is empty")
        .isEmpty()
    assertThat(getStateHandler(componentTree)?.initialStateContainer?.pendingStateHandlers)
        .describedAs("No pending StateHandlers")
        .isEmpty()
  }

  fun getStateHandler(componentTree: ComponentTree): StateHandler? {
    return componentTree.treeState?.resolveState
  }

  @Test
  fun useState_counterIncrementedTwiceBeforeStateCommit_bothIncrementsAreApplied() {
    class TestComponent : KComponent() {
      override fun ComponentScope.render(): Component? {
        val counter = useState { 0 }

        return Row(
            style = Style.viewTag("test_view").onClick { counter.update { value -> value + 1 } }) {
              child(
                  Text(
                      style = Style.viewTag("Counter: ${counter.value}"),
                      text = "Counter: ${counter.value}"))
            }
      }
    }

    val view =
        lithoViewRule.render(widthPx = exactly(100), heightPx = exactly(100)) { TestComponent() }

    lithoViewRule.act(view) {
      clickOnTag("test_view")
      clickOnTag("test_view")
    }

    assertThat(view.findViewWithTagOrNull("Counter: 2")).isNotNull()
  }

  @Test
  fun useState_synchronousUpdate_stateIsUpdatedSynchronously() {
    lateinit var stateRef: AtomicReference<String>

    class TestComponent : KComponent() {
      override fun ComponentScope.render(): Component? {
        val state = useState { "hello" }
        stateRef = AtomicReference(state.value)

        return Row(
            style =
                Style.height(100.dp).width(100.dp).viewTag("test_view").onClick {
                  state.updateSync("world")
                })
      }
    }

    val view =
        lithoViewRule.render(widthPx = exactly(100), heightPx = exactly(100)) { TestComponent() }

    assertThat(stateRef.get()).isEqualTo("hello")
    lithoViewRule.act(view) { clickOnTag("test_view") }

    assertThat(stateRef.get()).describedAs("String state is updated").isEqualTo("world")
  }

  @Test
  fun useState_reconciliation_stateIsUpdatedWithoutCallingRenderOnSibling() {
    val siblingRenderCount = AtomicInteger()

    class RootComponent : KComponent() {
      override fun ComponentScope.render(): Component? {
        return Row(style = Style.wrapInView()) {
          child(ClickableComponentWithState(tag = "test_view"))
          child(CountRendersComponent(renderCount = siblingRenderCount))
        }
      }
    }

    val view =
        lithoViewRule.render(widthPx = exactly(100), heightPx = exactly(100)) { RootComponent() }

    assertThat(siblingRenderCount.get()).isEqualTo(1)

    lithoViewRule.act(view) { clickOnTag("test_view") }

    // Using viewTag because Text is currently a drawable and harder to access directly
    assertThat(view.findViewWithTagOrNull("Counter: 1")).isNotNull()

    // Assert that the state update didn't cause the sibling to re-render
    assertThat(siblingRenderCount.get()).isEqualTo(1)
  }

  /**
   * While it's not exactly desired that the parent needs render() called on it again, this test is
   * to detect unexpected changes in that behavior.
   */
  @Test
  fun useState_reconciliation_renderCalledOnParentOfUpdatedComponent() {
    val siblingRenderCount = AtomicInteger()
    val parentRenderCount = AtomicInteger()

    class ParentOfComponentWithStateUpdate(private val renderCount: AtomicInteger) : KComponent() {
      override fun ComponentScope.render(): Component {
        renderCount.incrementAndGet()
        return ClickableComponentWithState(tag = "test_view")
      }
    }

    class RootComponent : KComponent() {
      override fun ComponentScope.render(): Component? {
        return Row(style = Style.wrapInView()) {
          child(ParentOfComponentWithStateUpdate(renderCount = parentRenderCount))
          child(CountRendersComponent(renderCount = siblingRenderCount))
        }
      }
    }

    val view =
        lithoViewRule.render(widthPx = exactly(100), heightPx = exactly(100)) { RootComponent() }

    assertThat(parentRenderCount.get()).isEqualTo(1)
    assertThat(siblingRenderCount.get()).isEqualTo(1)

    lithoViewRule.act(view) { clickOnTag("test_view") }

    // Assert that the state update still causes parent to re-render but not sibling
    assertThat(view.findViewWithTagOrNull("Counter: 1")).isNotNull()
    assertThat(parentRenderCount.get()).isEqualTo(2)
    assertThat(siblingRenderCount.get()).isEqualTo(1)
  }

  @Test
  fun `should throw exception when state updates are triggered too many times during layout`() {

    expectedException.expect(LithoMetadataExceptionWrapper::class.java)
    expectedException.expectMessage("State update loop during layout detected")

    class RootComponent : KComponent() {
      override fun ComponentScope.render(): Component {

        val state = useState { 0 }

        // unconditional state update
        state.updateSync { value -> value + 1 }

        return Row { child(Text(text = "hello world")) }
      }
    }

    lithoViewRule.render { RootComponent() }
  }

  @Test
  fun useState_updateState_stateIsUpdated_forMountable() {
    lateinit var stateRef: AtomicReference<String>

    class TestMountableComponent : MountableComponent() {
      override fun MountableComponentScope.render(): MountableRenderResult {
        val state = useState { "hello" }
        stateRef = AtomicReference(state.value)

        return MountableRenderResult(
            TestMountable(),
            style =
                Style.height(100.dp).width(100.dp).viewTag("test_view").onClick {
                  state.update("world")
                })
      }
    }

    val testLithoView = lithoViewRule.render { TestMountableComponent() }

    assertThat(stateRef.get()).isEqualTo("hello")

    lithoViewRule.act(testLithoView) { clickOnTag("test_view") }

    assertThat(stateRef.get()).describedAs("String state is updated").isEqualTo("world")
  }

  @Test
  fun useState_updateState_stateIsUpdated_forPrimitive() {
    lateinit var stateRef: AtomicReference<String>

    class TestPrimitiveComponent : PrimitiveComponent() {
      override fun PrimitiveComponentScope.render(): LithoPrimitive {
        val state = useState { "hello" }
        stateRef = AtomicReference(state.value)

        return LithoPrimitive(
            TestPrimitive(),
            style =
                Style.height(100.dp).width(100.dp).viewTag("test_view").onClick {
                  state.update("world")
                })
      }
    }

    val testLithoView = lithoViewRule.render { TestPrimitiveComponent() }

    assertThat(stateRef.get()).isEqualTo("hello")

    lithoViewRule.act(testLithoView) { clickOnTag("test_view") }

    assertThat(stateRef.get()).describedAs("String state is updated").isEqualTo("world")
  }

  @Test
  fun useStateOnHooks_updateTwoStatesWithSamePropertyName_bothStatesAreUpdatedIndependently_forMountable() {
    lateinit var state1Ref: AtomicReference<State<String>>
    lateinit var state2Ref: AtomicReference<State<Int>>

    class TestMountableComponent : MountableComponent() {
      override fun MountableComponentScope.render(): MountableRenderResult {
        val state1 = useCustomState("hello")
        val state2 = useCustomState(20)

        state1Ref = AtomicReference(state1)
        state2Ref = AtomicReference(state2)

        return MountableRenderResult(
            TestMountable(),
            style =
                Style.height(100.dp).width(100.dp).viewTag("test_view").onClick {
                  // The correct way to do this (at least until we have automatic batching)
                  // would be to store these states in the same obj to trigger only one state
                  // update
                  state1.update("world")
                  state2.update { value -> value + 1 }
                })
      }
    }

    val testLithoView = lithoViewRule.render { TestMountableComponent() }

    assertThat(state1Ref.get().value).isEqualTo("hello")
    assertThat(state2Ref.get().value).isEqualTo(20)
    lithoViewRule.act(testLithoView) { clickOnTag("test_view") }

    assertThat(state1Ref.get().value).describedAs("String state is updated").isEqualTo("world")
    assertThat(state2Ref.get().value).describedAs("Int state is updated").isEqualTo(21)
  }

  @Test
  fun useStateOnHooks_updateTwoStatesWithSamePropertyName_bothStatesAreUpdatedIndependently_forPrimitive() {
    lateinit var state1Ref: AtomicReference<State<String>>
    lateinit var state2Ref: AtomicReference<State<Int>>

    class TestPrimitiveComponent : PrimitiveComponent() {
      override fun PrimitiveComponentScope.render(): LithoPrimitive {
        val state1 = useCustomState("hello")
        val state2 = useCustomState(20)

        state1Ref = AtomicReference(state1)
        state2Ref = AtomicReference(state2)

        return LithoPrimitive(
            TestPrimitive(),
            style =
                Style.height(100.dp).width(100.dp).viewTag("test_view").onClick {
                  // The correct way to do this (at least until we have automatic batching)
                  // would be to store these states in the same obj to trigger only one state
                  // update
                  state1.update("world")
                  state2.update { value -> value + 1 }
                })
      }
    }

    val testLithoView = lithoViewRule.render { TestPrimitiveComponent() }

    assertThat(state1Ref.get().value).isEqualTo("hello")
    assertThat(state2Ref.get().value).isEqualTo(20)
    lithoViewRule.act(testLithoView) { clickOnTag("test_view") }

    assertThat(state1Ref.get().value).describedAs("String state is updated").isEqualTo("world")
    assertThat(state2Ref.get().value).describedAs("Int state is updated").isEqualTo(21)
  }

  @Test
  fun useState_calculateLayoutInTwoThreadsConcurrently_stateIsInitializedOnlyOnce_forMountable() {
    val initCounter = AtomicInteger(0)
    val countDownLatch = CountDownLatch(2)
    val firstCountDownLatch = CountDownLatch(1)
    val secondCountDownLatch = CountDownLatch(1)

    val view = lithoViewRule.createTestLithoView()

    val thread1 = Thread {
      view.setRootAndSizeSpecSync(
          CountDownLatchMountableComponent(firstCountDownLatch, secondCountDownLatch, initCounter),
          SizeSpec.makeSizeSpec(100, EXACTLY),
          SizeSpec.makeSizeSpec(100, EXACTLY))
      countDownLatch.countDown()
    }
    val thread2 = Thread {
      firstCountDownLatch.await()
      view.setRootAndSizeSpecSync(
          CountDownLatchMountableComponent(secondCountDownLatch, null, initCounter),
          SizeSpec.makeSizeSpec(200, EXACTLY),
          SizeSpec.makeSizeSpec(200, EXACTLY))
      countDownLatch.countDown()
    }

    thread1.start()
    thread2.start()

    countDownLatch.await()
    lithoViewRule.idle()

    assertThat(initCounter.get()).describedAs("initCounter is initialized only once").isEqualTo(1)
    val componentTree = view.componentTree
    assertThat(getStateHandler(componentTree)?.initialStateContainer?.initialStates)
        .describedAs("Initial hook state container is empty")
        .isEmpty()
    assertThat(getStateHandler(componentTree)?.initialStateContainer?.pendingStateHandlers)
        .describedAs("No pending StateHandlers")
        .isEmpty()
  }

  fun useState_calculateLayoutInTwoThreadsConcurrently_stateIsInitializedOnlyOnce_forPrimitive() {
    val initCounter = AtomicInteger(0)
    val countDownLatch = CountDownLatch(2)
    val firstCountDownLatch = CountDownLatch(1)
    val secondCountDownLatch = CountDownLatch(1)

    val view = lithoViewRule.createTestLithoView()

    val thread1 = Thread {
      view.setRootAndSizeSpecSync(
          CountDownLatchPrimitiveComponent(firstCountDownLatch, secondCountDownLatch, initCounter),
          SizeSpec.makeSizeSpec(100, EXACTLY),
          SizeSpec.makeSizeSpec(100, EXACTLY))
      countDownLatch.countDown()
    }
    val thread2 = Thread {
      firstCountDownLatch.await()
      view.setRootAndSizeSpecSync(
          CountDownLatchPrimitiveComponent(secondCountDownLatch, null, initCounter),
          SizeSpec.makeSizeSpec(200, EXACTLY),
          SizeSpec.makeSizeSpec(200, EXACTLY))
      countDownLatch.countDown()
    }

    thread1.start()
    thread2.start()

    countDownLatch.await()
    lithoViewRule.idle()

    assertThat(initCounter.get()).describedAs("initCounter is initialized only once").isEqualTo(1)
    val componentTree = view.componentTree
    assertThat(getStateHandler(componentTree)?.initialStateContainer?.initialStates)
        .describedAs("Initial hook state container is empty")
        .isEmpty()
    assertThat(getStateHandler(componentTree)?.initialStateContainer?.pendingStateHandlers)
        .describedAs("No pending StateHandlers")
        .isEmpty()
  }

  @Test
  fun useState_counterIncrementedTwiceBeforeStateCommit_bothIncrementsAreApplied_forMountable() {
    class TestMountableComponent : MountableComponent() {
      override fun MountableComponentScope.render(): MountableRenderResult {
        val counter = useState { 0 }

        return MountableRenderResult(
            TestTextMountable(
                text = "Counter: ${counter.value}", tag = "Counter: ${counter.value}"),
            style =
                Style.viewTag("Counter: ${counter.value}").onClick {
                  counter.update { value -> value + 1 }
                })
      }
    }

    val view =
        lithoViewRule.render(widthPx = exactly(100), heightPx = exactly(100)) {
          TestMountableComponent()
        }

    lithoViewRule.act(view) {
      clickOnTag("Counter: 0")
      clickOnTag("Counter: 0")
    }

    assertThat(view.findViewWithTagOrNull("Counter: 2")).isNotNull()
  }

  @Test
  fun useState_counterIncrementedTwiceBeforeStateCommit_bothIncrementsAreApplied_forPrimitive() {
    class TestPrimitiveComponent : PrimitiveComponent() {
      override fun PrimitiveComponentScope.render(): LithoPrimitive {
        val counter = useState { 0 }

        return LithoPrimitive(
            TestTextPrimitive(
                text = "Counter: ${counter.value}", tag = "Counter: ${counter.value}"),
            style =
                Style.viewTag("Counter: ${counter.value}").onClick {
                  counter.update { value -> value + 1 }
                })
      }
    }

    val view =
        lithoViewRule.render(widthPx = exactly(100), heightPx = exactly(100)) {
          TestPrimitiveComponent()
        }

    lithoViewRule.act(view) {
      clickOnTag("Counter: 0")
      clickOnTag("Counter: 0")
    }

    assertThat(view.findViewWithTagOrNull("Counter: 2")).isNotNull()
  }

  @Test
  fun useState_synchronousUpdate_stateIsUpdatedSynchronously_forMountable() {
    lateinit var stateRef: AtomicReference<String>

    class TestMountableComponent : MountableComponent() {
      override fun MountableComponentScope.render(): MountableRenderResult {
        val state = useState { "hello" }
        stateRef = AtomicReference(state.value)

        return MountableRenderResult(
            TestMountable(),
            style =
                Style.height(100.dp).width(100.dp).viewTag("test_view").onClick {
                  state.updateSync("world")
                })
      }
    }

    val view =
        lithoViewRule.render(widthPx = exactly(100), heightPx = exactly(100)) {
          TestMountableComponent()
        }

    assertThat(stateRef.get()).isEqualTo("hello")
    lithoViewRule.act(view) { clickOnTag("test_view") }

    assertThat(stateRef.get()).describedAs("String state is updated").isEqualTo("world")
  }

  @Test
  fun useState_synchronousUpdate_stateIsUpdatedSynchronously_forPrimitive() {
    lateinit var stateRef: AtomicReference<String>

    class TestPrimitiveComponent : PrimitiveComponent() {
      override fun PrimitiveComponentScope.render(): LithoPrimitive {
        val state = useState { "hello" }
        stateRef = AtomicReference(state.value)

        return LithoPrimitive(
            TestPrimitive(),
            style =
                Style.height(100.dp).width(100.dp).viewTag("test_view").onClick {
                  state.updateSync("world")
                })
      }
    }

    val view =
        lithoViewRule.render(widthPx = exactly(100), heightPx = exactly(100)) {
          TestPrimitiveComponent()
        }

    assertThat(stateRef.get()).isEqualTo("hello")
    lithoViewRule.act(view) { clickOnTag("test_view") }

    assertThat(stateRef.get()).describedAs("String state is updated").isEqualTo("world")
  }

  @Test
  fun useState_reconciliation_stateIsUpdatedWithoutCallingRenderOnSibling_forMountable() {
    val siblingRenderCount = AtomicInteger()

    class RootComponent : KComponent() {
      override fun ComponentScope.render(): Component? {
        return Row(style = Style.wrapInView()) {
          child(ClickableMountableComponentWithState())
          child(CountRendersMountableComponent(renderCount = siblingRenderCount))
        }
      }
    }

    val view =
        lithoViewRule.render(widthPx = exactly(100), heightPx = exactly(100)) { RootComponent() }

    assertThat(siblingRenderCount.get()).isEqualTo(1)

    lithoViewRule.act(view) { clickOnTag("Counter: 0") }

    // Using viewTag because Text is currently a drawable and harder to access directly
    assertThat(view.findViewWithTagOrNull("Counter: 1")).isNotNull()

    // Assert that the state update didn't cause the sibling to re-render
    assertThat(siblingRenderCount.get()).isEqualTo(1)
  }

  @Test
  fun useState_reconciliation_stateIsUpdatedWithoutCallingRenderOnSibling_forMountableWithConditionalStyle() {
    val siblingRenderCount = AtomicInteger()

    class RootComponent : KComponent() {
      override fun ComponentScope.render(): Component? {
        return Row(style = Style.wrapInView()) {
          child(ClickableMountableComponentWithStateAndConditionalStyle())
          child(CountRendersMountableComponent(renderCount = siblingRenderCount))
        }
      }
    }

    val view =
        lithoViewRule.render(widthPx = exactly(100), heightPx = exactly(100)) { RootComponent() }

    assertThat(siblingRenderCount.get()).isEqualTo(1)

    assertThat(view.findViewWithTagOrNull("Counter: 0")?.id).isEqualTo(42)

    lithoViewRule.act(view) { clickOnTag("Counter: 0") }

    // Using viewTag because Text is currently a drawable and harder to access directly
    assertThat(view.findViewWithTagOrNull("Counter: 1")).isNotNull()

    // Assert that the state update didn't cause the sibling to re-render
    assertThat(siblingRenderCount.get()).isEqualTo(1)

    assertThat(view.findViewWithTagOrNull("Counter: 1")?.id).isEqualTo(View.NO_ID)
  }

  @Test
  fun useState_reconciliation_stateIsUpdatedWithoutCallingRenderOnSibling_forPrimitive() {
    val siblingRenderCount = AtomicInteger()

    class RootComponent : KComponent() {
      override fun ComponentScope.render(): Component? {
        return Row(style = Style.wrapInView()) {
          child(ClickablePrimitiveComponentWithState())
          child(CountRendersPrimitiveComponent(renderCount = siblingRenderCount))
        }
      }
    }

    val view =
        lithoViewRule.render(widthPx = exactly(100), heightPx = exactly(100)) { RootComponent() }

    assertThat(siblingRenderCount.get()).isEqualTo(1)

    lithoViewRule.act(view) { clickOnTag("Counter: 0") }

    // Using viewTag because Text is currently a drawable and harder to access directly
    assertThat(view.findViewWithTagOrNull("Counter: 1")).isNotNull()

    // Assert that the state update didn't cause the sibling to re-render
    assertThat(siblingRenderCount.get()).isEqualTo(1)
  }

  @Test
  fun useState_reconciliation_stateIsUpdatedWithoutCallingRenderOnSibling_forPrimitiveWithConditionalStyle() {
    val siblingRenderCount = AtomicInteger()

    class RootComponent : KComponent() {
      override fun ComponentScope.render(): Component? {
        return Row(style = Style.wrapInView()) {
          child(ClickablePrimitiveComponentWithStateAndConditionalStyle())
          child(CountRendersPrimitiveComponent(renderCount = siblingRenderCount))
        }
      }
    }

    val view =
        lithoViewRule.render(widthPx = exactly(100), heightPx = exactly(100)) { RootComponent() }

    assertThat(siblingRenderCount.get()).isEqualTo(1)

    assertThat(view.findViewWithTagOrNull("Counter: 0")?.id).isEqualTo(42)

    lithoViewRule.act(view) { clickOnTag("Counter: 0") }

    // Using viewTag because Text is currently a drawable and harder to access directly
    assertThat(view.findViewWithTagOrNull("Counter: 1")).isNotNull()

    // Assert that the state update didn't cause the sibling to re-render
    assertThat(siblingRenderCount.get()).isEqualTo(1)

    assertThat(view.findViewWithTagOrNull("Counter: 1")?.id).isEqualTo(View.NO_ID)
  }

  /**
   * While it's not exactly desired that the parent needs render() called on it again, this test is
   * to detect unexpected changes in that behavior.
   */
  @Test
  fun useState_reconciliation_renderCalledOnParentOfUpdatedComponent_forMountable() {
    val siblingRenderCount = AtomicInteger()
    val parentRenderCount = AtomicInteger()

    class ParentOfComponentWithStateUpdate(private val renderCount: AtomicInteger) : KComponent() {
      override fun ComponentScope.render(): Component {
        renderCount.incrementAndGet()
        return ClickableMountableComponentWithState(tag = "test_view")
      }
    }

    class RootComponent : KComponent() {
      override fun ComponentScope.render(): Component? {
        return Row(style = Style.wrapInView()) {
          child(ParentOfComponentWithStateUpdate(renderCount = parentRenderCount))
          child(CountRendersMountableComponent(renderCount = siblingRenderCount))
        }
      }
    }

    val view =
        lithoViewRule.render(widthPx = exactly(100), heightPx = exactly(100)) { RootComponent() }

    assertThat(parentRenderCount.get()).isEqualTo(1)
    assertThat(siblingRenderCount.get()).isEqualTo(1)

    lithoViewRule.act(view) { clickOnTag("test_view") }

    // Assert that the state update still causes parent to re-render but not sibling
    assertThat(view.findViewWithText("Counter: 1")).isNotNull()
    assertThat(parentRenderCount.get()).isEqualTo(2)
    assertThat(siblingRenderCount.get()).isEqualTo(1)
  }

  @Test
  fun useState_reconciliation_renderCalledOnParentOfUpdatedComponent_forPrimitive() {
    val siblingRenderCount = AtomicInteger()
    val parentRenderCount = AtomicInteger()

    class ParentOfComponentWithStateUpdate(private val renderCount: AtomicInteger) : KComponent() {
      override fun ComponentScope.render(): Component {
        renderCount.incrementAndGet()
        return ClickablePrimitiveComponentWithState(tag = "test_view")
      }
    }

    class RootComponent : KComponent() {
      override fun ComponentScope.render(): Component? {
        return Row(style = Style.wrapInView()) {
          child(ParentOfComponentWithStateUpdate(renderCount = parentRenderCount))
          child(CountRendersPrimitiveComponent(renderCount = siblingRenderCount))
        }
      }
    }

    val view =
        lithoViewRule.render(widthPx = exactly(100), heightPx = exactly(100)) { RootComponent() }

    assertThat(parentRenderCount.get()).isEqualTo(1)
    assertThat(siblingRenderCount.get()).isEqualTo(1)

    lithoViewRule.act(view) { clickOnTag("test_view") }

    // Assert that the state update still causes parent to re-render but not sibling
    assertThat(view.findViewWithText("Counter: 1")).isNotNull()
    assertThat(parentRenderCount.get()).isEqualTo(2)
    assertThat(siblingRenderCount.get()).isEqualTo(1)
  }

  @Test
  fun `should throw exception when state updates are triggered too many times during layout for Mountable`() {

    expectedException.expect(LithoMetadataExceptionWrapper::class.java)
    expectedException.expectMessage("State update loop during layout detected")

    class RootComponent : MountableComponent() {
      override fun MountableComponentScope.render(): MountableRenderResult {

        val state = useState { 0 }

        // unconditional state update
        state.updateSync { value -> value + 1 }

        return MountableRenderResult(TestTextMountable(text = "hello world"), null)
      }
    }

    lithoViewRule.render { RootComponent() }
  }

  @Test
  fun `should throw exception when state updates are triggered too many times during layout for Primitive`() {

    expectedException.expect(LithoMetadataExceptionWrapper::class.java)
    expectedException.expectMessage("State update loop during layout detected")

    class RootComponent : PrimitiveComponent() {
      override fun PrimitiveComponentScope.render(): LithoPrimitive {

        val state = useState { 0 }

        // unconditional state update
        state.updateSync { value -> value + 1 }

        return LithoPrimitive(TestTextPrimitive(text = "hello world"), null)
      }
    }

    lithoViewRule.render { RootComponent() }
  }

  private class CountDownLatchComponent(
      val countDownLatch: CountDownLatch,
      val awaitable: CountDownLatch?,
      val initCounter: AtomicInteger
  ) : KComponent() {
    override fun ComponentScope.render(): Component? {
      countDownLatch.countDown()
      awaitable?.await()

      val state = useState { initCounter.incrementAndGet() }
      return Text("stateValue is ${state.value}")
    }
  }

  class ClickableComponentWithState(private val tag: String) : KComponent() {
    override fun ComponentScope.render(): Component? {
      val counter = useState { 0 }

      return Row(style = Style.viewTag(tag).onClick { counter.updateSync { value -> value + 1 } }) {
        child(
            Text(
                style = Style.viewTag("Counter: ${counter.value}"),
                text = "Counter: ${counter.value}"))
      }
    }
  }

  private class CountRendersComponent(private val renderCount: AtomicInteger) : KComponent() {
    override fun ComponentScope.render(): Component? {
      renderCount.incrementAndGet()
      return Row(style = Style.width(100.px).height(100.px))
    }
  }

  private class CountDownLatchMountableComponent(
      val countDownLatch: CountDownLatch,
      val awaitable: CountDownLatch?,
      val initCounter: AtomicInteger
  ) : MountableComponent() {
    override fun MountableComponentScope.render(): MountableRenderResult {
      countDownLatch.countDown()
      awaitable?.await()

      val state = useState { initCounter.incrementAndGet() }
      return MountableRenderResult(TestTextMountable(text = "stateValue is ${state.value}"), null)
    }
  }

  class ClickableMountableComponentWithState(private val tag: String? = null) :
      MountableComponent() {
    override fun MountableComponentScope.render(): MountableRenderResult {
      val counter = useState { 0 }

      return MountableRenderResult(
          TestTextMountable(text = "Counter: ${counter.value}", tag = "Counter: ${counter.value}"),
          style =
              Style.viewTag(tag ?: "Counter: ${counter.value}").contentDescription(tag).onClick {
                counter.updateSync { value -> value + 1 }
              })
    }
  }

  class ClickableMountableComponentWithStateAndConditionalStyle(private val tag: String? = null) :
      MountableComponent() {
    override fun MountableComponentScope.render(): MountableRenderResult {
      val counter = useState { 0 }

      var style =
          Style.viewTag(tag ?: "Counter: ${counter.value}").contentDescription(tag).onClick {
            counter.updateSync { value -> value + 1 }
          }

      if (counter.value % 2 == 0) {
        style = style.plus(Style.viewId(42))
      }

      return MountableRenderResult(
          TestTextMountable(text = "Counter: ${counter.value}", tag = "Counter: ${counter.value}"),
          style = style)
    }
  }

  private class CountRendersMountableComponent(private val renderCount: AtomicInteger) :
      MountableComponent() {
    override fun MountableComponentScope.render(): MountableRenderResult {
      renderCount.incrementAndGet()
      return MountableRenderResult(TestMountable(), style = Style.width(100.px).height(100.px))
    }
  }

  private class CountDownLatchPrimitiveComponent(
      val countDownLatch: CountDownLatch,
      val awaitable: CountDownLatch?,
      val initCounter: AtomicInteger
  ) : PrimitiveComponent() {
    override fun PrimitiveComponentScope.render(): LithoPrimitive {
      countDownLatch.countDown()
      awaitable?.await()

      val state = useState { initCounter.incrementAndGet() }
      return LithoPrimitive(TestTextPrimitive(text = "stateValue is ${state.value}"), null)
    }
  }

  class ClickablePrimitiveComponentWithStateAndConditionalStyle(private val tag: String? = null) :
      PrimitiveComponent() {
    override fun PrimitiveComponentScope.render(): LithoPrimitive {
      val counter = useState { 0 }

      var style =
          Style.viewTag(tag ?: "Counter: ${counter.value}").contentDescription(tag).onClick {
            counter.updateSync { value -> value + 1 }
          }

      if (counter.value % 2 == 0) {
        style = style.plus(Style.viewId(42))
      }

      return LithoPrimitive(
          TestTextPrimitive(text = "Counter: ${counter.value}", tag = "Counter: ${counter.value}"),
          style = style)
    }
  }

  class ClickablePrimitiveComponentWithState(private val tag: String? = null) :
      PrimitiveComponent() {
    override fun PrimitiveComponentScope.render(): LithoPrimitive {
      val counter = useState { 0 }

      return LithoPrimitive(
          TestTextPrimitive(text = "Counter: ${counter.value}", tag = "Counter: ${counter.value}"),
          style =
              Style.viewTag(tag ?: "Counter: ${counter.value}").contentDescription(tag).onClick {
                counter.updateSync { value -> value + 1 }
              })
    }
  }

  private class CountRendersPrimitiveComponent(private val renderCount: AtomicInteger) :
      PrimitiveComponent() {
    override fun PrimitiveComponentScope.render(): LithoPrimitive {
      renderCount.incrementAndGet()
      return LithoPrimitive(TestPrimitive(), style = Style.width(100.px).height(100.px))
    }
  }
}


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

package com.facebook.litho

import android.content.Context
import android.graphics.Rect
import androidx.test.core.app.ApplicationProvider.getApplicationContext
import com.facebook.litho.testing.helper.ComponentTestHelper
import com.facebook.litho.testing.testrunner.LithoTestRunner
import org.assertj.core.api.Assertions.assertThat
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith

/** Unit tests for [getTreeProp] and [createTreeProp]. */
@Suppress("MagicNumber")
@RunWith(LithoTestRunner::class)
class KTreePropsTest {

  private lateinit var context: ComponentContext

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

  @Test
  fun treePropValueIsPropagatedFromParentToChild() {
    val treeProp1Ref = TreePropHolder()
    val treeProp2Ref = TreePropHolder()
    val rect = Rect()

    class ChildComponent : KComponent() {
      override fun ComponentScope.render(): Component? {
        treeProp1Ref.prop = getTreeProp<Integer>()
        treeProp2Ref.prop = getTreeProp<Rect>()
        return null
      }
    }

    class ParentComponent : KComponent() {
      override fun ComponentScope.render(): Component? {
        return TreePropProvider(Integer::class.java to 32, Rect::class.java to rect) {
          ChildComponent()
        }
      }
    }

    ComponentTestHelper.mountComponent(context, ParentComponent())

    assertThat(treeProp1Ref.prop).isEqualTo(32)
    assertThat(treeProp2Ref.prop).isEqualTo(rect)
  }

  @Test
  fun treePropValueIsOverriddenByIntermediate() {
    val treePropRef = TreePropHolder()

    class ChildComponent : KComponent() {
      override fun ComponentScope.render(): Component? {
        treePropRef.prop = getTreeProp<Integer>()
        return null
      }
    }

    // Overrides tree prop from ParentComponent
    class IntermediateComponent : KComponent() {
      override fun ComponentScope.render(): Component? {
        return TreePropProvider(Integer::class.java to 24) { ChildComponent() }
      }
    }

    class ParentComponent : KComponent() {
      override fun ComponentScope.render(): Component? {
        return TreePropProvider(Integer::class.java to 18) { IntermediateComponent() }
      }
    }

    ComponentTestHelper.mountComponent(context, ParentComponent())

    assertThat(treePropRef.prop).isEqualTo(24)
  }

  @Test
  fun treePropsAreIsolatedBetweenSiblings() {
    val child1StringPropRef = TreePropHolder()
    val child1IntPropRef = TreePropHolder()
    val child2IntPropRef = TreePropHolder()

    class Child1Component : KComponent() {
      override fun ComponentScope.render(): Component? {
        child1StringPropRef.prop = getTreeProp<String>()
        child1IntPropRef.prop = getTreeProp<Integer>()
        return null
      }
    }

    class Child2Component : KComponent() {
      override fun ComponentScope.render(): Component? {
        child2IntPropRef.prop = getTreeProp<Integer>()
        return null
      }
    }

    class ParentComponent : KComponent() {
      override fun ComponentScope.render(): Component? {
        return TreePropProvider(String::class.java to "kavabanga") {
          Row {
            child(TreePropProvider(Integer::class.java to 42) { Child1Component() })
            child(Child2Component())
          }
        }
      }
    }

    ComponentTestHelper.mountComponent(context, ParentComponent())

    assertThat(child1StringPropRef.prop).isEqualTo("kavabanga")
    assertThat(child1IntPropRef.prop).isEqualTo(42)
    assertThat(child2IntPropRef.prop).isNull()
  }
}

class TreePropHolder {
  var prop: Any? = null
}


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

package com.facebook.litho

import com.facebook.litho.core.height
import com.facebook.litho.core.width
import com.facebook.litho.testing.LithoViewRule
import com.facebook.litho.testing.exactly
import com.facebook.litho.testing.testrunner.LithoTestRunner
import com.facebook.litho.view.viewTag
import com.facebook.litho.view.wrapInView
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

/** Tests for setting a manual key with the [key] function. */
@LooperMode(LooperMode.Mode.LEGACY)
@RunWith(LithoTestRunner::class)
class ManualKeyTest {

  @Rule @JvmField val lithoViewRule = LithoViewRule()

  private class KeyTestParentComponent(val manualKey: String, val i: Int) : KComponent() {
    override fun ComponentScope.render(): Component? {
      return key(manualKey) { KeyTestChildComponent() }
    }
  }

  private class KeyTestChildComponent() : KComponent() {
    override fun ComponentScope.render(): Component? {
      val state = useState { Object() }

      return Row(style = Style.viewTag(state.value).height(100.dp).width(200.dp).wrapInView())
    }
  }

  @Test
  fun useState_createNewLayoutWithSameManualKey_stateIsSame() {
    val testLithoView =
        lithoViewRule.render(widthPx = exactly(100), heightPx = exactly(100)) {
          KeyTestParentComponent(manualKey = "my_key", i = 0)
        }

    val originalTag = testLithoView.lithoView.tag
    assertThat(originalTag).isNotNull()

    testLithoView.setRoot(KeyTestParentComponent("my_key", i = 1))

    assertThat(testLithoView.lithoView.tag).isEqualTo(originalTag)
  }

  @Test
  fun useState_createNewLayoutWithDifferentManualKey_stateIsRecreated() {
    val testLithoView =
        lithoViewRule.render(widthPx = exactly(100), heightPx = exactly(100)) {
          KeyTestParentComponent(manualKey = "my_key", i = 0)
        }

    val originalTag = testLithoView.lithoView.tag
    assertThat(originalTag).isNotNull()

    testLithoView.setRoot(KeyTestParentComponent("a_different_key", i = 1))

    assertThat(testLithoView.lithoView.tag).isNotEqualTo(originalTag)
  }
}