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

package com.facebook.litho

import android.content.Context
import androidx.test.core.app.ApplicationProvider.getApplicationContext
import com.facebook.litho.kotlin.widget.Text
import com.facebook.litho.testing.helper.ComponentTestHelper
import com.facebook.litho.testing.testrunner.LithoTestRunner
import java.util.concurrent.atomic.AtomicInteger
import java.util.concurrent.atomic.AtomicReference
import org.assertj.core.api.Assertions.assertThat
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.annotation.LooperMode

/** Unit tests for [useCached]. */
@Suppress("MagicNumber")
@RunWith(LithoTestRunner::class)
@LooperMode(LooperMode.Mode.LEGACY)
class KCachedTest {

  private lateinit var context: ComponentContext
  private lateinit var emptyComponent: EmptyComponent

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

  @Test
  fun cachedValueIsCalculatedOnlyOnceWhenOneInputStayTheSame() {
    val initCounter = AtomicInteger(0)
    val lithoView = LithoView(context.androidContext)
    val componentTree = ComponentTree.create(context).build()

    class TestComponent : KComponent() {
      override fun ComponentScope.render(): Component? {
        val expensiveString =
            useCached("hello") {
              initCounter.incrementAndGet()
              expensiveRepeatFunc("hello")
            }
        return Text(text = expensiveString)
      }
    }

    ComponentTestHelper.mountComponent(lithoView, componentTree, TestComponent())
    assertThat(initCounter.get()).isEqualTo(1)

    // Clear root component from ComponentTree.
    ComponentTestHelper.mountComponent(lithoView, componentTree, emptyComponent)

    // Re-set root component and verify expensive function isn't called.
    ComponentTestHelper.mountComponent(lithoView, componentTree, TestComponent())
    assertThat(initCounter.get()).isEqualTo(1)
  }

  @Test
  fun cachedValueIsCalculatedOnlyOnceWhenTwoInputsStayTheSame() {
    val initCounter = AtomicInteger(0)
    val lithoView = LithoView(context.androidContext)
    val componentTree = ComponentTree.create(context).build()

    class TestComponent : KComponent() {
      override fun ComponentScope.render(): Component? {
        val expensiveString =
            useCached("hello", 100) {
              initCounter.incrementAndGet()
              expensiveRepeatFunc("hello", 100)
            }
        return Text(text = expensiveString)
      }
    }

    ComponentTestHelper.mountComponent(lithoView, componentTree, TestComponent())
    assertThat(initCounter.get()).isEqualTo(1)

    // Clear root component from ComponentTree.
    ComponentTestHelper.mountComponent(lithoView, componentTree, emptyComponent)

    // Re-set root component and verify expensive function isn't called.
    ComponentTestHelper.mountComponent(lithoView, componentTree, TestComponent())
    assertThat(initCounter.get()).isEqualTo(1)
  }

  @Test
  fun cachedValueIsCalculatedOnlyOnceWhenInputArrayStaysTheSame() {
    val initCounter = AtomicInteger(0)
    val lithoView = LithoView(context.androidContext)
    val componentTree = ComponentTree.create(context).build()

    class TestComponent : KComponent() {
      override fun ComponentScope.render(): Component? {
        val expensiveString =
            useCached("hello", 100, "litho") {
              initCounter.incrementAndGet()
              expensiveRepeatFunc("hello", 100, "litho")
            }
        return Text(text = expensiveString)
      }
    }

    ComponentTestHelper.mountComponent(lithoView, componentTree, TestComponent())
    assertThat(initCounter.get()).isEqualTo(1)

    // Clear root component from ComponentTree.
    ComponentTestHelper.mountComponent(lithoView, componentTree, emptyComponent)

    // Re-set root component and verify expensive function isn't called.
    ComponentTestHelper.mountComponent(lithoView, componentTree, TestComponent())
    assertThat(initCounter.get()).isEqualTo(1)
  }

  @Test
  fun cachedValueIsRecalculatedWhenOneInputChange() {
    val initCounter = AtomicInteger(0)
    val lithoView = LithoView(context.androidContext)
    val componentTree = ComponentTree.create(context).build()

    val repeatNum = AtomicInteger(100)
    class TestComponent : KComponent() {
      override fun ComponentScope.render(): Component? {
        val expensiveString =
            useCached("count" + repeatNum.get()) {
              initCounter.incrementAndGet()
              expensiveRepeatFunc("count" + repeatNum.get())
            }
        return Text(text = expensiveString)
      }
    }

    ComponentTestHelper.mountComponent(lithoView, componentTree, TestComponent())
    assertThat(initCounter.get()).isEqualTo(1)

    // Clear root component from ComponentTree.
    ComponentTestHelper.mountComponent(lithoView, componentTree, emptyComponent)

    // Increase repeat number.
    repeatNum.incrementAndGet()
    // Re-set root component and cache value is re-created.
    ComponentTestHelper.mountComponent(lithoView, componentTree, TestComponent())
    assertThat(initCounter.get()).isEqualTo(2)
  }

  @Test
  fun cachedValueIsRecalculatedWhenTwoInputsChange() {
    val initCounter = AtomicInteger(0)
    val lithoView = LithoView(context.androidContext)
    val componentTree = ComponentTree.create(context).build()

    val repeatNum = AtomicInteger(100)
    class TestComponent : KComponent() {
      override fun ComponentScope.render(): Component? {
        val expensiveString =
            useCached("world", repeatNum.get()) {
              initCounter.incrementAndGet()
              expensiveRepeatFunc("world", repeatNum.get())
            }
        return Text(text = expensiveString)
      }
    }

    ComponentTestHelper.mountComponent(lithoView, componentTree, TestComponent())
    assertThat(initCounter.get()).isEqualTo(1)

    // Clear root component from ComponentTree.
    ComponentTestHelper.mountComponent(lithoView, componentTree, emptyComponent)

    // Increase repeat number.
    repeatNum.incrementAndGet()
    // Re-set root component and cache value is re-created.
    ComponentTestHelper.mountComponent(lithoView, componentTree, TestComponent())
    assertThat(initCounter.get()).isEqualTo(2)
  }

  @Test
  fun cachedValueIsRecalculatedWhenInputArrayChange() {
    val initCounter = AtomicInteger(0)
    val lithoView = LithoView(context.androidContext)
    val componentTree = ComponentTree.create(context).build()

    val repeatNum = AtomicInteger(100)
    class TestComponent : KComponent() {
      override fun ComponentScope.render(): Component? {
        val expensiveString =
            useCached("world", repeatNum.get(), "litho") {
              initCounter.incrementAndGet()
              expensiveRepeatFunc("world", repeatNum.get(), "litho")
            }
        return Text(text = expensiveString)
      }
    }

    ComponentTestHelper.mountComponent(lithoView, componentTree, TestComponent())
    assertThat(initCounter.get()).isEqualTo(1)

    // Clear root component from ComponentTree.
    ComponentTestHelper.mountComponent(lithoView, componentTree, emptyComponent)

    // Increase repeat number.
    repeatNum.incrementAndGet()
    // Re-set root component and cache value is re-created.
    ComponentTestHelper.mountComponent(lithoView, componentTree, TestComponent())
    assertThat(initCounter.get()).isEqualTo(2)
  }

  @Test
  fun `cached value with single null value that never changes only calculates once`() {
    val initCounter = AtomicInteger(0)
    val lithoView = LithoView(context.androidContext)
    val componentTree = ComponentTree.create(context).build()

    class TestComponent : KComponent() {
      override fun ComponentScope.render(): Component? {
        val expensiveString =
            useCached(null) {
              initCounter.incrementAndGet()
              expensiveRepeatFunc("hello")
            }
        return Text(text = expensiveString)
      }
    }

    ComponentTestHelper.mountComponent(lithoView, componentTree, TestComponent())
    assertThat(initCounter.get()).isEqualTo(1)

    // Clear root component from ComponentTree.
    ComponentTestHelper.mountComponent(lithoView, componentTree, emptyComponent)

    // Re-set root component and verify expensive function isn't called.
    ComponentTestHelper.mountComponent(lithoView, componentTree, TestComponent())
    assertThat(initCounter.get()).isEqualTo(1)
  }

  @Test
  fun `cached value with null input is only calculated once when 2 inputs unchanged`() {
    val initCounter = AtomicInteger(0)
    val lithoView = LithoView(context.androidContext)
    val componentTree = ComponentTree.create(context).build()

    class TestComponent : KComponent() {
      override fun ComponentScope.render(): Component? {
        val expensiveString =
            useCached("hello", null) {
              initCounter.incrementAndGet()
              expensiveRepeatFunc("hello", 100)
            }
        return Text(text = expensiveString)
      }
    }

    ComponentTestHelper.mountComponent(lithoView, componentTree, TestComponent())
    assertThat(initCounter.get()).isEqualTo(1)

    // Clear root component from ComponentTree.
    ComponentTestHelper.mountComponent(lithoView, componentTree, emptyComponent)

    // Re-set root component and verify expensive function isn't called.
    ComponentTestHelper.mountComponent(lithoView, componentTree, TestComponent())
    assertThat(initCounter.get()).isEqualTo(1)
  }

