/*
 * 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.testing.api

import com.facebook.litho.Column
import com.facebook.litho.Component
import com.facebook.litho.ComponentScope
import com.facebook.litho.KComponent
import com.facebook.litho.Row
import com.facebook.litho.Style
import com.facebook.litho.core.padding
import com.facebook.litho.dp
import com.facebook.litho.flexbox.flex
import com.facebook.litho.kotlin.widget.Text
import com.facebook.litho.testing.testrunner.LithoTestRunner
import com.facebook.litho.view.onClick
import com.facebook.litho.view.testKey
import com.facebook.litho.widget.Text
import com.facebook.litho.widget.collection.LazyList
import org.assertj.core.api.Assertions
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.annotation.LooperMode

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

  @get:Rule val rule: LithoRule = LithoRule()

  @Test
  fun `simple component hierarchy`() {
    val actual =
        rule.render { SimpleComponent() }.selectNode(hasType<Text>()).printToString().trim()
    val expected =
        """
        -Text
           isEnabled = false
           text = Hello
           Actions = [OnClick]
        """
            .trimIndent()
    Assertions.assertThat(actual).isEqualTo(expected)
  }

  @Test
  fun `multi-level component hierarchy with max depth`() {
    val actual =
        rule.render { StyledComponent() }.selectNode(isRoot()).printToString(maxDepth = 1).trim()
    val expected =
        """
        -StyledComponent(children=1)
           isEnabled = false
         |-Column(children=1)
            isEnabled = false
          """
            .trimIndent()
    Assertions.assertThat(actual).isEqualTo(expected)
  }

  @Test
  fun `multi-level component hierarchy with partial tree`() {
    val actual =
        rule.render { CollectionComponent() }.selectNode(hasType<Row>()).printToString().trim()
    val expected =
        """
        -Row(children=2)
           isEnabled = false
         |-Text
         |  isEnabled = false
         |  text = Hello world
         |-Text
            isEnabled = false
            text = Info
            Actions = [OnClick]
        """
            .trimIndent()
    Assertions.assertThat(actual).isEqualTo(expected)
  }

  @Test
  fun `multi-level component hierarchy`() {
    val actual = rule.render { StyledComponent() }.selectNode(isRoot()).printToString().trim()
    val expected =
        """
        -StyledComponent(children=1)
           isEnabled = false
         |-Column(children=1)
            isEnabled = false
          |-Text
             isEnabled = false
             text = Hello world
        """
            .trimIndent()
    Assertions.assertThat(actual).isEqualTo(expected)
  }

  @Test
  fun `component with collection hierarchy`() {
    val actual = rule.render { CollectionComponent() }.selectNode(isRoot()).printToString().trim()
    val expected =
        """
        -CollectionComponent(children=1)
           isEnabled = false
         |-Column(children=2)
            isEnabled = false
          |-Row(children=2)
          |  isEnabled = false
           |-Text
           |  isEnabled = false
           |  text = Hello world
           |-Text
              isEnabled = false
              text = Info
              Actions = [OnClick]
          |-LazyCollection(children=1)
             isEnabled = false
           |-CollectionRecycler(children=1)
              isEnabled = false
            |-Recycler(children=10)
               isEnabled = false
             |-Text
             |  testKey = item-#1
             |  isEnabled = false
             |  text = Item #1
             |-Text
             |  testKey = item-#2
             |  isEnabled = false
             |  text = Item #2
             |-Text
             |  testKey = item-#3
             |  isEnabled = false
             |  text = Item #3
             |-Text
             |  testKey = item-#4
             |  isEnabled = false
             |  text = Item #4
             |-Text
             |  testKey = item-#5
             |  isEnabled = false
             |  text = Item #5
             |-Text
             |  testKey = item-#6
             |  isEnabled = false
             |  text = Item #6
             |-Text
             |  testKey = item-#7
             |  isEnabled = false
             |  text = Item #7
             |-Text
             |  testKey = item-#8
             |  isEnabled = false
             |  text = Item #8
             |-Text
             |  testKey = item-#9
             |  isEnabled = false
             |  text = Item #9
             |-Text
                testKey = item-#10
                isEnabled = false
                text = Item #10
          """
            .trimIndent()
    Assertions.assertThat(actual).isEqualTo(expected)
  }

  @Test
  fun `multiple matching nodes hierarchy`() {
    val actual =
        rule.render { CollectionComponent() }.selectNodes(hasType<Text>()).printToString().trim()
    val expected =
        """
        Found 12 matching node(s)
        -Text
           isEnabled = false
           text = Hello world
        -Text
           isEnabled = false
           text = Info
           Actions = [OnClick]
        -Text
           testKey = item-#1
           isEnabled = false
           text = Item #1
        -Text
           testKey = item-#2
           isEnabled = false
           text = Item #2
        -Text
           testKey = item-#3
           isEnabled = false
           text = Item #3
        -Text
           testKey = item-#4
           isEnabled = false
           text = Item #4
        -Text
           testKey = item-#5
           isEnabled = false
           text = Item #5
        -Text
           testKey = item-#6
           isEnabled = false
           text = Item #6
        -Text
           testKey = item-#7
           isEnabled = false
           text = Item #7
        -Text
           testKey = item-#8
           isEnabled = false
           text = Item #8
        -Text
           testKey = item-#9
           isEnabled = false
           text = Item #9
        -Text
           testKey = item-#10
           isEnabled = false
           text = Item #10
          """
            .trimIndent()
    Assertions.assertThat(actual).isEqualTo(expected)
  }

  @Test
  fun `multiple matching nodes with multi-level hierarchy`() {
    val actual =
        rule
            .render { CollectionComponent() }
            .selectNodes(hasChild(hasType<Text>()))
            .printToString(maxDepth = Int.MAX_VALUE)
            .trim()
    val expected =
        """
        Found 2 matching node(s)
        -Row(children=2)
           isEnabled = false
         |-Text
         |  isEnabled = false
         |  text = Hello world
         |-Text
            isEnabled = false
            text = Info
            Actions = [OnClick]
        -Recycler(children=10)
           isEnabled = false
         |-Text
         |  testKey = item-#1
         |  isEnabled = false
         |  text = Item #1
         |-Text
         |  testKey = item-#2
         |  isEnabled = false
         |  text = Item #2
         |-Text
         |  testKey = item-#3
         |  isEnabled = false
         |  text = Item #3
         |-Text
         |  testKey = item-#4
         |  isEnabled = false
         |  text = Item #4
         |-Text
         |  testKey = item-#5
         |  isEnabled = false
         |  text = Item #5
         |-Text
         |  testKey = item-#6
         |  isEnabled = false
         |  text = Item #6
         |-Text
         |  testKey = item-#7
         |  isEnabled = false
         |  text = Item #7
         |-Text
         |  testKey = item-#8
         |  isEnabled = false
         |  text = Item #8
         |-Text
         |  testKey = item-#9
         |  isEnabled = false
         |  text = Item #9
         |-Text
            testKey = item-#10
            isEnabled = false
            text = Item #10
          """
            .trimIndent()
    Assertions.assertThat(actual).isEqualTo(expected)
  }

  private class SimpleComponent : KComponent() {
    override fun ComponentScope.render(): Component {
      return Text("Hello", style = Style.onClick { /* no-op */})
    }
  }

  private class StyledComponent : KComponent() {

    override fun ComponentScope.render(): Component {
      return Column(style = Style.padding(20.dp)) { child(Text("Hello world")) }
    }
  }

  private class CollectionComponent : KComponent() {

    override fun ComponentScope.render(): Component {
      return Column(style = Style.flex(grow = 1f)) {
        child(
            Row {
              child(Text("Hello world"))
              child(Text("Info", style = Style.onClick { /* no-op */}))
            })

        child(
            LazyList(style = Style.flex(grow = 1f)) {
              children(items = (1..10), id = { it }) {
                Text("Item #$it", Style.testKey("item-#$it"))
              }
            })
      }
    }
  }
}


