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

package com.facebook.litho

import android.content.Context
import android.view.ViewGroup
import androidx.test.core.app.ApplicationProvider
import com.facebook.litho.config.TempComponentsConfigurations
import com.facebook.litho.testing.LegacyLithoViewRule
import com.facebook.litho.testing.helper.ComponentTestHelper
import com.facebook.litho.testing.inlinelayoutspec.InlineLayoutSpec
import com.facebook.litho.testing.testrunner.LithoTestRunner
import com.facebook.litho.widget.LayoutWithInnerClickableChildTester
import com.facebook.litho.widget.SimpleLayoutSpecWithClickHandlersTester
import com.facebook.litho.widget.SimpleMountSpecTester
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

@RunWith(LithoTestRunner::class)
class MountStateViewClickTest {

  private lateinit var context: ComponentContext

  @JvmField @Rule val legacyLithoViewRule = LegacyLithoViewRule()

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

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

  @Test
  fun testInnerComponentHostClickable() {
    val component =
        LayoutWithInnerClickableChildTester.create(context).shouldSetClickHandler(true).build()
    legacyLithoViewRule.setRoot(component)
    val lithoView = legacyLithoViewRule.lithoView
    setupLithoViewParentAndComponentTree(lithoView, component)
    legacyLithoViewRule.attachToWindow().measure().layout()
    assertThat(lithoView.childCount).isEqualTo(1)
    assertThat(lithoView.isClickable).isFalse
    assertThat(lithoView.isLongClickable).isFalse
    val innerHost = lithoView.getChildAt(0) as ComponentHost
    assertThat(innerHost.isClickable).isTrue
    assertThat(innerHost.isLongClickable).isFalse
  }

  @Test
  fun testInnerComponentHostClickableWithLongClickHandler() {
    val component =
        LayoutWithInnerClickableChildTester.create(context).shouldSetLongClickHandler(true).build()
    legacyLithoViewRule.setRoot(component)
    val lithoView = legacyLithoViewRule.lithoView
    setupLithoViewParentAndComponentTree(lithoView, component)
    legacyLithoViewRule.attachToWindow().measure().layout()
    assertThat(lithoView.childCount).isEqualTo(1)
    assertThat(lithoView.isClickable).isFalse
    assertThat(lithoView.isLongClickable).isFalse
    val innerHost = lithoView.getChildAt(0) as ComponentHost
    assertThat(innerHost.isClickable).isFalse
    assertThat(innerHost.isLongClickable).isTrue
  }

  @Test
  fun testRootHostClickable() {
    val lithoView =
        ComponentTestHelper.mountComponent(
            context,
            object : InlineLayoutSpec() {
              override fun onCreateLayout(c: ComponentContext): Component =
                  Column.create(c)
                      .clickHandler(c.newEventHandler(1) as? EventHandler<ClickEvent>)
                      .child(SimpleMountSpecTester.create(c))
                      .build()
            })
    assertThat(lithoView.childCount).isEqualTo(1)
    assertThat(lithoView.getChildAt(0).isClickable).isTrue
  }

  @Test
  fun testRootHostClickableWithLongClickHandler() {
    val lithoView =
        ComponentTestHelper.mountComponent(
            context,
            object : InlineLayoutSpec() {
              override fun onCreateLayout(c: ComponentContext): Component =
                  Column.create(c)
                      .longClickHandler(c.newEventHandler(1) as? EventHandler<LongClickEvent>)
                      .child(SimpleMountSpecTester.create(c))
                      .build()
            })
    assertThat(lithoView.childCount).isEqualTo(1)
    assertThat(lithoView.getChildAt(0).isLongClickable).isTrue
  }