  @Test
  fun `cached value with null inputs is only calculated once when array of inputs unchanged`() {
    val initCounter = AtomicInteger(0)
    val lithoView = LithoView(context.androidContext)
    val componentTree = ComponentTree.create(context).build()

    class TestComponent : KComponent() {
      override fun ComponentScope.render(): Component? {
        val expensiveString =
            useCached(null, "hello", null) {
              initCounter.incrementAndGet()
              expensiveRepeatFunc("hello", 100, "hey")
            }
        return Text(text = expensiveString)
      }
    }

    ComponentTestHelper.mountComponent(lithoView, componentTree, TestComponent())
    assertThat(initCounter.get()).isEqualTo(1)

    // Clear root component from ComponentTree.
    ComponentTestHelper.mountComponent(lithoView, componentTree, emptyComponent)

    // Re-set root component and verify expensive function isn't called.
    ComponentTestHelper.mountComponent(lithoView, componentTree, TestComponent())
    assertThat(initCounter.get()).isEqualTo(1)
  }

  @Test
  fun `cached value is recalculated when one null input changes`() {
    val initCounter = AtomicInteger(0)
    val lithoView = LithoView(context.androidContext)
    val componentTree = ComponentTree.create(context).build()

    val nullableRepeatNum = AtomicReference<Integer>(null)
    class TestComponent : KComponent() {
      override fun ComponentScope.render(): Component? {
        val expensiveString =
            useCached(nullableRepeatNum.get()) {
              initCounter.incrementAndGet()
              expensiveRepeatFunc("count" + nullableRepeatNum.get())
            }
        return Text(text = expensiveString)
      }
    }

    ComponentTestHelper.mountComponent(lithoView, componentTree, TestComponent())
    assertThat(initCounter.get()).isEqualTo(1)

    // Clear root component from ComponentTree.
    ComponentTestHelper.mountComponent(lithoView, componentTree, emptyComponent)

    // Set repeat number to non-null value.
    nullableRepeatNum.set(Integer(100))
    // Re-set root component and cache value is re-created.
    ComponentTestHelper.mountComponent(lithoView, componentTree, TestComponent())
    assertThat(initCounter.get()).isEqualTo(2)
  }

  @Test
  fun `cached value is recalculated when two inputs with nullable value change`() {
    val initCounter = AtomicInteger(0)
    val lithoView = LithoView(context.androidContext)
    val componentTree = ComponentTree.create(context).build()

    val nullableRepeatNum = AtomicReference<Integer>(null)
    class TestComponent : KComponent() {
      override fun ComponentScope.render(): Component? {
        val expensiveString =
            useCached("world", nullableRepeatNum.get()) {
              initCounter.incrementAndGet()
              expensiveRepeatFunc("world", nullableRepeatNum.get()?.toInt() ?: 0)
            }
        return Text(text = expensiveString)
      }
    }

    ComponentTestHelper.mountComponent(lithoView, componentTree, TestComponent())
    assertThat(initCounter.get()).isEqualTo(1)

    // Clear root component from ComponentTree.
    ComponentTestHelper.mountComponent(lithoView, componentTree, emptyComponent)

    // Set repeat number to non-null value.
    nullableRepeatNum.set(Integer(100))
    // Re-set root component and cache value is re-created.
    ComponentTestHelper.mountComponent(lithoView, componentTree, TestComponent())
    assertThat(initCounter.get()).isEqualTo(2)
  }

  @Test
  fun `cached value is recalculated when input array with nullable input changes`() {
    val initCounter = AtomicInteger(0)
    val lithoView = LithoView(context.androidContext)
    val componentTree = ComponentTree.create(context).build()

    val nullableRepeatNum = AtomicReference<Integer>(null)
    class TestComponent : KComponent() {
      override fun ComponentScope.render(): Component? {
        val expensiveString =
            useCached("world", nullableRepeatNum.get(), "litho") {
              initCounter.incrementAndGet()
              expensiveRepeatFunc("world", nullableRepeatNum.get()?.toInt() ?: 0, "litho")
            }
        return Text(text = expensiveString)
      }
    }

    ComponentTestHelper.mountComponent(lithoView, componentTree, TestComponent())
    assertThat(initCounter.get()).isEqualTo(1)

    // Clear root component from ComponentTree.
    ComponentTestHelper.mountComponent(lithoView, componentTree, emptyComponent)

    // Set repeat number to non-null value.
    nullableRepeatNum.set(Integer(100))
    // Re-set root component and cache value is re-created.
    ComponentTestHelper.mountComponent(lithoView, componentTree, TestComponent())
    assertThat(initCounter.get()).isEqualTo(2)
  }

  @Test
  fun cachedValueIsNotReusedBetweenComponentsOfSameTypeWhenInputsStayTheSame() {
    val initCounter = AtomicInteger(0)
    val lithoView = LithoView(context.androidContext)
    val componentTree = ComponentTree.create(context).build()

    class TestComponent : KComponent() {
      override fun ComponentScope.render(): Component? {
        return Row() {
          child(Leaf1("hello", 100, initCounter))
          child(Column() { child(Leaf1("hello", 100, initCounter)) })
        }
      }
    }

    ComponentTestHelper.mountComponent(lithoView, componentTree, TestComponent())

    assertThat(initCounter.get())
        .describedAs(
            "CacheValue should not be shared between two `Leaf1` components under the same parent.")
        .isEqualTo(2)
  }

  @Test
  fun cachedValuesWithTheSameInputsAndNameAreNotReusedBetweenComponentsOfDifferentTypes() {
    val initCounter = AtomicInteger(0)
    val lithoView = LithoView(context.androidContext)
    val componentTree = ComponentTree.create(context).build()

    class TestComponent(val showLeaf1: Boolean, val dummyCacheBustingProp: Int) : KComponent() {
      override fun ComponentScope.render(): Component? {
        return Row {
          if (showLeaf1) {
            child(Leaf1("hello", 100, initCounter))
          } else {
            child(Leaf2("hello", 100, initCounter))
          }
        }
      }
    }

    ComponentTestHelper.mountComponent(
        lithoView, componentTree, TestComponent(showLeaf1 = true, dummyCacheBustingProp = 0))
    ComponentTestHelper.mountComponent(
        lithoView, componentTree, TestComponent(showLeaf1 = true, dummyCacheBustingProp = 1))

    assertThat(initCounter.get()).isEqualTo(1)

    ComponentTestHelper.mountComponent(
        lithoView, componentTree, TestComponent(showLeaf1 = false, dummyCacheBustingProp = 2))

    assertThat(initCounter.get())
        .describedAs("CacheValue should not be shared between two components of different types")
        .isEqualTo(2)
  }

  @Test
  fun cachedValuesWithDifferentNamesAreCalculatedAndReusedIndependentlyEvenWhenHaveSameInputs() {
    val initCounter = AtomicInteger(0)
    val lithoView = LithoView(context.androidContext)
    val componentTree = ComponentTree.create(context).build()

    val root = ComponentWithTwoCachedValuesWithSameInputs("hello", 20, initCounter)
    ComponentTestHelper.mountComponent(lithoView, componentTree, root)
    assertThat(initCounter.get()).isEqualTo(2)

    // Clear root component from ComponentTree.
    ComponentTestHelper.mountComponent(lithoView, componentTree, emptyComponent)

    // Re-set root component and verify expensive function isn't called.
    ComponentTestHelper.mountComponent(lithoView, componentTree, root)
    assertThat(initCounter.get()).isEqualTo(2)
  }

  @Test
  fun cachedValueIsCalculatedOnlyOnceWhenOneInputStayTheSameForMountable() {
    val initCounter = AtomicInteger(0)
    val lithoView = LithoView(context.androidContext)
    val componentTree = ComponentTree.create(context).build()

    class TestMountableComponent : MountableComponent() {
      override fun MountableComponentScope.render(): MountableRenderResult {
        val expensiveString =
            useCached("hello") {
              initCounter.incrementAndGet()
              expensiveRepeatFunc("hello")
            }
        return MountableRenderResult(TestTextMountable(text = expensiveString), null)
      }
    }

    ComponentTestHelper.mountComponent(lithoView, componentTree, TestMountableComponent())
    assertThat(initCounter.get()).isEqualTo(1)

    // Clear root component from ComponentTree.
    ComponentTestHelper.mountComponent(lithoView, componentTree, emptyComponent)

    // Re-set root component and verify expensive function isn't called.
    ComponentTestHelper.mountComponent(lithoView, componentTree, TestMountableComponent())
    assertThat(initCounter.get()).isEqualTo(1)
  }