/*
 * 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.testing.api

import com.facebook.litho.AttributeKey
import com.facebook.litho.Component
import com.facebook.litho.ComponentScope
import com.facebook.litho.KComponent
import com.facebook.litho.kotlin.widget.Text
import com.facebook.litho.widget.Text
import org.assertj.core.api.Assertions
import org.junit.Test

class FiltersTest {

  private val counterKey = AttributeKey<Int>("counter")

  @Test
  fun `hasType successfully matches when the test node component has the same class`() {
    val testNode = TestNode(DummyComponent())
    val filter = hasType<DummyComponent>()

    Assertions.assertThat(filter.matches(testNode)).isTrue
  }

  @Test
  fun `hasType fails to match when the test node component has the another class`() {
    val testNode = TestNode(DummyComponent())
    val filter = hasType<Text>()

    Assertions.assertThat(filter.matches(testNode)).isFalse
  }

  @Test
  fun `hasAttribute successfully matches when the attribute has the correct value`() {
    val component = DummyComponent()
    component.setDebugAttributeKey(counterKey, 10)

    val filter = hasAttribute(counterKey, 10)
    val testNode = TestNode(component)
    Assertions.assertThat(filter.matches(testNode)).isTrue
  }

  @Test
  fun `hasAttribute fails to match when the attribute has a different value`() {
    val component = DummyComponent()
    component.setDebugAttributeKey(counterKey, 10)

    val filter = hasAttribute(counterKey, 20)
    val testNode = TestNode(component)
    Assertions.assertThat(filter.matches(testNode)).isFalse
  }

  private class DummyComponent : KComponent() {

    override fun ComponentScope.render(): Component {
      return Text("Hello")
    }
  }
}


/*
 * 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.testing.api

import android.content.Context
import android.view.View
import androidx.test.core.app.ApplicationProvider
import com.facebook.litho.Column
import com.facebook.litho.Component
import com.facebook.litho.ComponentContext
import com.facebook.litho.ComponentScope
import com.facebook.litho.ComponentTree
import com.facebook.litho.KComponent
import com.facebook.litho.LithoView
import com.facebook.litho.Row
import com.facebook.litho.Style
import com.facebook.litho.kotlin.widget.Text
import com.facebook.litho.kotlin.widget.VerticalScroll
import com.facebook.litho.testing.api.helpers.LayoutWithSizeSpecs
import com.facebook.litho.testing.api.helpers.SimpleLayoutWithSizeSpecs
import com.facebook.litho.testing.testrunner.LithoTestRunner
import com.facebook.litho.view.testKey
import com.facebook.litho.widget.collection.LazyList
import org.assertj.core.api.Assertions
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.annotation.LooperMode

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

  private val traverser = LithoViewComponentsTraverser()

  @Test
  fun `traverse should be able to traverser a simple hierarchy`() {
    val lithoView = renderLithoViewWithComponent(RootComponent())

    val result = StringBuilder()

    traverser.traverse(lithoView) { component, parent ->
      result.appendComponentData("component", component)
      parent?.let {
        result.append("  ")
        result.appendComponentData("parent", parent)
      }
      result.append('\n')
    }

    val actual = result.toString()
    val expected =
        """
              component[type=RootComponent]
              component[type=Column]  parent[type=RootComponent]
              component[type=Text, testKey=hello]  parent[type=Column]
              component[type=Header]  parent[type=Column]
              component[type=Row]  parent[type=Header]
              component[type=Text, testKey=first-name]  parent[type=Row]
              component[type=Text, testKey=last-name]  parent[type=Row]

              """
            .trimIndent()

    Assertions.assertThat(actual).isEqualTo(expected)
  }

  @Test
  fun `traverse should be able to traverser a complex hierarchy`() {
    val lithoView = renderLithoViewWithComponent(CollectionRootComponent())

    val result = StringBuilder()

    traverser.traverse(lithoView) { component, parent ->
      result.appendComponentData("component", component)
      parent?.let {
        result.append("  ")
        result.appendComponentData("parent", parent)
      }
      result.append('\n')
    }

    val actual = result.toString()
    val expected =
        """
          component[type=CollectionRootComponent]
          component[type=Column]  parent[type=CollectionRootComponent]
          component[type=Text, testKey=hello]  parent[type=Column]
          component[type=Header]  parent[type=Column]
          component[type=Row]  parent[type=Header]
          component[type=Text, testKey=first-name]  parent[type=Row]
          component[type=Text, testKey=last-name]  parent[type=Row]
          component[type=CollectionComponent]  parent[type=Column]
          component[type=LazyCollection]  parent[type=CollectionComponent]
          component[type=CollectionRecycler]  parent[type=LazyCollection]
          component[type=Recycler]  parent[type=CollectionRecycler]
          component[type=Text, testKey=item-#0]  parent[type=Recycler]
          component[type=Text, testKey=item-#1]  parent[type=Recycler]
          component[type=Text, testKey=item-#2]  parent[type=Recycler]
          component[type=Text, testKey=item-#3]  parent[type=Recycler]
          component[type=Text, testKey=item-#4]  parent[type=Recycler]
          component[type=Text, testKey=item-#5]  parent[type=Recycler]
          component[type=Text, testKey=item-#6]  parent[type=Recycler]
          component[type=Text, testKey=item-#7]  parent[type=Recycler]
          component[type=Text, testKey=item-#8]  parent[type=Recycler]
          component[type=Text, testKey=item-#9]  parent[type=Recycler]
          component[type=Text, testKey=item-#10]  parent[type=Recycler]

          """
            .trimIndent()

    Assertions.assertThat(actual).isEqualTo(expected)
  }

  @Test
  fun `traverse should be able to traverser a complex hierarchy which uses a complex nested tree result`() {
    val lithoView = renderLithoViewWithComponent(ComponentWithLayoutWithSizeSpecs())

    val result = StringBuilder()

    traverser.traverse(lithoView) { component, parent ->
      result.appendComponentData("component", component)
      parent?.let {
        result.append("  ")
        result.appendComponentData("parent", parent)
      }
      result.append('\n')
    }

    val actual = result.toString()
    val expected =
        """
          component[type=ComponentWithLayoutWithSizeSpecs]
          component[type=Column]  parent[type=ComponentWithLayoutWithSizeSpecs]
          component[type=Text, testKey=header]  parent[type=Column]
          component[type=Row]  parent[type=Column]
          component[type=LayoutWithSizeSpecs]  parent[type=Row]
          component[type=Row]  parent[type=LayoutWithSizeSpecs]
          component[type=Text, testKey=text-with-size-specs]  parent[type=Row]
          component[type=Text, testKey=row-text]  parent[type=Row]
          component[type=Text, testKey=footer]  parent[type=Column]

          """
            .trimIndent()

    Assertions.assertThat(actual).isEqualTo(expected)
  }

  @Test
  fun `traverse should be able to traverser a complex hierarchy which uses a simple nested tree result`() {
    val lithoView = renderLithoViewWithComponent(ComponentWithSimpleLayoutWithSizeSpecs())

    val result = StringBuilder()

    traverser.traverse(lithoView) { component, parent ->
      result.appendComponentData("component", component)
      parent?.let {
        result.append("  ")
        result.appendComponentData("parent", parent)
      }
      result.append('\n')
    }

    val actual = result.toString()
    val expected =
        """
          component[type=ComponentWithSimpleLayoutWithSizeSpecs]
          component[type=Column]  parent[type=ComponentWithSimpleLayoutWithSizeSpecs]
          component[type=Text, testKey=header]  parent[type=Column]
          component[type=Row]  parent[type=Column]
          component[type=SimpleLayoutWithSizeSpecs]  parent[type=Row]
          component[type=Text, testKey=text-with-size-specs]  parent[type=SimpleLayoutWithSizeSpecs]
          component[type=Text, testKey=row-text]  parent[type=Row]
          component[type=Text, testKey=footer]  parent[type=Column]

          """
            .trimIndent()

    Assertions.assertThat(actual).isEqualTo(expected)
  }

  @Test
  fun `traverse should be able to traverser a complex hierarchy with a vertical scroll wrapper`() {
    val lithoView = renderLithoViewWithComponent(ComponentWithVerticalScroll())

    val result = StringBuilder()

    traverser.traverse(lithoView) { component, parent ->
      result.appendComponentData("component", component)
      parent?.let {
        result.append("  ")
        result.appendComponentData("parent", parent)
      }
      result.append('\n')
    }

    val actual = result.toString()
    val expected =
        """
        component[type=ComponentWithVerticalScroll]
        component[type=VerticalScroll]  parent[type=ComponentWithVerticalScroll]
        component[type=Column]  parent[type=VerticalScroll]
        component[type=Header]  parent[type=Column]
        component[type=Row]  parent[type=Header]
        component[type=Text, testKey=first-name]  parent[type=Row]
        component[type=Text, testKey=last-name]  parent[type=Row]
        component[type=RootComponent]  parent[type=Column]
        component[type=Column]  parent[type=RootComponent]
        component[type=Text, testKey=hello]  parent[type=Column]
        component[type=Header]  parent[type=Column]
        component[type=Row]  parent[type=Header]
        component[type=Text, testKey=first-name]  parent[type=Row]
        component[type=Text, testKey=last-name]  parent[type=Row]

        """
            .trimIndent()

    Assertions.assertThat(actual).isEqualTo(expected)
  }

  private fun renderLithoViewWithComponent(component: Component): LithoView {
    val componentContext = ComponentContext(ApplicationProvider.getApplicationContext<Context>())
    val lithoView = LithoView(componentContext)
    val componentTree = ComponentTree.create(componentContext).build()

    componentTree.setRoot(component)

    lithoView.componentTree = componentTree

    lithoView.onAttachedToWindowForTest()
    lithoView.measure(
        View.MeasureSpec.makeMeasureSpec(1080, View.MeasureSpec.EXACTLY),
        View.MeasureSpec.makeMeasureSpec(1080, View.MeasureSpec.EXACTLY))
    lithoView.layout(0, 0, lithoView.measuredWidth, lithoView.measuredHeight)
    return lithoView
  }

  private fun StringBuilder.appendComponentData(label: String, component: Component) {
    append(label)
    append("[type=${component.javaClass.simpleName}")

    val testKey = component.commonProps?.testKey
    if (!testKey.isNullOrBlank() && testKey != "null") {
      append(", testKey=$testKey")
    }

    append("]")
  }

  private class RootComponent : KComponent() {

    override fun ComponentScope.render(): Component? {
      return Column {
        child(Text("Hello", Style.testKey("hello")))

        child(Header())
      }
    }
  }

  private class Header : KComponent() {
    override fun ComponentScope.render(): Component? {
      return Row {
        child(Text("First Name", Style.testKey("first-name")))

        child(Text("Last name", Style.testKey("last-name")))
      }
    }
  }

  private class CollectionRootComponent : KComponent() {

    override fun ComponentScope.render(): Component? {
      return Column {
        child(Text("Hello", Style.testKey("hello")))
        child(Header())
        child(CollectionComponent())
      }
    }
  }

  private class CollectionComponent : KComponent() {
    override fun ComponentScope.render(): Component? {
      return LazyList {
        children(items = (0..10), id = { it }) { Text("Item #$it", Style.testKey("item-#$it")) }
      }
    }
  }

  /**
   * This component uses a [LayoutWithSizeSpecs] which uses the [@OnCreateLayoutWithSizeSpecs].
   * Internally this creates a [NestedTreeHolderResult], which requires a special handling when
   * traversing down the Component Tree to build the Testing Tree.
   *
   * Therefore, we need to test this special scenario.
   */
  private class ComponentWithLayoutWithSizeSpecs : KComponent() {

    override fun ComponentScope.render(): Component? {
      return Column {
        child(Text("Header", Style.testKey("header")))
        child(
            Row {
              child(LayoutWithSizeSpecs.create(context).build())
              child(Text("Row-text", Style.testKey("row-text")))
            })

        child(Text("Footer", Style.testKey("footer")))
      }
    }
  }

  private class ComponentWithSimpleLayoutWithSizeSpecs : KComponent() {

    override fun ComponentScope.render(): Component? {
      return Column {
        child(Text("Header", Style.testKey("header")))
        child(
            Row {
              child(SimpleLayoutWithSizeSpecs.create(context).build())
              child(Text("Row-text", Style.testKey("row-text")))
            })

        child(Text("Footer", Style.testKey("footer")))
      }
    }
  }

  private class ComponentWithVerticalScroll : KComponent() {
    override fun ComponentScope.render(): Component {
      return VerticalScroll {
        Column {
          child(Header())
          child(RootComponent())
        }
      }
    }
  }
}