  @Test
  fun testRootHostClickableUnmount() {
    val component =
        SimpleLayoutSpecWithClickHandlersTester.create(legacyLithoViewRule.context).build()
    legacyLithoViewRule.setRoot(component)
    setupLithoViewParentAndComponentTree(legacyLithoViewRule.lithoView, component)
    legacyLithoViewRule.attachToWindow().measure().layout()
    val rootHost = legacyLithoViewRule.lithoView.getChildAt(0)
    assertThat(rootHost.isClickable).isTrue
    assertThat(rootHost.isLongClickable).isTrue
    legacyLithoViewRule.lithoView.unmountAllItems()
    assertThat(rootHost.isClickable).isFalse
    assertThat(rootHost.isLongClickable).isFalse
  }

  // When testing a LithoView via a LegacyLithoViewRule - we must set the parent and component tree
  // prior to attach / measure / layout for that LithoView otherwise mounting will not behave
  // properly.
  private fun setupLithoViewParentAndComponentTree(lithoView: LithoView, component: Component) {
    val parent: ViewGroup =
        object : ViewGroup(lithoView.context) {
          override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) = Unit
        }
    parent.addView(lithoView)
    lithoView.componentTree =
        ComponentTree.create(context, component)
            .incrementalMount(false)
            .layoutDiffing(false)
            .visibilityProcessing(false)
            .build()
  }
}


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

package com.facebook.litho

import android.content.Context
import android.util.SparseArray
import androidx.test.core.app.ApplicationProvider
import com.facebook.litho.config.TempComponentsConfigurations
import com.facebook.litho.testing.helper.ComponentTestHelper
import com.facebook.litho.testing.inlinelayoutspec.InlineLayoutSpec
import com.facebook.litho.testing.testrunner.LithoTestRunner
import com.facebook.litho.widget.SimpleMountSpecTester
import org.assertj.core.api.Assertions.assertThat
import org.junit.After
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith

@RunWith(LithoTestRunner::class)
class MountStateViewTagsTest {

  private lateinit var context: ComponentContext

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

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

  @Test
  fun testInnerComponentHostViewTags() {
    val tag1 = Any()
    val tags1 = SparseArray<Any>(1)
    tags1.put(DUMMY_ID, tag1)
    val tag2 = Any()
    val tags2 = SparseArray<Any>(1)
    tags2.put(DUMMY_ID, tag2)
    val lithoView =
        ComponentTestHelper.mountComponent(
            context,
            object : InlineLayoutSpec() {
              override fun onCreateLayout(c: ComponentContext): Component =
                  Column.create(c)
                      .child(
                          Column.create(c)
                              .viewTags(tags1)
                              .child(SimpleMountSpecTester.create(c))
                              .child(SimpleMountSpecTester.create(c)))
                      .child(SimpleMountSpecTester.create(c))
                      .child(SimpleMountSpecTester.create(c).viewTags(tags2))
                      .build()
            })
    val innerHost1 = lithoView.getChildAt(0)
    val innerHost2 = lithoView.getChildAt(1)
    assertThat(innerHost1.getTag(DUMMY_ID)).isEqualTo(tag1)
    assertThat(innerHost2.getTag(DUMMY_ID)).isEqualTo(tag2)
  }

  @Test
  fun testRootHostViewTags() {
    val tag = Any()
    val tags = SparseArray<Any>(1)
    tags.put(DUMMY_ID, tag)
    val lithoView =
        ComponentTestHelper.mountComponent(
            context,
            object : InlineLayoutSpec() {
              override fun onCreateLayout(c: ComponentContext): Component =
                  Column.create(c)
                      .viewTags(tags)
                      .child(SimpleMountSpecTester.create(c))
                      .child(SimpleMountSpecTester.create(c))
                      .build()
            })
    val innerHost = lithoView.getChildAt(0)
    assertThat(innerHost.getTag(DUMMY_ID)).isEqualTo(tag)
  }

  companion object {
    private const val DUMMY_ID = 0x10000000
  }
}


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

package com.facebook.litho