  @Test
  fun cachedValueIsCalculatedOnlyOnceWhenOneInputStayTheSameForPrimitive() {
    val initCounter = AtomicInteger(0)
    val lithoView = LithoView(context.androidContext)
    val componentTree = ComponentTree.create(context).build()

    class TestPrimitiveComponent : PrimitiveComponent() {
      override fun PrimitiveComponentScope.render(): LithoPrimitive {
        val expensiveString =
            useCached("hello") {
              initCounter.incrementAndGet()
              expensiveRepeatFunc("hello")
            }
        return LithoPrimitive(TestTextPrimitive(text = expensiveString), null)
      }
    }

    ComponentTestHelper.mountComponent(lithoView, componentTree, TestPrimitiveComponent())
    assertThat(initCounter.get()).isEqualTo(1)

    // Clear root component from ComponentTree.
    ComponentTestHelper.mountComponent(lithoView, componentTree, emptyComponent)

    // Re-set root component and verify expensive function isn't called.
    ComponentTestHelper.mountComponent(lithoView, componentTree, TestPrimitiveComponent())
    assertThat(initCounter.get()).isEqualTo(1)
  }

  @Test
  fun cachedValueIsCalculatedOnlyOnceWhenTwoInputsStayTheSameForMountable() {
    val initCounter = AtomicInteger(0)
    val lithoView = LithoView(context.androidContext)
    val componentTree = ComponentTree.create(context).build()

    class TestComponent : MountableComponent() {
      override fun MountableComponentScope.render(): MountableRenderResult {
        val expensiveString =
            useCached("hello", 100) {
              initCounter.incrementAndGet()
              expensiveRepeatFunc("hello", 100)
            }
        return MountableRenderResult(TestTextMountable(text = expensiveString), null)
      }
    }

    ComponentTestHelper.mountComponent(lithoView, componentTree, TestComponent())
    assertThat(initCounter.get()).isEqualTo(1)

    // Clear root component from ComponentTree.
    ComponentTestHelper.mountComponent(lithoView, componentTree, emptyComponent)

    // Re-set root component and verify expensive function isn't called.
    ComponentTestHelper.mountComponent(lithoView, componentTree, TestComponent())
    assertThat(initCounter.get()).isEqualTo(1)
  }

  @Test
  fun cachedValueIsCalculatedOnlyOnceWhenTwoInputsStayTheSameForPrimitive() {
    val initCounter = AtomicInteger(0)
    val lithoView = LithoView(context.androidContext)
    val componentTree = ComponentTree.create(context).build()

    class TestComponent : PrimitiveComponent() {
      override fun PrimitiveComponentScope.render(): LithoPrimitive {
        val expensiveString =
            useCached("hello", 100) {
              initCounter.incrementAndGet()
              expensiveRepeatFunc("hello", 100)
            }
        return LithoPrimitive(TestTextPrimitive(text = expensiveString), null)
      }
    }

    ComponentTestHelper.mountComponent(lithoView, componentTree, TestComponent())
    assertThat(initCounter.get()).isEqualTo(1)

    // Clear root component from ComponentTree.
    ComponentTestHelper.mountComponent(lithoView, componentTree, emptyComponent)

    // Re-set root component and verify expensive function isn't called.
    ComponentTestHelper.mountComponent(lithoView, componentTree, TestComponent())
    assertThat(initCounter.get()).isEqualTo(1)
  }

  @Test
  fun cachedValueIsCalculatedOnlyOnceWhenInputArrayStaysTheSameForMountable() {
    val initCounter = AtomicInteger(0)
    val lithoView = LithoView(context.androidContext)
    val componentTree = ComponentTree.create(context).build()

    class TestComponent : MountableComponent() {
      override fun MountableComponentScope.render(): MountableRenderResult {
        val expensiveString =
            useCached("hello", 100, "litho") {
              initCounter.incrementAndGet()
              expensiveRepeatFunc("hello", 100, "litho")
            }
        return MountableRenderResult(TestTextMountable(text = expensiveString), null)
      }
    }

    ComponentTestHelper.mountComponent(lithoView, componentTree, TestComponent())
    assertThat(initCounter.get()).isEqualTo(1)

    // Clear root component from ComponentTree.
    ComponentTestHelper.mountComponent(lithoView, componentTree, emptyComponent)

    // Re-set root component and verify expensive function isn't called.
    ComponentTestHelper.mountComponent(lithoView, componentTree, TestComponent())
    assertThat(initCounter.get()).isEqualTo(1)
  }

  @Test
  fun cachedValueIsCalculatedOnlyOnceWhenInputArrayStaysTheSameForPrimitive() {
    val initCounter = AtomicInteger(0)
    val lithoView = LithoView(context.androidContext)
    val componentTree = ComponentTree.create(context).build()

    class TestComponent : PrimitiveComponent() {
      override fun PrimitiveComponentScope.render(): LithoPrimitive {
        val expensiveString =
            useCached("hello", 100, "litho") {
              initCounter.incrementAndGet()
              expensiveRepeatFunc("hello", 100, "litho")
            }
        return LithoPrimitive(TestTextPrimitive(text = expensiveString), null)
      }
    }

    ComponentTestHelper.mountComponent(lithoView, componentTree, TestComponent())
    assertThat(initCounter.get()).isEqualTo(1)

    // Clear root component from ComponentTree.
    ComponentTestHelper.mountComponent(lithoView, componentTree, emptyComponent)

    // Re-set root component and verify expensive function isn't called.
    ComponentTestHelper.mountComponent(lithoView, componentTree, TestComponent())
    assertThat(initCounter.get()).isEqualTo(1)
  }

  @Test
  fun cachedValueIsRecalculatedWhenOneInputChangeForMountable() {
    val initCounter = AtomicInteger(0)
    val lithoView = LithoView(context.androidContext)
    val componentTree = ComponentTree.create(context).build()

    val repeatNum = AtomicInteger(100)
    class TestComponent : MountableComponent() {
      override fun MountableComponentScope.render(): MountableRenderResult {
        val expensiveString =
            useCached("count" + repeatNum.get()) {
              initCounter.incrementAndGet()
              expensiveRepeatFunc("count" + repeatNum.get())
            }
        return MountableRenderResult(TestTextMountable(text = expensiveString), null)
      }
    }

    ComponentTestHelper.mountComponent(lithoView, componentTree, TestComponent())
    assertThat(initCounter.get()).isEqualTo(1)

    // Clear root component from ComponentTree.
    ComponentTestHelper.mountComponent(lithoView, componentTree, emptyComponent)

    // Increase repeat number.
    repeatNum.incrementAndGet()
    // Re-set root component and cache value is re-created.
    ComponentTestHelper.mountComponent(lithoView, componentTree, TestComponent())
    assertThat(initCounter.get()).isEqualTo(2)
  }

  @Test
  fun cachedValueIsRecalculatedWhenOneInputChangeForPrimitive() {
    val initCounter = AtomicInteger(0)
    val lithoView = LithoView(context.androidContext)
    val componentTree = ComponentTree.create(context).build()

    val repeatNum = AtomicInteger(100)
    class TestComponent : PrimitiveComponent() {
      override fun PrimitiveComponentScope.render(): LithoPrimitive {
        val expensiveString =
            useCached("count" + repeatNum.get()) {
              initCounter.incrementAndGet()
              expensiveRepeatFunc("count" + repeatNum.get())
            }
        return LithoPrimitive(TestTextPrimitive(text = expensiveString), null)
      }
    }

    ComponentTestHelper.mountComponent(lithoView, componentTree, TestComponent())
    assertThat(initCounter.get()).isEqualTo(1)

    // Clear root component from ComponentTree.
    ComponentTestHelper.mountComponent(lithoView, componentTree, emptyComponent)

    // Increase repeat number.
    repeatNum.incrementAndGet()
    // Re-set root component and cache value is re-created.
    ComponentTestHelper.mountComponent(lithoView, componentTree, TestComponent())
    assertThat(initCounter.get()).isEqualTo(2)
  }