/*
 * 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.testing.api

import com.facebook.litho.Column
import com.facebook.litho.Component
import com.facebook.litho.ComponentScope
import com.facebook.litho.KComponent
import com.facebook.litho.Row
import com.facebook.litho.Style
import com.facebook.litho.flexbox.flex
import com.facebook.litho.kotlin.widget.Image
import com.facebook.litho.kotlin.widget.Text
import com.facebook.litho.testing.testrunner.LithoTestRunner
import com.facebook.litho.view.testKey
import com.facebook.litho.widget.Image
import com.facebook.litho.widget.Text
import com.facebook.litho.widget.collection.LazyList
import org.assertj.core.api.Assertions.assertThatThrownBy
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.annotation.LooperMode

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

  @get:Rule val rule: LithoRule = LithoRule()

  @Test
  fun `should be able to assert the existence of multiple test nodes that respect the selection`() {
    rule.render { CollectionComponent() }

    rule.selectNodes(hasType<Text>()).assertExists()
  }

  @Test
  fun `should be able to assert the non existence of test nodes that respect the selection`() {
    rule.render { CollectionComponent() }

    rule.selectNodes(hasType<Image>()).assertDoesNotExist()
  }

  @Test
  fun `should throw an error when there is not a single test node respecting the selection`() {
    rule.render { CollectionComponent() }

    assertThatThrownBy { rule.selectNodes(hasType<Image>()).assertExists() }
        .isInstanceOf(AssertionError::class.java)
  }

  @Test
  fun `should throw an error when there is at least one test node contradicts the selection`() {
    rule.render { CollectionComponent() }

    assertThatThrownBy { rule.selectNodes(hasType<Text>()).assertDoesNotExist() }
        .isInstanceOf(AssertionError::class.java)
  }

  @Test
  fun `should be able to select an item of a collection`() {
    rule.render { CollectionComponent() }

    rule.selectNodes(hasType<Text>()).selectAtIndex(2).assert(hasText("Item #1"))
  }

  @Test
  fun `should be able to assert correct size of component collection`() {
    rule.render { CollectionComponent() }

    rule.selectNodes(hasTextContaining("Item")).assertCount(30)
  }

  @Test
  fun `should throw an error if count assertion fails`() {
    rule.render { CollectionComponent() }

    assertThatThrownBy { rule.selectNodes(hasTextContaining("Item")).assertCount(25) }
        .isInstanceOf(AssertionError::class.java)
  }

  @Test
  fun `should assert that all selected nodes match a condition`() {
    rule.render { CollectionComponent() }

    rule.selectNodes(hasTextContaining("Item")).assertAll(hasType<Text>())
  }

  @Test
  fun `should throw an error if not all of the selected nodes match a condition`() {
    rule.render { CollectionComponent() }

    assertThatThrownBy {
          rule.selectNodes(hasTextContaining("Item")).assertAll(hasTestKey("item-#1"))
        }
        .isInstanceOf(AssertionError::class.java)
        .hasMessageStartingWith("Failed: assertAll")
  }

  @Test
  fun `should assert that any of the selected nodes match a condition`() {
    rule.render { CollectionComponent() }

    rule.selectNodes(hasType<Text>()).assertAny(hasText("Hello world"))
  }

  @Test
  fun `should throw an error if none of the selected nodes match a condition`() {
    rule.render { CollectionComponent() }

    assertThatThrownBy {
          rule.selectNodes(hasType<Text>()).assertAny(hasTextContaining("Goodbye world"))
        }
        .isInstanceOf(AssertionError::class.java)
        .hasMessageStartingWith("Failed: assertAny")
  }

  private class CollectionComponent : KComponent() {

    override fun ComponentScope.render(): Component {
      return Column(style = Style.flex(grow = 1f)) {
        child(Row { child(Text("Hello world")) })

        child(
            LazyList(style = Style.flex(grow = 1f)) {
              children(items = 0 until 30, id = { it }) {
                Text("Item #$it", Style.testKey("item-#$it"))
              }
            })
      }
    }
  }
}


/*
 * 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.testing.api

import com.facebook.litho.Column
import com.facebook.litho.Component
import com.facebook.litho.ComponentScope
import com.facebook.litho.KComponent
import com.facebook.litho.Row
import com.facebook.litho.Style
import com.facebook.litho.core.padding
import com.facebook.litho.dp
import com.facebook.litho.flexbox.flex
import com.facebook.litho.kotlin.widget.Text
import com.facebook.litho.testing.testrunner.LithoTestRunner
import com.facebook.litho.view.testKey
import com.facebook.litho.widget.Text
import com.facebook.litho.widget.collection.LazyList
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.annotation.LooperMode

/**
 * The goal of this set of tests is just to be a health check that basic scenarios of the nodes
 * resolution and verification are working.
 */
@RunWith(LithoTestRunner::class)
@LooperMode(LooperMode.Mode.LEGACY)
class NodeResolutionWithAssertionsTest : RunWithDebugInfoTest() {

  @get:Rule val rule: LithoRule = LithoRule()

  @Test
  fun `one level component hierarchy`() {
    rule.render { SimpleComponent() }.selectNode(hasType<Text>()).assertExists()
  }

  @Test
  fun `multi-level component hierarchy`() {
    rule.render { ParentComponent() }

    rule.selectNode(hasType<Text>()).assertExists()
    rule.selectNode(hasType<ParentComponent>()).assertExists()
    rule.selectNode(hasType<ChildComponent>()).assertExists()
    rule.selectNode(hasType<SimpleComponent>()).assertDoesNotExist()
  }

  @Test
  fun `one level component with collection hierarchy`() {
    rule.render { CollectionComponent() }

    rule.selectNode(hasTestKey("item-#0")).assertExists()
    rule.selectNode(hasTestKey("item-#15")).assertExists()
    rule.selectNode(hasTestKey("item-#30")).assertExists()
  }