import android.graphics.Color
import android.graphics.Rect
import android.graphics.drawable.ColorDrawable
import android.graphics.drawable.Drawable
import android.view.ContextThemeWrapper
import androidx.test.core.app.ApplicationProvider
import com.facebook.litho.config.TempComponentsConfigurations
import com.facebook.litho.it.R
import com.facebook.litho.testing.LegacyLithoViewRule
import com.facebook.litho.testing.TestViewComponent
import com.facebook.litho.testing.ViewGroupWithLithoViewChildren
import com.facebook.litho.testing.inlinelayoutspec.InlineLayoutSpec
import com.facebook.litho.testing.testrunner.LithoTestRunner
import com.facebook.litho.widget.MountSpecLifecycleTester
import com.facebook.litho.widget.SolidColor
import com.facebook.litho.widget.Text
import com.facebook.litho.widget.TextInput
import com.facebook.yoga.YogaEdge
import org.assertj.core.api.Assertions.assertThat
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith

@RunWith(LithoTestRunner::class)
class MountStateViewTest {

  @JvmField @Rule val legacyLithoViewRule: LegacyLithoViewRule = LegacyLithoViewRule()
  private lateinit var context: ComponentContext

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

  @Test
  fun testViewPaddingAndBackground() {
    val color = 0xFFFF0000.toInt()
    val component: InlineLayoutSpec =
        object : InlineLayoutSpec() {
          override fun onCreateLayout(c: ComponentContext): Component =
              Column.create(c)
                  .child(
                      TextInput.create(c)
                          .paddingPx(YogaEdge.LEFT, 5)
                          .paddingPx(YogaEdge.TOP, 6)
                          .paddingPx(YogaEdge.RIGHT, 7)
                          .paddingPx(YogaEdge.BOTTOM, 8)
                          .backgroundColor(color))
                  .build()
        }
    legacyLithoViewRule.setRoot(component)
    legacyLithoViewRule.attachToWindow().measure().layout()
    val lithoView = legacyLithoViewRule.lithoView
    val child = lithoView.getChildAt(0)
    val background = child.background
    assertThat(child.paddingLeft).isEqualTo(5)
    assertThat(child.paddingTop).isEqualTo(6)
    assertThat(child.paddingRight).isEqualTo(7)
    assertThat(child.paddingBottom).isEqualTo(8)
    assertThat(background).isInstanceOf(ColorDrawable::class.java)
    assertThat((background as ColorDrawable).color).isEqualTo(color)
  }

  @Test
  fun testSettingCustomPaddingOverridesDefaultBackgroundPadding() {
    val c =
        ComponentContext(
            ContextThemeWrapper(
                ApplicationProvider.getApplicationContext(),
                R.style.TestTheme_BackgroundWithPadding))
    val component = TextInput.create(c).paddingPx(YogaEdge.ALL, 9).build()
    legacyLithoViewRule.useContext(c)
    legacyLithoViewRule.attachToWindow().setRoot(component).measure().layout()
    val lithoView = legacyLithoViewRule.lithoView
    val child = lithoView.getChildAt(0)
    assertThat(child.paddingLeft).isEqualTo(9)
    assertThat(child.paddingTop).isEqualTo(9)
    assertThat(child.paddingRight).isEqualTo(9)
    assertThat(child.paddingBottom).isEqualTo(9)
  }

  @Test
  fun testSettingOneSidePaddingClearsTheRest() {
    val c =
        ComponentContext(
            ContextThemeWrapper(
                ApplicationProvider.getApplicationContext(),
                R.style.TestTheme_BackgroundWithPadding))
    val component = TextInput.create(c).paddingPx(YogaEdge.LEFT, 12).build()
    legacyLithoViewRule.useContext(c)
    legacyLithoViewRule.attachToWindow().setRoot(component).measure().layout()
    val lithoView = legacyLithoViewRule.lithoView
    val child = lithoView.getChildAt(0)
    assertThat(child.paddingLeft).isEqualTo(12)
    assertThat(child.paddingTop).isZero
    assertThat(child.paddingRight).isZero
    assertThat(child.paddingBottom).isZero
  }

