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

import android.graphics.Color
import android.text.Layout
import com.facebook.litho.Border
import com.facebook.litho.Component
import com.facebook.litho.ComponentHost
import com.facebook.litho.ComponentScope
import com.facebook.litho.KComponent
import com.facebook.litho.LithoView
import com.facebook.litho.Row
import com.facebook.litho.Style
import com.facebook.litho.core.height
import com.facebook.litho.core.heightPercent
import com.facebook.litho.core.margin
import com.facebook.litho.core.maxHeight
import com.facebook.litho.core.maxWidth
import com.facebook.litho.core.minHeight
import com.facebook.litho.core.minWidth
import com.facebook.litho.core.padding
import com.facebook.litho.core.width
import com.facebook.litho.core.widthPercent
import com.facebook.litho.px
import com.facebook.litho.testing.LegacyLithoViewRule
import com.facebook.litho.testing.assertMatches
import com.facebook.litho.testing.child
import com.facebook.litho.testing.match
import com.facebook.litho.testing.setRoot
import com.facebook.litho.testing.testrunner.LithoTestRunner
import com.facebook.litho.testing.unspecified
import com.facebook.litho.view.wrapInView
import com.facebook.yoga.YogaAlign
import com.facebook.yoga.YogaDirection
import com.facebook.yoga.YogaEdge
import com.facebook.yoga.YogaPositionType
import org.assertj.core.api.Assertions.assertThat
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith

/** Unit tests for common styles defined in [Style]. */
@RunWith(LithoTestRunner::class)
class FlexboxStylesTest {

  @Rule @JvmField val lithoViewRule = LegacyLithoViewRule()

  @Test
  fun widthAndHeight_whenSet_isRespected() {
    lithoViewRule
        .setSizeSpecs(unspecified(), unspecified())
        .setRoot { Row(style = Style.width(100.px).height(100.px)) }
        .assertMatches(match<LithoView> { bounds(0, 0, 100, 100) })
  }

  @Test
  fun widthPercentAndHeightPercent_whenSet_isRespected() {
    lithoViewRule
        .setSizeSpecs(unspecified(), unspecified())
        .setRoot {
          Row(style = Style.width(100.px).height(100.px)) {
            child(Row(style = Style.heightPercent(50f).widthPercent(50f).wrapInView()))
          }
        }
        .assertMatches(
            match<LithoView> {
              bounds(0, 0, 100, 100)
              child<ComponentHost> { bounds(0, 0, 50, 50) }
            })
  }

  @Test
  fun maxWidth_whenSet_isRespected() {
    lithoViewRule
        .setSizeSpecs(unspecified(), unspecified())
        .setRoot {
          Row(style = Style.height(100.px).maxWidth(200.px)) {
            child(Row(style = Style.width(500.px)))
          }
        }
        .assertMatches(match<LithoView> { bounds(0, 0, 200, 100) })
  }

  @Test
  fun minWidth_whenSet_isRespected() {
    lithoViewRule
        .setSizeSpecs(unspecified(), unspecified())
        .setRoot { Row(style = Style.height(100.px).minWidth(200.px)) }
        .assertMatches(match<LithoView> { bounds(0, 0, 200, 100) })
  }

  @Test
  fun maxHeight_whenSet_isRespected() {
    lithoViewRule
        .setSizeSpecs(unspecified(), unspecified())
        .setRoot {
          Row(style = Style.width(100.px).maxHeight(200.px)) {
            child(Row(style = Style.height(500.px)))
          }
        }
        .assertMatches(match<LithoView> { bounds(0, 0, 100, 200) })
  }

  @Test
  fun minHeight_whenSet_isRespected() {
    lithoViewRule
        .setSizeSpecs(unspecified(), unspecified())
        .setRoot { Row(style = Style.width(100.px).minHeight(200.px)) }
        .assertMatches(match<LithoView> { bounds(0, 0, 100, 200) })
  }

  @Test
  fun flexBasis_whenSet_becomesChildWidth() {
    lithoViewRule
        .setSizeSpecs(unspecified(), unspecified())
        .setRoot {
          Row(style = Style.width(100.px).height(100.px)) {
            child(Row(style = Style.flex(basis = 50.px).wrapInView()))
          }
        }
        .assertMatches(
            match<LithoView> {
              bounds(0, 0, 100, 100)
              child<ComponentHost> { bounds(0, 0, 50, 100) }
            })
  }

  @Test
  fun flexBasisPercent_whenSet_becomesChildWidth() {
    lithoViewRule
        .setSizeSpecs(unspecified(), unspecified())
        .setRoot {
          Row(style = Style.width(200.px).height(100.px)) {
            child(Row(style = Style.flex(basisPercent = 50f).wrapInView()))
          }
        }
        .assertMatches(
            match<LithoView> {
              bounds(0, 0, 200, 100)
              child<ComponentHost> { bounds(0, 0, 100, 100) }
            })
  }

  @Test
  fun flexGrow_whenSet_childTakesWholeSpace() {
    lithoViewRule
        .setSizeSpecs(unspecified(), unspecified())
        .setRoot {
          Row(style = Style.width(100.px).height(100.px)) {
            child(Row(style = Style.flex(grow = 1f).wrapInView()))
          }
        }
        .assertMatches(
            match<LithoView> {
              bounds(0, 0, 100, 100)
              child<ComponentHost> { bounds(0, 0, 100, 100) }
            })
  }

  @Test
  fun flexShrink_whenSet_makesChildAsSmallAsPossible() {
    lithoViewRule
        .setSizeSpecs(unspecified(), unspecified())
        .setRoot {
          Row(alignItems = YogaAlign.STRETCH, style = Style.width(100.px).height(100.px)) {
            child(Row(style = Style.height(100.px).minWidth(50.px).flex(shrink = 1f).wrapInView()))
          }
        }
        .assertMatches(
            match<LithoView> {
              bounds(0, 0, 100, 100)
              child<ComponentHost> { bounds(0, 0, 50, 100) }
            })
  }

  @Test
  fun alignSelf_whenSet_isRespected() {
    lithoViewRule
        .setSizeSpecs(unspecified(), unspecified())
        .setRoot {
          Row(style = Style.width(100.px).height(100.px)) {
            child(Row(style = Style.width(100.px).alignSelf(YogaAlign.STRETCH).wrapInView()))
          }
        }
        .assertMatches(
            match<LithoView> {
              bounds(0, 0, 100, 100)
              child<ComponentHost> { bounds(0, 0, 100, 100) }
            })
  }

  @Test
  fun padding_whenGranularPaddingSetWithLeftRight_isRespected() {
    val left = 10
    val top = 20
    val right = 30
    val bottom = 40

    lithoViewRule
        .setSizeSpecs(unspecified(), unspecified())
        .setRoot {
          Row(
              alignItems = YogaAlign.STRETCH,
              style =
                  Style.width(100.px)
                      .height(100.px)
                      .padding(
                          left = left.px, top = top.px, right = right.px, bottom = bottom.px)) {
                child(Row(style = Style.flex(grow = 1f).wrapInView()))
              }
        }
        .assertMatches(
            match<LithoView> {
              bounds(0, 0, 100, 100)
              child<ComponentHost> { bounds(left, top, 100 - left - right, 100 - top - bottom) }
            })
  }

  @Test
  fun padding_whenStartEndPaddingSet_isRespected() {
    val start = 10
    val end = 20

    lithoViewRule
        .setSizeSpecs(unspecified(), unspecified())
        .setRoot {
          Row(
              alignItems = YogaAlign.STRETCH,
              style = Style.width(100.px).height(100.px).padding(start = start.px, end = end.px)) {
                child(Row(style = Style.flex(grow = 1f).wrapInView()))
              }
        }
        .assertMatches(
            match<LithoView> {
              bounds(0, 0, 100, 100)
              child<ComponentHost> { bounds(start, 0, 100 - start - end, 100) }
            })
  }

  @Test
  fun padding_whenHorizontalVerticalPaddingSet_isRespected() {
    val horizontal = 10
    val vertical = 20

    lithoViewRule
        .setSizeSpecs(unspecified(), unspecified())
        .setRoot {
          Row(
              alignItems = YogaAlign.STRETCH,
              style =
                  Style.width(100.px)
                      .height(100.px)
                      .padding(horizontal = horizontal.px, vertical = vertical.px)) {
                child(Row(style = Style.flex(grow = 1f).wrapInView()))
              }
        }
        .assertMatches(
            match<LithoView> {
              bounds(0, 0, 100, 100)
              child<ComponentHost> {
                bounds(horizontal, vertical, 100 - 2 * horizontal, 100 - 2 * vertical)
              }
            })
  }

  @Test
  fun padding_whenAllPaddingSet_isRespected() {
    val padding = 32

    lithoViewRule
        .setSizeSpecs(unspecified(), unspecified())
        .setRoot {
          Row(
              alignItems = YogaAlign.STRETCH,
              style = Style.width(100.px).height(100.px).padding(padding.px)) {
                child(Row(style = Style.flex(grow = 1f).wrapInView()))
              }
        }
        .assertMatches(
            match<LithoView> {
              bounds(0, 0, 100, 100)
              child<ComponentHost> {
                bounds(padding, padding, 100 - 2 * padding, 100 - 2 * padding)
              }
            })
  }

  @Test
  fun margin_whenGranularMarginSet_isRespected() {
    val left = 10
    val top = 20
    val right = 30
    val bottom = 40

    lithoViewRule
        .setSizeSpecs(unspecified(), unspecified())
        .setRoot {
          Row(alignItems = YogaAlign.STRETCH, style = Style.width(100.px).height(100.px)) {
            child(
                Row(
                    style =
                        Style.margin(
                                left = left.px, top = top.px, right = right.px, bottom = bottom.px)
                            .flex(grow = 1f)
                            .wrapInView()))
          }
        }
        .assertMatches(
            match<LithoView> {
              bounds(0, 0, 100, 100)
              child<ComponentHost> { bounds(left, top, 100 - left - right, 100 - top - bottom) }
            })
  }

  @Test
  fun margin_whenMarginStartEndSet_isRespected() {
    val start = 10
    val end = 20

    lithoViewRule
        .setSizeSpecs(unspecified(), unspecified())
        .setRoot {
          Row(alignItems = YogaAlign.STRETCH, style = Style.width(100.px).height(100.px)) {
            child(
                Row(
                    style =
                        Style.margin(start = start.px, end = end.px).flex(grow = 1f).wrapInView()))
          }
        }
        .assertMatches(
            match<LithoView> {
              bounds(0, 0, 100, 100)
              child<ComponentHost> { bounds(start, 0, 100 - start - end, 100) }
            })
  }