  @Test
  fun `root node selection`() {
    rule
        .render { SimpleComponent() }
        .selectNode(isRoot())
        .assertExists()
        .assert(hasType<SimpleComponent>())

    rule
        .render { ParentComponent() }
        .selectNode(isRoot())
        .assertExists()
        .assert(hasType<ParentComponent>())
    rule
        .render { CollectionComponent() }
        .selectNode(isRoot())
        .assertExists()
        .assert(hasType<CollectionComponent>())
  }

  private class SimpleComponent : KComponent() {

    override fun ComponentScope.render(): Component {
      return Text("Hello")
    }
  }

  private class ParentComponent : KComponent() {

    override fun ComponentScope.render(): Component {
      return Column(style = Style.padding(20.dp)) { child(ChildComponent()) }
    }
  }

  private class ChildComponent : KComponent() {
    override fun ComponentScope.render(): Component {
      return Text("Hello")
    }
  }

  class CollectionComponent : KComponent() {

    override fun ComponentScope.render(): Component {
      return Column(style = Style.flex(grow = 1f)) {
        child(Row { child(Text("Hello world")) })

        child(
            LazyList(style = Style.flex(grow = 1f)) {
              children(items = (0..30), id = { it }) {
                Text("Item #$it", Style.testKey("item-#$it"))
              }
            })
      }
    }
  }
}


/*
 * 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.testing.api

import com.facebook.litho.config.ComponentsConfiguration
import org.junit.After
import org.junit.Before

/**
 * This is a not desirable approach to force the test class that extends this one to run with
 * [ComponentsConfiguration.isEndToEndTestRun] always set to `true`.
 *
 * This is needed at the moment since this flag allows to keep extra information about the mounted
 * components, which allows us to build the test nodes tree even with lazy lists/sections. When this
 * flag is disabled this information is discarded and we can't inspect the components inside the
 * collection.
 */
abstract class RunWithDebugInfoTest {

  private val defaultIsEndToEndTest = ComponentsConfiguration.isEndToEndTestRun

  @Before
  fun setup() {
    ComponentsConfiguration.isEndToEndTestRun = true
  }

  @After
  fun teardown() {
    ComponentsConfiguration.isEndToEndTestRun = defaultIsEndToEndTest
  }
}