  @Test
  fun testComponentDeepUnmount() {
    val lifecycleTracker = LifecycleTracker()
    val testComponent =
        MountSpecLifecycleTester.create(context).lifecycleTracker(lifecycleTracker).build()
    val mountedTestComponent: Component =
        object : InlineLayoutSpec() {
          override fun onCreateLayout(c: ComponentContext): Component =
              Column.create(c)
                  .child(Wrapper.create(c).delegate(testComponent).widthPx(10).heightPx(10))
                  .build()
        }
    legacyLithoViewRule.setRoot(mountedTestComponent).attachToWindow().measure().layout()
    assertThat(lifecycleTracker.isMounted).isTrue
    val viewGroup = ViewGroupWithLithoViewChildren(context.androidContext)
    val child = legacyLithoViewRule.lithoView
    viewGroup.addView(child)
    legacyLithoViewRule.setRoot(TestViewComponent.create(context).testView(viewGroup).build())
    assertThat(lifecycleTracker.isMounted).isTrue
    legacyLithoViewRule.lithoView.unmountAllItems()
    assertThat(lifecycleTracker.isMounted).isFalse
  }

  @Test
  fun onMountedContentSize_shouldBeEqualToLayoutOutputSize() {
    val component =
        Column.create(context)
            .child(TextInput.create(context).widthPx(100).heightPx(100))
            .child(SolidColor.create(context).color(Color.BLACK).widthPx(100).heightPx(100))
            .child(
                Text.create(context)
                    .text("hello world")
                    .widthPx(80)
                    .heightPx(80)
                    .paddingPx(YogaEdge.ALL, 10)
                    .marginPx(YogaEdge.ALL, 10))
            .build()
    legacyLithoViewRule.setRoot(component)
    legacyLithoViewRule.attachToWindow().measure().layout()
    val root = legacyLithoViewRule.lithoView
    val view = root.getChildAt(0)
    val viewBounds = root.getMountItemAt(0).renderTreeNode.getAbsoluteBounds(Rect())
    assertThat(view.width).isEqualTo(viewBounds.width())
    assertThat(view.height).isEqualTo(viewBounds.height())
    val item1 = root.getMountItemAt(1)
    val drawableOutputBounds = item1.renderTreeNode.getAbsoluteBounds(Rect())
    val drawablesActualBounds = (item1.content as Drawable).bounds
    assertThat(drawablesActualBounds.width()).isEqualTo(drawableOutputBounds.width())
    assertThat(drawablesActualBounds.height()).isEqualTo(drawableOutputBounds.height())
    val item2 = root.getMountItemAt(2)
    val textOutputBounds = item2.renderTreeNode.getAbsoluteBounds(Rect())
    val textActualBounds = (item2.content as Drawable).bounds
    assertThat(textActualBounds.width()).isEqualTo(textOutputBounds.width())
    assertThat(textActualBounds.height()).isEqualTo(textOutputBounds.height())
  }

  @Test
  fun onMountContentWithPadded9PatchDrawable_shouldNotSetPaddingOnHost() {
    TempComponentsConfigurations.setShouldAddHostViewForRootComponent(true)
    val component =
        Column.create(context)
            .backgroundRes(R.drawable.background_with_padding)
            .child(Text.create(context).text("hello world").textSizeSp(20f))
            .build()
    legacyLithoViewRule.attachToWindow().setRoot(component).measure().layout()
    assertThat(legacyLithoViewRule.lithoView.paddingTop).isEqualTo(0)
    assertThat(legacyLithoViewRule.lithoView.paddingRight).isEqualTo(0)
    assertThat(legacyLithoViewRule.lithoView.paddingBottom).isEqualTo(0)
    assertThat(legacyLithoViewRule.lithoView.paddingLeft).isEqualTo(0)
    TempComponentsConfigurations.restoreShouldAddHostViewForRootComponent()
  }
}