  @Test
  fun margin_whenHorizontalVerticalMarginSet_isRespected() {
    val horizontal = 10
    val vertical = 20

    lithoViewRule
        .setSizeSpecs(unspecified(), unspecified())
        .setRoot {
          Row(alignItems = YogaAlign.STRETCH, style = Style.width(100.px).height(100.px)) {
            child(
                Row(
                    style =
                        Style.margin(horizontal = horizontal.px, vertical = vertical.px)
                            .flex(grow = 1f)
                            .wrapInView()))
          }
        }
        .assertMatches(
            match<LithoView> {
              bounds(0, 0, 100, 100)
              child<ComponentHost> {
                bounds(horizontal, vertical, 100 - 2 * horizontal, 100 - 2 * vertical)
              }
            })
  }

  @Test
  fun margin_whenAllMarginSet_isRespected() {
    val margin = 32

    lithoViewRule
        .setSizeSpecs(unspecified(), unspecified())
        .setRoot {
          Row(alignItems = YogaAlign.STRETCH, style = Style.width(100.px).height(100.px)) {
            child(Row(style = Style.margin(margin.px).flex(grow = 1f).wrapInView()))
          }
        }
        .assertMatches(
            match<LithoView> {
              bounds(0, 0, 100, 100)
              child<ComponentHost> { bounds(margin, margin, 100 - 2 * margin, 100 - 2 * margin) }
            })
  }

  @Test
  fun position_whenLeftRightSet_isRespected() {
    val left = 10
    val top = 20
    val right = 30
    val bottom = 40

    lithoViewRule
        .setSizeSpecs(unspecified(), unspecified())
        .setRoot {
          Row(style = Style.width(100.px).height(100.px)) {
            child(
                Row(
                    style =
                        Style.positionType(YogaPositionType.ABSOLUTE)
                            .position(
                                left = left.px, top = top.px, right = right.px, bottom = bottom.px)
                            .wrapInView()))
          }
        }
        .assertMatches(
            match<LithoView> {
              bounds(0, 0, 100, 100)
              child<ComponentHost> { bounds(left, top, 100 - left - right, 100 - top - bottom) }
            })
  }

  @Test
  fun position_whenStartEndSet_isRespected() {
    val start = 10
    val top = 20
    val end = 30
    val bottom = 40

    lithoViewRule
        .setSizeSpecs(unspecified(), unspecified())
        .setRoot {
          Row(style = Style.width(100.px).height(100.px)) {
            child(
                Row(
                    style =
                        Style.positionType(YogaPositionType.ABSOLUTE)
                            .position(
                                start = start.px, top = top.px, end = end.px, bottom = bottom.px)
                            .wrapInView()))
          }
        }
        .assertMatches(
            match<LithoView> {
              bounds(0, 0, 100, 100)
              child<ComponentHost> { bounds(start, top, 100 - start - end, 100 - top - bottom) }
            })
  }

  @Test
  fun position_whenAllSet_isRespected() {
    val all = 10

    lithoViewRule
        .setSizeSpecs(unspecified(), unspecified())
        .setRoot {
          Row(style = Style.width(100.px).height(100.px)) {
            child(
                Row(
                    style =
                        Style.positionType(YogaPositionType.ABSOLUTE)
                            .position(all = all.px)
                            .wrapInView()))
          }
        }
        .assertMatches(
            match<LithoView> {
              bounds(0, 0, 100, 100)
              child<ComponentHost> { bounds(all, all, 100 - 2 * all, 100 - 2 * all) }
            })
  }

  @Test
  fun position_whenHorizontalSet_isRespected() {
    val horizontal = 10
    val all = 50

    lithoViewRule
        .setSizeSpecs(unspecified(), unspecified())
        .setRoot {
          Row(style = Style.width(100.px).height(100.px)) {
            child(
                Row(
                    style =
                        Style.positionType(YogaPositionType.ABSOLUTE)
                            .position(all = all.px, horizontal = horizontal.px)
                            .wrapInView()))
          }
        }
        .assertMatches(
            match<LithoView> {
              bounds(0, 0, 100, 100)
              child<ComponentHost> {
                bounds(horizontal, 100 - all, 100 - 2 * horizontal, 100 - 2 * all)
              }
            })
  }

  @Test
  fun position_whenVerticalSet_isRespected() {
    val vertical = 10
    val all = 50

    lithoViewRule
        .setSizeSpecs(unspecified(), unspecified())
        .setRoot {
          Row(style = Style.width(100.px).height(100.px)) {
            child(
                Row(
                    style =
                        Style.positionType(YogaPositionType.ABSOLUTE)
                            .position(vertical = vertical.px, all = all.px)
                            .wrapInView()))
          }
        }
        .assertMatches(
            match<LithoView> {
              bounds(0, 0, 100, 100)
              child<ComponentHost> { bounds(all, vertical, 100 - 2 * all, 100 - 2 * vertical) }
            })
  }

  @Test
  fun layoutDirection_whenSet_isRespected() {
    lithoViewRule
        .setSizeSpecs(unspecified(), unspecified())
        .setRoot {
          Row(style = Style.width(100.px).height(100.px).layoutDirection(YogaDirection.RTL))
        }
        .measure()
        .layout()
        .attachToWindow()

    assertThat(lithoViewRule.currentRootNode?.getYogaNode()?.getLayoutDirection())
        .isEqualTo(YogaDirection.RTL)
  }

  /**
   * Test is using [Layout] and [NodeInfo] classes as a workaround for the issue with 'libyoga.so
   * already loaded in another classloader exception' caused by multiple ClassLoaders trying to load
   * Yoga when using @Config to specify a different target sdk. See:
   * https://www.internalfb.com/intern/staticdocs/litho/docs/testing/unit-testing/
   */
  @Test
  fun border_whenSet_isRespected() {
    class ComponentWithBorder : KComponent() {
      override fun ComponentScope.render(): Component? {
        return Row(
            style =
                Style.border(
                    Border.create(context)
                        .color(YogaEdge.LEFT, Color.BLUE)
                        .color(YogaEdge.TOP, Color.RED)
                        .color(YogaEdge.RIGHT, Color.BLACK)
                        .color(YogaEdge.BOTTOM, Color.WHITE)
                        .radiusDip(Border.Corner.TOP_LEFT, 5f)
                        .radiusDip(Border.Corner.TOP_RIGHT, 6f)
                        .radiusDip(Border.Corner.BOTTOM_RIGHT, 7f)
                        .radiusDip(Border.Corner.BOTTOM_LEFT, 8f)
                        .build()))
      }
    }

    val node = LegacyLithoViewRule.getRootLayout(lithoViewRule, ComponentWithBorder())?.node
    assertThat(node?.borderColors)
        .isEqualTo(intArrayOf(Color.BLUE, Color.RED, Color.BLACK, Color.WHITE))
    assertThat(node?.borderRadius).isEqualTo(floatArrayOf(5f, 6f, 7f, 8f))
  }

  @Test
  fun marginAuto_whenSet_isRespected() {
    lithoViewRule
        .setSizeSpecs(200, 200)
        .setRoot {
          Row(
              style =
                  Style.width(100.px)
                      .height(100.px)
                      .marginAuto(YogaEdge.LEFT)
                      .marginAuto(YogaEdge.TOP))
        }
        .measure()
        .layout()
        .attachToWindow()

    assertThat(lithoViewRule.currentRootNode?.yogaNode?.getMargin(YogaEdge.LEFT).toString())
        .isEqualTo("auto")
    assertThat(lithoViewRule.currentRootNode?.yogaNode?.getMargin(YogaEdge.TOP).toString())
        .isEqualTo("auto")
  }

  @Test
  fun isReferenceBaseline_whenSet_isRespected() {
    lithoViewRule
        .setSizeSpecs(200, 200)
        .setRoot { Row(style = Style.isReferenceBaseline(true)) }
        .measure()
        .layout()
        .attachToWindow()

    assertThat(lithoViewRule.currentRootNode?.yogaNode?.isReferenceBaseline).isEqualTo(true)
  }

  @Test
  fun useHeightAsBaseline_whenSet_isRespected() {
    lithoViewRule
        .setSizeSpecs(200, 200)
        .setRoot { Row(style = Style.useHeightAsBaseline(true)) }
        .measure()
        .layout()
        .attachToWindow()

    assertThat(lithoViewRule.currentRootNode?.yogaNode?.isBaselineDefined).isEqualTo(true)
  }
}


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

package com.facebook.litho.recyclerview

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.sections.SectionContext
import com.facebook.litho.sections.common.SingleComponentSection
import com.facebook.litho.sections.widget.RecyclerCollectionComponent
import com.facebook.litho.testing.LithoViewRule
import com.facebook.litho.testing.assertj.LithoAssertions
import com.facebook.litho.widget.Text
import com.facebook.litho.widget.collection.LazyList
import org.assertj.core.api.Assertions.assertThat
import org.assertj.core.api.Assertions.assertThatThrownBy
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner
import org.robolectric.annotation.LooperMode

/**
 * Tests which ensure that helpful errors are displayed to the developer when operating with the
 * TestCollection API. This utilizes the [RobolectricTestRunner] to better emulate the end user's
 * test setup.
 */
@LooperMode(LooperMode.Mode.LEGACY)
@RunWith(RobolectricTestRunner::class)
class TestCollectionTest {

  @Rule @JvmField val lithoViewRule = LithoViewRule()

  @Test
  fun `test helpful error when recycler collection components recycler view is not mounted`() {

    val testView =
        lithoViewRule.render {
          TestComponent(
              RecyclerCollectionComponent(
                  SingleComponentSection.create(SectionContext(context))
                      .component(Text("0"))
                      .build()))
        }

    val listComponent = testView.findCollectionComponent()
    assertThat(listComponent).isNotNull

    assertThatThrownBy { LithoAssertions.assertThat(listComponent).hasVisibleText("0") }
        .hasMessageContaining("could not find a mounted recycler view")
        .hasMessageContaining("add a width and height")
  }

  @Test
  fun `test helpful error when lazy list recycler view is unable to properly measure`() {
    assertThatThrownBy {
          lithoViewRule.render {
            TestComponent(LazyList { child(id = 0, component = Text("child0")) })
          }
        }
        .hasMessageContaining(
            "Height mode has to be EXACTLY OR AT MOST for a vertical scrolling RecyclerView")
  }

  private class TestComponent(private val childComponent: Component) : KComponent() {
    override fun ComponentScope.render(): Component = Column { child(childComponent) }
  }
}


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

import android.view.View
import com.facebook.litho.Component
import com.facebook.litho.ComponentScope
import com.facebook.rendercore.testing.ViewAssertions
import com.facebook.rendercore.testing.match.ViewMatchNode

/** Shorthand helper to create a root component that needs a ComponentScope. */
fun LegacyLithoViewRule.setRoot(componentFunction: ComponentScope.() -> Component) =
    setRoot(with(ComponentScope(context)) { componentFunction() })