/*
 * 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.testing.api

import com.facebook.litho.ComponentScope
import com.facebook.litho.KComponent
import com.facebook.litho.kotlin.widget.Text
import org.assertj.core.api.Assertions
import org.junit.Test

class SelectionTest {

  private val identitySelector = TestNodeSelector("identity") { SelectionResult(it) }

  @Test
  fun `materializing exactly 1 match completes successfully`() {
    val testContext = TestTestContext(listOf(TestNode(SimpleComponent())))
    val selection = TestNodeSelection(testContext, identitySelector)

    runCatching { selection.fetchTestNode() }
        .onFailure { Assertions.fail("Excepted success. Found $it") }
  }

  @Test
  fun `assertDoesNotExist passes successfully when no match is found`() {
    val testContext = TestTestContext(emptyList())
    val selection = TestNodeSelection(testContext, identitySelector)

    runCatching { selection.assertDoesNotExist() }
        .onFailure { Assertions.fail("Excepted success. Found $it") }
  }

  @Test
  fun `materializing an empty match throws exception`() {
    val testContext = TestTestContext(emptyList())
    val selection = TestNodeSelection(testContext, identitySelector)

    Assertions.assertThatThrownBy { selection.fetchTestNode() }
        .isInstanceOf(AssertionError::class.java)
  }

  @Test
  fun `materializing a selection with more than 1 match throws exception`() {
    val testNode = TestNode(SimpleComponent())
    val testContext = TestTestContext(listOf(testNode, testNode))
    val selection = TestNodeSelection(testContext, identitySelector)

    Assertions.assertThatThrownBy { selection.fetchTestNode() }
        .isInstanceOf(AssertionError::class.java)
  }
}

internal class TestTestContext(private val nodes: List<TestNode>) : TestContext {
  override fun provideAllTestNodes(): List<TestNode> = nodes
}

internal class SimpleComponent : KComponent() {
  override fun ComponentScope.render() = Text("Hello")
}


/*
 * 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.testing.api

import com.facebook.litho.Component
import com.facebook.litho.ComponentScope
import com.facebook.litho.KComponent
import com.facebook.litho.kotlin.widget.Text
import com.facebook.litho.widget.Text
import org.assertj.core.api.Assertions.assertThat
import org.junit.Test

class TestNodeMatcherTest {

  private val testNode = TestNode(CounterComponent(10))

  private val alwaysFailMatcher = TestNodeMatcher("always fails") { false }
  private val alwaysPassMatcher = TestNodeMatcher("always pass") { true }
  private val counterComponentTypeMatcher = hasType(CounterComponent::class.java)
  private val textTypeMatcher = hasType(Text::class.java)

  @Test
  fun `the TestNodeMatcher created via an and connection of two other matchers will match if both match`() {
    val testNode = TestNode(CounterComponent(10))
    val andConnectionMatcher = counterComponentTypeMatcher and alwaysPassMatcher
    assertThat(andConnectionMatcher.matches(testNode)).isTrue
  }

  @Test
  fun `the TestNodeMatcher created via an and connection of two other matchers will fail if one of them fails to match`() {
    val testNode = TestNode(CounterComponent(10))
    val andConnectionMatcher = counterComponentTypeMatcher and alwaysFailMatcher
    assertThat(andConnectionMatcher.matches(testNode)).isFalse
  }

  @Test
  fun `the TestNodeMatcher created via an or connection of two other matchers will match if one matches`() {
    val testNode = TestNode(CounterComponent(10))
    val orConnectionMatcher = alwaysFailMatcher or counterComponentTypeMatcher
    assertThat(orConnectionMatcher.matches(testNode)).isTrue
  }

  @Test
  fun `the TestNodeMatcher created via an or connection of two other matchers will fail if both of them fail to match`() {
    val orConnectionMatcher = textTypeMatcher or alwaysFailMatcher
    assertThat(orConnectionMatcher.matches(testNode)).isFalse
  }

  @Test
  fun `the TestNodeMatcher negation will pass if the original matcher fails`() {
    assertThat(counterComponentTypeMatcher.matches(testNode)).isTrue

    val negationMatcher = !counterComponentTypeMatcher
    assertThat(negationMatcher.matches(testNode)).isFalse
  }

  @Test
  fun `the TestNodeMatcher negation will fail if the original matcher passes`() {
    assertThat(textTypeMatcher.matches(testNode)).isFalse

    val negationMatcher = !textTypeMatcher
    assertThat(negationMatcher.matches(testNode)).isTrue
  }

  internal class CounterComponent(private val counter: Int) : KComponent() {

    override fun ComponentScope.render(): Component {
      return Text("Counter: $counter")
    }
  }
}


/*
 * 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.testing.api

import com.facebook.litho.ComponentScope
import com.facebook.litho.KComponent
import com.facebook.litho.kotlin.widget.Text
import com.facebook.litho.widget.Text
import org.assertj.core.api.Assertions
import org.junit.Test

class TestSelectionAssertionsTest {

  private val identitySelector = TestNodeSelector("identity") { SelectionResult(it) }

  @Test
  fun `if assertion succeeds it should not throw an assertion error`() {
    val testContext = FakeTestContext(listOf(TestNode(SimpleComponent())))
    val testNodeSelection = TestNodeSelection(testContext, identitySelector)

    runCatching { testNodeSelection.assert(hasType(SimpleComponent::class.java)) }
        .onFailure { Assertions.fail("Should not throw an assertion: $it") }
  }

  @Test
  fun `if assertion fails it should throw an assertion error with the matcher description`() {
    val testContext = FakeTestContext(listOf(TestNode(SimpleComponent())))
    val testNodeSelection = TestNodeSelection(testContext, identitySelector)

    val error = Assertions.catchThrowable { testNodeSelection.assert(hasType(Text::class.java)) }

    Assertions.assertThat(error)
        .isInstanceOf(AssertionError::class.java)
        .hasMessageStartingWith(
            "Failed assertion: is a component of type com.facebook.litho.widget.Text")
  }

  private class FakeTestContext(private val nodes: List<TestNode>) : TestContext {
    override fun provideAllTestNodes(): List<TestNode> = nodes
  }

  private class SimpleComponent : KComponent() {
    override fun ComponentScope.render() = Text("Hello")
  }
}


/*
 * 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.testing.api.behavior

import com.facebook.litho.Column
import com.facebook.litho.Component
import com.facebook.litho.ComponentScope
import com.facebook.litho.KComponent
import com.facebook.litho.Row
import com.facebook.litho.kotlin.widget.Text
import com.facebook.litho.testing.api.LithoRule
import com.facebook.litho.testing.api.hasAncestor
import com.facebook.litho.testing.api.hasText
import com.facebook.litho.testing.api.hasType
import com.facebook.litho.testing.testrunner.LithoTestRunner
import com.facebook.litho.widget.Image
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.annotation.LooperMode

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

  @get:Rule val rule: LithoRule = LithoRule()

  @Test
  fun `hasAncestor filters direct ancestor (parent)`() {
    rule
        .render { TopLevelComponent() }
        .selectNode(hasAncestor(hasType<BottomLevelComponent>()))
        .assert(hasText("bottom-level"))
  }

  @Test
  fun `hasAncestors filters far up ancestors in the same direct line (parents of parents)`() {
    rule
        .render { TopLevelComponent() }
        .selectNode(hasAncestor(hasType<MidLevelComponent>()) and hasText("bottom-level"))
        .assertExists()
  }

  @Test
  fun `hasAncestor will filter out not direct ancestors (uncles)`() {
    rule
        .render { TopLevelComponent() }
        .selectNode(hasAncestor(hasType<MidLevel2Component>()) and hasText("bottom-level"))
        .assertDoesNotExist()
  }

  @Test
  fun `hasAncestor will have no results if the condition does not match any of the ancestors`() {
    rule
        .render { TopLevelComponent() }
        .selectNode(hasAncestor(hasType<Image>()))
        .assertDoesNotExist()
  }

  private class TopLevelComponent : KComponent() {

    override fun ComponentScope.render(): Component {
      return Column {
        child(Text("top-level"))

        child(
            Row {
              child(MidLevelComponent())
              child(MidLevel2Component())
            })
      }
    }
  }

  private class MidLevel2Component : KComponent() {

    override fun ComponentScope.render(): Component {
      return Text("mid-level-2")
    }
  }

  private class MidLevelComponent : KComponent() {

    override fun ComponentScope.render(): Component {
      return Column {
        child(Text("mid-level"))
        child(BottomLevelComponent())
      }
    }
  }

  private class BottomLevelComponent : KComponent() {

    override fun ComponentScope.render(): Component {
      return Text("bottom-level")
    }
  }
}


/*
 * 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.testing.api.behavior

import com.facebook.litho.Column
import com.facebook.litho.Component
import com.facebook.litho.ComponentScope
import com.facebook.litho.KComponent
import com.facebook.litho.kotlin.widget.Text
import com.facebook.litho.testing.api.LithoRule
import com.facebook.litho.testing.api.hasChild
import com.facebook.litho.testing.api.hasText
import com.facebook.litho.testing.api.hasType
import com.facebook.litho.testing.testrunner.LithoTestRunner
import com.facebook.litho.widget.Image
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.annotation.LooperMode

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

  @get:Rule val rule: LithoRule = LithoRule()

  @Test
  fun `hasChild will select the direct children`() {
    rule
        .render { TopLevelComponent() }
        .selectNode(hasChild(hasText("bottom-level")))
        .assert(hasType<BottomLevelComponent>())
  }

  @Test
  fun `hasChild select nothing if there is no child respecting the condition`() {
    rule.render { TopLevelComponent() }.selectNode(hasChild(hasType<Image>())).assertDoesNotExist()
  }

  private class TopLevelComponent : KComponent() {

    override fun ComponentScope.render(): Component {
      return Column {
        child(Text("top-level"))
        child(MidLevelComponent())
      }
    }
  }

  private class MidLevelComponent : KComponent() {

    override fun ComponentScope.render(): Component {
      return Column {
        child(Text("mid-level"))
        child(BottomLevelComponent())
      }
    }
  }

  private class BottomLevelComponent : KComponent() {

    override fun ComponentScope.render(): Component {
      return Text("bottom-level")
    }
  }
}


/*
 * 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.testing.api.behavior

import com.facebook.litho.Column
import com.facebook.litho.Component
import com.facebook.litho.ComponentScope
import com.facebook.litho.KComponent
import com.facebook.litho.Style
import com.facebook.litho.accessibility.contentDescription
import com.facebook.litho.kotlin.widget.Text
import com.facebook.litho.testing.api.LithoRule
import com.facebook.litho.testing.api.hasContentDescription
import com.facebook.litho.testing.testrunner.LithoTestRunner
import org.assertj.core.api.Assertions.assertThatThrownBy
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.annotation.LooperMode

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

  @get:Rule val rule: LithoRule = LithoRule()

  @Test
  fun `will succeed when there is a component with the given content description`() {
    rule.render { DummyComponent("dummy content description") }

    rule.selectNode(hasContentDescription("dummy content description")).assertExists()
    rule.selectNode(hasContentDescription("this is description #1")).assertExists()
  }

  @Test
  fun `will throw assertion when there is no component with the given text`() {
    assertThatThrownBy {
          rule
              .render { DummyComponent("dummy content description") }
              .selectNode(hasContentDescription("Hello my friend!"))
              .assertExists()
        }
        .isInstanceOf(AssertionError::class.java)
  }

  private class DummyComponent(val contentDescription: String) : KComponent() {

    override fun ComponentScope.render(): Component {
      return Column {
        (1..3).forEach { index ->
          child(Text("Hello #$index", Style.contentDescription("this is description #$index")))
        }

        child(Text("World", Style.contentDescription(contentDescription)))
      }
    }
  }
}


/*
 * 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.testing.api.behavior

import com.facebook.litho.Column
import com.facebook.litho.Component
import com.facebook.litho.ComponentScope
import com.facebook.litho.KComponent
import com.facebook.litho.Row
import com.facebook.litho.kotlin.widget.Text
import com.facebook.litho.testing.api.LithoRule
import com.facebook.litho.testing.api.hasDescendant
import com.facebook.litho.testing.api.hasText
import com.facebook.litho.testing.api.hasType
import com.facebook.litho.testing.testrunner.LithoTestRunner
import com.facebook.litho.widget.Image
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.annotation.LooperMode

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

  @get:Rule val rule: LithoRule = LithoRule()

  @Test
  fun `hasDescendant will select the direct children`() {
    rule
        .render { TopLevelComponent() }
        .selectNode(
            hasDescendant(hasType<MidLevelComponent>()) and
                hasDescendant(hasType<MidLevel2Component>()) and
                hasType<TopLevelComponent>())
        .assertExists()
  }

  @Test
  fun `hasDescendant will select the children of children`() {
    rule
        .render { TopLevelComponent() }
        .selectNode(hasDescendant(hasText("bottom-level")) and hasType<MidLevelComponent>())
        .assertExists()
  }

  @Test
  fun `hasDescendant select nothing if there is no descendant respecting the condition`() {
    rule
        .render { TopLevelComponent() }
        .selectNode(hasDescendant(hasType<Image>()))
        .assertDoesNotExist()
  }

  private class TopLevelComponent : KComponent() {

    override fun ComponentScope.render(): Component {
      return Column {
        child(Text("top-level"))

        child(
            Row {
              child(MidLevelComponent())
              child(MidLevel2Component())
            })
      }
    }
  }

  private class MidLevel2Component : KComponent() {

    override fun ComponentScope.render(): Component {
      return Text("mid-level-2")
    }
  }

  private class MidLevelComponent : KComponent() {

    override fun ComponentScope.render(): Component {
      return Column {
        child(Text("mid-level"))
        child(BottomLevelComponent())
      }
    }
  }

  private class BottomLevelComponent : KComponent() {

    override fun ComponentScope.render(): Component {
      return Text("bottom-level")
    }
  }
}


/*
 * 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.testing.api.behavior

import com.facebook.litho.Column
import com.facebook.litho.Component
import com.facebook.litho.ComponentScope
import com.facebook.litho.KComponent
import com.facebook.litho.kotlin.widget.Text
import com.facebook.litho.testing.api.LithoRule
import com.facebook.litho.testing.api.hasParent
import com.facebook.litho.testing.api.hasText
import com.facebook.litho.testing.api.hasType
import com.facebook.litho.testing.testrunner.LithoTestRunner
import com.facebook.litho.widget.Image
import org.assertj.core.api.Assertions.assertThatThrownBy
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.annotation.LooperMode

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

  @get:Rule val rule: LithoRule = LithoRule()

  @Test
  fun `hasParent will succeed when there is a parent component that respects the condition`() {
    rule
        .render { TopLevelComponent() }
        .selectNode(hasParent(hasType<BottomLevelComponent>()))
        .assert(hasText("bottom-level"))
  }

  @Test
  fun `hasParent will throw an error when there is no parent component that respects the condition`() {
    assertThatThrownBy {
          rule.render { TopLevelComponent() }.selectNode(hasParent(hasType<Image>())).assertExists()
        }
        .isInstanceOf(AssertionError::class.java)
  }

  private class TopLevelComponent : KComponent() {

    override fun ComponentScope.render(): Component {
      return Column {
        child(Text("top-level"))
        child(MidLevelComponent())
      }
    }
  }

  private class MidLevelComponent : KComponent() {

    override fun ComponentScope.render(): Component {
      return Column {
        child(Text("mid-level"))
        child(BottomLevelComponent())
      }
    }
  }

  private class BottomLevelComponent : KComponent() {

    override fun ComponentScope.render(): Component {
      return Text("bottom-level")
    }
  }
}


/*
 * 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.testing.api.behavior

import com.facebook.litho.Component
import com.facebook.litho.ComponentScope
import com.facebook.litho.KComponent
import com.facebook.litho.kotlin.widget.Text
import com.facebook.litho.testing.api.LithoRule
import com.facebook.litho.testing.api.hasText
import com.facebook.litho.testing.api.hasTextContaining
import com.facebook.litho.testing.testrunner.LithoTestRunner
import org.assertj.core.api.Assertions.assertThatThrownBy
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.annotation.LooperMode

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

  @get:Rule val rule: LithoRule = LithoRule()

  @Test
  fun `hasText will succeed when there is a component with the exact given text`() {
    rule
        .render { DummyComponent("Hello world!") }
        .selectNode(hasText("Hello world!"))
        .assertExists()
  }

  @Test
  fun `hasTextContaining will succeed when there is a component whose text contains the given text`() {
    rule
        .render { DummyComponent("Hello world!") }
        .selectNode(hasTextContaining("wor"))
        .assertExists()
  }

  @Test
  fun `hasText will throw assertion when there is no component with the exact given text`() {
    assertThatThrownBy {
          rule
              .render { DummyComponent("Hello world!") }
              .selectNode(hasText("Hello world!!"))
              .assertExists()
        }
        .isInstanceOf(AssertionError::class.java)
  }

  @Test
  fun `hasTextContaining will throw assertion when there is no component containing given text`() {
    assertThatThrownBy {
          rule
              .render { DummyComponent("Hello world!") }
              .selectNode(hasTextContaining("word"))
              .assertExists()
        }
        .isInstanceOf(AssertionError::class.java)
  }

  private class DummyComponent(val text: String) : KComponent() {

    override fun ComponentScope.render(): Component {
      return Text(text)
    }
  }
}


/*
 * 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.testing.api.behavior

import com.facebook.litho.Component
import com.facebook.litho.ComponentScope
import com.facebook.litho.KComponent
import com.facebook.litho.Style
import com.facebook.litho.kotlin.widget.Text
import com.facebook.litho.testing.api.LithoRule
import com.facebook.litho.testing.api.hasType
import com.facebook.litho.testing.api.isEnabled
import com.facebook.litho.testing.api.isNotEnabled
import com.facebook.litho.testing.testrunner.LithoTestRunner
import com.facebook.litho.view.enabled
import com.facebook.litho.widget.Text
import org.assertj.core.api.Assertions.assertThatThrownBy
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.annotation.LooperMode

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

  @get:Rule val rule: LithoRule = LithoRule()

  @Test
  fun `selecting via isEnabled will succeed when there is an enabled component`() {
    rule.render { DummyComponent(isEnabled = true) }

    rule.selectNode(hasType<Text>() and isEnabled()).assertExists()
  }

  @Test
  fun `selecting via isEnabled will throw assertion when there is no enabled component`() {
    assertThatThrownBy {
          rule
              .render { DummyComponent(isEnabled = false) }
              .selectNode(hasType<Text>() and isEnabled())
              .assertExists()
        }
        .isInstanceOf(AssertionError::class.java)
  }

  @Test
  fun `selecting via isNotEnabled will succeed when there is a disabled component`() {
    rule.render { DummyComponent(isEnabled = false) }

    rule.selectNode(hasType<Text>() and isNotEnabled()).assertExists()
  }

  @Test
  fun `selecting via isNotEnabled will throw assertion when there is no disabled component`() {
    assertThatThrownBy {
          rule
              .render { DummyComponent(isEnabled = true) }
              .selectNode(hasType<Text>() and isNotEnabled())
              .assertExists()
        }
        .isInstanceOf(AssertionError::class.java)
  }

  private class DummyComponent(private val isEnabled: Boolean) : KComponent() {

    override fun ComponentScope.render(): Component {
      return Text("Not enabled", Style.enabled(isEnabled))
    }
  }
}


/*
 * 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.testing.api.behavior

import com.facebook.litho.Column
import com.facebook.litho.Component
import com.facebook.litho.ComponentScope
import com.facebook.litho.KComponent
import com.facebook.litho.Style
import com.facebook.litho.kotlin.widget.Text
import com.facebook.litho.testing.api.LithoRule
import com.facebook.litho.testing.api.hasText
import com.facebook.litho.testing.api.performClick
import com.facebook.litho.testing.testrunner.LithoTestRunner
import com.facebook.litho.useState
import com.facebook.litho.view.onClick
import org.assertj.core.api.Assertions.assertThatThrownBy
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.annotation.LooperMode

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

  @get:Rule val rule: LithoRule = LithoRule()

  @Test
  fun `performs click if the test node selection is valid`() {
    rule.render { DummyComponent() }
    rule.selectNode(hasText("Number of clicks: 0")).assertExists()
    rule.selectNode(hasText("Increment")).performClick()
    rule.selectNode(hasText("Number of clicks: 1")).assertExists()
  }

  @Test
  fun `performing click in an unexisting node will throw an assertion`() {
    assertThatThrownBy {
          rule.render { DummyComponent() }.selectNode(hasText("I don't exist")).performClick()
        }
        .isInstanceOf(AssertionError::class.java)
  }

  @Test
  fun `performing click in an existing node without a click handler will throw an assertion`() {
    assertThatThrownBy {
          rule.render { DummyComponent() }.selectNode(hasText("Number of clicks: 0")).performClick()
        }
        .isInstanceOf(AssertionError::class.java)
  }

  private class DummyComponent : KComponent() {

    override fun ComponentScope.render(): Component {
      val numClicks = useState { 0 }

      return Column {
        child(Text("Number of clicks: ${numClicks.value}"))
        child(Text("Increment", Style.onClick { numClicks.update { it + 1 } }))
        child(Text("* 10", Style.onClick { numClicks.update { it * 10 } }))
      }
    }
  }
}


/*
 * 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.testing.api.behavior

import com.facebook.litho.Column
import com.facebook.litho.Component
import com.facebook.litho.ComponentScope
import com.facebook.litho.KComponent
import com.facebook.litho.Row
import com.facebook.litho.Style
import com.facebook.litho.kotlin.widget.Text
import com.facebook.litho.testing.api.LithoRule
import com.facebook.litho.testing.api.hasTestKey
import com.facebook.litho.testing.api.hasType
import com.facebook.litho.testing.testrunner.LithoTestRunner
import com.facebook.litho.view.testKey
import org.assertj.core.api.Assertions.assertThatThrownBy
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.annotation.LooperMode

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

  @get:Rule val rule: LithoRule = LithoRule()

  @Test
  fun `selectChildAt will select the direct children`() {
    rule
        .render { TopLevelComponent() }
        .selectNode(hasTestKey("mid-level-items-container"))
        .selectChildAt(1)
        .assert(hasType<MidLevel2Component>())
  }

  @Test
  fun `selectChildAt will throw error if there is no child at given index`() {
    assertThatThrownBy {
          rule
              .render { TopLevelComponent() }
              .selectNode(hasTestKey("mid-level-items-container"))
              .selectChildAt(2)
              .assert(hasType<MidLevelComponent>())
        }
        .isInstanceOf(AssertionError::class.java)
  }

  @Test
  fun `selectChildAt will throw error if current selection is not a single node`() {
    assertThatThrownBy {
          rule
              .render { TopLevelComponent() }
              .selectNode(hasType<Column>())
              .selectChildAt(2)
              .assert(hasType<MidLevelComponent>())
        }
        .isInstanceOf(AssertionError::class.java)
  }

  private class TopLevelComponent : KComponent() {

    override fun ComponentScope.render(): Component {
      return Column {
        child(Text("top-level"))

        child(
            Row(style = Style.testKey("mid-level-items-container")) {
              child(MidLevelComponent())
              child(MidLevel2Component())
            })
      }
    }
  }

  private class MidLevel2Component : KComponent() {

    override fun ComponentScope.render(): Component {
      return Text("mid-level-2")
    }
  }

  private class MidLevelComponent : KComponent() {

    override fun ComponentScope.render(): Component {
      return Column {
        child(Text("mid-level"))
        child(BottomLevelComponent())
      }
    }
  }

  private class BottomLevelComponent : KComponent() {

    override fun ComponentScope.render(): Component {
      return Text("bottom-level")
    }
  }
}


/*
 * 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.testing.api.behavior

import com.facebook.litho.Column
import com.facebook.litho.Component
import com.facebook.litho.ComponentScope
import com.facebook.litho.KComponent
import com.facebook.litho.Row
import com.facebook.litho.Style
import com.facebook.litho.kotlin.widget.Text
import com.facebook.litho.testing.api.LithoRule
import com.facebook.litho.testing.api.hasTestKey
import com.facebook.litho.testing.api.hasText
import com.facebook.litho.testing.testrunner.LithoTestRunner
import com.facebook.litho.view.testKey
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith

@RunWith(LithoTestRunner::class)
class SelectChildrenBehaviorTest {

  @get:Rule val rule: LithoRule = LithoRule()

  @Test
  fun `select children will select the direct children`() {
    rule
        .render { TopLevelComponent() }
        .selectNode(hasTestKey("mid-level-items-container"))
        .selectChildren()
        .assertCount(2)
  }

  @Test
  fun `select children will select nothing when there is no children`() {
    rule
        .render { TopLevelComponent() }
        .selectNode(hasText("bottom-level"))
        .selectChildren()
        .assertCount(0)
  }

  private class TopLevelComponent : KComponent() {

    override fun ComponentScope.render(): Component {
      return Column {
        child(Text("top-level"))

        child(
            Row(style = Style.testKey("mid-level-items-container")) {
              child(MidLevelComponent())
              child(MidLevel2Component())
            })
      }
    }
  }

  private class MidLevel2Component : KComponent() {

    override fun ComponentScope.render(): Component {
      return Text("mid-level-2")
    }
  }

  private class MidLevelComponent : KComponent() {

    override fun ComponentScope.render(): Component {
      return Column {
        child(Text("mid-level"))
        child(BottomLevelComponent())
      }
    }
  }

  private class BottomLevelComponent : KComponent() {

    override fun ComponentScope.render(): Component {
      return Text("bottom-level")
    }
  }
}


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

import com.facebook.kotlin.compilerplugins.dataclassgenerate.annotation.DataClassGenerate
import com.facebook.kotlin.compilerplugins.dataclassgenerate.annotation.Mode
import com.facebook.litho.debug.LithoDebugEvent
import com.facebook.litho.debug.LithoDebugEventAttributes.Breadcrumb
import com.facebook.litho.debug.LithoDebugEventAttributes.HasMainThreadLayoutState
import com.facebook.litho.debug.LithoDebugEventAttributes.MainThreadLayoutStatePrettySizeSpecs
import com.facebook.litho.debug.LithoDebugEventAttributes.MainThreadLayoutStateRootId
import com.facebook.litho.debug.LithoDebugEventAttributes.MeasurePrettySizeSpecs
import com.facebook.litho.debug.LithoDebugEventAttributes.Root
import com.facebook.litho.debug.LithoDebugEventAttributes.RootId
import com.facebook.rendercore.debug.DebugEvent
import com.facebook.rendercore.debug.DebugEventSubscriber

/**
 * This subscriber listens for Litho framework events related to sources of renders forced to be
 * done in the main thread. This was created with the goal of flagging these inconsistencies in
 * different features that use Litho due to the need to specify the size specs in advance.
 */