  @Test
  fun cachedValueIsRecalculatedWhenTwoInputsChangeForMountable() {
    val initCounter = AtomicInteger(0)
    val lithoView = LithoView(context.androidContext)
    val componentTree = ComponentTree.create(context).build()

    val repeatNum = AtomicInteger(100)
    class TestComponent : MountableComponent() {
      override fun MountableComponentScope.render(): MountableRenderResult {
        val expensiveString =
            useCached("world", repeatNum.get()) {
              initCounter.incrementAndGet()
              expensiveRepeatFunc("world", repeatNum.get())
            }
        return MountableRenderResult(TestTextMountable(text = expensiveString), null)
      }
    }

    ComponentTestHelper.mountComponent(lithoView, componentTree, TestComponent())
    assertThat(initCounter.get()).isEqualTo(1)

    // Clear root component from ComponentTree.
    ComponentTestHelper.mountComponent(lithoView, componentTree, emptyComponent)

    // Increase repeat number.
    repeatNum.incrementAndGet()
    // Re-set root component and cache value is re-created.
    ComponentTestHelper.mountComponent(lithoView, componentTree, TestComponent())
    assertThat(initCounter.get()).isEqualTo(2)
  }

  @Test
  fun cachedValueIsRecalculatedWhenTwoInputsChangeForPrimitive() {
    val initCounter = AtomicInteger(0)
    val lithoView = LithoView(context.androidContext)
    val componentTree = ComponentTree.create(context).build()

    val repeatNum = AtomicInteger(100)
    class TestComponent : PrimitiveComponent() {
      override fun PrimitiveComponentScope.render(): LithoPrimitive {
        val expensiveString =
            useCached("world", repeatNum.get()) {
              initCounter.incrementAndGet()
              expensiveRepeatFunc("world", repeatNum.get())
            }
        return LithoPrimitive(TestTextPrimitive(text = expensiveString), null)
      }
    }

    ComponentTestHelper.mountComponent(lithoView, componentTree, TestComponent())
    assertThat(initCounter.get()).isEqualTo(1)

    // Clear root component from ComponentTree.
    ComponentTestHelper.mountComponent(lithoView, componentTree, emptyComponent)

    // Increase repeat number.
    repeatNum.incrementAndGet()
    // Re-set root component and cache value is re-created.
    ComponentTestHelper.mountComponent(lithoView, componentTree, TestComponent())
    assertThat(initCounter.get()).isEqualTo(2)
  }

  @Test
  fun cachedValueIsRecalculatedWhenInputArrayChangeForMountable() {
    val initCounter = AtomicInteger(0)
    val lithoView = LithoView(context.androidContext)
    val componentTree = ComponentTree.create(context).build()

    val repeatNum = AtomicInteger(100)
    class TestComponent : MountableComponent() {
      override fun MountableComponentScope.render(): MountableRenderResult {
        val expensiveString =
            useCached("world", repeatNum.get(), "litho") {
              initCounter.incrementAndGet()
              expensiveRepeatFunc("world", repeatNum.get(), "litho")
            }
        return MountableRenderResult(TestTextMountable(text = expensiveString), null)
      }
    }

    ComponentTestHelper.mountComponent(lithoView, componentTree, TestComponent())
    assertThat(initCounter.get()).isEqualTo(1)

    // Clear root component from ComponentTree.
    ComponentTestHelper.mountComponent(lithoView, componentTree, emptyComponent)

    // Increase repeat number.
    repeatNum.incrementAndGet()
    // Re-set root component and cache value is re-created.
    ComponentTestHelper.mountComponent(lithoView, componentTree, TestComponent())
    assertThat(initCounter.get()).isEqualTo(2)
  }

  @Test
  fun cachedValueIsRecalculatedWhenInputArrayChangeForPrimitive() {
    val initCounter = AtomicInteger(0)
    val lithoView = LithoView(context.androidContext)
    val componentTree = ComponentTree.create(context).build()

    val repeatNum = AtomicInteger(100)
    class TestComponent : PrimitiveComponent() {
      override fun PrimitiveComponentScope.render(): LithoPrimitive {
        val expensiveString =
            useCached("world", repeatNum.get(), "litho") {
              initCounter.incrementAndGet()
              expensiveRepeatFunc("world", repeatNum.get(), "litho")
            }
        return LithoPrimitive(TestTextPrimitive(text = expensiveString), null)
      }
    }

    ComponentTestHelper.mountComponent(lithoView, componentTree, TestComponent())
    assertThat(initCounter.get()).isEqualTo(1)

    // Clear root component from ComponentTree.
    ComponentTestHelper.mountComponent(lithoView, componentTree, emptyComponent)

    // Increase repeat number.
    repeatNum.incrementAndGet()
    // Re-set root component and cache value is re-created.
    ComponentTestHelper.mountComponent(lithoView, componentTree, TestComponent())
    assertThat(initCounter.get()).isEqualTo(2)
  }

  @Test
  fun `cached value with single null value that never changes only calculates once for mountable`() {
    val initCounter = AtomicInteger(0)
    val lithoView = LithoView(context.androidContext)
    val componentTree = ComponentTree.create(context).build()

    class TestComponent : MountableComponent() {
      override fun MountableComponentScope.render(): MountableRenderResult {
        val expensiveString =
            useCached(null) {
              initCounter.incrementAndGet()
              expensiveRepeatFunc("hello")
            }
        return MountableRenderResult(TestTextMountable(text = expensiveString), null)
      }
    }

    ComponentTestHelper.mountComponent(lithoView, componentTree, TestComponent())
    assertThat(initCounter.get()).isEqualTo(1)

    // Clear root component from ComponentTree.
    ComponentTestHelper.mountComponent(lithoView, componentTree, emptyComponent)

    // Re-set root component and verify expensive function isn't called.
    ComponentTestHelper.mountComponent(lithoView, componentTree, TestComponent())
    assertThat(initCounter.get()).isEqualTo(1)
  }

  @Test
  fun `cached value with single null value that never changes only calculates once for primitive`() {
    val initCounter = AtomicInteger(0)
    val lithoView = LithoView(context.androidContext)
    val componentTree = ComponentTree.create(context).build()

    class TestComponent : PrimitiveComponent() {
      override fun PrimitiveComponentScope.render(): LithoPrimitive {
        val expensiveString =
            useCached(null) {
              initCounter.incrementAndGet()
              expensiveRepeatFunc("hello")
            }
        return LithoPrimitive(TestTextPrimitive(text = expensiveString), null)
      }
    }

    ComponentTestHelper.mountComponent(lithoView, componentTree, TestComponent())
    assertThat(initCounter.get()).isEqualTo(1)

    // Clear root component from ComponentTree.
    ComponentTestHelper.mountComponent(lithoView, componentTree, emptyComponent)

    // Re-set root component and verify expensive function isn't called.
    ComponentTestHelper.mountComponent(lithoView, componentTree, TestComponent())
    assertThat(initCounter.get()).isEqualTo(1)
  }

  @Test
  fun `cached value with null input is only calculated once when 2 inputs unchanged for mountable`() {
    val initCounter = AtomicInteger(0)
    val lithoView = LithoView(context.androidContext)
    val componentTree = ComponentTree.create(context).build()

    class TestComponent : MountableComponent() {
      override fun MountableComponentScope.render(): MountableRenderResult {
        val expensiveString =
            useCached("hello", null) {
              initCounter.incrementAndGet()
              expensiveRepeatFunc("hello", 100)
            }
        return MountableRenderResult(TestTextMountable(text = expensiveString), null)
      }
    }

    ComponentTestHelper.mountComponent(lithoView, componentTree, TestComponent())
    assertThat(initCounter.get()).isEqualTo(1)

    // Clear root component from ComponentTree.
    ComponentTestHelper.mountComponent(lithoView, componentTree, emptyComponent)

    // Re-set root component and verify expensive function isn't called.
    ComponentTestHelper.mountComponent(lithoView, componentTree, TestComponent())
    assertThat(initCounter.get()).isEqualTo(1)
  }

  @Test
  fun `cached value with null input is only calculated once when 2 inputs unchanged for primitive`() {
    val initCounter = AtomicInteger(0)
    val lithoView = LithoView(context.androidContext)
    val componentTree = ComponentTree.create(context).build()

    class TestComponent : PrimitiveComponent() {
      override fun PrimitiveComponentScope.render(): LithoPrimitive {
        val expensiveString =
            useCached("hello", null) {
              initCounter.incrementAndGet()
              expensiveRepeatFunc("hello", 100)
            }
        return LithoPrimitive(TestTextPrimitive(text = expensiveString), null)
      }
    }

    ComponentTestHelper.mountComponent(lithoView, componentTree, TestComponent())
    assertThat(initCounter.get()).isEqualTo(1)

    // Clear root component from ComponentTree.
    ComponentTestHelper.mountComponent(lithoView, componentTree, emptyComponent)

    // Re-set root component and verify expensive function isn't called.
    ComponentTestHelper.mountComponent(lithoView, componentTree, TestComponent())
    assertThat(initCounter.get()).isEqualTo(1)
  }