/**
 * Measures/lays out/attaches to window if not already done, then matches the hierarchy against the
 * provided [ViewMatchNode].
 */
fun LegacyLithoViewRule.assertMatches(matchNode: ViewMatchNode) {
  measure().layout().attachToWindow()
  ViewAssertions.assertThat(lithoView).matches(matchNode)
}

/** Kotlin DSL for creating and configuring a root ViewMatchNode. */
inline fun <reified T : View> match(init: (ViewMatchNode.() -> Unit) = {}) =
    ViewMatchNode.forType(T::class.java).apply(init)

/** Kotlin DSL for creating and configuring a child ViewMatchNode. */
inline fun <reified T : View> ViewMatchNode.child(init: (ViewMatchNode.() -> Unit) = {}) {
  this.child(match<T>(init))
}


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

import android.graphics.Color
import android.graphics.drawable.ColorDrawable
import android.text.Layout
import android.util.SparseArray
import android.view.MotionEvent
import android.view.ViewOutlineProvider
import com.facebook.litho.Component
import com.facebook.litho.ComponentHost
import com.facebook.litho.ComponentScope
import com.facebook.litho.KComponent
import com.facebook.litho.LithoView
import com.facebook.litho.MatrixDrawable
import com.facebook.litho.Row
import com.facebook.litho.Style
import com.facebook.litho.core.height
import com.facebook.litho.core.width
import com.facebook.litho.key
import com.facebook.litho.px
import com.facebook.litho.testing.LegacyLithoViewRule
import com.facebook.litho.testing.assertMatches
import com.facebook.litho.testing.child
import com.facebook.litho.testing.match
import com.facebook.litho.testing.setRoot
import com.facebook.litho.testing.testrunner.LithoTestRunner
import com.facebook.litho.testing.unspecified
import com.facebook.litho.transition.transitionKey
import java.util.concurrent.atomic.AtomicBoolean
import org.assertj.core.api.Assertions.assertThat
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.kotlin.mock

/** Unit tests for common styles defined in [Style]. */
@RunWith(LithoTestRunner::class)
class ViewStylesTest {

  @Rule @JvmField val lithoViewRule = LegacyLithoViewRule()

  @Test
  fun background_whenSet_isRespected() {
    lithoViewRule
        .setSizeSpecs(unspecified(), unspecified())
        .setRoot {
          Row(style = Style.width(100.px).height(100.px).background(ColorDrawable(Color.WHITE)))
        }
        .measure()
        .layout()
        .attachToWindow()

    assertHasColorDrawableOfColor(lithoViewRule.lithoView, Color.WHITE)
  }

  @Test
  fun backgroundColor_whenSet_isRespected() {
    lithoViewRule
        .setSizeSpecs(unspecified(), unspecified())
        .setRoot { Row(style = Style.width(100.px).height(100.px).backgroundColor(Color.WHITE)) }
        .measure()
        .layout()
        .attachToWindow()

    assertHasColorDrawableOfColor(lithoViewRule.lithoView, Color.WHITE)
  }

  @Test
  fun clickable_whenSet_isRespected() {
    lithoViewRule
        .setSizeSpecs(unspecified(), unspecified())
        .setRoot { Row(style = Style.width(100.px).height(100.px).clickable(true)) }
        .measure()
        .layout()
        .attachToWindow()

    assertThat(lithoViewRule.lithoView.isClickable).isTrue

    lithoViewRule
        .setSizeSpecs(unspecified(), unspecified())
        .setRoot { Row(style = Style.width(100.px).height(100.px).clickable(false)) }
        .measure()
        .layout()
        .attachToWindow()

    assertThat(lithoViewRule.lithoView.isClickable).isFalse
  }

  @Test
  fun clipChildren_whenSet_isRespected() {
    lithoViewRule
        .setSizeSpecs(unspecified(), unspecified())
        .setRoot { Row(style = Style.width(100.px).height(100.px).clipChildren(true)) }
        .measure()
        .layout()
        .attachToWindow()

    assertThat(lithoViewRule.lithoView.clipChildren).isTrue

    lithoViewRule
        .setSizeSpecs(unspecified(), unspecified())
        .setRoot { Row(style = Style.width(100.px).height(100.px).clipChildren(false)) }
        .measure()
        .layout()
        .attachToWindow()

    assertThat(lithoViewRule.lithoView.clipChildren).isFalse
  }

  @Test
  fun `duplicateChildrenStates - when set to true enables node to duplicate children states`() {
    class DuplicateChildrenStatesComponent : KComponent() {
      override fun ComponentScope.render(): Component? =
          Row(style = Style.width(200.px).height(200.px).duplicateChildrenStates(true)) {
            child(Row(style = Style.width(100.px).height(100.px)))
          }
    }

    val node =
        LegacyLithoViewRule.getRootLayout(lithoViewRule, DuplicateChildrenStatesComponent())?.node
    assertThat(node?.isDuplicateChildrenStatesEnabled).isTrue
  }

  @Test
  fun `duplicateChildrenStates - when set to false disables node from duplicating children states`() {
    class DuplicateChildrenStatesComponent : KComponent() {
      override fun ComponentScope.render(): Component? =
          Row(style = Style.width(200.px).height(200.px).duplicateChildrenStates(false)) {
            child(Row(style = Style.width(100.px).height(100.px)))
          }
    }

    val node =
        LegacyLithoViewRule.getRootLayout(lithoViewRule, DuplicateChildrenStatesComponent())?.node
    assertThat(node?.isDuplicateChildrenStatesEnabled).isFalse
  }

  @Test
  fun `duplicateParentState - when set to true sets node enabled to duplicate parent state`() {
    class DuplicateParentStateComponent : KComponent() {
      override fun ComponentScope.render(): Component? =
          Row(style = Style.width(200.px).height(200.px)) {
            child(Row(style = Style.width(100.px).height(100.px).duplicateParentState(true)))
          }
    }

    val node =
        LegacyLithoViewRule.getRootLayout(lithoViewRule, DuplicateParentStateComponent())?.node
    val childNode = node?.getChildAt(0)
    assertThat(childNode?.isDuplicateParentStateEnabled).isTrue
  }

  @Test
  fun `duplicateParentState - when set to false sets node disabled from duplicating parent state`() {
    class DuplicateParentStateComponent : KComponent() {
      override fun ComponentScope.render(): Component? =
          Row(style = Style.width(200.px).height(200.px)) {
            child(Row(style = Style.width(100.px).height(100.px).duplicateParentState(false)))
          }
    }

    val node =
        LegacyLithoViewRule.getRootLayout(lithoViewRule, DuplicateParentStateComponent())?.node
    val childNode = node?.getChildAt(0)
    assertThat(childNode?.isDuplicateParentStateEnabled).isFalse
  }

  @Test
  fun focusable_whenSet_isRespected() {
    lithoViewRule
        .setSizeSpecs(unspecified(), unspecified())
        .setRoot { Row(style = Style.width(100.px).height(100.px).focusable(true)) }
        .measure()
        .layout()
        .attachToWindow()

    assertThat(lithoViewRule.lithoView.isFocusable).isTrue
  }

  @Test
  fun foreground_whenSet_isRespected() {
    lithoViewRule
        .setSizeSpecs(unspecified(), unspecified())
        .setRoot {
          Row(style = Style.width(100.px).height(100.px).foreground(ColorDrawable(Color.WHITE)))
        }
        .measure()
        .layout()
        .attachToWindow()

    assertHasColorDrawableOfColor(lithoViewRule.lithoView, Color.WHITE)
  }

  @Test
  fun onClick_whenSet_isDispatchedOnClick() {
    val wasClicked = AtomicBoolean(false)

    lithoViewRule
        .setSizeSpecs(unspecified(), unspecified())
        .setRoot {
          Row(style = Style.width(200.px).height(200.px)) {
            child(
                Row(
                    style =
                        Style.width(100.px).height(100.px).viewTag("click_me").onClick {
                          wasClicked.set(true)
                        }))
          }
        }
        .measure()
        .layout()
        .attachToWindow()

    assertThat(wasClicked.get()).isFalse()
    lithoViewRule.findViewWithTag("click_me").performClick()
    assertThat(wasClicked.get()).isTrue()
  }

  @Test
  fun `onClick - when set respects enable check`() {
    lithoViewRule
        .setSizeSpecs(unspecified(), unspecified())
        .setRoot {
          Row(style = Style.width(200.px).height(200.px)) {
            child(
                Row(
                    style =
                        Style.width(100.px).height(100.px).viewTag("click_me").onClick(
                            enabled = false) {
                              error("We should have not executed this code block")
                            }))
          }
        }
        .measure()
        .layout()
        .attachToWindow()

    lithoViewRule.findViewWithTag("click_me").performClick()
  }

  @Test
  fun `onClick - when override respects last setup`() {
    lithoViewRule
        .setSizeSpecs(unspecified(), unspecified())
        .setRoot {
          Row(style = Style.width(200.px).height(200.px)) {
            child(
                Row(
                    style =
                        Style.width(100.px)
                            .height(100.px)
                            .viewTag("click_me")
                            .onClick { error("We should have not executed this code block") }
                            .onClick(enabled = false) {
                              error("We should have not executed this code block")
                            }))
          }
        }
        .measure()
        .layout()
        .attachToWindow()

    lithoViewRule.findViewWithTag("click_me").performClick()
  }

  @Test
  fun onLongClick_whenSet_isDispatchedOnLongClick() {
    val wasLongClicked = AtomicBoolean(false)

    lithoViewRule
        .setSizeSpecs(unspecified(), unspecified())
        .setRoot {
          Row(
              style =
                  Style.width(100.px).height(100.px).viewTag("click_me").onLongClick {
                    wasLongClicked.set(true)
                    true
                  })
        }
        .measure()
        .layout()
        .attachToWindow()

    assertThat(wasLongClicked.get()).isFalse()
    lithoViewRule.findViewWithTag("click_me").performLongClick()
    assertThat(wasLongClicked.get()).isTrue()
  }

  @Test
  fun `onLongClick - when set respects enable check`() {
    lithoViewRule
        .setSizeSpecs(unspecified(), unspecified())
        .setRoot {
          Row(style = Style.width(200.px).height(200.px)) {
            child(
                Row(
                    style =
                        Style.width(100.px).height(100.px).viewTag("click_me").onLongClick(
                            enabled = false) {
                              error("We should have not executed this code block")
                              true
                            }))
          }
        }
        .measure()
        .layout()
        .attachToWindow()

    lithoViewRule.findViewWithTag("click_me").performLongClick()
  }