class MainThreadRenderSourcesDebugger(
    private val onMainThreadRenderDetected: OnMainThreadRenderDetected
) : DebugEventSubscriber(LithoDebugEvent.RenderOnMainThreadStarted) {

  override fun onEvent(event: DebugEvent) {
    val eventBreadcrumb: String = event.attributeOrNull(Breadcrumb) ?: return
    if (event.type != LithoDebugEvent.RenderOnMainThreadStarted) return

    val hasMainThreadLayoutState = event.attribute(HasMainThreadLayoutState, false)
    if (hasMainThreadLayoutState) {
      onMainThreadLayoutStateMismatch(eventBreadcrumb, event)
    } else {
      onMainThreadLayoutStateNotPresent(eventBreadcrumb, event)
    }
  }

  private fun onMainThreadLayoutStateMismatch(breadcrumb: String, event: DebugEvent) {
    onMainThreadRenderDetected.onMainThreadRenderDetected(
        breadcrumb,
        MainThreadRenderSource.MainThreadLayoutStateMismatch(
            root = event.attribute(Root),
            mainThreadLayoutStateSizeSpecs = event.attribute(MainThreadLayoutStatePrettySizeSpecs),
            mainThreadLayoutRootId = event.attribute(MainThreadLayoutStateRootId),
            measureSizeSpecs = event.attribute(MeasurePrettySizeSpecs),
            rootId = event.attribute(RootId),
            breadcrumb = breadcrumb))
  }

  private fun onMainThreadLayoutStateNotPresent(breadcrumb: String, event: DebugEvent) {
    onMainThreadRenderDetected.onMainThreadRenderDetected(
        breadcrumb,
        MainThreadRenderSource.NoMainThreadLayoutState(
            root = event.attribute(Root), breadcrumb = breadcrumb))
  }

  fun interface OnMainThreadRenderDetected {

    fun onMainThreadRenderDetected(breadcrumb: String, source: MainThreadRenderSource)
  }

  sealed class MainThreadRenderSource {

    @DataClassGenerate(toString = Mode.KEEP, equalsHashCode = Mode.KEEP)
    data class NoMainThreadLayoutState(val root: String, val breadcrumb: String) :
        MainThreadRenderSource()

    @DataClassGenerate(toString = Mode.KEEP, equalsHashCode = Mode.KEEP)
    data class MainThreadLayoutStateMismatch(
        val root: String,
        val mainThreadLayoutStateSizeSpecs: String,
        val mainThreadLayoutRootId: Int,
        val measureSizeSpecs: String,
        val rootId: Int,
        val breadcrumb: String
    ) : MainThreadRenderSource()
  }
}


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