/*
 * 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.BackgroundLayoutLooperRule
import com.facebook.litho.testing.LegacyLithoViewRule
import com.facebook.litho.testing.assertj.LithoViewAssert.assertThat
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.assertThat
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.annotation.LooperMode
import org.robolectric.shadows.ShadowLooper

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

  @JvmField @Rule var backgroundLayoutLooperRule = BackgroundLayoutLooperRule()
  @JvmField @Rule var lithoViewRule = LegacyLithoViewRule()

  /*

    root
     /\
    A  B
       /\
     [C] D

  */
  @Test
  fun `when component C is updated sync, A, B, and D should not re-render`() {
    val renderCounts = RenderCounts()
    lithoViewRule
        .setRoot(RootComponent(renderCounts = renderCounts, asyncUpdates = false))
        .measure()
        .layout()
        .attachToWindow()

    lithoViewRule.findViewWithTag("C").performClick()

    assertThat(renderCounts.A).hasValue(1)
    assertThat(renderCounts.B).hasValue(1)
    assertThat(renderCounts.C).hasValue(2)
    assertThat(renderCounts.D).hasValue(1)

    assertThat(lithoViewRule.lithoView).hasVisibleText("Count[A]: 0")
    assertThat(lithoViewRule.lithoView).hasVisibleText("Count[B]: 0")
    assertThat(lithoViewRule.lithoView).hasVisibleText("Count[C]: 1")
    assertThat(lithoViewRule.lithoView).hasVisibleText("Count[D]: 0")
  }

  /*

    root
     /\
    A  B
       /\
     [C] D

  */
  @Test
  fun `when component C is updated async, A, B, and D should not re-render`() {
    val renderCounts = RenderCounts()
    lithoViewRule
        .setRoot(RootComponent(renderCounts = renderCounts, asyncUpdates = true))
        .measure()
        .layout()
        .attachToWindow()

    lithoViewRule.findViewWithTag("C").performClick()

    backgroundLayoutLooperRule.runToEndOfTasksSync()
    ShadowLooper.idleMainLooper()

    assertThat(renderCounts.A).hasValue(1)
    assertThat(renderCounts.B).hasValue(1)
    assertThat(renderCounts.C).hasValue(2)
    assertThat(renderCounts.D).hasValue(1)

    assertThat(lithoViewRule.lithoView).hasVisibleText("Count[A]: 0")
    assertThat(lithoViewRule.lithoView).hasVisibleText("Count[B]: 0")
    assertThat(lithoViewRule.lithoView).hasVisibleText("Count[C]: 1")
    assertThat(lithoViewRule.lithoView).hasVisibleText("Count[D]: 0")
  }

  /*

    root
     /\
    A  B
       /\
      C [D]

  */
  @Test
  fun `when component D is updated sync, A, B, and C should not re-render`() {
    val renderCounts = RenderCounts()
    lithoViewRule
        .setRoot(RootComponent(renderCounts = renderCounts, asyncUpdates = false))
        .measure()
        .layout()
        .attachToWindow()

    lithoViewRule.findViewWithTag("D").performClick()

    assertThat(renderCounts.A).hasValue(1)
    assertThat(renderCounts.B).hasValue(1)
    assertThat(renderCounts.C).hasValue(1)
    assertThat(renderCounts.D).hasValue(2)

    assertThat(lithoViewRule.lithoView).hasVisibleText("Count[A]: 0")
    assertThat(lithoViewRule.lithoView).hasVisibleText("Count[B]: 0")
    assertThat(lithoViewRule.lithoView).hasVisibleText("Count[C]: 0")
    assertThat(lithoViewRule.lithoView).hasVisibleText("Count[D]: 1")
  }

  /*

    root
     /\
    A  B
       /\
      C [D]

  */
  @Test
  fun `when component D is updated async, A, B, and C should not re-render`() {
    val renderCounts = RenderCounts()
    lithoViewRule
        .setRoot(RootComponent(renderCounts = renderCounts, asyncUpdates = true))
        .measure()
        .layout()
        .attachToWindow()

    lithoViewRule.findViewWithTag("D").performClick()

    backgroundLayoutLooperRule.runToEndOfTasksSync()
    ShadowLooper.idleMainLooper()

    assertThat(renderCounts.A).hasValue(1)
    assertThat(renderCounts.B).hasValue(1)
    assertThat(renderCounts.C).hasValue(1)
    assertThat(renderCounts.D).hasValue(2)

    assertThat(lithoViewRule.lithoView).hasVisibleText("Count[A]: 0")
    assertThat(lithoViewRule.lithoView).hasVisibleText("Count[B]: 0")
    assertThat(lithoViewRule.lithoView).hasVisibleText("Count[C]: 0")
    assertThat(lithoViewRule.lithoView).hasVisibleText("Count[D]: 1")
  }

  /*

    root
     /\
    A  B
       /\
     [C][D]

  */
  @Test
  fun `when components C and D are updated async in the same frame, A and B should not re-render`() {
    val renderCounts = RenderCounts()
    lithoViewRule
        .setRoot(RootComponent(renderCounts = renderCounts, asyncUpdates = true))
        .measure()
        .layout()
        .attachToWindow()

    lithoViewRule.findViewWithTag("C").performClick()
    lithoViewRule.findViewWithTag("D").performClick()

    backgroundLayoutLooperRule.runToEndOfTasksSync()
    ShadowLooper.idleMainLooper()

    assertThat(renderCounts.A).hasValue(1)
    assertThat(renderCounts.B).hasValue(1)
    assertThat(renderCounts.C).hasValue(2)
    assertThat(renderCounts.D).hasValue(2)

    assertThat(lithoViewRule.lithoView).hasVisibleText("Count[A]: 0")
    assertThat(lithoViewRule.lithoView).hasVisibleText("Count[B]: 0")
    assertThat(lithoViewRule.lithoView).hasVisibleText("Count[C]: 1")
    assertThat(lithoViewRule.lithoView).hasVisibleText("Count[D]: 1")
  }

  class RenderCounts {
    val A = AtomicInteger(0)
    val B = AtomicInteger(0)
    val C = AtomicInteger(0)
    val D = AtomicInteger(0)
  }

  private class StateUpdatingLeafComponent(
      val viewTag: String,
      val renderCount: AtomicInteger,
      val asyncUpdates: Boolean
  ) : KComponent() {
    override fun ComponentScope.render(): Component? {
      val clickCount = useState { 0 }

      renderCount.incrementAndGet()

      return Text(
          style =
              Style.viewTag(viewTag).onClick {
                if (asyncUpdates) {
                  clickCount.update { value -> value + 1 }
                } else {
                  clickCount.updateSync { value -> value + 1 }
                }
              },
          text = "Count[$viewTag]: ${clickCount.value}")
    }
  }

  private class BComponent(val renderCounts: RenderCounts, val asyncUpdates: Boolean) :
      KComponent() {
    override fun ComponentScope.render(): Component? {
      val clickCount = useState { 0 }

      renderCounts.B.incrementAndGet()

      return Column {
        child(
            StateUpdatingLeafComponent(
                viewTag = "C", renderCount = renderCounts.C, asyncUpdates = asyncUpdates))
        child(
            StateUpdatingLeafComponent(
                viewTag = "D", renderCount = renderCounts.D, asyncUpdates = asyncUpdates))
        child(
            Text(
                style =
                    Style.viewTag("B").onClick {
                      if (asyncUpdates) {
                        clickCount.update { value -> value + 1 }
                      } else {
                        clickCount.updateSync { value -> value + 1 }
                      }
                    },
                text = "Count[B]: ${clickCount.value}"))
      }
    }
  }

  /*
    Our goal is to build this hierarchy where you can click on any of A, B, C, D and cause them to
    perform a state update:

    root
     /\
    A  B
       /\
      C  D

  */
  private class RootComponent(val renderCounts: RenderCounts, val asyncUpdates: Boolean) :
      KComponent() {
    override fun ComponentScope.render(): Component? {
      return Column {
        child(
            StateUpdatingLeafComponent(
                viewTag = "A", renderCount = renderCounts.A, asyncUpdates = asyncUpdates))
        child(BComponent(renderCounts = renderCounts, asyncUpdates = asyncUpdates))
      }
    }
  }
}