  @Test
  fun `onTouch - when set is dispatched on touch`() {
    val wasTouched = AtomicBoolean(false)

    lithoViewRule
        .setSizeSpecs(unspecified(), unspecified())
        .setRoot {
          Row(style = Style.width(200.px).height(200.px)) {
            child(
                Row(
                    style =
                        Style.width(100.px).height(100.px).viewTag("touch_me").onTouch {
                          wasTouched.set(true)
                          true
                        }))
          }
        }
        .measure()
        .layout()
        .attachToWindow()

    assertThat(wasTouched.get()).isFalse()
    lithoViewRule.findViewWithTag("touch_me").dispatchTouchEvent(getMotionEvent())
    assertThat(wasTouched.get()).isTrue()
  }

  @Test
  fun `onTouch - when set respects enable check`() {
    lithoViewRule
        .setSizeSpecs(unspecified(), unspecified())
        .setRoot {
          Row(style = Style.width(200.px).height(200.px)) {
            child(
                Row(
                    style =
                        Style.width(100.px).height(100.px).viewTag("touch_me").onTouch(
                            enabled = false) {
                              error("We should have not executed this code block")
                              true
                            }))
          }
        }
        .measure()
        .layout()
        .attachToWindow()

    lithoViewRule.findViewWithTag("touch_me").dispatchTouchEvent(getMotionEvent())
  }

  @Test
  fun `onInterceptTouch - when set is dispatched on touch`() {
    val wasTouchedIntercepted = AtomicBoolean(false)

    lithoViewRule
        .setSizeSpecs(unspecified(), unspecified())
        .setRoot {
          Row(style = Style.width(200.px).height(200.px)) {
            child(
                Row(
                    style =
                        Style.width(100.px).height(100.px).viewTag("touch_me").onInterceptTouch {
                          wasTouchedIntercepted.set(true)
                          true
                        }))
          }
        }
        .measure()
        .layout()
        .attachToWindow()

    assertThat(wasTouchedIntercepted.get()).isFalse()
    lithoViewRule.findViewWithTag("touch_me").dispatchTouchEvent(getMotionEvent())
    assertThat(wasTouchedIntercepted.get()).isTrue()
  }

  @Test
  fun `onInterceptTouch - when set respects enable check`() {
    lithoViewRule
        .setSizeSpecs(unspecified(), unspecified())
        .setRoot {
          Row(style = Style.width(200.px).height(200.px)) {
            child(
                Row(
                    style =
                        Style.width(100.px).height(100.px).viewTag("touch_me").onInterceptTouch(
                            enabled = false) {
                              error("We should have not executed this code block")
                              true
                            }))
          }
        }
        .measure()
        .layout()
        .attachToWindow()

    lithoViewRule.findViewWithTag("touch_me").dispatchTouchEvent(getMotionEvent())
  }

  @Test
  fun rotation_whenSet_isRespected() {
    lithoViewRule
        .setSizeSpecs(unspecified(), unspecified())
        .setRoot {
          Row(
              style =
                  Style.width(100.px).height(100.px).rotation(90f).rotationX(45f).rotationY(30f))
        }
        .measure()
        .layout()
        .attachToWindow()

    assertThat(lithoViewRule.lithoView.rotation).isEqualTo(90f)
    assertThat(lithoViewRule.lithoView.rotationX).isEqualTo(45f)
    assertThat(lithoViewRule.lithoView.rotationY).isEqualTo(30f)
  }

  @Test
  fun scale_whenSet_isRespected() {
    lithoViewRule
        .setSizeSpecs(unspecified(), unspecified())
        .setRoot { Row(style = Style.width(100.px).height(100.px).scale(0.5f)) }
        .measure()
        .layout()
        .attachToWindow()

    assertThat(lithoViewRule.lithoView.scaleX).isEqualTo(0.5f)
    assertThat(lithoViewRule.lithoView.scaleY).isEqualTo(0.5f)
  }

  @Test
  fun selected_whenSet_isRespected() {
    lithoViewRule
        .setSizeSpecs(unspecified(), unspecified())
        .setRoot { Row(style = Style.width(100.px).height(100.px).selected(true)) }
        .measure()
        .layout()
        .attachToWindow()

    assertThat(lithoViewRule.lithoView.isSelected).isTrue
  }

  @Test
  fun wrapInView_whenSet_isRespected() {
    lithoViewRule
        .setSizeSpecs(unspecified(), unspecified())
        .setRoot {
          Row(style = Style.width(100.px).height(100.px)) {
            child(Row(style = Style.width(1.px).height(1.px).wrapInView()))
          }
        }
        .assertMatches(
            match<LithoView> {
              bounds(0, 0, 100, 100)
              child<ComponentHost>()
            })
  }

  @Test
  fun viewTag_whenSet_isAddedToView() {
    assertThat(
            lithoViewRule
                .setSizeSpecs(unspecified(), unspecified())
                .setRoot {
                  Row(style = Style.width(200.px).height(200.px)) {
                    child(Row(style = Style.width(100.px).height(100.px).viewTag("view_tag")))
                  }
                }
                .measure()
                .layout()
                .attachToWindow()
                .findViewWithTagOrNull("view_tag"))
        .isNotNull()
  }

  @Test
  fun viewTags_whenSet_areAddedToView() {
    val viewTags =
        SparseArray<String>().apply {
          append(123, "first tag")
          append(456, "second tag")
        }

    lithoViewRule
        .setSizeSpecs(unspecified(), unspecified())
        .setRoot { Row(style = Style.width(100.px).height(100.px).viewTags(viewTags)) }
        .measure()
        .layout()
        .attachToWindow()

    assertThat(lithoViewRule.lithoView.getTag(123)).isEqualTo("first tag")
    assertThat(lithoViewRule.lithoView.getTag(456)).isEqualTo("second tag")
  }

  @Test
  fun alpha_whenSet_isRespected() {
    val alpha = 0.5f

    lithoViewRule
        .setSizeSpecs(unspecified(), unspecified())
        .setRoot { Row(style = Style.width(100.px).height(100.px).alpha(alpha)) }
        .measure()
        .layout()
        .attachToWindow()

    assertThat(lithoViewRule.lithoView.alpha).isEqualTo(alpha)
  }

  /**
   * Test is using [Layout] and [NodeInfo] classes as a workaround for the issue with 'libyoga.so
   * already loaded in another classloader exception' caused by multiple ClassLoaders trying to load
   * Yoga when using @Config to specify a different target sdk. See:
   * https://www.internalfb.com/intern/staticdocs/litho/docs/testing/unit-testing/
   */
  @Test
  fun elevation_whenSet_isRespected() {
    val elevation = 1f

    class ElevationComponent : KComponent() {
      override fun ComponentScope.render(): Component? {
        return Row(style = Style.elevation(elevation.px))
      }
    }

    val node = LegacyLithoViewRule.getRootLayout(lithoViewRule, ElevationComponent())?.node
    val nodeInfo = node?.nodeInfo
    assertThat(nodeInfo?.shadowElevation).isEqualTo(elevation)
  }

  @Test
  fun shadow_whenSet_isRespected() {
    val outlineProvider = mock<ViewOutlineProvider>()

    class ShadowComponent : KComponent() {
      override fun ComponentScope.render(): Component? {
        return Row(
            style =
                Style.shadow(
                    elevation = 16.px,
                    outlineProvider = outlineProvider,
                    ambientShadowColor = Color.RED,
                    spotShadowColor = Color.BLUE))
      }
    }

    val node = LegacyLithoViewRule.getRootLayout(lithoViewRule, ShadowComponent())?.node
    val nodeInfo = node?.nodeInfo
    assertThat(nodeInfo?.shadowElevation).isEqualTo(16.0f)
    assertThat(nodeInfo?.outlineProvider).isEqualTo(outlineProvider)
    assertThat(nodeInfo?.ambientShadowColor).isEqualTo(Color.RED)
    assertThat(nodeInfo?.spotShadowColor).isEqualTo(Color.BLUE)
  }

  /** See comment on [elevation_whenSet_isRespected] above. */
  @Test
  fun outlineProvider_whenSet_isRespected() {
    val outlineProvider = ViewOutlineProvider.BOUNDS
    class OutlineProviderComponent : KComponent() {
      override fun ComponentScope.render(): Component? {
        return Row(style = Style.outlineProvider(outlineProvider))
      }
    }

    val node = LegacyLithoViewRule.getRootLayout(lithoViewRule, OutlineProviderComponent())?.node
    val nodeInfo = node?.nodeInfo
    assertThat(nodeInfo?.outlineProvider).isEqualTo(outlineProvider)
  }

  /** See comment on [elevation_whenSet_isRespected] above. */
  @Test
  fun clipToOutline_whenSet_isRespected() {
    class ComponentThatClips : KComponent() {
      override fun ComponentScope.render(): Component? {
        return Row(style = Style.clipToOutline(true))
      }
    }

    val node = LegacyLithoViewRule.getRootLayout(lithoViewRule, ComponentThatClips())?.node
    assertThat(node?.nodeInfo?.clipToOutline).isTrue

    node?.mutableNodeInfo()?.setClipToOutline(false)
    assertThat(node?.nodeInfo?.clipToOutline).isFalse
  }

  /** See comment on [elevation_whenSet_isRespected] above. */
  @Test
  fun transitionName_whenSet_isRespected() {
    class ElevationComponent : KComponent() {
      override fun ComponentScope.render(): Component? {
        return Row(style = Style.transitionName("test"))
      }
    }

    val node = LegacyLithoViewRule.getRootLayout(lithoViewRule, ElevationComponent())?.node
    val nodeInfo = node?.nodeInfo
    assertThat(nodeInfo?.transitionName).isEqualTo("test")
  }

  /** See comment on [elevation_whenSet_isRespected] above. */
  @Test
  fun testKey_whenSet_isRespected() {
    class TestKeyComponent : KComponent() {
      override fun ComponentScope.render(): Component? {
        return Row(style = Style.testKey("test"))
      }
    }

    val node = LegacyLithoViewRule.getRootLayout(lithoViewRule, TestKeyComponent())?.node
    assertThat(node?.testKey).isEqualTo("test")
  }

  @Test
  fun transitionOwnerKey_whenSet_isFromCorrectComponentContext() {
    class TestKComponent(private val style: Style) : KComponent() {
      override fun ComponentScope.render(): Component {
        return Row(style = style)
      }
    }

    class TestComponent : KComponent() {
      override fun ComponentScope.render(): Component {
        return key("testKcomponent") {
          TestKComponent(style = Style.transitionKey(context, "test"))
        }
      }
    }

    val node =
        LegacyLithoViewRule.getRootLayout(lithoViewRule, key("root") { TestComponent() })?.node
    assertThat(node?.transitionKey).isEqualTo("test")
    assertThat(node?.transitionOwnerKey).isEqualTo("\$root")
  }