import android.graphics.Color
import android.graphics.Paint
import android.graphics.Rect
import android.graphics.drawable.Drawable
import android.graphics.drawable.PaintDrawable
import android.graphics.drawable.ShapeDrawable
import com.facebook.litho.LithoView
import com.facebook.rendercore.debug.DebugEvent
import com.facebook.rendercore.debug.DebugEventAttribute
import com.facebook.rendercore.debug.DebugEventSubscriber
import com.facebook.rendercore.debug.Duration

/**
 * This [DebugEventSubscriber] listens for mount related events for the [view] passed in the
 * constructor and it renders a flashing overlay on top of the view. This overlay will have a
 * different color according to the time the [RenderTree] takes to mount.
 *
 * It will also add a border around the items that are mounted, unmounted or updated with different
 * colors.
 */
class MountUiDebugger(private val view: LithoView) :
    DebugEventSubscriber(
        DebugEvent.RenderTreeMounted,
        DebugEvent.RenderUnitMounted,
        DebugEvent.RenderUnitUnmounted,
        DebugEvent.RenderUnitUpdated) {

  override fun onEvent(event: DebugEvent) {
    val hostHashCode: Int = event.attribute(DebugEventAttribute.RootHostHashCode)
    if (hostHashCode != view.hashCode()) return

    when (event.type) {
      DebugEvent.RenderTreeMounted -> onRenderTreeMounted(event)
      DebugEvent.RenderUnitMounted -> onRenderUnitMounted(event)
      DebugEvent.RenderUnitUnmounted -> onRenderUnitUnmounted(event)
      DebugEvent.RenderUnitUpdated -> onRenderUnitUpdated(event)
    }
  }

  private fun onRenderTreeMounted(event: DebugEvent) {
    val duration: Duration = event.attribute(DebugEventAttribute.duration)

    val color =
        when {
          duration.value < 4_000_000 -> GOOD
          duration.value < 8_000_000 -> MEDIUM
          else -> BAD
        }

    flash(color)
  }

  private fun onRenderUnitMounted(event: DebugEvent) =
      drawBordersOnBounds(Color.YELLOW, event.attribute(DebugEventAttribute.Bounds))

  private fun onRenderUnitUnmounted(event: DebugEvent) =
      drawBordersOnBounds(Color.RED, event.attribute(DebugEventAttribute.Bounds))

  private fun onRenderUnitUpdated(event: DebugEvent) =
      drawBordersOnBounds(Color.GREEN, event.attribute(DebugEventAttribute.Bounds))

  private fun drawBordersOnBounds(color: Int, bounds: Rect) {
    val d = ShapeDrawable()
    d.paint.style = Paint.Style.STROKE
    d.paint.color = color
    d.bounds = bounds
    view.overlay.add(d)
  }

  private fun flash(color: Int) {
    val d: Drawable = PaintDrawable(color)
    d.alpha = 125
    view.post {
      d.setBounds(0, 0, view.width, view.height)
      view.overlay.add(d)
      view.postDelayed({ view.overlay.remove(d) }, 500)
    }
  }

  private val GOOD = Color.parseColor("#81C784")
  private val MEDIUM = Color.parseColor("#FDDA0D")
  private val BAD = Color.parseColor("#E74C3C")
}


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