  @Test
  fun `cached value with null inputs is only calculated once when array of inputs unchanged for Mountable`() {
    val initCounter = AtomicInteger(0)
    val lithoView = LithoView(context.androidContext)
    val componentTree = ComponentTree.create(context).build()

    class TestComponent : MountableComponent() {
      override fun MountableComponentScope.render(): MountableRenderResult {
        val expensiveString =
            useCached(null, "hello", null) {
              initCounter.incrementAndGet()
              expensiveRepeatFunc("hello", 100, "hey")
            }
        return MountableRenderResult(TestTextMountable(text = expensiveString), null)
      }
    }

    ComponentTestHelper.mountComponent(lithoView, componentTree, TestComponent())
    assertThat(initCounter.get()).isEqualTo(1)

    // Clear root component from ComponentTree.
    ComponentTestHelper.mountComponent(lithoView, componentTree, emptyComponent)

    // Re-set root component and verify expensive function isn't called.
    ComponentTestHelper.mountComponent(lithoView, componentTree, TestComponent())
    assertThat(initCounter.get()).isEqualTo(1)
  }

  @Test
  fun `cached value with null inputs is only calculated once when array of inputs unchanged for Primitive`() {
    val initCounter = AtomicInteger(0)
    val lithoView = LithoView(context.androidContext)
    val componentTree = ComponentTree.create(context).build()

    class TestComponent : PrimitiveComponent() {
      override fun PrimitiveComponentScope.render(): LithoPrimitive {
        val expensiveString =
            useCached(null, "hello", null) {
              initCounter.incrementAndGet()
              expensiveRepeatFunc("hello", 100, "hey")
            }
        return LithoPrimitive(TestTextPrimitive(text = expensiveString), null)
      }
    }

    ComponentTestHelper.mountComponent(lithoView, componentTree, TestComponent())
    assertThat(initCounter.get()).isEqualTo(1)

    // Clear root component from ComponentTree.
    ComponentTestHelper.mountComponent(lithoView, componentTree, emptyComponent)

    // Re-set root component and verify expensive function isn't called.
    ComponentTestHelper.mountComponent(lithoView, componentTree, TestComponent())
    assertThat(initCounter.get()).isEqualTo(1)
  }

  @Test
  fun `cached value is recalculated when one null input changes for mountable`() {
    val initCounter = AtomicInteger(0)
    val lithoView = LithoView(context.androidContext)
    val componentTree = ComponentTree.create(context).build()

    val nullableRepeatNum = AtomicReference<Integer>(null)
    class TestComponent : MountableComponent() {
      override fun MountableComponentScope.render(): MountableRenderResult {
        val expensiveString =
            useCached(nullableRepeatNum.get()) {
              initCounter.incrementAndGet()
              expensiveRepeatFunc("count" + nullableRepeatNum.get())
            }
        return MountableRenderResult(TestTextMountable(text = expensiveString), null)
      }
    }

    ComponentTestHelper.mountComponent(lithoView, componentTree, TestComponent())
    assertThat(initCounter.get()).isEqualTo(1)

    // Clear root component from ComponentTree.
    ComponentTestHelper.mountComponent(lithoView, componentTree, emptyComponent)

    // Set repeat number to non-null value.
    nullableRepeatNum.set(Integer(100))
    // Re-set root component and cache value is re-created.
    ComponentTestHelper.mountComponent(lithoView, componentTree, TestComponent())
    assertThat(initCounter.get()).isEqualTo(2)
  }

  @Test
  fun `cached value is recalculated when one null input changes for primitive`() {
    val initCounter = AtomicInteger(0)
    val lithoView = LithoView(context.androidContext)
    val componentTree = ComponentTree.create(context).build()

    val nullableRepeatNum = AtomicReference<Integer>(null)
    class TestComponent : PrimitiveComponent() {
      override fun PrimitiveComponentScope.render(): LithoPrimitive {
        val expensiveString =
            useCached(nullableRepeatNum.get()) {
              initCounter.incrementAndGet()
              expensiveRepeatFunc("count" + nullableRepeatNum.get())
            }
        return LithoPrimitive(TestTextPrimitive(text = expensiveString), null)
      }
    }

    ComponentTestHelper.mountComponent(lithoView, componentTree, TestComponent())
    assertThat(initCounter.get()).isEqualTo(1)

    // Clear root component from ComponentTree.
    ComponentTestHelper.mountComponent(lithoView, componentTree, emptyComponent)

    // Set repeat number to non-null value.
    nullableRepeatNum.set(Integer(100))
    // Re-set root component and cache value is re-created.
    ComponentTestHelper.mountComponent(lithoView, componentTree, TestComponent())
    assertThat(initCounter.get()).isEqualTo(2)
  }

  @Test
  fun `cached value is recalculated when two inputs with nullable value change for mountable`() {
    val initCounter = AtomicInteger(0)
    val lithoView = LithoView(context.androidContext)
    val componentTree = ComponentTree.create(context).build()

    val nullableRepeatNum = AtomicReference<Integer>(null)
    class TestComponent : MountableComponent() {
      override fun MountableComponentScope.render(): MountableRenderResult {
        val expensiveString =
            useCached("world", nullableRepeatNum.get()) {
              initCounter.incrementAndGet()
              expensiveRepeatFunc("world", nullableRepeatNum.get()?.toInt() ?: 0)
            }
        return MountableRenderResult(TestTextMountable(text = expensiveString), null)
      }
    }

    ComponentTestHelper.mountComponent(lithoView, componentTree, TestComponent())
    assertThat(initCounter.get()).isEqualTo(1)

    // Clear root component from ComponentTree.
    ComponentTestHelper.mountComponent(lithoView, componentTree, emptyComponent)

    // Set repeat number to non-null value.
    nullableRepeatNum.set(Integer(100))
    // Re-set root component and cache value is re-created.
    ComponentTestHelper.mountComponent(lithoView, componentTree, TestComponent())
    assertThat(initCounter.get()).isEqualTo(2)
  }

  @Test
  fun `cached value is recalculated when two inputs with nullable value change for primitive`() {
    val initCounter = AtomicInteger(0)
    val lithoView = LithoView(context.androidContext)
    val componentTree = ComponentTree.create(context).build()

    val nullableRepeatNum = AtomicReference<Integer>(null)
    class TestComponent : PrimitiveComponent() {
      override fun PrimitiveComponentScope.render(): LithoPrimitive {
        val expensiveString =
            useCached("world", nullableRepeatNum.get()) {
              initCounter.incrementAndGet()
              expensiveRepeatFunc("world", nullableRepeatNum.get()?.toInt() ?: 0)
            }
        return LithoPrimitive(TestTextPrimitive(text = expensiveString), null)
      }
    }

    ComponentTestHelper.mountComponent(lithoView, componentTree, TestComponent())
    assertThat(initCounter.get()).isEqualTo(1)

    // Clear root component from ComponentTree.
    ComponentTestHelper.mountComponent(lithoView, componentTree, emptyComponent)

    // Set repeat number to non-null value.
    nullableRepeatNum.set(Integer(100))
    // Re-set root component and cache value is re-created.
    ComponentTestHelper.mountComponent(lithoView, componentTree, TestComponent())
    assertThat(initCounter.get()).isEqualTo(2)
  }

  @Test
  fun `cached value is recalculated when input array with nullable input changes for mountable`() {
    val initCounter = AtomicInteger(0)
    val lithoView = LithoView(context.androidContext)
    val componentTree = ComponentTree.create(context).build()

    val nullableRepeatNum = AtomicReference<Integer>(null)
    class TestComponent : MountableComponent() {
      override fun MountableComponentScope.render(): MountableRenderResult {
        val expensiveString =
            useCached("world", nullableRepeatNum.get(), "litho") {
              initCounter.incrementAndGet()
              expensiveRepeatFunc("world", nullableRepeatNum.get()?.toInt() ?: 0, "litho")
            }
        return MountableRenderResult(TestTextMountable(text = expensiveString), null)
      }
    }

    ComponentTestHelper.mountComponent(lithoView, componentTree, TestComponent())
    assertThat(initCounter.get()).isEqualTo(1)

    // Clear root component from ComponentTree.
    ComponentTestHelper.mountComponent(lithoView, componentTree, emptyComponent)

    // Set repeat number to non-null value.
    nullableRepeatNum.set(Integer(100))
    // Re-set root component and cache value is re-created.
    ComponentTestHelper.mountComponent(lithoView, componentTree, TestComponent())
    assertThat(initCounter.get()).isEqualTo(2)
  }