  private fun assertHasColorDrawableOfColor(componentHost: ComponentHost, color: Int) {
    assertThat(componentHost.drawables).hasSize(1).first().isInstanceOf(MatrixDrawable::class.java)
    assertThat((componentHost.drawables[0] as MatrixDrawable<ColorDrawable>).mountedDrawable)
        .isInstanceOf(ColorDrawable::class.java)
        .extracting("color")
        .containsExactly(color)
  }

  private fun getMotionEvent() = MotionEvent.obtain(0L, 1L, MotionEvent.ACTION_DOWN, 0f, 0f, 0)
}


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

package com.facebook.litho.visibility

import android.widget.FrameLayout
import com.facebook.litho.Row
import com.facebook.litho.Style
import com.facebook.litho.core.height
import com.facebook.litho.core.width
import com.facebook.litho.px
import com.facebook.litho.testing.LithoViewRule
import com.facebook.litho.testing.testrunner.LithoTestRunner
import com.facebook.litho.testing.unspecified
import java.util.concurrent.atomic.AtomicBoolean
import org.assertj.core.api.Assertions.assertThat
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith

/** Unit tests for visibility event styles. */
@RunWith(LithoTestRunner::class)
class VisibilityStylesTest {

  @Rule @JvmField val lithoViewRule = LithoViewRule()

  @Test
  fun onVisible_whenSet_firesWhenVisible() {
    val eventFired = AtomicBoolean(false)

    lithoViewRule.render {
      Row(style = Style.width(200.px).height(200.px).onVisible { eventFired.set(true) })
    }

    assertThat(eventFired.get()).isTrue()
  }

  @Test
  fun onInvisible_whenSet_firesWhenVisibleThenInvisible() {
    val eventFired = AtomicBoolean(false)

    val testLithoView =
        lithoViewRule.render {
          Row(style = Style.width(200.px).height(200.px).onInvisible { eventFired.set(true) })
        }

    assertThat(eventFired.get()).isFalse()

    testLithoView.lithoView.setVisibilityHint(false)

    assertThat(eventFired.get()).isTrue()
  }

  @Test
  fun onFocusedVisibleAndUnfocusedVisible_whenSet_firesWhenFocusedAndUnfocusedVisible() {
    val focusFired = AtomicBoolean(false)
    val unfocusFired = AtomicBoolean(false)

    val testLithoView =
        lithoViewRule
            .render {
              Row(
                  style =
                      Style.width(200.px)
                          .height(200.px)
                          .onFocusedVisible { focusFired.set(true) }
                          .onUnfocusedVisible { unfocusFired.set(true) })
            }
            .attachToWindow()

    // FocusedVisible requires a measured parent
    val frameLayout = FrameLayout(lithoViewRule.context.androidContext)
    frameLayout.addView(testLithoView.lithoView)
    frameLayout.measure(unspecified(), unspecified())
    frameLayout.layout(0, 0, frameLayout.measuredWidth, frameLayout.measuredHeight)

    assertThat(focusFired.get()).isTrue()

    testLithoView.lithoView.setVisibilityHint(false)

    assertThat(unfocusFired.get()).isTrue()
  }

  @Test
  fun onFullImpression_whenSet_firesWhenVisible() {
    val eventFired = AtomicBoolean(false)

    lithoViewRule.render {
      Row(style = Style.width(200.px).height(200.px).onFullImpression { eventFired.set(true) })
    }

    assertThat(eventFired.get()).isTrue()
  }

  @Test
  fun onVisibilityChanged_whenSet_firesWhenVisibilityBoundsChange() {
    val eventFired = AtomicBoolean(false)

    lithoViewRule.render {
      Row(style = Style.width(200.px).height(200.px).onVisibilityChanged { eventFired.set(true) })
    }

    assertThat(eventFired.get()).isTrue()
  }
}


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

package com.facebook.litho

import kotlin.coroutines.CoroutineContext
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.cancel
import kotlinx.coroutines.launch

/**
 * Lazily creates a [CoroutineScope] that will be active as long as the [ComponentTree] is not
 * released. This scope will be canceled when the ComponentTree is released.
 */
val LithoTree.lithoTreeScope: CoroutineScope
  get() {
    while (true) {
      val existing = internalScopeRef.get() as? CoroutineScope
      if (existing != null) {
        return existing
      }
      val job = SupervisorJob()
      val newScope = ComponentTreeScope(this, job + Dispatchers.Main.immediate)
      if (internalScopeRef.compareAndSet(null, newScope)) {
        newScope.register()
        job.invokeOnCompletion { internalScopeRef.compareAndSet(newScope, null) }
        return newScope
      }
    }
  }

/**
 * A [CoroutineScope] that is scoped to whether the [ComponentTree] is released.
 *
 * If the tree is already released, this scope will immediately be canceled.
 */
class ComponentTreeScope(
    private val lithoTree: LithoTree,
    override val coroutineContext: CoroutineContext,
) : CoroutineScope {

  init {
    if (lithoTree.lithoTreeLifecycleProvider.isReleased) {
      coroutineContext.cancel()
    }
  }

  private fun cancelScopeContext() {
    coroutineContext.cancel()
  }

  fun register() {
    launch(Dispatchers.Main.immediate) {
      if (lithoTree.lithoTreeLifecycleProvider.isReleased) {
        cancelScopeContext()
      } else {
        lithoTree.lithoTreeLifecycleProvider.addOnReleaseListener { cancelScopeContext() }
      }
    }
  }
}


/*
 * 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.annotations.Hook
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch

/**
 * Launches a coroutine with [onLaunch] whenever [keys] change. The coroutine will be canceled when
 * the component is detached, or if the coroutine will be relaunched.
 *
 * The [CoroutineScope] for this coroutine must be specified through a TreeProp.
 */
@Hook
fun ComponentScope.useCoroutine(vararg keys: Any?, onLaunch: suspend CoroutineScope.() -> Unit) {
  useEffect(*keys) {
    val job = context.lithoTree?.lithoTreeScope?.launch { onLaunch() }
    onCleanup { job?.cancel() }
  }
}

/**
 * Launches a coroutine with [onLaunch]. The coroutine will be canceled when the component is
 * detached, or if the coroutine will be relaunched.
 *
 * It is an error to call [ComponentScope.useCoroutine] without keys parameter.
 */
// This deprecated-error function shadows the varargs overload so that the varargs version is not
// used without keys parameters.
@Deprecated(USE_COROUTINE_NO_KEYS_ERROR, level = DeprecationLevel.ERROR)
@Suppress("unused", "UNUSED_PARAMETER")
@Hook
fun ComponentScope.useCoroutine(onLaunch: suspend CoroutineScope.() -> Unit): Unit =
    throw IllegalStateException(USE_COROUTINE_NO_KEYS_ERROR)

private const val USE_COROUTINE_NO_KEYS_ERROR =
    "useCoroutine must provide 'keys' parameter that determines whether the existing coroutine will be canceled, and the new coroutine will be launched"


/*
 * 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.annotations.Hook
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.StateFlow

/**
 * Uses the current value of a given [stateFlow] in a Litho Kotlin component.
 *
 * The flow will be collected on the CoroutineScope given by [getTreeProp], and will be canceled
 * when this component is detached.
 */
@Hook
fun <T> ComponentScope.useFlow(stateFlow: StateFlow<T>): T =
    useFlow(initialValue = { stateFlow.value }, stateFlow)

/**
 * Uses the collection of a given [flow] in a Litho Kotlin component, with a provided initial value.
 *
 * The flow will be collected on the CoroutineScope given by [getTreeProp], and will be canceled
 * when this component is detached.
 */
@Hook
fun <T> ComponentScope.useFlow(initialValue: () -> T, flow: Flow<T>): T =
    useProducer(initialValue, flow) { flow.collect { update(it) } }

/**
 * Uses the collection of a flow that is dynamically supplied by [flowBlock], which will be executed
 * whenever the given [keys] change.
 *
 * The flow will be collected on the CoroutineScope given by [getTreeProp], and will be canceled
 * when this component is detached or if any [keys] change.
 */
@Hook
fun <T> ComponentScope.useFlow(
    initialValue: () -> T,
    vararg keys: Any?,
    flowBlock: () -> Flow<T>,
): T = useProducer(initialValue, *keys) { flowBlock().collect { update(it) } }

/**
 * Uses the collection of a flow that is dynamically supplied by [flowBlock].
 *
 * It is an error to call [ComponentScope.useFlow] without keys parameter.
 */
// This deprecated-error function shadows the varargs overload so that the varargs version is not
// used without keys parameters.
@Deprecated(USE_FLOW_NO_KEYS_ERROR, level = DeprecationLevel.ERROR)
@Suppress("unused", "UNUSED_PARAMETER")
@Hook
fun <T> ComponentScope.useFlow(initialValue: () -> T, flowBlock: () -> Flow<T>): Unit =
    throw IllegalStateException(USE_FLOW_NO_KEYS_ERROR)

private const val USE_FLOW_NO_KEYS_ERROR =
    "useFlow must provide 'keys' parameter that determines whether the existing flow will be canceled, and the new flow will be collected"


/*
 * 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.annotations.Hook
import kotlin.coroutines.CoroutineContext
import kotlin.coroutines.EmptyCoroutineContext
import kotlinx.coroutines.CoroutineScope

interface StateProducerScope<T> : CoroutineScope {
  fun update(newValue: T)

  fun update(newValueFunction: (T) -> T)

  fun updateSync(newValue: T)

  fun updateSync(newValueFunction: (T) -> T)
}

private class StateProducerScopeImpl<T>(
    private val state: State<T>,
    override var coroutineContext: CoroutineContext = EmptyCoroutineContext,
) : StateProducerScope<T> {

  override fun update(newValue: T) {
    state.update(newValue)
  }

  override fun update(newValueFunction: (T) -> T) {
    state.update(newValueFunction)
  }

  override fun updateSync(newValue: T) {
    state.updateSync(newValue)
  }

  override fun updateSync(newValueFunction: (T) -> T) {
    state.updateSync(newValueFunction)
  }
}

/**
 * Creates a value that is updatable by a given [producer] coroutine. Whenever producer updates the
 * value, the component will be re-rendered.
 *
 * The [producer] will be re-run whenever the given keys change.
 *
 * The [producer] is run in the CoroutineScope specified by [getTreeProp].
 *
 * @return the latest value given by the [producer], or by [initialValue].
 */
@Hook
fun <T> ComponentScope.useProducer(
    initialValue: () -> T,
    vararg keys: Any?,
    producer: suspend StateProducerScope<T>.() -> Unit,
): T {
  val state = useState { initialValue() }
  val producerScope = useCached { StateProducerScopeImpl(state) }
  useCoroutine(*keys) {
    producerScope.coroutineContext = coroutineContext
    producerScope.producer()
  }
  return state.value
}