import android.graphics.Path
import android.graphics.PathDashPathEffect
import androidx.annotation.ColorInt
import com.facebook.litho.Border
import com.facebook.litho.Dimen
import com.facebook.litho.ResourceResolver
import com.facebook.litho.ResourcesScope
import com.facebook.yoga.YogaEdge

/**
 * Creates a border style to be applied to a component
 *
 * @param edgeAll sets the props for all edges.
 * @param edgeHorizontal customises both horizontal edges.
 * @param edgeVertical customises both vertical edges.
 * @param edgeStart customises the start edge, from top to bottom.
 * @param edgeEnd customises end edge, from top to bottom.
 * @param edgeTop customises the top edge of the border.
 * @param edgeBottom customises the bottom edge of the border.
 * @param edgeLeft customises the left edge of the border.
 * @param edgeRight customises for the right edge of the border.
 * @param radius customises each corner radius.
 * @param effect applies an effect to the border.
 */
inline fun ResourcesScope.Border(
    // separate edge properties
    edgeAll: BorderEdge? = null,

    // misc
    edgeHorizontal: BorderEdge? = null,
    edgeVertical: BorderEdge? = null,
    edgeStart: BorderEdge? = null,
    edgeEnd: BorderEdge? = null,

    // standard edges
    edgeTop: BorderEdge? = null,
    edgeBottom: BorderEdge? = null,
    edgeLeft: BorderEdge? = null,
    edgeRight: BorderEdge? = null,

    // radius properties
    radius: BorderRadius? = null,

    // extra path effects
    effect: BorderEffect? = null
): Border =
    Border.create(context)
        .apply {
          edgeAll?.let { it.apply(YogaEdge.ALL, this, resourceResolver) }

          edgeHorizontal?.let { it.apply(YogaEdge.HORIZONTAL, this, resourceResolver) }
          edgeVertical?.let { it.apply(YogaEdge.VERTICAL, this, resourceResolver) }
          edgeStart?.let { it.apply(YogaEdge.START, this, resourceResolver) }
          edgeEnd?.let { it.apply(YogaEdge.END, this, resourceResolver) }

          edgeTop?.let { it.apply(YogaEdge.TOP, this, resourceResolver) }
          edgeBottom?.let { it.apply(YogaEdge.BOTTOM, this, resourceResolver) }
          edgeLeft?.let { it.apply(YogaEdge.LEFT, this, resourceResolver) }
          edgeRight?.let { it.apply(YogaEdge.RIGHT, this, resourceResolver) }

          radius?.let { it.apply(this, resourceResolver) }

          effect?.let { it.apply(this) }
        }
        .build()

/**
 * Stores properties that are used to customise border edges
 *
 * @param color the colour to be applied to the edge
 * @param width the width of the edge
 */
class BorderEdge(@ColorInt val color: Int? = null, val width: Dimen? = null) {

  @PublishedApi
  internal inline fun apply(
      edge: YogaEdge,
      builder: Border.Builder,
      resourceResolver: ResourceResolver
  ) {
    color?.let { builder.color(edge, it) }
    width?.let { builder.widthPx(edge, it.toPixels(resourceResolver)) }
  }
}

/**
 * Stores properties that are used to customise the border radii
 *
 * @param all the radius to be applied to all corners
 * @param topLeft the radius to be applied to the top left corner
 * @param topRight the radius to be applied to the top right corner
 * @param bottomLeft the radius to be applied to the bottom left corner
 * @param bottomRight the radius to be applied to the bottom right corner
 */
class BorderRadius(
    val all: Dimen? = null,
    val topLeft: Dimen? = null,
    val topRight: Dimen? = null,
    val bottomLeft: Dimen? = null,
    val bottomRight: Dimen? = null,
) {
  @PublishedApi
  internal inline fun apply(builder: Border.Builder, resourceResolver: ResourceResolver) {
    all?.let { builder.radiusPx(it.toPixels(resourceResolver)) }
    topLeft?.let { builder.radiusPx(Border.Corner.TOP_LEFT, it.toPixels(resourceResolver)) }
    topRight?.let { builder.radiusPx(Border.Corner.TOP_RIGHT, it.toPixels(resourceResolver)) }
    bottomLeft?.let { builder.radiusPx(Border.Corner.BOTTOM_LEFT, it.toPixels(resourceResolver)) }
    bottomRight?.let { builder.radiusPx(Border.Corner.BOTTOM_RIGHT, it.toPixels(resourceResolver)) }
  }
}

/**
 * Stores properties that are used to customise an effect that is applied to the border.
 *
 * @see BorderEffect.path
 * @see BorderEffect.dashed
 * @see BorderEffect.discrete
 */
class BorderEffect {

  // shared properties
  var phase: Float? = null

  // dash properties
  var intervals: FloatArray? = null

  // path properties
  var pathShape: Path? = null
  var pathAdvance: Float? = null
  var pathStyle: PathDashPathEffect.Style? = null

  // discrete effect properties
  var segmentLength: Float? = null
  var deviation: Float? = null

  private constructor(intervals: FloatArray, phase: Float) {
    this.intervals = intervals
    this.phase = phase
  }

  private constructor(
      pathShape: Path,
      pathAdvance: Float,
      pathPhase: Float,
      pathStyle: PathDashPathEffect.Style
  ) {
    this.pathShape = pathShape
    this.pathAdvance = pathAdvance
    this.phase = pathPhase
    this.pathStyle = pathStyle
  }

  private constructor(segmentLength: Float, deviation: Float) {
    this.segmentLength = segmentLength
    this.deviation = deviation
  }

  @PublishedApi
  internal inline fun apply(builder: Border.Builder) {
    // apply dashed border effect.
    if (this.intervals != null) {
      builder.dashEffect(intervals, phase ?: 0f)
    }

    // apply path border effect.
    if (this.pathShape != null) {
      builder.pathDashEffect(pathShape, pathAdvance ?: 0f, phase ?: 0f, pathStyle)
    }

    // apply discrete path border effect.
    if (this.segmentLength != null) {
      builder.discreteEffect(segmentLength ?: 0f, deviation ?: 0f)
    }
  }

  companion object {

    /**
     * Applies a path dash effect to the border
     *
     * @see Border.Builder.pathDashEffect
     */
    fun path(
        shape: Path,
        advance: Float,
        phase: Float,
        style: PathDashPathEffect.Style
    ): BorderEffect {
      return BorderEffect(shape, advance, phase, style)
    }

    /**
     * Applies a dash effect to the border.
     *
     * @see Border.Builder.dashEffect
     */
    fun dashed(intervals: FloatArray, phase: Float): BorderEffect {
      return BorderEffect(intervals, phase)
    }

    /**
     * Applies a discrete effect to the border.
     *
     * @see Border.Builder.discreteEffect
     */
    fun discrete(segmentLength: Float, deviation: Float): BorderEffect {
      return BorderEffect(segmentLength, deviation)
    }
  }
}


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

import android.graphics.Color
import androidx.annotation.ColorInt
import com.facebook.litho.Component
import com.facebook.litho.Dimen
import com.facebook.litho.ResourcesScope
import com.facebook.litho.Style
import com.facebook.litho.dp
import com.facebook.litho.kotlinStyle
import com.facebook.litho.widget.Card

/** Builder function for creating [CardSpec] components. */
@Suppress("FunctionName")
inline fun ResourcesScope.Card(
    style: Style? = null,
    @ColorInt cardBackgroundColor: Int = Color.WHITE,
    cornerRadius: Dimen = 2.dp,
    elevation: Dimen = 2.dp,
    @ColorInt clippingColor: Int = Integer.MIN_VALUE,
    @ColorInt shadowStartColor: Int = 0x37000000,
    @ColorInt shadowEndColor: Int = 0x03000000,
    shadowBottomOverride: Dimen? = null,
    disableClipTopLeft: Boolean = false,
    disableClipTopRight: Boolean = false,
    disableClipBottomLeft: Boolean = false,
    disableClipBottomRight: Boolean = false,
    transparencyEnabled: Boolean = false,
    child: ResourcesScope.() -> Component
): Card =
    Card.create(context)
        .transparencyEnabled(transparencyEnabled)
        .cardBackgroundColor(cardBackgroundColor)
        .cornerRadiusPx(cornerRadius.toPixels().toFloat())
        .elevationPx(elevation.toPixels().toFloat())
        .clippingColor(clippingColor)
        .shadowStartColor(shadowStartColor)
        .shadowEndColor(shadowEndColor)
        .shadowBottomOverridePx(shadowBottomOverride?.toPixels() ?: -1)
        .disableClipTopLeft(disableClipTopLeft)
        .disableClipTopRight(disableClipTopRight)
        .disableClipBottomLeft(disableClipBottomLeft)
        .disableClipBottomRight(disableClipBottomRight)
        .content(child())
        .kotlinStyle(style)
        .build()