  @Test
  fun `cached value is recalculated when input array with nullable input changes for primitive`() {
    val initCounter = AtomicInteger(0)
    val lithoView = LithoView(context.androidContext)
    val componentTree = ComponentTree.create(context).build()

    val nullableRepeatNum = AtomicReference<Integer>(null)
    class TestComponent : PrimitiveComponent() {
      override fun PrimitiveComponentScope.render(): LithoPrimitive {
        val expensiveString =
            useCached("world", nullableRepeatNum.get(), "litho") {
              initCounter.incrementAndGet()
              expensiveRepeatFunc("world", nullableRepeatNum.get()?.toInt() ?: 0, "litho")
            }
        return LithoPrimitive(TestTextPrimitive(text = expensiveString), null)
      }
    }

    ComponentTestHelper.mountComponent(lithoView, componentTree, TestComponent())
    assertThat(initCounter.get()).isEqualTo(1)

    // Clear root component from ComponentTree.
    ComponentTestHelper.mountComponent(lithoView, componentTree, emptyComponent)

    // Set repeat number to non-null value.
    nullableRepeatNum.set(Integer(100))
    // Re-set root component and cache value is re-created.
    ComponentTestHelper.mountComponent(lithoView, componentTree, TestComponent())
    assertThat(initCounter.get()).isEqualTo(2)
  }

  @Test
  fun cachedValueIsNotReusedBetweenComponentsOfSameTypeWhenInputsStayTheSameForMountable() {
    val initCounter = AtomicInteger(0)
    val lithoView = LithoView(context.androidContext)
    val componentTree = ComponentTree.create(context).build()

    class TestComponent : KComponent() {
      override fun ComponentScope.render(): Component? {
        return Row() {
          child(MountableLeaf1("hello", 100, initCounter))
          child(Column() { child(MountableLeaf1("hello", 100, initCounter)) })
        }
      }
    }

    ComponentTestHelper.mountComponent(lithoView, componentTree, TestComponent())

    assertThat(initCounter.get())
        .describedAs(
            "CacheValue should not be shared between two `Leaf1` components under the same parent.")
        .isEqualTo(2)
  }

  @Test
  fun cachedValueIsNotReusedBetweenComponentsOfSameTypeWhenInputsStayTheSameForPrimitive() {
    val initCounter = AtomicInteger(0)
    val lithoView = LithoView(context.androidContext)
    val componentTree = ComponentTree.create(context).build()

    class TestComponent : KComponent() {
      override fun ComponentScope.render(): Component? {
        return Row() {
          child(PrimitiveLeaf1("hello", 100, initCounter))
          child(Column() { child(PrimitiveLeaf1("hello", 100, initCounter)) })
        }
      }
    }

    ComponentTestHelper.mountComponent(lithoView, componentTree, TestComponent())

    assertThat(initCounter.get())
        .describedAs(
            "CacheValue should not be shared between two `Leaf1` components under the same parent.")
        .isEqualTo(2)
  }

  @Test
  fun cachedValuesWithTheSameInputsAndNameAreNotReusedBetweenComponentsOfDifferentTypesForMountable() {
    val initCounter = AtomicInteger(0)
    val lithoView = LithoView(context.androidContext)
    val componentTree = ComponentTree.create(context).build()

    class TestComponent(val showLeaf1: Boolean, val dummyCacheBustingProp: Int) : KComponent() {
      override fun ComponentScope.render(): Component? {
        return Row {
          if (showLeaf1) {
            child(MountableLeaf1("hello", 100, initCounter))
          } else {
            child(MountableLeaf2("hello", 100, initCounter))
          }
        }
      }
    }

    ComponentTestHelper.mountComponent(
        lithoView, componentTree, TestComponent(showLeaf1 = true, dummyCacheBustingProp = 0))
    ComponentTestHelper.mountComponent(
        lithoView, componentTree, TestComponent(showLeaf1 = true, dummyCacheBustingProp = 1))

    assertThat(initCounter.get()).isEqualTo(1)

    ComponentTestHelper.mountComponent(
        lithoView, componentTree, TestComponent(showLeaf1 = false, dummyCacheBustingProp = 2))

    assertThat(initCounter.get())
        .describedAs("CacheValue should not be shared between two components of different types")
        .isEqualTo(2)
  }

  @Test
  fun cachedValuesWithTheSameInputsAndNameAreNotReusedBetweenComponentsOfDifferentTypesForPrimitive() {
    val initCounter = AtomicInteger(0)
    val lithoView = LithoView(context.androidContext)
    val componentTree = ComponentTree.create(context).build()

    class TestComponent(val showLeaf1: Boolean, val dummyCacheBustingProp: Int) : KComponent() {
      override fun ComponentScope.render(): Component? {
        return Row {
          if (showLeaf1) {
            child(PrimitiveLeaf1("hello", 100, initCounter))
          } else {
            child(PrimitiveLeaf2("hello", 100, initCounter))
          }
        }
      }
    }

    ComponentTestHelper.mountComponent(
        lithoView, componentTree, TestComponent(showLeaf1 = true, dummyCacheBustingProp = 0))
    ComponentTestHelper.mountComponent(
        lithoView, componentTree, TestComponent(showLeaf1 = true, dummyCacheBustingProp = 1))

    assertThat(initCounter.get()).isEqualTo(1)

    ComponentTestHelper.mountComponent(
        lithoView, componentTree, TestComponent(showLeaf1 = false, dummyCacheBustingProp = 2))

    assertThat(initCounter.get())
        .describedAs("CacheValue should not be shared between two components of different types")
        .isEqualTo(2)
  }

  @Test
  fun cachedValuesWithDifferentNamesAreCalculatedAndReusedIndependentlyEvenWhenHaveSameInputsForMountable() {
    val initCounter = AtomicInteger(0)
    val lithoView = LithoView(context.androidContext)
    val componentTree = ComponentTree.create(context).build()

    val root = MountableComponentWithTwoCachedValuesWithSameInputs("hello", 20, initCounter)
    ComponentTestHelper.mountComponent(lithoView, componentTree, root)
    assertThat(initCounter.get()).isEqualTo(2)

    // Clear root component from ComponentTree.
    ComponentTestHelper.mountComponent(lithoView, componentTree, emptyComponent)

    // Re-set root component and verify expensive function isn't called.
    ComponentTestHelper.mountComponent(lithoView, componentTree, root)
    assertThat(initCounter.get()).isEqualTo(2)
  }

  @Test
  fun cachedValuesWithDifferentNamesAreCalculatedAndReusedIndependentlyEvenWhenHaveSameInputsForPrimitive() {
    val initCounter = AtomicInteger(0)
    val lithoView = LithoView(context.androidContext)
    val componentTree = ComponentTree.create(context).build()

    val root = PrimitiveComponentWithTwoCachedValuesWithSameInputs("hello", 20, initCounter)
    ComponentTestHelper.mountComponent(lithoView, componentTree, root)
    assertThat(initCounter.get()).isEqualTo(2)

    // Clear root component from ComponentTree.
    ComponentTestHelper.mountComponent(lithoView, componentTree, emptyComponent)

    // Re-set root component and verify expensive function isn't called.
    ComponentTestHelper.mountComponent(lithoView, componentTree, root)
    assertThat(initCounter.get()).isEqualTo(2)
  }

  private class Leaf1(val str: String, val repeatNum: Int, val initCounter: AtomicInteger) :
      KComponent() {
    override fun ComponentScope.render(): Component? {
      val expensiveString =
          useCached(str, repeatNum) {
            initCounter.incrementAndGet()
            expensiveRepeatFunc(str, repeatNum)
          }
      return Text(text = expensiveString)
    }
  }

  private class Leaf2(val str: String, val repeatNum: Int, val initCounter: AtomicInteger) :
      KComponent() {
    override fun ComponentScope.render(): Component? {
      val expensiveString =
          useCached(str, repeatNum) {
            initCounter.incrementAndGet()
            expensiveRepeatFunc(str, repeatNum)
          }
      return Text(text = expensiveString)
    }
  }

  private class ComponentWithTwoCachedValuesWithSameInputs(
      val str: String,
      val repeatNum: Int,
      val initCounter: AtomicInteger
  ) : KComponent() {
    override fun ComponentScope.render(): Component? {
      val expensiveString1 =
          useCached(str, repeatNum) {
            initCounter.incrementAndGet()
            expensiveRepeatFunc(str, repeatNum)
          }

      val expensiveString2 =
          useCached(str, repeatNum) {
            initCounter.incrementAndGet()
            expensiveRepeatFunc(str, repeatNum)
          }

      return Row() {
        child(Text(text = expensiveString1))
        child(Text(text = expensiveString2))
      }
    }
  }