/*
 * 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.app.Activity
import com.facebook.litho.testing.LithoViewRule
import com.facebook.litho.testing.testrunner.LithoTestRunner
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.awaitCancellation
import kotlinx.coroutines.job
import kotlinx.coroutines.launch
import kotlinx.coroutines.test.TestCoroutineDispatcher
import kotlinx.coroutines.test.setMain
import org.assertj.core.api.Assertions.assertThat
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.Robolectric

/** Unit tests for [ComponentTreeScope]. */
@OptIn(ExperimentalCoroutinesApi::class)
@RunWith(LithoTestRunner::class)
class ComponentTreeScopeTest {

  @Rule @JvmField val lithoViewRule = LithoViewRule()

  private val testDispatcher = TestCoroutineDispatcher()
  private val context = Robolectric.buildActivity(Activity::class.java).create().get()

  @Before
  fun setUp() {
    testDispatcher.pauseDispatcher()
    Dispatchers.setMain(testDispatcher)
  }

  @Test
  fun `componentTreeScope can start when LithoView is attached`() {
    val events = mutableListOf<String>()

    val tree = ComponentTree.create(ComponentContext(context)).build()
    val lithoTree = LithoTree.create(tree)

    lithoTree.lithoTreeScope.launch { events += "launch" }
    testDispatcher.runCurrent()

    assertThat(events).containsExactly("launch")
  }

  @Test
  fun `componentTreeScope is canceled when LithoView is detached`() {
    val events = mutableListOf<String>()

    val tree = ComponentTree.create(ComponentContext(context)).build()
    val lithoTree = LithoTree.create(tree)

    lithoTree.lithoTreeScope.launch {
      try {
        awaitCancellation()
      } finally {
        events += "cancel"
      }
    }
    testDispatcher.runCurrent()

    assertThat(events).isEmpty()

    tree.release()
    testDispatcher.runCurrent()

    assertThat(events).containsExactly("cancel")
  }

  @Test
  fun `componentTreeScope starts as canceled if view is detached`() {
    val tree = ComponentTree.create(ComponentContext(context)).build()
    val lithoTree = LithoTree.create(tree)

    tree.release()

    assertThat(lithoTree.lithoTreeScope.coroutineContext.job.isCancelled).isTrue
  }
}


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

package com.facebook.litho

import com.facebook.litho.testing.LithoViewRule
import com.facebook.litho.testing.testrunner.LithoTestRunner
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.awaitCancellation
import kotlinx.coroutines.test.TestCoroutineDispatcher
import kotlinx.coroutines.test.setMain
import org.assertj.core.api.Assertions.assertThat
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith

/** Unit tests for [useCoroutine]. */
@OptIn(ExperimentalCoroutinesApi::class)
@RunWith(LithoTestRunner::class)
class UseCoroutineTest {

  @Rule @JvmField val lithoViewRule = LithoViewRule()

  private val testDispatcher = TestCoroutineDispatcher()

  @Before
  fun setUp() {
    testDispatcher.pauseDispatcher()
    Dispatchers.setMain(testDispatcher)
  }

  @Test
  fun `coroutine is launched and canceled`() {
    val useCoroutineCalls = mutableListOf<String>()

    class UseCoroutineComponent : KComponent() {
      override fun ComponentScope.render(): Component {
        useCoroutine(Unit) {
          useCoroutineCalls += "launch"
          try {
            awaitCancellation()
          } finally {
            useCoroutineCalls += "cancel"
          }
        }
        return Row()
      }
    }

    val lithoView = lithoViewRule.render { UseCoroutineComponent() }
    testDispatcher.runCurrent()
    lithoView.detachFromWindow().release()
    testDispatcher.runCurrent()

    assertThat(useCoroutineCalls).containsExactly("launch", "cancel")
  }