  private class MountableLeaf1(
      val str: String,
      val repeatNum: Int,
      val initCounter: AtomicInteger
  ) : MountableComponent() {
    override fun MountableComponentScope.render(): MountableRenderResult {
      val expensiveString =
          useCached(str, repeatNum) {
            initCounter.incrementAndGet()
            expensiveRepeatFunc(str, repeatNum)
          }
      return MountableRenderResult(TestTextMountable(text = expensiveString), null)
    }
  }

  private class MountableLeaf2(
      val str: String,
      val repeatNum: Int,
      val initCounter: AtomicInteger
  ) : MountableComponent() {
    override fun MountableComponentScope.render(): MountableRenderResult {
      val expensiveString =
          useCached(str, repeatNum) {
            initCounter.incrementAndGet()
            expensiveRepeatFunc(str, repeatNum)
          }
      return MountableRenderResult(TestTextMountable(text = expensiveString), null)
    }
  }

  private class MountableComponentWithTwoCachedValuesWithSameInputs(
      val str: String,
      val repeatNum: Int,
      val initCounter: AtomicInteger
  ) : MountableComponent() {
    override fun MountableComponentScope.render(): MountableRenderResult {
      val expensiveString1 =
          useCached(str, repeatNum) {
            initCounter.incrementAndGet()
            expensiveRepeatFunc(str, repeatNum)
          }

      val expensiveString2 =
          useCached(str, repeatNum) {
            initCounter.incrementAndGet()
            expensiveRepeatFunc(str, repeatNum)
          }

      return MountableRenderResult(
          TestTextMountable(text = expensiveString1, tag = expensiveString2), null)
    }
  }

  private class PrimitiveLeaf1(
      val str: String,
      val repeatNum: Int,
      val initCounter: AtomicInteger
  ) : PrimitiveComponent() {
    override fun PrimitiveComponentScope.render(): LithoPrimitive {
      val expensiveString =
          useCached(str, repeatNum) {
            initCounter.incrementAndGet()
            expensiveRepeatFunc(str, repeatNum)
          }
      return LithoPrimitive(TestTextPrimitive(text = expensiveString), null)
    }
  }

  private class PrimitiveLeaf2(
      val str: String,
      val repeatNum: Int,
      val initCounter: AtomicInteger
  ) : PrimitiveComponent() {
    override fun PrimitiveComponentScope.render(): LithoPrimitive {
      val expensiveString =
          useCached(str, repeatNum) {
            initCounter.incrementAndGet()
            expensiveRepeatFunc(str, repeatNum)
          }
      return LithoPrimitive(TestTextPrimitive(text = expensiveString), null)
    }
  }

  private class PrimitiveComponentWithTwoCachedValuesWithSameInputs(
      val str: String,
      val repeatNum: Int,
      val initCounter: AtomicInteger
  ) : PrimitiveComponent() {
    override fun PrimitiveComponentScope.render(): LithoPrimitive {
      val expensiveString1 =
          useCached(str, repeatNum) {
            initCounter.incrementAndGet()
            expensiveRepeatFunc(str, repeatNum)
          }

      val expensiveString2 =
          useCached(str, repeatNum) {
            initCounter.incrementAndGet()
            expensiveRepeatFunc(str, repeatNum)
          }

      return LithoPrimitive(
          TestTextPrimitive(text = expensiveString1, tag = expensiveString2), null)
    }
  }

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


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

package com.facebook.litho

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

/** Unit tests for props equivalence of KComponent classes. */
@RunWith(LithoTestRunner::class)
class KComponentEquivalenceTest {

  @Test
  fun kcomponentWithPrivateProps_isEquivalentTo_checksAllFields() {
    class ComponentWithPrivateProps(private val counter: Int, private val name: String) :
        KComponent() {
      override fun ComponentScope.render() = null
    }

    assertThat(
            ComponentWithPrivateProps(0, "Ada").isEquivalentTo(ComponentWithPrivateProps(0, "Ada")))
        .isTrue()
    assertThat(
            ComponentWithPrivateProps(0, "Ada").isEquivalentTo(ComponentWithPrivateProps(1, "Ada")))
        .isFalse()
    assertThat(
            ComponentWithPrivateProps(0, "Ada")
                .isEquivalentTo(ComponentWithPrivateProps(0, "Grace")))
        .isFalse()
  }

  @Test
  fun kcomponentWithPublicProps_isEquivalentTo_checksAllFields() {
    class ComponentWithPublicProps(val counter: Int, val name: String) : KComponent() {
      override fun ComponentScope.render() = null
    }

    assertThat(
            ComponentWithPublicProps(0, "Ada").isEquivalentTo(ComponentWithPublicProps(0, "Ada")))
        .isTrue()
    assertThat(
            ComponentWithPublicProps(0, "Ada").isEquivalentTo(ComponentWithPublicProps(1, "Ada")))
        .isFalse()
    assertThat(
            ComponentWithPublicProps(0, "Ada").isEquivalentTo(ComponentWithPublicProps(0, "Grace")))
        .isFalse()
  }

  @Test
  fun kcomponentWithPublicJVMFieldProps_isEquivalentTo_checksAllFields() {
    class ComponentWithPublicProps(@JvmField val counter: Int, @JvmField val name: String) :
        KComponent() {
      override fun ComponentScope.render() = null
    }

    assertThat(
            ComponentWithPublicProps(0, "Ada").isEquivalentTo(ComponentWithPublicProps(0, "Ada")))
        .isTrue()
    assertThat(
            ComponentWithPublicProps(0, "Ada").isEquivalentTo(ComponentWithPublicProps(1, "Ada")))
        .isFalse()
    assertThat(
            ComponentWithPublicProps(0, "Ada").isEquivalentTo(ComponentWithPublicProps(0, "Grace")))
        .isFalse()
  }

  @Test
  fun kcomponentWithNoProps_isEquivalentTo_isTrue() {
    class ComponentWithNoProps : KComponent() {
      override fun ComponentScope.render() = null
    }

    assertThat(ComponentWithNoProps().isEquivalentTo(ComponentWithNoProps())).isTrue()
  }

  @Test
  fun kcomponentWithNoProps_isEquivalentToWithDifferentType_isFalse() {
    class ComponentWithNoProps : KComponent() {
      override fun ComponentScope.render() = null
    }

    class OtherComponentWithNoProps : KComponent() {
      override fun ComponentScope.render() = null
    }

    assertThat(ComponentWithNoProps().isEquivalentTo(OtherComponentWithNoProps())).isFalse()
  }

  @Test
  fun kcomponentWithComplexProps_isEquivalentTo_usesEqualsMethod() {
    class Counter(val count: Int) {
      override fun equals(other: Any?): Boolean {
        return other is Counter && count == other.count
      }

      override fun hashCode(): Int {
        return count
      }
    }

    data class Name(val name: String)

    class ComponentWithComplexProps(private val counter: Counter, private val name: Name) :
        KComponent() {
      override fun ComponentScope.render() = null
    }

    assertThat(
            ComponentWithComplexProps(Counter(0), Name("Ada"))
                .isEquivalentTo(ComponentWithComplexProps(Counter(0), Name("Ada"))))
        .isTrue()
    assertThat(
            ComponentWithComplexProps(Counter(0), Name("Ada"))
                .isEquivalentTo(ComponentWithComplexProps(Counter(1), Name("Ada"))))
        .isFalse()
    assertThat(
            ComponentWithComplexProps(Counter(0), Name("Ada"))
                .isEquivalentTo(ComponentWithComplexProps(Counter(0), Name("Grace"))))
        .isFalse()
  }
}


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

package com.facebook.litho

import com.facebook.litho.config.TempComponentsConfigurations
import com.facebook.litho.testing.LithoViewRule
import com.facebook.litho.testing.testrunner.LithoTestRunner
import com.facebook.litho.view.alpha
import com.facebook.litho.view.onClick
import java.lang.Exception
import org.assertj.core.api.Assertions.assertThat
import org.junit.After
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith

/** Unit tests for common props equivalence of KComponent classes. */
@RunWith(LithoTestRunner::class)
class KComponentEquivalenceWithCommonPropsTest {

  @Rule @JvmField val lithoViewRule = LithoViewRule()

  @Before
  @Throws(Exception::class)
  fun setup() {
    TempComponentsConfigurations.setShouldCompareCommonPropsInIsEquivalentTo(true)
  }

  @After
  fun restoreConfiguration() {
    TempComponentsConfigurations.restoreShouldCompareCommonPropsInIsEquivalentTo()
  }

  @Test
  fun kcomponentWithCommonProps_isEquivalentTo_checksAllFields() {
    class ComponentWithStyleProp(val style: Style) : KComponent() {
      override fun ComponentScope.render(): Component =
          Row.create(context).kotlinStyle(style).build()
    }

    val onClick: (ClickEvent) -> Unit = {}

    assertThat(
            ComponentWithStyleProp(Style.alpha(.5f).onClick(action = onClick))
                .isEquivalentTo(ComponentWithStyleProp(Style.alpha(.5f).onClick(action = onClick))))
        .isTrue
    assertThat(
            ComponentWithStyleProp(Style.alpha(.5f).onClick(action = onClick))
                .isEquivalentTo(ComponentWithStyleProp(Style.alpha(1f).onClick(action = onClick))))
        .isFalse
    assertThat(
            ComponentWithStyleProp(Style.alpha(.5f).onClick(action = onClick))
                .isEquivalentTo(ComponentWithStyleProp(Style.alpha(.5f).onClick {})))
        .isFalse
  }

  @Test
  fun kcomponentComponentProp_isEquivalentTo_checksAllFields() {
    class ComponentWithComponentProp(val component: Component) : KComponent() {
      override fun ComponentScope.render(): Component = component
    }

    class ComponentWithStyleProp(val style: Style) : KComponent() {
      override fun ComponentScope.render(): Component =
          Row.create(context).kotlinStyle(style).build()
    }

    val onClick: (ClickEvent) -> Unit = {}

    assertThat(
            ComponentWithComponentProp(
                    ComponentWithStyleProp(Style.alpha(.5f).onClick(action = onClick)))
                .isEquivalentTo(
                    ComponentWithComponentProp(
                        ComponentWithStyleProp(Style.alpha(.5f).onClick(action = onClick)))))
        .isTrue
    assertThat(
            ComponentWithComponentProp(
                    ComponentWithStyleProp(Style.alpha(.5f).onClick(action = onClick)))
                .isEquivalentTo(
                    ComponentWithComponentProp(
                        ComponentWithStyleProp(Style.alpha(1f).onClick(action = onClick)))))
        .isFalse
    assertThat(
            ComponentWithComponentProp(
                    ComponentWithStyleProp(Style.alpha(.5f).onClick(action = onClick)))
                .isEquivalentTo(
                    ComponentWithComponentProp(
                        ComponentWithStyleProp(Style.alpha(.5f).onClick {}))))
        .isFalse
  }

  @Test
  fun kcomponentInRow_isEquivalentToWithCommonProps_checksAllFields() {
    class ComponentWithStyleProp(val style: Style) : KComponent() {
      override fun ComponentScope.render(): Component =
          Row.create(context).kotlinStyle(style).build()
    }

    val onClick: (ClickEvent) -> Unit = {}

    with(ComponentScope(lithoViewRule.context)) {
      assertThat(
              Row { child(ComponentWithStyleProp(Style.alpha(.5f).onClick(action = onClick))) }
                  .isEquivalentTo(
                      Row {
                        child(ComponentWithStyleProp(Style.alpha(.5f).onClick(action = onClick)))
                      }))
          .isTrue
      assertThat(
              Row { child(ComponentWithStyleProp(Style.alpha(.5f).onClick(action = onClick))) }
                  .isEquivalentTo(
                      Row {
                        child(ComponentWithStyleProp(Style.alpha(.1f).onClick(action = onClick)))
                      }))
          .isFalse
      assertThat(
              Row { child(ComponentWithStyleProp(Style.alpha(.5f).onClick(action = onClick))) }
                  .isEquivalentTo(
                      Row { child(ComponentWithStyleProp(Style.alpha(.5f).onClick {})) }))
          .isFalse
    }
  }

  @Test
  fun rowWithCommonProps_isEquivalentToWithCommonProps_checksAllFields() {
    val onClick: (ClickEvent) -> Unit = {}

    with(ComponentScope(lithoViewRule.context)) {
      assertThat(
              Row(style = Style.alpha(.5f).onClick(action = onClick)) {}
                  .isEquivalentTo(Row(style = Style.alpha(.5f).onClick(action = onClick)) {}))
          .isTrue
      assertThat(
              Row(style = Style.alpha(.5f).onClick(action = onClick)) {}
                  .isEquivalentTo(Row(style = Style.alpha(.1f).onClick(action = onClick)) {}))
          .isFalse
      assertThat(
              Row(style = Style.alpha(.5f).onClick(action = onClick)) {}
                  .isEquivalentTo(Row(style = Style.alpha(.5f).onClick {}) {}))
          .isFalse
    }
  }

  @Test
  fun kcomponentInColumn_isEquivalentToWithCommonProps_checksAllFields() {
    class ComponentWithStyleProp(val style: Style) : KComponent() {
      override fun ComponentScope.render(): Component =
          Row.create(context).kotlinStyle(style).build()
    }

    val onClick: (ClickEvent) -> Unit = {}

    with(ComponentScope(lithoViewRule.context)) {
      assertThat(
              Column { child(ComponentWithStyleProp(Style.alpha(.5f).onClick(action = onClick))) }
                  .isEquivalentTo(
                      Column {
                        child(ComponentWithStyleProp(Style.alpha(.5f).onClick(action = onClick)))
                      }))
          .isTrue
      assertThat(
              Column { child(ComponentWithStyleProp(Style.alpha(.5f).onClick(action = onClick))) }
                  .isEquivalentTo(
                      Column {
                        child(ComponentWithStyleProp(Style.alpha(.1f).onClick(action = onClick)))
                      }))
          .isFalse
      assertThat(
              Column { child(ComponentWithStyleProp(Style.alpha(.5f).onClick(action = onClick))) }
                  .isEquivalentTo(
                      Column { child(ComponentWithStyleProp(Style.alpha(.5f).onClick {})) }))
          .isFalse
    }
  }

  @Test
  fun columnWithCommonProps_isEquivalentToWithCommonProps_checksAllFields() {
    val onClick: (ClickEvent) -> Unit = {}

    with(ComponentScope(lithoViewRule.context)) {
      assertThat(
              Column(style = Style.alpha(.5f).onClick(action = onClick)) {}
                  .isEquivalentTo(Column(style = Style.alpha(.5f).onClick(action = onClick)) {}))
          .isTrue
      assertThat(
              Column(style = Style.alpha(.5f).onClick(action = onClick)) {}
                  .isEquivalentTo(Column(style = Style.alpha(.1f).onClick(action = onClick)) {}))
          .isFalse
      assertThat(
              Column(style = Style.alpha(.5f).onClick(action = onClick)) {}
                  .isEquivalentTo(Column(style = Style.alpha(.5f).onClick {}) {}))
          .isFalse
    }
  }

  @Test
  fun kcomponentInWrapper_isEquivalentToWithCommonProps_checksAllFields() {
    class ComponentWithStyleProp(val style: Style) : KComponent() {
      override fun ComponentScope.render(): Component =
          Row.create(context).kotlinStyle(style).build()
    }

    val onClick: (ClickEvent) -> Unit = {}

    fun Wrapper(style: Style? = null, delegate: () -> Component?): Component =
        Wrapper.create(lithoViewRule.context).delegate(delegate()).kotlinStyle(style).build()

    assertThat(
            Wrapper { ComponentWithStyleProp(Style.alpha(.5f).onClick(action = onClick)) }
                .isEquivalentTo(
                    Wrapper { ComponentWithStyleProp(Style.alpha(.5f).onClick(action = onClick)) }))
        .isTrue
    assertThat(
            Wrapper { ComponentWithStyleProp(Style.alpha(.5f).onClick(action = onClick)) }
                .isEquivalentTo(
                    Wrapper { ComponentWithStyleProp(Style.alpha(.1f).onClick(action = onClick)) }))
        .isFalse
    assertThat(
            Wrapper { ComponentWithStyleProp(Style.alpha(.5f).onClick(action = onClick)) }
                .isEquivalentTo(Wrapper { ComponentWithStyleProp(Style.alpha(.5f).onClick {}) }))
        .isFalse
  }

  @Test
  fun kcomponentWithWrappedCommonProps_isEquivalentTo_checksAllFields() {
    val onClick: (ClickEvent) -> Unit = {}

    fun Wrapper(style: Style? = null, delegate: () -> Component?): Component =
        Wrapper.create(lithoViewRule.context).delegate(delegate()).kotlinStyle(style).build()

    assertThat(
            Wrapper(Style.alpha(.5f).onClick(action = onClick)) { null }
                .isEquivalentTo(Wrapper(Style.alpha(.5f).onClick(action = onClick)) { null }))
        .isTrue
    assertThat(
            Wrapper(Style.alpha(.5f).onClick(action = onClick)) { null }
                .isEquivalentTo(Wrapper(Style.alpha(.1f).onClick(action = onClick)) { null }))
        .isFalse
    assertThat(
            Wrapper(Style.alpha(.5f).onClick(action = onClick)) { null }
                .isEquivalentTo(Wrapper(Style.alpha(.5f).onClick {}) { null }))
        .isFalse
  }
}