  @Test
  fun `coroutine restarted on each update`() {
    val useCoroutineCalls = mutableListOf<String>()

    class UseCoroutineComponent(val seq: Int) : KComponent() {
      override fun ComponentScope.render(): Component {
        useCoroutine(Any()) {
          useCoroutineCalls += "attach $seq"
          try {
            awaitCancellation()
          } finally {
            useCoroutineCalls += "detach $seq"
          }
        }
        return Row()
      }
    }

    val lithoView = lithoViewRule.render { UseCoroutineComponent(seq = 0) }
    testDispatcher.runCurrent()

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

    lithoView.setRoot(UseCoroutineComponent(seq = 1))
    testDispatcher.runCurrent()

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

    lithoView.release()
    testDispatcher.runCurrent()

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

  @Test
  fun `coroutine only restarted if dependency changes`() {
    val useCoroutineCalls = mutableListOf<String>()

    class UseCoroutineComponent(val dep: Int, val seq: Int) : KComponent() {
      override fun ComponentScope.render(): Component {
        useCoroutine(dep) {
          useCoroutineCalls += "attach $seq"
          try {
            awaitCancellation()
          } finally {
            useCoroutineCalls += "detach $seq"
          }
        }
        return Row()
      }
    }

    val lithoView = lithoViewRule.render { UseCoroutineComponent(dep = 0, seq = 0) }
    testDispatcher.runCurrent()

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

    lithoView.setRoot(UseCoroutineComponent(dep = 0, seq = 1))
    testDispatcher.runCurrent()

    assertThat(useCoroutineCalls).isEmpty()
    useCoroutineCalls.clear()

    lithoView.setRoot(UseCoroutineComponent(dep = 1, seq = 2))
    testDispatcher.runCurrent()

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

    lithoView.release()
    testDispatcher.runCurrent()

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

  @Test
  fun `coroutine only restarts if nullable dependency changes`() {
    val useCoroutineCalls = mutableListOf<String>()

    class UseCoroutineComponent(val dep: Int?, val seq: Int) : KComponent() {
      override fun ComponentScope.render(): Component {
        useCoroutine(dep) {
          useCoroutineCalls += "attach $seq"
          try {
            awaitCancellation()
          } finally {
            useCoroutineCalls += "detach $seq"
          }
        }
        return Row()
      }
    }

    val lithoView = lithoViewRule.render { UseCoroutineComponent(dep = null, seq = 0) }
    testDispatcher.runCurrent()

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

    lithoView.setRoot(UseCoroutineComponent(dep = null, seq = 1))
    testDispatcher.runCurrent()

    assertThat(useCoroutineCalls).isEmpty()
    useCoroutineCalls.clear()

    lithoView.setRoot(UseCoroutineComponent(dep = 1, seq = 2))
    testDispatcher.runCurrent()

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

    lithoView.setRoot(UseCoroutineComponent(dep = null, seq = 3))
    testDispatcher.runCurrent()

    assertThat(useCoroutineCalls).containsExactly("detach 2", "attach 3")
    useCoroutineCalls.clear()

    lithoView.release()
    testDispatcher.runCurrent()

    assertThat(useCoroutineCalls).containsExactly("detach 3")
  }

  @Test
  fun `coroutine only restarts if any dependency changes`() {
    val useCoroutineCalls = mutableListOf<String>()

    class UseCoroutineComponent(
        val dep1: Int,
        val dep2: Int,
        val seq: Int,
    ) : KComponent() {
      override fun ComponentScope.render(): Component {
        useCoroutine(dep1, dep2) {
          useCoroutineCalls += "attach $dep1:$dep2:$seq"
          try {
            awaitCancellation()
          } finally {
            useCoroutineCalls += "detach $dep1:$dep2:$seq"
          }
        }
        return Row()
      }
    }

    val lithoView = lithoViewRule.render { UseCoroutineComponent(dep1 = 0, dep2 = 0, seq = 0) }
    testDispatcher.runCurrent()

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

    lithoView.setRoot(UseCoroutineComponent(dep1 = 0, dep2 = 0, seq = 1))
    testDispatcher.runCurrent()

    assertThat(useCoroutineCalls).isEmpty()
    useCoroutineCalls.clear()

    lithoView.setRoot(UseCoroutineComponent(dep1 = 0, dep2 = 1, seq = 2))
    testDispatcher.runCurrent()

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

    lithoView.setRoot(UseCoroutineComponent(dep1 = 1, dep2 = 1, seq = 3))
    testDispatcher.runCurrent()

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

    lithoView.setRoot(UseCoroutineComponent(dep1 = 2, dep2 = 2, seq = 4))
    testDispatcher.runCurrent()

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

    lithoView.release()
    testDispatcher.runCurrent()

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

  @Test
  fun `multiple coroutines are launched independently`() {
    val useCoroutineCalls = mutableListOf<String>()

    class UseCoroutineComponent(val dep1: Int, val dep2: Int, val dep3: Int, val seq: Int) :
        KComponent() {
      override fun ComponentScope.render(): Component? {
        useCoroutine(dep1) {
          useCoroutineCalls += "dep1: attach $seq"
          try {
            awaitCancellation()
          } finally {
            useCoroutineCalls += "dep1: detach $seq"
          }
        }
        useCoroutine(dep2) {
          useCoroutineCalls += "dep2: attach $seq"
          try {
            awaitCancellation()
          } finally {
            useCoroutineCalls += "dep2: detach $seq"
          }
        }
        useCoroutine(dep3) {
          useCoroutineCalls += "dep3: attach $seq"
          try {
            awaitCancellation()
          } finally {
            useCoroutineCalls += "dep3: detach $seq"
          }
        }
        return Row()
      }
    }

    val lithoView =
        lithoViewRule.render { UseCoroutineComponent(dep1 = 0, dep2 = 0, dep3 = 0, seq = 0) }
    testDispatcher.runCurrent()

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

    lithoView.setRoot(UseCoroutineComponent(dep1 = 0, dep2 = 1, dep3 = 0, seq = 1))
    testDispatcher.runCurrent()

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

    lithoView.setRoot(UseCoroutineComponent(dep1 = 1, dep2 = 1, dep3 = 1, seq = 2))
    testDispatcher.runCurrent()

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

    lithoView.setRoot(UseCoroutineComponent(dep1 = 1, dep2 = 1, dep3 = 1, seq = 3))
    testDispatcher.runCurrent()

    assertThat(useCoroutineCalls).isEmpty()
    useCoroutineCalls.clear()

    lithoView.release()
    testDispatcher.runCurrent()

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


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

package com.facebook.litho

import com.facebook.litho.testing.LithoViewRule
import com.facebook.litho.testing.testrunner.LithoTestRunner
import java.util.concurrent.atomic.AtomicInteger
import java.util.concurrent.atomic.AtomicReference
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.awaitCancellation
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.test.TestCoroutineDispatcher
import kotlinx.coroutines.test.setMain
import org.assertj.core.api.Assertions.assertThat
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.annotation.LooperMode

/** Unit tests for [useFlow]. */
@OptIn(ExperimentalCoroutinesApi::class)
@LooperMode(LooperMode.Mode.LEGACY)
@RunWith(LithoTestRunner::class)
class UseFlowTest {

  @Rule @JvmField val lithoViewRule = LithoViewRule()

  private val testDispatcher = TestCoroutineDispatcher()

  @Before
  fun setUp() {
    testDispatcher.pauseDispatcher()
    Dispatchers.setMain(testDispatcher)
  }

  @Test
  fun `collect from a flow`() {
    val stateRef = AtomicReference<Boolean>()
    val testFlow = flow { emit(true) }

    class UseCollectAsStateComponent : KComponent() {
      override fun ComponentScope.render(): Component {
        val value = useFlow(initialValue = { false }, testFlow)
        stateRef.set(value)
        return Row()
      }
    }

    lithoViewRule.render { UseCollectAsStateComponent() }

    assertThat(stateRef.get()).isFalse

    testDispatcher.runCurrent()

    assertThat(stateRef.get()).isFalse

    lithoViewRule.idle()

    assertThat(stateRef.get()).isTrue
  }

  @Test
  fun `collect with a FlowState`() {
    val stateRef = AtomicReference<Boolean>()
    val testFlow = MutableStateFlow(false)

    class UseCollectAsStateComponent : KComponent() {
      override fun ComponentScope.render(): Component {
        val value = useFlow(testFlow)
        stateRef.set(value)
        return Row()
      }
    }

    val lithoView = lithoViewRule.render { UseCollectAsStateComponent() }
    assertThat(stateRef.get()).isFalse

    lithoViewRule.act(lithoView) { testDispatcher.runCurrent() }

    assertThat(stateRef.get()).isFalse

    lithoViewRule.act(lithoView) {
      testFlow.value = true
      testDispatcher.runCurrent()
    }

    assertThat(stateRef.get()).isTrue
  }

  @Test
  fun `collect flow block`() {
    val stateRef = AtomicInteger()

    class UseCollectAsStateComponent(val dep: Int) : KComponent() {
      override fun ComponentScope.render(): Component {
        val value = useFlow(initialValue = { 0 }, Any()) { flow { emit(dep) } }
        stateRef.set(value)
        return Row()
      }
    }

    val lithoView = lithoViewRule.render { UseCollectAsStateComponent(1) }

    assertThat(stateRef.get()).isEqualTo(0)

    lithoViewRule.act(lithoView) { testDispatcher.runCurrent() }

    assertThat(stateRef.get()).isEqualTo(1)

    lithoView.setRoot(UseCollectAsStateComponent(2))
    lithoViewRule.act(lithoView) { testDispatcher.runCurrent() }

    assertThat(stateRef.get()).isEqualTo(2)
  }

  @Test
  fun `collect flow block only rerun on dependency change`() {
    val stateRef = AtomicInteger()
    val flowEvents = mutableListOf<String>()

    class UseCollectAsStateComponent(private val dep: Int) : KComponent() {
      override fun ComponentScope.render(): Component {
        val value =
            useFlow(initialValue = { 0 }, dep) {
              flow {
                flowEvents.add("launch $dep")
                try {
                  awaitCancellation()
                } finally {
                  flowEvents.add("cancel $dep")
                }
              }
            }
        stateRef.set(value)
        return Row()
      }
    }

    val lithoView = lithoViewRule.render { UseCollectAsStateComponent(1) }

    assertThat(flowEvents).isEmpty()

    testDispatcher.runCurrent()
    lithoViewRule.idle()

    assertThat(flowEvents).containsExactly("launch 1")

    lithoView.setRoot(UseCollectAsStateComponent(1))
    lithoViewRule.act(lithoView) { testDispatcher.runCurrent() }

    assertThat(flowEvents).containsExactly("launch 1")

    lithoView.setRoot(UseCollectAsStateComponent(2))
    lithoViewRule.act(lithoView) { testDispatcher.runCurrent() }

    assertThat(flowEvents).containsExactly("launch 1", "cancel 1", "launch 2")
  }

  @Test
  fun `collect flow block only rerun on one of many dependencies change`() {
    val stateRef = AtomicInteger()
    val flowEvents = mutableListOf<String>()

    class UseCollectAsStateComponent(private val dep1: Int, private val dep2: Int) : KComponent() {
      override fun ComponentScope.render(): Component {
        val value =
            useFlow(initialValue = { 0 }, dep1, dep2) {
              flow {
                flowEvents.add("launch $dep1:$dep2")
                try {
                  awaitCancellation()
                } finally {
                  flowEvents.add("cancel $dep1:$dep2")
                }
              }
            }
        stateRef.set(value)
        return Row()
      }
    }

    val lithoView = lithoViewRule.render { UseCollectAsStateComponent(1, 1) }

    assertThat(flowEvents).isEmpty()

    lithoViewRule.act(lithoView) { testDispatcher.runCurrent() }

    assertThat(flowEvents).containsExactly("launch 1:1")
    flowEvents.clear()

    lithoView.setRoot(UseCollectAsStateComponent(1, 1))
    lithoViewRule.act(lithoView) { lithoViewRule.idle() }

    assertThat(flowEvents).isEmpty()

    lithoView.setRoot(UseCollectAsStateComponent(2, 1))
    lithoViewRule.act(lithoView) { testDispatcher.runCurrent() }

    assertThat(flowEvents).containsExactly("cancel 1:1", "launch 2:1")
    flowEvents.clear()

    lithoView.setRoot(UseCollectAsStateComponent(2, 2))
    lithoViewRule.act(lithoView) { testDispatcher.runCurrent() }

    assertThat(flowEvents).containsExactly("cancel 2:1", "launch 2:2")
    flowEvents.clear()
  }
}


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

package com.facebook.litho

import com.facebook.litho.testing.LithoViewRule
import com.facebook.litho.testing.testrunner.LithoTestRunner
import java.util.concurrent.atomic.AtomicInteger
import java.util.concurrent.atomic.AtomicReference
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.awaitCancellation
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.test.TestCoroutineDispatcher
import kotlinx.coroutines.test.setMain
import org.assertj.core.api.Assertions.assertThat
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.annotation.LooperMode

/** Unit tests for [useProducer]. */
@OptIn(ExperimentalCoroutinesApi::class)
@LooperMode(LooperMode.Mode.LEGACY)
@RunWith(LithoTestRunner::class)
class UseProducerTest {

  @Rule @JvmField val lithoViewRule = LithoViewRule()

  private val testDispatcher = TestCoroutineDispatcher()

  @Before
  fun setUp() {
    testDispatcher.pauseDispatcher()
    Dispatchers.setMain(testDispatcher)
  }

  @Test
  fun `produces state`() {
    val stateRef = AtomicReference<Boolean>()
    val stateFlow = MutableStateFlow(false)

    class UseStateProducerComponent : KComponent() {
      override fun ComponentScope.render(): Component {
        val value = useProducer(initialValue = { false }, Unit) { stateFlow.collect { update(it) } }
        stateRef.set(value)
        return Row()
      }
    }

    val lithoView = lithoViewRule.render { UseStateProducerComponent() }
    lithoViewRule.act(lithoView) {
      // Make sure flow starts collecting, so that emissions aren't dropped.
      testDispatcher.runCurrent()
      stateFlow.value = true
      testDispatcher.runCurrent()
    }

    assertThat(stateRef.get()).isTrue
  }

  @Test
  fun `produces state synchronously`() {
    val stateRef = AtomicReference<Boolean>()
    val stateFlow = MutableStateFlow(false)

    class UseStateProducerComponent : KComponent() {
      override fun ComponentScope.render(): Component {
        val value =
            useProducer(initialValue = { false }, Unit) { stateFlow.collect { updateSync(it) } }
        stateRef.set(value)
        return Row()
      }
    }

    val lithoView = lithoViewRule.render { UseStateProducerComponent() }
    lithoViewRule.act(lithoView) {
      testDispatcher.runCurrent()
      stateFlow.value = true
      testDispatcher.runCurrent()
    }

    assertThat(stateRef.get()).isTrue
  }

  @Test
  fun `produces state updates based on current value`() {
    val stateRef = AtomicInteger()
    val sharedFlow = MutableSharedFlow<Unit>(extraBufferCapacity = 1)

    class UseStateProducerComponent : KComponent() {
      override fun ComponentScope.render(): Component {
        val value =
            useProducer(initialValue = { 0 }, Unit) { sharedFlow.collect { update { it + 1 } } }
        stateRef.set(value)
        return Row()
      }
    }

    val lithoView = lithoViewRule.render { UseStateProducerComponent() }
    lithoViewRule.act(lithoView) {
      testDispatcher.runCurrent()
      sharedFlow.tryEmit(Unit)
      testDispatcher.runCurrent()
    }

    assertThat(stateRef.get()).isEqualTo(1)

    lithoViewRule.act(lithoView) {
      sharedFlow.tryEmit(Unit)
      testDispatcher.runCurrent()
    }

    assertThat(stateRef.get()).isEqualTo(2)
  }

  @Test
  fun `produces state updates synchronously based on current value`() {
    val stateRef = AtomicInteger()
    val sharedFlow = MutableSharedFlow<Unit>(extraBufferCapacity = 1)

    class UseStateProducerComponent : KComponent() {
      override fun ComponentScope.render(): Component {
        val value =
            useProducer(initialValue = { 0 }, Unit) { sharedFlow.collect { updateSync { it + 1 } } }
        stateRef.set(value)
        return Row()
      }
    }

    val lithoView = lithoViewRule.render { UseStateProducerComponent() }
    lithoViewRule.act(lithoView) {
      testDispatcher.runCurrent()
      sharedFlow.tryEmit(Unit)
      testDispatcher.runCurrent()
    }

    assertThat(stateRef.get()).isEqualTo(1)

    lithoView.setRoot(UseStateProducerComponent())
    lithoViewRule.act(lithoView) {
      sharedFlow.tryEmit(Unit)
      testDispatcher.runCurrent()
    }

    assertThat(stateRef.get()).isEqualTo(2)
  }

  @Test
  fun `producer restarts on key change`() {
    val events = mutableListOf<String>()

    class UseStateCounterComponent(private val dep: Int) : KComponent() {
      override fun ComponentScope.render(): Component {
        useProducer(initialValue = { 0 }, dep) {
          events.add("produce $dep")
          try {
            awaitCancellation()
          } finally {
            events.add("cancel $dep")
          }
        }
        return Row()
      }
    }

    val lithoView = lithoViewRule.render { UseStateCounterComponent(dep = 0) }
    lithoViewRule.act(lithoView) { testDispatcher.runCurrent() }

    assertThat(events).containsExactly("produce 0")
    events.clear()

    lithoView.setRoot(UseStateCounterComponent(dep = 0))
    lithoViewRule.act(lithoView) { testDispatcher.runCurrent() }

    assertThat(events).isEmpty()

    lithoView.setRoot(UseStateCounterComponent(dep = 1))
    lithoViewRule.act(lithoView) { testDispatcher.runCurrent() }

    assertThat(events).containsExactly("cancel 0", "produce 1")
  }

  @Test
  fun `restart producer when one of many keys change`() {
    val events = mutableListOf<String>()

    class UseStateCounterComponent(private val dep1: Int, private val dep2: Int) : KComponent() {
      override fun ComponentScope.render(): Component {
        useProducer(initialValue = { 0 }, dep1, dep2) {
          events.add("produce $dep1:$dep2")
          try {
            awaitCancellation()
          } finally {
            events.add("cancel $dep1:$dep2")
          }
        }
        return Row()
      }
    }

    val lithoView = lithoViewRule.render { UseStateCounterComponent(dep1 = 1, dep2 = 1) }
    lithoViewRule.act(lithoView) { testDispatcher.runCurrent() }

    assertThat(events).containsExactly("produce 1:1")
    events.clear()

    lithoView.setRoot(UseStateCounterComponent(dep1 = 1, dep2 = 1))
    lithoViewRule.act(lithoView) { testDispatcher.runCurrent() }

    assertThat(events).isEmpty()
    events.clear()

    lithoView.setRoot(UseStateCounterComponent(dep1 = 100, dep2 = 1))
    lithoViewRule.act(lithoView) { testDispatcher.runCurrent() }

    assertThat(events).containsExactly("cancel 1:1", "produce 100:1")
    events.clear()

    lithoView.setRoot(UseStateCounterComponent(dep1 = 100, dep2 = 100))
    lithoViewRule.act(lithoView) { testDispatcher.runCurrent() }

    assertThat(events).containsExactly("cancel 100:1", "produce 100:100")
    events.clear()
  }
}


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

import java.lang.reflect.AccessibleObject
import java.lang.reflect.Field
import java.lang.reflect.Method

object Reflection {
  private val cache = mutableMapOf<String, AccessibleObject>()

  private inline fun <reified T> fromCache(key: String): T? = cache[key] as? T

  private fun getCacheKey(obj: Any, name: String, vararg types: Class<*>) =
      StringBuilder()
          .apply {
            val klass = obj as? Class<*> ?: obj::class.java
            append(klass.name).append('#').append(name)
            if (types.isNotEmpty()) {
              append('(')
              var separator = ""
              types.forEach {
                append(separator).append(it.name)
                separator = ","
              }
              append(')')
            }
          }
          .toString()

  fun getMethod(obj: Any?, name: String, vararg types: Class<*>): Method? {
    if (obj == null) return null
    val key = getCacheKey(obj, name, *types)
    return fromCache(key)
        ?: run {
          var klass: Class<*>? = obj as? Class<*> ?: obj.javaClass
          while (klass != null) {
            try {
              klass.getDeclaredMethod(name, *types).let { method ->
                if (!method.isAccessible) method.isAccessible = true
                cache[key] = method
                return method
              }
            } catch (ignored: NoSuchMethodException) {}
            klass = klass.superclass
          }
          null
        }
  }

  inline fun <reified T> invoke(
      obj: Any?,
      name: String,
      types: Array<Class<*>> = emptyArray(),
      vararg args: Any
  ): T? =
      try {
        getMethod(obj, name, *types)?.run { invoke(obj, *args) as? T }
      } catch (e: Exception) {
        null
      }

  fun <T> getValueUNSAFE(f: Field, node: Any?): T? {
    return try {
      val oldAccessibility = f.isAccessible
      f.isAccessible = true
      var value: T? = null
      f[node]?.let { v -> value = v as T }

      f.isAccessible = oldAccessibility
      value
    } catch (e: IllegalArgumentException) {
      throw RuntimeException(e)
    } catch (e: IllegalAccessException) {
      throw RuntimeException(e)
    }
  }

  fun <T> setValueUNSAFE(f: Field, node: Any?, value: T) {
    try {
      val oldAccessibility = f.isAccessible
      f.isAccessible = true
      f[node] = value
      f.isAccessible = oldAccessibility
    } catch (e: IllegalArgumentException) {
      throw RuntimeException(e)
    } catch (e: IllegalAccessException) {
      throw RuntimeException(e)
    } catch (e: SecurityException) {
      throw RuntimeException(e)
    }
  }

  fun geFieldUNSAFE(clazz: Class<*>, field: String): Field? {
    return try {
      clazz.getDeclaredField(field)
    } catch (e: SecurityException) {
      throw RuntimeException(e)
    } catch (e: NoSuchFieldException) {
      throw RuntimeException(e)
    }
  }
}


/*
 * 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.editor.instances

import com.facebook.litho.editor.Editor
import com.facebook.litho.editor.EditorRegistry
import com.facebook.litho.editor.Reflection
import com.facebook.litho.editor.model.EditorShape
import com.facebook.litho.editor.model.EditorValue
import java.lang.reflect.Field

class ListEditorInstance : Editor {

  override fun read(f: Field, node: Any?): EditorValue {
    val list = Reflection.getValueUNSAFE<List<Any?>?>(f, node) ?: return EditorValue.string("null")

    val resolvedList =
        list.map {
          when (it) {
            null -> EditorValue.string("null")
            else -> EditorRegistry.readValueThatIsNotAField(it.javaClass, it)
                    ?: EditorValue.string(it.javaClass.toString())
          }
        }

    return EditorValue.array(resolvedList)
  }

  override fun write(f: Field, node: Any?, values: EditorValue): Boolean {
    val editedValues = (values as? EditorShape?)?.value ?: return true

    val oldList = Reflection.getValueUNSAFE<List<Any?>?>(f, node) ?: return true

    val newList = mutableListOf<Any?>()
    newList.addAll(oldList)

    for (index in newList.indices) {
      if (editedValues.containsKey(index.toString())) {
        val oldValue = oldList[index] ?: continue // Overwriting null not supported
        val newValue = editedValues[index.toString()] ?: EditorValue.string("null")
        val writtenValue =
            EditorRegistry.writeValueThatIsNotAField(oldValue.javaClass, oldValue, newValue)
        if (writtenValue.hasUpdated() == true) {
          newList[index] = writtenValue.value()
        }
      }
    }

    Reflection.setValueUNSAFE(f, node, newList)
    return true
  }
}


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

package com.facebook.litho.editor.instances

import com.facebook.litho.editor.Editor
import com.facebook.litho.editor.EditorRegistry
import com.facebook.litho.editor.Reflection
import com.facebook.litho.editor.model.EditorShape
import com.facebook.litho.editor.model.EditorValue
import java.lang.reflect.Field

class MapEditorInstance : Editor {

  companion object {
    val instance: MapEditorInstance by lazy { MapEditorInstance() }
  }

  override fun read(f: Field, node: Any?): EditorValue {
    val dataClassInstance =
        Reflection.getValueUNSAFE<Map<Any?, Any?>>(f, node) ?: return EditorValue.string("null")

    val keyToEditorValue: MutableMap<String, EditorValue> = mutableMapOf()
    for ((key, value) in dataClassInstance.entries) {

      val editorValue: EditorValue =
          when (value) {
            null -> EditorValue.string("null")
            else -> EditorRegistry.readValueThatIsNotAField(value.javaClass, value)
                    ?: EditorValue.string(value.javaClass.toString())
          }
      keyToEditorValue[key.toString()] = editorValue
    }
    return EditorValue.shape(keyToEditorValue)
  }

  override fun write(f: Field, node: Any?, values: EditorValue): Boolean {
    val editedParams = (values as? EditorShape)?.value ?: return true

    val oldMap = Reflection.getValueUNSAFE<Map<Any?, Any?>>(f, node) ?: return true

    val newMap = mutableMapOf<Any?, Any?>()
    newMap.putAll(oldMap)

    for ((key, value) in newMap) {

      if (editedParams.containsKey(key.toString())) {
        // There is a new value. Overwrite it.
        val oldValue = oldMap[key] ?: continue
        val newValue = editedParams[key.toString()] ?: EditorValue.string("null")

        val writtenValue =
            EditorRegistry.writeValueThatIsNotAField(oldValue.javaClass, oldValue, newValue)
        if (writtenValue.hasUpdated() == true) {

          newMap[key] = writtenValue.value()
        }
      }
    }
    Reflection.setValueUNSAFE(f, node, newMap)
    return true
  }
}


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

package com.facebook.litho.editor.instances

import com.facebook.litho.editor.Editor
import com.facebook.litho.editor.EditorRegistry
import com.facebook.litho.editor.Reflection
import com.facebook.litho.editor.model.EditorShape
import com.facebook.litho.editor.model.EditorValue
import java.lang.reflect.Field

class ObjectEditorInstance : Editor {

  companion object {
    val instance: ObjectEditorInstance by lazy { ObjectEditorInstance() }
  }

  override fun read(f: Field, node: Any?): EditorValue {
    val dataClassInstance =
        Reflection.getValueUNSAFE<Any?>(f, node) ?: return EditorValue.string("null")

    val filedNameToEditorValue =
        dataClassInstance.javaClass.declaredFields
            .map { field ->
              field.isAccessible = true
              val editorValue =
                  when (val value = field[dataClassInstance]) {
                    null -> EditorValue.string("null")
                    else -> EditorRegistry.readValueThatIsNotAField(value.javaClass, value)
                            ?: EditorValue.string(value.javaClass.toString())
                  }
              field.name to editorValue
            }
            .toMap()

    return EditorValue.shape(filedNameToEditorValue)
  }

  override fun write(f: Field, node: Any?, values: EditorValue): Boolean {
    val editedParams = (values as? EditorShape)?.value ?: return true

    val oldDataClassInstance = Reflection.getValueUNSAFE<Any?>(f, node) ?: return true
    val newInstance = copyDataClass(oldDataClassInstance)
    for (field in oldDataClassInstance.javaClass.declaredFields) {
      field.isAccessible = true

      if (editedParams.containsKey(field.name)) {
        // There is a new value. Overwrite it.
        val oldValue = field[oldDataClassInstance] ?: continue
        val newValue = editedParams[field.name] ?: EditorValue.string("null")

        val writtenValue =
            EditorRegistry.writeValueThatIsNotAField(oldValue.javaClass, oldValue, newValue)

        if (writtenValue.hasUpdated() == true) {

          field[newInstance] = writtenValue.value()
        }
      }
    }
    Reflection.setValueUNSAFE(f, node, newInstance)
    return true
  }

  private inline fun <reified T> copyDataClass(obj: T): T {
    return Reflection.invoke(obj, "copy") ?: obj
  }
}