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

import android.view.View

/**
 * Size constraints for measuring layouts. The parent container can decide what [SizeConstraints]
 * will be passed to its children, which will use these constraints to measure themselves. When a
 * child measures itself, it should choose a size that fits within the [SizeConstraints] provided by
 * the parent:
 * - `minWidth` <= `childWidth` <= `maxWidth`
 * - `minHeight` <= `childHeight` <= `maxHeight`
 *
 * In order to avoid runtime overhead, [SizeConstraints] is a Kotlin value class that wraps a Long
 * value. That wrapped Long value encodes [minWidth], [minHeight], [maxWidth], and [maxHeight]
 * values. Each of them is represented on 16 bits, which means that the range of possible values is
 * 0 - 65536. There is also a special value for Infinity. When the decoded value is >= 0xFFFF, then
 * [Infinity] is returned. Creating [SizeConstraints] with values that don't fit this range will
 * throw an exception.
 */
@JvmInline
value class SizeConstraints internal constructor(@PublishedApi internal val encodedValue: Long) {
  /** Minimum width in pixels. It'll be always <= maxWidth. */
  val minWidth: Int
    get() {
      val value = encodedValue.shr(48).and(BitMask).toInt()
      return if (value < MaxValue) value else Infinity
    }

  /** Maximum width in pixels. It'll be always >= minWidth and <= Infinity. */
  val maxWidth: Int
    get() {
      val value = encodedValue.shr(32).and(BitMask).toInt()
      return if (value < MaxValue) value else Infinity
    }

  /** Minimum height in pixels. It'll be always <= maxHeight. */
  val minHeight: Int
    get() {
      val value = encodedValue.shr(16).and(BitMask).toInt()
      return if (value < MaxValue) value else Infinity
    }

  /** Maximum height in pixels. It'll be always >= minWidth and <= Infinity. */
  val maxHeight: Int
    get() {
      val value = encodedValue.and(BitMask).toInt()
      return if (value < MaxValue) value else Infinity
    }

  /** Returns true if maxWidth is != Infinity, false otherwise. */
  val hasBoundedWidth: Boolean
    get() = maxWidth != Infinity

  /** Returns true if maxHeight is != Infinity, false otherwise. */
  val hasBoundedHeight: Boolean
    get() = maxHeight != Infinity

  /**
   * Returns true if width is represented as a single value, otherwise returns false when width
   * represents a range of values
   */
  val hasExactWidth
    get() = maxWidth == minWidth

  /**
   * Returns true if height is represented as a single value, otherwise returns false when height
   * represents a range of values
   */
  val hasExactHeight
    get() = maxHeight == minHeight

  /** Returns true if the area of the component is zero. */
  val isZeroSize
    get() = maxWidth == 0 || maxHeight == 0

  fun copy(
      minWidth: Int = this.minWidth,
      maxWidth: Int = this.maxWidth,
      minHeight: Int = this.minHeight,
      maxHeight: Int = this.maxHeight
  ): SizeConstraints {
    return SizeConstraints(minWidth, maxWidth, minHeight, maxHeight)
  }

  override fun toString(): String {
    val maxWidthDescription = if (maxWidth == Infinity) "Infinity" else maxWidth
    val maxHeightDescription = if (maxHeight == Infinity) "Infinity" else maxHeight
    return "SizeConstraints[minWidth = $minWidth, maxWidth = $maxWidthDescription, " +
        "minHeight = $minHeight, maxHeight = $maxHeightDescription]"
  }

  companion object {

    /** A mask used for bit operations. */
    internal const val BitMask: Long = 0xFFFF

    /** A maximum value that can be stored in SizeConstraints. */
    const val MaxValue: Int = 0xFFFF

    /**
     * A value representing Infinity. It should be used to represent unbounded sizes. When
     * [maxWidth] is set to Infinity, then [hasBoundedWidth] returns false. Similarly when
     * [maxHeight] is set to Infinity, then [hasBoundedHeight] returns false.
     */
    // 0x20000000 is the largest power of two smaller than Int.MAX_VALUE / 2
    const val Infinity: Int = 0x20000000 - 1

    /** Creates [SizeConstraints] from the provided width and height [View.MeasureSpec]s. */
    fun fromMeasureSpecs(widthSpec: Int, heightSpec: Int): SizeConstraints {
      val widthMode = View.MeasureSpec.getMode(widthSpec)
      val widthSize = View.MeasureSpec.getSize(widthSpec).coerceIn(0, MaxValue - 1)
      val minWidth: Int
      val maxWidth: Int
      when (widthMode) {
        View.MeasureSpec.EXACTLY -> {
          minWidth = widthSize
          maxWidth = widthSize
        }
        View.MeasureSpec.UNSPECIFIED -> {
          minWidth = 0
          maxWidth = Infinity
        }
        View.MeasureSpec.AT_MOST -> {
          minWidth = 0
          maxWidth = widthSize
        }
        else -> throw IllegalStateException("Unknown width spec mode.")
      }

      val heightMode = View.MeasureSpec.getMode(heightSpec)
      val heightSize = View.MeasureSpec.getSize(heightSpec).coerceIn(0, MaxValue - 1)
      val minHeight: Int
      val maxHeight: Int
      when (heightMode) {
        View.MeasureSpec.EXACTLY -> {
          minHeight = heightSize
          maxHeight = heightSize
        }
        View.MeasureSpec.UNSPECIFIED -> {
          minHeight = 0
          maxHeight = Infinity
        }
        View.MeasureSpec.AT_MOST -> {
          minHeight = 0
          maxHeight = heightSize
        }
        else -> throw IllegalStateException("Unknown height spec mode.")
      }

      validateSizes(
          minWidth,
          maxWidth,
          minHeight,
          maxHeight,
          "minWidth=$minWidth, maxWidth=$maxWidth, minHeight=$minHeight, maxHeight=$maxHeight, widthSpec=[${View.MeasureSpec.toString(widthSpec)}], heightSpec=[${View.MeasureSpec.toString(heightSpec)}]")
      return SizeConstraints(minWidth, maxWidth, minHeight, maxHeight)
    }

    internal fun validateSizes(
        minWidth: Int,
        maxWidth: Int,
        minHeight: Int,
        maxHeight: Int,
        additionalErrorMessage: String
    ) {
      if (minWidth < 0) {
        throw IllegalArgumentException(
            "minWidth must be >= 0, but was: $minWidth $additionalErrorMessage")
      }
      if (minHeight < 0) {
        throw IllegalArgumentException(
            "minHeight must be >= 0, but was: $minHeight $additionalErrorMessage")
      }
      if (maxWidth >= MaxValue && maxWidth != Infinity) {
        throw IllegalArgumentException(
            "maxWidth must be < $MaxValue, but was: $maxWidth. Components this big may affect performance and lead to out of memory errors. $additionalErrorMessage")
      }
      if (maxHeight >= MaxValue && maxHeight != Infinity) {
        throw IllegalArgumentException(
            "maxHeight must be < $MaxValue, but was: $maxHeight. Components this big may affect performance and lead to out of memory errors. $additionalErrorMessage")
      }
      if (minWidth > maxWidth) {
        throw IllegalArgumentException(
            "maxWidth must be >= minWidth, but was: maxWidth=$maxWidth; minWidth=$minWidth $additionalErrorMessage")
      }
      if (minHeight > maxHeight) {
        throw IllegalArgumentException(
            "maxHeight must be >= minHeight, but was: maxHeight=$maxHeight; minHeight=$minHeight $additionalErrorMessage")
      }
    }

    internal fun valueOrInfinity(value: Int): Long {
      return (if (value == Infinity) MaxValue else value).toLong()
    }
  }
}

/** Creates [SizeConstraints] from the provided values. */
fun SizeConstraints(
    minWidth: Int,
    maxWidth: Int,
    minHeight: Int,
    maxHeight: Int,
): SizeConstraints {
  SizeConstraints.validateSizes(
      minWidth,
      maxWidth,
      minHeight,
      maxHeight,
      "minWidth=$minWidth, maxWidth=$maxWidth, minHeight=$minHeight, maxHeight=$maxHeight")
  val v1 = SizeConstraints.valueOrInfinity(minWidth)
  val v2 = SizeConstraints.valueOrInfinity(maxWidth)
  val v3 = SizeConstraints.valueOrInfinity(minHeight)
  val v4 = SizeConstraints.valueOrInfinity(maxHeight)
  return SizeConstraints(
      encodedValue = v1.shl(48) or v2.shl(32) or v3.shl(16) or (v4 and SizeConstraints.BitMask))
}

/**
 * Returns the minWidth and maxWidth represented as width [View.MeasureSpec].
 *
 * **IMPORTANT** - this is a lossy conversion. With [View.MeasureSpec], it's not possible to
 * represent a range of values where the minimum is != 0 so the information about minWidth may be
 * lost.
 */
fun SizeConstraints.toWidthSpec(): Int {
  if (minWidth == maxWidth) {
    return View.MeasureSpec.makeMeasureSpec(maxWidth, View.MeasureSpec.EXACTLY)
  }
  if (maxWidth != SizeConstraints.Infinity) {
    return View.MeasureSpec.makeMeasureSpec(maxWidth, View.MeasureSpec.AT_MOST)
  }
  return View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED)
}

/**
 * Returns the minHeight and maxHeight represented as width [View.MeasureSpec].
 *
 * **IMPORTANT** - this is a lossy conversion. With [View.MeasureSpec], it's not possible to
 * represent a range of values where the minimum is != 0 so the information about minHeight may be
 * lost.
 */
fun SizeConstraints.toHeightSpec(): Int {
  if (minHeight == maxHeight) {
    return View.MeasureSpec.makeMeasureSpec(maxHeight, View.MeasureSpec.EXACTLY)
  }
  if (maxHeight != SizeConstraints.Infinity) {
    return View.MeasureSpec.makeMeasureSpec(maxHeight, View.MeasureSpec.AT_MOST)
  }
  return View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED)
}

/** Returns true if the size fits within the given constraints, otherwise returns false. */
fun Size.fitsWithin(sizeConstraints: SizeConstraints): Boolean {
  return (width in sizeConstraints.minWidth..sizeConstraints.maxWidth) &&
      (height in sizeConstraints.minHeight..sizeConstraints.maxHeight)
}


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

import android.content.Context
import androidx.annotation.IdRes

/**
 * This wrapper allows to add binders to the original [renderUnit] without modifying it directly.
 * [addOptionalMountBinder] and [addAttachBinder] methods gather additional Binders in the
 * [WrapperRenderUnit] instance and all other methods are delegated to the original [renderUnit].
 */
// Don't add support for fixedMountBinders in WrapperRenderUnit unless you add support for them in
// BindData handling logic.
class WrapperRenderUnit<ContentType>(private val renderUnit: RenderUnit<ContentType>) :
    RenderUnit<ContentType>(renderUnit.renderType) {

  override fun getId(): Long = renderUnit.id

  override fun getContentAllocator(): ContentAllocator<ContentType> = renderUnit.contentAllocator

  override fun doesMountRenderTreeHosts(): Boolean = renderUnit.doesMountRenderTreeHosts()

  override fun getRenderContentType(): Class<*> = renderUnit.renderContentType

  override fun getDescription(): String = renderUnit.description

  override fun <T> getExtra(@IdRes key: Int): T? = renderUnit.getExtra(key)

  override fun addOptionalMountBinder(binder: DelegateBinder<*, in ContentType, *>) {
    if (renderUnit.containsOptionalMountBinder(binder)) {
      throw IllegalArgumentException(
          "Binder ${binder.getDescription()} already exists in the wrapped ${renderUnit.getDescription()}")
    }
    super.addOptionalMountBinder(binder)
  }

  override fun addAttachBinder(binder: DelegateBinder<*, in ContentType, *>) {
    if (renderUnit.containsAttachBinder(binder)) {
      throw IllegalArgumentException(
          "Binder ${binder.getDescription()} already exists in the wrapped ${renderUnit.getDescription()}")
    }
    super.addAttachBinder(binder)
  }

  override fun <T : Binder<*, *, *>> findAttachBinderByClass(klass: Class<T>): T? =
      renderUnit.findAttachBinderByClass(klass) ?: super.findAttachBinderByClass(klass)

  override fun mountBinders(
      context: Context,
      content: ContentType,
      layoutData: Any?,
      bindData: BindData,
      tracer: Systracer
  ) {
    renderUnit.mountBinders(context, content, layoutData, bindData, tracer)
    super.mountBinders(context, content, layoutData, bindData, tracer)
  }

  override fun unmountBinders(
      context: Context,
      content: ContentType,
      layoutData: Any?,
      bindData: BindData,
      tracer: Systracer
  ) {
    super.unmountBinders(context, content, layoutData, bindData, tracer)
    renderUnit.unmountBinders(context, content, layoutData, bindData, tracer)
  }

  override fun attachBinders(
      context: Context,
      content: ContentType,
      layoutData: Any?,
      bindData: BindData,
      tracer: Systracer
  ) {
    renderUnit.attachBinders(context, content, layoutData, bindData, tracer)
    super.attachBinders(context, content, layoutData, bindData, tracer)
  }

  override fun detachBinders(
      context: Context,
      content: ContentType,
      layoutData: Any?,
      bindData: BindData,
      tracer: Systracer
  ) {
    super.detachBinders(context, content, layoutData, bindData, tracer)
    renderUnit.detachBinders(context, content, layoutData, bindData, tracer)
  }

  override fun updateBinders(
      context: Context,
      content: ContentType,
      currentRenderUnit: RenderUnit<ContentType>,
      currentLayoutData: Any?,
      newLayoutData: Any?,
      mountDelegate: MountDelegate?,
      bindData: BindData,
      isAttached: Boolean,
      tracer: Systracer,
  ) {
    renderUnit.updateBinders(
        context,
        content,
        (currentRenderUnit as WrapperRenderUnit).renderUnit,
        currentLayoutData,
        newLayoutData,
        mountDelegate,
        bindData,
        isAttached,
        tracer,
    )
    super.updateBinders(
        context,
        content,
        currentRenderUnit,
        currentLayoutData,
        newLayoutData,
        mountDelegate,
        bindData,
        isAttached,
        tracer,
    )
  }
}


/*
 * 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.rendercore.debug

import android.util.Log
import com.facebook.rendercore.LogLevel

/** This debug event subscriber listens to all events and prints them to logcat */
class DebugEventLogger : DebugEventSubscriber(DebugEvent.All) {
  override fun onEvent(event: DebugEvent) {
    when (event.logLevel) {
      LogLevel.VERBOSE -> Log.v("rc-debug-events", event.toString())
      LogLevel.DEBUG -> Log.d("rc-debug-events", event.toString())
      LogLevel.WARNING -> Log.w("rc-debug-events", event.toString())
      LogLevel.ERROR -> Log.e("rc-debug-events", event.toString())
      LogLevel.FATAL -> Log.e("rc-debug-events", event.toString())
    }
  }
}


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

package com.facebook.rendercore.debug

import com.facebook.kotlin.compilerplugins.dataclassgenerate.annotation.DataClassGenerate
import com.facebook.rendercore.LogLevel
import java.util.concurrent.TimeUnit
import java.util.concurrent.atomic.AtomicInteger
import java.util.concurrent.atomic.AtomicReference

/** Base class of all debug events */
sealed class DebugEvent(
    val type: String,
    val renderStateId: String,
    val threadName: String = Thread.currentThread().name,
    val logLevel: LogLevel = LogLevel.DEBUG,
    private val attributes: Map<String, Any?> = emptyMap()
) {

  companion object {
    const val All = "*"
    const val RenderTreeCommitted = "RenderCore.RenderTreeCommitted"
    const val RenderTreeCalculated = "RenderCore.RenderTreeCalculated"
    const val RenderTreeMounted = "RenderCore.RenderTreeMounted"
    const val MountItemMount = "RenderCore.MountItem.Mount"
    const val RenderUnitMounted = "RenderCore.RenderUnit.Mounted"
    const val RenderUnitUnmounted = "RenderCore.RenderUnit.Unmounted"
    const val RenderUnitUpdated = "RenderCore.RenderUnit.Updated"
    const val RenderUnitOnVisible = "RenderCore.RenderUnit.OnVisible"
    const val RenderUnitOnInvisible = "RenderCore.RenderUnit.OnInvisible"
  }

  /** Returns the value of attribute with [name]. */
  fun <T> attribute(name: String): T = attributes[name] as T

  /** Returns the value of attribute with [name] or `null` if not present. */
  fun <T> attributeOrNull(name: String): T? = attributes[name] as? T

  /**
   * Returns the value of the attribute with [name]. If there is no value present, then it will
   * return the [default].
   */
  fun <T> attribute(name: String, default: T): T = attributeOrNull(name) ?: default

  override fun toString(): String {
    return """
      |[DebugEvent]
      |  type = '$type',
      |  renderStateId = '$renderStateId',
      |  thread = '$threadName',
      |  attributes = ${attributes.entries.joinToString(
        prefix = "{\n",
        separator = ",\n",
        postfix = "\n|  }",
        transform = { e -> "|    ${e.key} = ${e.value}" }
    )}
    """
        .trimMargin()
  }
}

/** Collection of attributes */
object DebugEventAttribute {
  const val timestamp = "timestamp"
  const val duration = "duration"
  const val version = "version"
  const val width = "width"
  const val height = "height"
  const val widthSpec = "widthSpec"
  const val heightSpec = "heightSpec"
  const val source = "source"
  const val RenderUnitId = "renderUnitId"
  const val Description = "description"
  const val Bounds = "bounds"
  const val RootHostHashCode = "rootHostHashCode"
  const val Name = "name"
}

/** Base class for marker events */
class DebugMarkerEvent(
    val timestamp: Long = System.currentTimeMillis(), // for calendar time
    type: String,
    renderStateId: String,
    threadName: String = Thread.currentThread().name,
    logLevel: LogLevel = LogLevel.DEBUG,
    attributes: Map<String, Any?> = emptyMap()
) :
    DebugEvent(
        type = type,
        renderStateId = renderStateId,
        threadName = threadName,
        logLevel = logLevel,
        attributes =
            buildMap {
              put(DebugEventAttribute.timestamp, timestamp)
              putAll(attributes)
            })

/** Base class for process events */
class DebugProcessEvent(
    val timestamp: Long = System.currentTimeMillis(), // for calendar time
    val duration: Duration,
    type: String,
    renderStateId: String,
    threadName: String = Thread.currentThread().name,
    logLevel: LogLevel = LogLevel.DEBUG,
    attributes: Map<String, Any?> = emptyMap()
) :
    DebugEvent(
        type = type,
        renderStateId = renderStateId,
        threadName = threadName,
        logLevel = logLevel,
        attributes =
            buildMap {
              put(DebugEventAttribute.timestamp, timestamp)
              put(DebugEventAttribute.duration, duration)
              putAll(attributes)
            })

/** Base class for event subscribers */
abstract class DebugEventSubscriber(vararg val events: String) {
  abstract fun onEvent(event: DebugEvent)
}

/** Object to dispatch debug events */
object DebugEventDispatcher {

  private val minLogLevelRef = AtomicReference(LogLevel.DEBUG)

  @JvmStatic
  var minLogLevel: LogLevel
    @JvmStatic get() = minLogLevelRef.get()
    @JvmStatic
    set(value) {
      minLogLevelRef.set(value)
    }

  private val mutableSubscribers: MutableSet<DebugEventSubscriber> = mutableSetOf()

  val subscribers: Set<DebugEventSubscriber>
    @Synchronized get() = mutableSubscribers

  @JvmStatic
  inline fun dispatch(
      type: String,
      renderStateId: () -> String,
      timestamp: Long = System.currentTimeMillis(), // for calender time
      logLevel: LogLevel = LogLevel.DEBUG,
      attributesAccumulator: (MutableMap<String, Any?>) -> Unit = {},
  ) {
    if (logLevel < this.minLogLevel) {
      return
    }

    val subscribersToNotify =
        subscribers.filter { it.events.contains(type) || it.events.contains(DebugEvent.All) }
    if (subscribersToNotify.isNotEmpty()) {
      val attributes = LinkedHashMap<String, Any?>()
      attributesAccumulator(attributes)

      val event =
          DebugMarkerEvent(
              type = type,
              renderStateId = renderStateId(),
              attributes = attributes,
              timestamp = timestamp,
              logLevel = logLevel,
          )
      subscribersToNotify.forEach { subscriber -> subscriber.onEvent(event) }
    }
  }

  @JvmStatic
  inline fun dispatch(
      type: String,
      renderStateId: () -> String,
      attributesAccumulator: (MutableMap<String, Any?>) -> Unit = {}
  ) {
    dispatch(
        type = type,
        renderStateId = renderStateId,
        timestamp = System.currentTimeMillis(), // for calender time
        attributesAccumulator = attributesAccumulator,
    )
  }

  @JvmStatic
  inline fun dispatch(
      type: String,
      renderStateId: () -> String,
      logLevel: LogLevel = LogLevel.DEBUG,
      attributesAccumulator: (MutableMap<String, Any?>) -> Unit = {}
  ) {
    dispatch(
        type = type,
        renderStateId = renderStateId,
        timestamp = System.currentTimeMillis(), // for calender time
        logLevel = logLevel,
        attributesAccumulator = attributesAccumulator,
    )
  }

  private val lastTraceIdentifier = AtomicInteger(0)
  private val traceIdsToEvents: MutableMap<Int, EventData> = HashMap()

  /**
   * Returns an identifier for a trace for the given [type]. This identifier will always be unique
   * across a session, and if `null` it means this event is not to be traced.
   *
   * This is to be used whenever the [DebugEvent] API is called from Java.
   */
  @JvmStatic
  fun generateTraceIdentifier(type: String): Int? =
      if (subscribers.any { type in it.events || DebugEvent.All in it.events }) {
        lastTraceIdentifier.getAndIncrement()
      } else {
        null
      }

  /**
   * Starts recording a trace block for the given [type].
   *
   * This is intended to be used with Java clients due to performance reasons. The recommended usage
   * is the following:
   * ```
   * Integer traceIdentifier = DebugEvents.getTraceIdentifier(DebugEvents.MyEvent);
   * if(traceIdentifier != null) {
   *    DebugEvents.beginTrace(traceIdentifier, ...);
   * }
   *
   * // code for the process
   *
   * if(traceIdentifier != null) {
   *   DebugEvents.endTrace(traceIdentifier);
   * }
   * ```
   */
  @JvmStatic
  fun beginTrace(
      traceIdentifier: Int,
      type: String,
      renderStateId: String,
      attributes: Map<String, Any?>,
  ) {
    traceIdsToEvents[traceIdentifier] =
        EventData(
            type = type,
            renderStateId = renderStateId,
            attributes = attributes,
            startTimestamp = System.nanoTime())
  }

  @JvmStatic
  fun endTrace(traceIdentifier: Int) {
    val endTime = System.nanoTime()
    val last = traceIdsToEvents.remove(traceIdentifier) ?: return
    val type = last.type

    // find the subscribers listening for this event
    val subscribersToNotify =
        subscribers.filter { subscriber ->
          subscriber.events.contains(type) || subscriber.events.contains(DebugEvent.All)
        }

    if (subscribersToNotify.isEmpty()) {
      return
    }

    val event =
        DebugProcessEvent(
            type = type,
            timestamp = last.startTimestamp, // for calender time
            renderStateId = last.renderStateId,
            duration = Duration(value = endTime - last.startTimestamp),
            attributes = last.attributes,
        )

    subscribersToNotify.forEach { it.onEvent(event) }
  }

  inline fun <T> trace(
      type: String,
      renderStateId: () -> String,
      attributesAccumulator: (MutableMap<String, Any?>) -> Unit = {},
      block: (TraceScope?) -> T,
  ): T {
    // find the subscribers listening for this event
    val subscribersToNotify =
        subscribers.filter { subscriber ->
          subscriber.events.contains(type) || subscriber.events.contains(DebugEvent.All)
        }

    // run the block if there are no subscribers
    if (subscribersToNotify.isEmpty()) {
      return block(null)
    }

    val attributes = LinkedHashMap<String, Any?>()
    attributesAccumulator(attributes)

    val startTime = System.nanoTime()
    val res = block(TraceScope(attributes = attributes))
    val endTime = System.nanoTime()

    val event =
        DebugProcessEvent(
            type = type,
            timestamp = startTime,
            renderStateId = renderStateId(),
            duration = Duration(value = endTime - startTime),
            attributes = attributes,
        )

    subscribersToNotify.forEach { it.onEvent(event) }

    return res
  }

  @Synchronized
  fun subscribe(subscriber: DebugEventSubscriber) {
    mutableSubscribers.add(subscriber)
  }

  @Synchronized
  internal fun unsubscribe(subscriber: DebugEventSubscriber) {
    mutableSubscribers.remove(subscriber)
  }

  @Synchronized
  fun unsubscribeAll() {
    mutableSubscribers.clear()
  }

  class TraceScope(private val attributes: LinkedHashMap<String, Any?>) {
    fun attribute(name: String, value: Any?) {
      attributes[name] = value
    }
  }

  @DataClassGenerate
  private data class EventData(
      val type: String,
      val renderStateId: String,
      val attributes: Map<String, Any?>,
      val startTimestamp: Long
  )
}

/** Object to subscribe to debug events */
object DebugEventBus {

  @JvmStatic
  inline fun subscribe(subscriber: DebugEventSubscriber) {
    DebugEventDispatcher.subscribe(subscriber)
  }

  @JvmStatic
  fun unsubscribe(subscriber: DebugEventSubscriber) {
    DebugEventDispatcher.unsubscribe(subscriber)
  }

  @JvmStatic
  fun unsubscribeAll() {
    DebugEventDispatcher.unsubscribeAll()
  }
}

@JvmInline
value class Duration(val value: Long) {
  override fun toString(): String {
    return if (value < 1_000) {
      "$value ns"
    } else if (value < 1_000_0000) {
      "${TimeUnit.NANOSECONDS.toMicros(value)} Âµs"
    } else {
      "${TimeUnit.NANOSECONDS.toMillis(value)} ms"
    }
  }
}


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

@file:JvmName("SizeUtils")

package com.facebook.rendercore.utils

import com.facebook.rendercore.Size
import com.facebook.rendercore.SizeConstraints
import kotlin.math.ceil
import kotlin.math.max
import kotlin.math.min

/**
 * Returns a [Size] with width and height set based on the provided [SizeConstraints].
 *
 * This should only be used for Components which do not measure themselves - it's the parent that
 * has determined the exact size for this child.
 *
 * @param sizeConstraints The size constraints that will be used during measurement.
 * @return [Size] containing computed width and height if the [SizeConstraints] are exact, otherwise
 *   returns [Size.Invalid]
 */
fun Size.Companion.exact(sizeConstraints: SizeConstraints): Size {
  if (!sizeConstraints.hasExactWidth || !sizeConstraints.hasExactHeight) {
    return Size.Invalid
  }
  return Size(sizeConstraints.maxWidth, sizeConstraints.maxHeight)
}

/**
 * Returns a [Size] with width and height set based on the [SizeConstraints] if the constraints are
 * bounded, otherwise uses fallback values or min constraint values.
 *
 * @param sizeConstraints The size constraints that will be used during measurement.
 * @param fallbackWidth The pixel value for the width of the measured component when the provided
 *   maxWidth constraint is equal to Infinity.
 * @param fallbackHeight The pixel value for the height of the measured component when the provided
 *   maxHeight constraint is equal to Infinity.
 * @return [Size] containing computed width and height
 */
fun Size.Companion.fillSpace(
    sizeConstraints: SizeConstraints,
    fallbackWidth: Int,
    fallbackHeight: Int
): Size {
  if (!sizeConstraints.hasBoundedWidth && !sizeConstraints.hasBoundedHeight) {
    return Size(
        max(sizeConstraints.minWidth, fallbackWidth),
        max(sizeConstraints.minHeight, fallbackHeight))
  }

  val width = if (sizeConstraints.hasBoundedWidth) sizeConstraints.maxWidth else fallbackWidth
  val height = if (sizeConstraints.hasBoundedHeight) sizeConstraints.maxHeight else fallbackHeight

  return Size(width, height)
}

/**
 * Returns a [Size] with width and height set according to an aspect ratio and [SizeConstraints]. It
 * will respect the intrinsic size of the component being measured if it fits within the provided
 * constraints.
 *
 * @param sizeConstraints The size constraints that will be used during measurement.
 * @param aspectRatio The aspect ratio for calculating size.
 * @param intrinsicWidth The pixel value for the intrinsic width of the measured component.
 * @param intrinsicHeight The pixel value for the intrinsic height of the measured component.
 * @return [Size] containing computed width and height if [aspectRatio] is > 0, otherwise returns
 *   [Size.Invalid]
 */
fun Size.Companion.withAspectRatio(
    sizeConstraints: SizeConstraints,
    aspectRatio: Float,
    intrinsicWidth: Int,
    intrinsicHeight: Int
): Size {
  var constraints = sizeConstraints
  if (sizeConstraints.hasBoundedWidth &&
      (intrinsicWidth in sizeConstraints.minWidth..sizeConstraints.maxWidth)) {
    constraints = constraints.copy(maxWidth = intrinsicWidth)
  }
  if (sizeConstraints.hasBoundedHeight &&
      (intrinsicHeight in sizeConstraints.minHeight..sizeConstraints.maxHeight)) {
    constraints = constraints.copy(maxHeight = intrinsicHeight)
  }

  return Size.withAspectRatio(constraints, aspectRatio)
}

/**
 * Returns a [Size] with width and height set according to an aspect ratio and [SizeConstraints]. It
 * will respect the intrinsic size of the component being measured if it fits within the provided
 * constraints.
 *
 * @param sizeConstraints The size constraints that will be used during measurement.
 * @param aspectRatio The aspect ratio for calculating size.
 * @param intrinsicWidth The pixel value for the intrinsic width of the measured component.
 * @param intrinsicHeight The pixel value for the intrinsic height of the measured component.
 * @return [Size] containing computed width and height if [aspectRatio] is > 0, otherwise returns
 *   [Size.Invalid]
 */
inline fun Size.Companion.withAspectRatio(
    sizeConstraints: SizeConstraints,
    aspectRatio: Double,
    intrinsicWidth: Int,
    intrinsicHeight: Int
): Size {
  return Size.withAspectRatio(
      sizeConstraints, aspectRatio.toFloat(), intrinsicWidth, intrinsicHeight)
}

/**
 * Returns a [Size] measured according to an aspect ratio and [SizeConstraints].
 *
 * @param sizeConstraints The size constraints that will be used during measurement.
 * @param aspectRatio The aspect ratio for calculating size.
 * @return [Size] containing computed width and height if [aspectRatio] is > 0, otherwise returns
 *   [Size.Invalid]
 */
fun Size.Companion.withAspectRatio(sizeConstraints: SizeConstraints, aspectRatio: Float): Size {
  if (aspectRatio <= 0f) {
    return Size.Invalid
  }

  if (!sizeConstraints.hasBoundedWidth && !sizeConstraints.hasBoundedHeight) {
    // default to size [minWidth, minHeight] because both width and height are unbounded
    return Size(sizeConstraints.minWidth, sizeConstraints.minHeight)
  }

  val widthBasedHeight = ceil((sizeConstraints.maxWidth / aspectRatio).toDouble()).toInt()
  val heightBasedWidth = ceil((sizeConstraints.maxHeight * aspectRatio).toDouble()).toInt()

  var outputWidth = 0
  var outputHeight = 0

  if (!sizeConstraints.hasExactWidth &&
      sizeConstraints.hasBoundedWidth &&
      !sizeConstraints.hasExactHeight &&
      sizeConstraints.hasBoundedHeight) {
    // Both width and height are bounded but not exact. Find the largest possible size which
    // respects both constraints.
    if (widthBasedHeight > sizeConstraints.maxHeight) {
      outputWidth = heightBasedWidth
      outputHeight = sizeConstraints.maxHeight
    } else {
      outputWidth = sizeConstraints.maxWidth
      outputHeight = widthBasedHeight
    }
  } else if (sizeConstraints.hasExactWidth) {
    // Width is exact and the height is either unbounded or is allowed to be large enough to
    // accommodate the given aspect ratio.
    outputWidth = sizeConstraints.maxWidth
    outputHeight =
        if (!sizeConstraints.hasBoundedHeight || widthBasedHeight <= sizeConstraints.maxHeight) {
          widthBasedHeight
        } else {
          sizeConstraints.maxHeight
        }
  } else if (sizeConstraints.hasExactHeight) {
    // Height is exact and the width is either unbounded or is allowed to be large enough to
    // accommodate the given aspect ratio.
    outputHeight = sizeConstraints.maxHeight
    outputWidth =
        if (!sizeConstraints.hasBoundedWidth || heightBasedWidth <= sizeConstraints.maxWidth) {
          heightBasedWidth
        } else {
          sizeConstraints.maxWidth
        }
  } else if (sizeConstraints.hasBoundedWidth) {
    // Width is bounded. If that is the case then height must be unbounded.
    outputWidth = sizeConstraints.maxWidth
    outputHeight = widthBasedHeight
  } else if (sizeConstraints.hasBoundedHeight) {
    // Height is bounded. If that is the case then width must be unbounded.
    outputWidth = heightBasedWidth
    outputHeight = sizeConstraints.maxHeight
  }

  // Ensure that the resulting width and height are in appropriate ranges.
  outputWidth =
      if (outputWidth == SizeConstraints.Infinity) {
        SizeConstraints.Infinity
      } else {
        outputWidth.coerceIn(
            sizeConstraints.minWidth, min(sizeConstraints.maxWidth, SizeConstraints.MaxValue - 1))
      }
  outputHeight =
      if (outputHeight == SizeConstraints.Infinity) {
        SizeConstraints.Infinity
      } else {
        outputHeight.coerceIn(
            sizeConstraints.minHeight, min(sizeConstraints.maxHeight, SizeConstraints.MaxValue - 1))
      }

  return Size(outputWidth, outputHeight)
}

/**
 * Returns a [Size] measured according to an aspect ratio and [SizeConstraints].
 *
 * @param sizeConstraints The size constraints that will be used during measurement.
 * @param aspectRatio The aspect ratio for calculating size.
 * @return [Size] containing computed width and height if [aspectRatio] is > 0, otherwise returns
 *   [Size.Invalid]
 */
inline fun Size.Companion.withAspectRatio(
    sizeConstraints: SizeConstraints,
    aspectRatio: Double
): Size {
  return Size.withAspectRatio(sizeConstraints, aspectRatio.toFloat())
}

/**
 * Returns a [Size] that respects both [SizeConstraints] and tries to keep both width and height
 * equal. This will not guarantee equal width and height only if the constraints don't allow for it.
 *
 * @param sizeConstraints The size constraints that will be used during measurement.
 * @return [Size] containing computed width and height
 */
fun Size.Companion.withEqualDimensions(sizeConstraints: SizeConstraints): Size {
  return Size.withAspectRatio(sizeConstraints, 1f)
}

/**
 * Returns [Size] that respects both [SizeConstraints] and the preferred width and height. The
 * preferred size is usually the necessary pixels to render the inner content.
 *
 * @param sizeConstraints The size constraints that will be used during measurement.
 * @param preferredWidth The desired width in pixels.
 * @param preferredHeight The desired height in pixels.
 * @return [Size] containing computed width and height
 */
fun Size.Companion.withPreferredSize(
    sizeConstraints: SizeConstraints,
    preferredWidth: Int,
    preferredHeight: Int
): Size {
  val width =
      if (sizeConstraints.hasExactWidth) {
        sizeConstraints.maxWidth
      } else if (sizeConstraints.hasBoundedWidth) {
        min(sizeConstraints.maxWidth, preferredWidth)
      } else {
        preferredWidth
      }

  val height =
      if (sizeConstraints.hasExactHeight) {
        sizeConstraints.maxHeight
      } else if (sizeConstraints.hasBoundedHeight) {
        min(sizeConstraints.maxHeight, preferredHeight)
      } else {
        preferredHeight
      }

  return Size(width, height)
}


// (c) Meta Platforms, Inc. and affiliates. Confidential and proprietary.

package com.facebook.rendercore

import android.view.View
import com.facebook.rendercore.utils.MeasureSpecUtils
import java.lang.IllegalArgumentException
import org.assertj.core.api.Assertions.assertThat
import org.assertj.core.api.Assertions.assertThatThrownBy
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner

@RunWith(RobolectricTestRunner::class)
class SizeConstraintsTest {

  @Test
  fun `create -  exact size constraints - is successful`() {
    val c = SizeConstraints(minWidth = 0, maxWidth = 0, minHeight = 0, maxHeight = 0)
    assertThat(c).isNotNull
    assertThat(c.minWidth).isEqualTo(0)
    assertThat(c.maxWidth).isEqualTo(0)
    assertThat(c.minHeight).isEqualTo(0)
    assertThat(c.maxHeight).isEqualTo(0)
    assertThat(c.hasBoundedWidth).isTrue
    assertThat(c.hasBoundedHeight).isTrue
    assertThat(c.hasExactWidth).isTrue
    assertThat(c.hasExactHeight).isTrue
    assertThat(c.isZeroSize).isTrue
    assertThat(Size(width = 0, height = 0).fitsWithin(c)).isTrue
    assertThat(Size(width = 1, height = 0).fitsWithin(c)).isFalse
    assertThat(Size(width = 0, height = 1).fitsWithin(c)).isFalse
    assertThat(Size(width = 1, height = 1).fitsWithin(c)).isFalse
  }

  @Test
  fun `create - bounded size constraints - is successful`() {
    val c = SizeConstraints(minWidth = 10, maxWidth = 50, minHeight = 100, maxHeight = 300)
    assertThat(c).isNotNull
    assertThat(c.minWidth).isEqualTo(10)
    assertThat(c.maxWidth).isEqualTo(50)
    assertThat(c.minHeight).isEqualTo(100)
    assertThat(c.maxHeight).isEqualTo(300)
    assertThat(c.hasBoundedWidth).isTrue
    assertThat(c.hasBoundedHeight).isTrue
    assertThat(c.hasExactWidth).isFalse
    assertThat(c.hasExactHeight).isFalse
    assertThat(c.isZeroSize).isFalse
    assertThat(Size(width = 20, height = 200).fitsWithin(c)).isTrue
    assertThat(Size(width = 100, height = 200).fitsWithin(c)).isFalse
    assertThat(Size(width = 20, height = 400).fitsWithin(c)).isFalse
    assertThat(Size(width = 100, height = 400).fitsWithin(c)).isFalse
  }

  @Test
  fun `create - unbounded width size constraints with zero minWidth - is successful`() {
    val c =
        SizeConstraints(
            minWidth = 0, maxWidth = SizeConstraints.Infinity, minHeight = 100, maxHeight = 300)
    assertThat(c).isNotNull
    assertThat(c.minWidth).isEqualTo(0)
    assertThat(c.maxWidth).isEqualTo(SizeConstraints.Infinity)
    assertThat(c.minHeight).isEqualTo(100)
    assertThat(c.maxHeight).isEqualTo(300)
    assertThat(c.hasBoundedWidth).isFalse
    assertThat(c.hasBoundedHeight).isTrue
    assertThat(c.hasExactWidth).isFalse
    assertThat(c.hasExactHeight).isFalse
    assertThat(c.isZeroSize).isFalse
    assertThat(Size(width = 100, height = 200).fitsWithin(c)).isTrue
    assertThat(Size(width = 100, height = 400).fitsWithin(c)).isFalse
  }

  @Test
  fun `create - unbounded height size constraints with zero minHeight - is successful`() {
    val c =
        SizeConstraints(
            minWidth = 42, maxWidth = 84, minHeight = 0, maxHeight = SizeConstraints.Infinity)
    assertThat(c).isNotNull
    assertThat(c.minWidth).isEqualTo(42)
    assertThat(c.maxWidth).isEqualTo(84)
    assertThat(c.minHeight).isEqualTo(0)
    assertThat(c.maxHeight).isEqualTo(SizeConstraints.Infinity)
    assertThat(c.hasBoundedWidth).isTrue
    assertThat(c.hasBoundedHeight).isFalse
    assertThat(c.hasExactWidth).isFalse
    assertThat(c.hasExactHeight).isFalse
    assertThat(c.isZeroSize).isFalse
    assertThat(Size(width = 50, height = 100).fitsWithin(c)).isTrue
    assertThat(Size(width = 100, height = 100).fitsWithin(c)).isFalse
  }

  @Test
  fun `create - unbounded width size constraints with non-zero minWidth - is successful`() {
    val c =
        SizeConstraints(
            minWidth = 10, maxWidth = SizeConstraints.Infinity, minHeight = 100, maxHeight = 300)
    assertThat(c).isNotNull
    assertThat(c.minWidth).isEqualTo(10)
    assertThat(c.maxWidth).isEqualTo(SizeConstraints.Infinity)
    assertThat(c.minHeight).isEqualTo(100)
    assertThat(c.maxHeight).isEqualTo(300)
    assertThat(c.hasBoundedWidth).isFalse
    assertThat(c.hasBoundedHeight).isTrue
    assertThat(c.hasExactWidth).isFalse
    assertThat(c.hasExactHeight).isFalse
    assertThat(c.isZeroSize).isFalse
    assertThat(Size(width = 20, height = 200).fitsWithin(c)).isTrue
    assertThat(Size(width = 5, height = 200).fitsWithin(c)).isFalse
    assertThat(Size(width = 20, height = 400).fitsWithin(c)).isFalse
    assertThat(Size(width = 5, height = 400).fitsWithin(c)).isFalse
  }

  @Test
  fun `create - unbounded height size constraints with non-zero minHeight - is successful`() {
    val c =
        SizeConstraints(
            minWidth = 42, maxWidth = 84, minHeight = 10, maxHeight = SizeConstraints.Infinity)
    assertThat(c).isNotNull
    assertThat(c.minWidth).isEqualTo(42)
    assertThat(c.maxWidth).isEqualTo(84)
    assertThat(c.minHeight).isEqualTo(10)
    assertThat(c.maxHeight).isEqualTo(SizeConstraints.Infinity)
    assertThat(c.hasBoundedWidth).isTrue
    assertThat(c.hasBoundedHeight).isFalse
    assertThat(c.hasExactWidth).isFalse
    assertThat(c.hasExactHeight).isFalse
    assertThat(c.isZeroSize).isFalse
    assertThat(Size(width = 50, height = 100).fitsWithin(c)).isTrue
    assertThat(Size(width = 30, height = 100).fitsWithin(c)).isFalse
    assertThat(Size(width = 50, height = 5).fitsWithin(c)).isFalse
    assertThat(Size(width = 30, height = 5).fitsWithin(c)).isFalse
  }

  @Test
  fun `create - infinite size constraints - is successful`() {
    val c =
        SizeConstraints(
            minWidth = SizeConstraints.Infinity,
            maxWidth = SizeConstraints.Infinity,
            minHeight = SizeConstraints.Infinity,
            maxHeight = SizeConstraints.Infinity)
    assertThat(c).isNotNull
    assertThat(c.minWidth).isEqualTo(SizeConstraints.Infinity)
    assertThat(c.maxWidth).isEqualTo(SizeConstraints.Infinity)
    assertThat(c.minHeight).isEqualTo(SizeConstraints.Infinity)
    assertThat(c.maxHeight).isEqualTo(SizeConstraints.Infinity)
    assertThat(c.hasBoundedWidth).isFalse
    assertThat(c.hasBoundedHeight).isFalse
    assertThat(c.hasExactWidth).isTrue
    assertThat(c.hasExactHeight).isTrue
    assertThat(c.isZeroSize).isFalse
    assertThat(
            Size(width = SizeConstraints.Infinity, height = SizeConstraints.Infinity).fitsWithin(c))
        .isTrue
    assertThat(Size(width = 100, height = 100).fitsWithin(c)).isFalse
  }

  @Test
  fun `create - size constraints with negative minWidth - should throw`() {
    assertThatThrownBy {
          SizeConstraints(minWidth = -1, maxWidth = 0, minHeight = 0, maxHeight = 0)
        }
        .isInstanceOf(IllegalArgumentException::class.java)
        .hasMessageContaining("minWidth must be >= 0")
  }

  @Test
  fun `create - size constraints with negative maxWidth - should throw`() {
    assertThatThrownBy {
          SizeConstraints(minWidth = 0, maxWidth = -1, minHeight = 0, maxHeight = 0)
        }
        .isInstanceOf(IllegalArgumentException::class.java)
        .hasMessageContaining("maxWidth must be >= minWidth")
  }

  @Test
  fun `create - size constraints with negative minHeight - should throw`() {
    assertThatThrownBy {
          SizeConstraints(minWidth = 0, maxWidth = 0, minHeight = -1, maxHeight = 0)
        }
        .isInstanceOf(IllegalArgumentException::class.java)
        .hasMessageContaining("minHeight must be >= 0")
  }

  @Test
  fun `create - size constraints with negative maxHeight - should throw`() {
    assertThatThrownBy {
          SizeConstraints(minWidth = 0, maxWidth = 0, minHeight = 0, maxHeight = -1)
        }
        .isInstanceOf(IllegalArgumentException::class.java)
        .hasMessageContaining("maxHeight must be >= minHeight")
  }

  @Test
  fun `create - size constraints with maxWidth larger than MaxValue - should throw`() {
    assertThatThrownBy {
          SizeConstraints(minWidth = 0, maxWidth = 70000, minHeight = 0, maxHeight = 0)
        }
        .isInstanceOf(IllegalArgumentException::class.java)
        .hasMessageContaining("maxWidth must be < 65535")
  }

  @Test
  fun `create - size constraints with maxHeight larger than MaxValue - should throw`() {
    assertThatThrownBy {
          SizeConstraints(minWidth = 0, maxWidth = 0, minHeight = 0, maxHeight = 70000)
        }
        .isInstanceOf(IllegalArgumentException::class.java)
        .hasMessageContaining("maxHeight must be < 65535")
  }

  @Test
  fun `copy - exact size constraints - is successful`() {
    val c =
        SizeConstraints(minWidth = 0, maxWidth = 0, minHeight = 0, maxHeight = 0)
            .copy(minWidth = 10, maxWidth = 10, minHeight = 10, maxHeight = 10)
    assertThat(c).isNotNull
    assertThat(c.minWidth).isEqualTo(10)
    assertThat(c.maxWidth).isEqualTo(10)
    assertThat(c.minHeight).isEqualTo(10)
    assertThat(c.maxHeight).isEqualTo(10)
    assertThat(c.hasBoundedWidth).isTrue
    assertThat(c.hasBoundedHeight).isTrue
    assertThat(c.hasExactWidth).isTrue
    assertThat(c.hasExactHeight).isTrue
    assertThat(c.isZeroSize).isFalse
    assertThat(Size(width = 10, height = 10).fitsWithin(c)).isTrue
    assertThat(Size(width = 20, height = 10).fitsWithin(c)).isFalse
    assertThat(Size(width = 10, height = 20).fitsWithin(c)).isFalse
    assertThat(Size(width = 20, height = 20).fitsWithin(c)).isFalse
  }

  @Test
  fun `copy - bounded size constraints - is successful`() {
    val c =
        SizeConstraints(minWidth = 0, maxWidth = 0, minHeight = 0, maxHeight = 0)
            .copy(minWidth = 10, maxWidth = 50, minHeight = 100, maxHeight = 300)
    assertThat(c).isNotNull
    assertThat(c.minWidth).isEqualTo(10)
    assertThat(c.maxWidth).isEqualTo(50)
    assertThat(c.minHeight).isEqualTo(100)
    assertThat(c.maxHeight).isEqualTo(300)
    assertThat(c.hasBoundedWidth).isTrue
    assertThat(c.hasBoundedHeight).isTrue
    assertThat(c.hasExactWidth).isFalse
    assertThat(c.hasExactHeight).isFalse
    assertThat(c.isZeroSize).isFalse
    assertThat(Size(width = 20, height = 200).fitsWithin(c)).isTrue
    assertThat(Size(width = 5, height = 200).fitsWithin(c)).isFalse
    assertThat(Size(width = 20, height = 400).fitsWithin(c)).isFalse
    assertThat(Size(width = 5, height = 400).fitsWithin(c)).isFalse
  }

  @Test
  fun `copy - unbounded width size constraints with zero minWidth - is successful`() {
    val c =
        SizeConstraints(minWidth = 0, maxWidth = 0, minHeight = 0, maxHeight = 0)
            .copy(
                minWidth = 0, maxWidth = SizeConstraints.Infinity, minHeight = 100, maxHeight = 300)
    assertThat(c).isNotNull
    assertThat(c.minWidth).isEqualTo(0)
    assertThat(c.maxWidth).isEqualTo(SizeConstraints.Infinity)
    assertThat(c.minHeight).isEqualTo(100)
    assertThat(c.maxHeight).isEqualTo(300)
    assertThat(c.hasBoundedWidth).isFalse
    assertThat(c.hasBoundedHeight).isTrue
    assertThat(c.hasExactWidth).isFalse
    assertThat(c.hasExactHeight).isFalse
    assertThat(c.isZeroSize).isFalse
    assertThat(Size(width = 10, height = 200).fitsWithin(c)).isTrue
    assertThat(Size(width = 10, height = 400).fitsWithin(c)).isFalse
  }

  @Test
  fun `copy - unbounded height size constraints with zero minHeight - is successful`() {
    val c =
        SizeConstraints(minWidth = 0, maxWidth = 0, minHeight = 0, maxHeight = 0)
            .copy(minWidth = 42, maxWidth = 84, minHeight = 0, maxHeight = SizeConstraints.Infinity)
    assertThat(c).isNotNull
    assertThat(c.minWidth).isEqualTo(42)
    assertThat(c.maxWidth).isEqualTo(84)
    assertThat(c.minHeight).isEqualTo(0)
    assertThat(c.maxHeight).isEqualTo(SizeConstraints.Infinity)
    assertThat(c.hasBoundedWidth).isTrue
    assertThat(c.hasBoundedHeight).isFalse
    assertThat(c.hasExactWidth).isFalse
    assertThat(c.hasExactHeight).isFalse
    assertThat(c.isZeroSize).isFalse
    assertThat(Size(width = 50, height = 100).fitsWithin(c)).isTrue
    assertThat(Size(width = 30, height = 100).fitsWithin(c)).isFalse
  }

  @Test
  fun `copy - unbounded width size constraints with non-zero minWidth - is successful`() {
    val c =
        SizeConstraints(minWidth = 0, maxWidth = 0, minHeight = 0, maxHeight = 0)
            .copy(
                minWidth = 10,
                maxWidth = SizeConstraints.Infinity,
                minHeight = 100,
                maxHeight = 300)
    assertThat(c).isNotNull
    assertThat(c.minWidth).isEqualTo(10)
    assertThat(c.maxWidth).isEqualTo(SizeConstraints.Infinity)
    assertThat(c.minHeight).isEqualTo(100)
    assertThat(c.maxHeight).isEqualTo(300)
    assertThat(c.hasBoundedWidth).isFalse
    assertThat(c.hasBoundedHeight).isTrue
    assertThat(c.hasExactWidth).isFalse
    assertThat(c.hasExactHeight).isFalse
    assertThat(c.isZeroSize).isFalse
    assertThat(Size(width = 20, height = 200).fitsWithin(c)).isTrue
    assertThat(Size(width = 5, height = 200).fitsWithin(c)).isFalse
    assertThat(Size(width = 20, height = 400).fitsWithin(c)).isFalse
    assertThat(Size(width = 5, height = 400).fitsWithin(c)).isFalse
  }

  @Test
  fun `copy - unbounded height size constraints with non-zero minHeight - is successful`() {
    val c =
        SizeConstraints(minWidth = 0, maxWidth = 0, minHeight = 0, maxHeight = 0)
            .copy(
                minWidth = 42, maxWidth = 84, minHeight = 10, maxHeight = SizeConstraints.Infinity)
    assertThat(c).isNotNull
    assertThat(c.minWidth).isEqualTo(42)
    assertThat(c.maxWidth).isEqualTo(84)
    assertThat(c.minHeight).isEqualTo(10)
    assertThat(c.maxHeight).isEqualTo(SizeConstraints.Infinity)
    assertThat(c.hasBoundedWidth).isTrue
    assertThat(c.hasBoundedHeight).isFalse
    assertThat(c.hasExactWidth).isFalse
    assertThat(c.hasExactHeight).isFalse
    assertThat(c.isZeroSize).isFalse
    assertThat(Size(width = 50, height = 100).fitsWithin(c)).isTrue
    assertThat(Size(width = 30, height = 100).fitsWithin(c)).isFalse
    assertThat(Size(width = 50, height = 5).fitsWithin(c)).isFalse
    assertThat(Size(width = 30, height = 5).fitsWithin(c)).isFalse
  }

  @Test
  fun `copy - infinite size constraints - is successful`() {
    val c =
        SizeConstraints(minWidth = 0, maxWidth = 0, minHeight = 0, maxHeight = 0)
            .copy(
                minWidth = SizeConstraints.Infinity,
                maxWidth = SizeConstraints.Infinity,
                minHeight = SizeConstraints.Infinity,
                maxHeight = SizeConstraints.Infinity)
    assertThat(c).isNotNull
    assertThat(c.minWidth).isEqualTo(SizeConstraints.Infinity)
    assertThat(c.maxWidth).isEqualTo(SizeConstraints.Infinity)
    assertThat(c.minHeight).isEqualTo(SizeConstraints.Infinity)
    assertThat(c.maxHeight).isEqualTo(SizeConstraints.Infinity)
    assertThat(c.hasBoundedWidth).isFalse
    assertThat(c.hasBoundedHeight).isFalse
    assertThat(c.hasExactWidth).isTrue
    assertThat(c.hasExactHeight).isTrue
    assertThat(c.isZeroSize).isFalse
    assertThat(
            Size(width = SizeConstraints.Infinity, height = SizeConstraints.Infinity).fitsWithin(c))
        .isTrue
    assertThat(Size(width = 100, height = 100).fitsWithin(c)).isFalse
  }

  @Test
  fun `copy - size constraints with negative minWidth - should throw`() {
    assertThatThrownBy {
          SizeConstraints(minWidth = 0, maxWidth = 0, minHeight = 0, maxHeight = 0)
              .copy(minWidth = -1, maxWidth = 0, minHeight = 0, maxHeight = 0)
        }
        .isInstanceOf(IllegalArgumentException::class.java)
        .hasMessageContaining("minWidth must be >= 0")
  }

  @Test
  fun `copy - size constraints with negative maxWidth - should throw`() {
    assertThatThrownBy {
          SizeConstraints(minWidth = 0, maxWidth = 0, minHeight = 0, maxHeight = 0)
              .copy(minWidth = 0, maxWidth = -1, minHeight = 0, maxHeight = 0)
        }
        .isInstanceOf(IllegalArgumentException::class.java)
        .hasMessageContaining("maxWidth must be >= minWidth")
  }

  @Test
  fun `copy - size constraints with negative minHeight - should throw`() {
    assertThatThrownBy {
          SizeConstraints(minWidth = 0, maxWidth = 0, minHeight = 0, maxHeight = 0)
              .copy(minWidth = 0, maxWidth = 0, minHeight = -1, maxHeight = 0)
        }
        .isInstanceOf(IllegalArgumentException::class.java)
        .hasMessageContaining("minHeight must be >= 0")
  }

  @Test
  fun `copy - size constraints with negative maxHeight - should throw`() {
    assertThatThrownBy {
          SizeConstraints(minWidth = 0, maxWidth = 0, minHeight = 0, maxHeight = 0)
              .copy(minWidth = 0, maxWidth = 0, minHeight = 0, maxHeight = -1)
        }
        .isInstanceOf(IllegalArgumentException::class.java)
        .hasMessageContaining("maxHeight must be >= minHeight")
  }

  @Test
  fun `copy - size constraints with maxWidth larger than MaxValue - should throw`() {
    assertThatThrownBy {
          SizeConstraints(minWidth = 0, maxWidth = 0, minHeight = 0, maxHeight = 0)
              .copy(minWidth = 0, maxWidth = 70000, minHeight = 0, maxHeight = 0)
        }
        .isInstanceOf(IllegalArgumentException::class.java)
        .hasMessageContaining("maxWidth must be < 65535")
  }

  @Test
  fun `copy - size constraints with maxHeight larger than MaxValue - should throw`() {
    assertThatThrownBy {
          SizeConstraints(minWidth = 0, maxWidth = 0, minHeight = 0, maxHeight = 0)
              .copy(minWidth = 0, maxWidth = 0, minHeight = 0, maxHeight = 70000)
        }
        .isInstanceOf(IllegalArgumentException::class.java)
        .hasMessageContaining("maxHeight must be < 65535")
  }

  @Test
  fun `create - size constraints with exact width and exact height measure specs - is successful`() {
    val c =
        SizeConstraints.fromMeasureSpecs(MeasureSpecUtils.exactly(10), MeasureSpecUtils.exactly(20))
    assertThat(c).isNotNull
    assertThat(c.minWidth).isEqualTo(10)
    assertThat(c.maxWidth).isEqualTo(10)
    assertThat(c.minHeight).isEqualTo(20)
    assertThat(c.maxHeight).isEqualTo(20)
  }

  @Test
  fun `create - size constraints with at most width and unspecified height measure specs - is successful`() {
    val c =
        SizeConstraints.fromMeasureSpecs(
            MeasureSpecUtils.atMost(10), MeasureSpecUtils.unspecified())
    assertThat(c).isNotNull
    assertThat(c.minWidth).isEqualTo(0)
    assertThat(c.maxWidth).isEqualTo(10)
    assertThat(c.minHeight).isEqualTo(0)
    assertThat(c.maxHeight).isEqualTo(SizeConstraints.Infinity)
  }

  @Test
  fun `create - size constraints with unspecified width and at most height measure specs - is successful`() {
    val c =
        SizeConstraints.fromMeasureSpecs(
            MeasureSpecUtils.unspecified(), MeasureSpecUtils.atMost(10))
    assertThat(c).isNotNull
    assertThat(c.minWidth).isEqualTo(0)
    assertThat(c.maxWidth).isEqualTo(SizeConstraints.Infinity)
    assertThat(c.minHeight).isEqualTo(0)
    assertThat(c.maxHeight).isEqualTo(10)
  }

  @Test
  fun `convert - size constraints to exact width and exact height measure specs - is successful`() {
    val c = SizeConstraints(minWidth = 0, maxWidth = 0, minHeight = 0, maxHeight = 0)
    val widthSpec = c.toWidthSpec()
    val heightSpec = c.toHeightSpec()
    assertThat(MeasureSpecUtils.getMode(widthSpec)).isEqualTo(View.MeasureSpec.EXACTLY)
    assertThat(MeasureSpecUtils.getSize(widthSpec)).isEqualTo(0)
    assertThat(MeasureSpecUtils.getMode(heightSpec)).isEqualTo(View.MeasureSpec.EXACTLY)
    assertThat(MeasureSpecUtils.getSize(heightSpec)).isEqualTo(0)
  }

  @Test
  fun `convert - size constraints to unspecified width and unspecified height measure specs - is successful`() {
    val c =
        SizeConstraints(
            minWidth = 0,
            maxWidth = SizeConstraints.Infinity,
            minHeight = 0,
            maxHeight = SizeConstraints.Infinity)
    val widthSpec = c.toWidthSpec()
    val heightSpec = c.toHeightSpec()
    assertThat(MeasureSpecUtils.getMode(widthSpec)).isEqualTo(View.MeasureSpec.UNSPECIFIED)
    assertThat(MeasureSpecUtils.getSize(widthSpec)).isEqualTo(0)
    assertThat(MeasureSpecUtils.getMode(heightSpec)).isEqualTo(View.MeasureSpec.UNSPECIFIED)
    assertThat(MeasureSpecUtils.getSize(heightSpec)).isEqualTo(0)
  }

  @Test
  fun `convert - size constraints to at most width and at most height measure specs - is successful`() {
    val c = SizeConstraints(minWidth = 0, maxWidth = 100, minHeight = 0, maxHeight = 200)
    val widthSpec = c.toWidthSpec()
    val heightSpec = c.toHeightSpec()
    assertThat(MeasureSpecUtils.getMode(widthSpec)).isEqualTo(View.MeasureSpec.AT_MOST)
    assertThat(MeasureSpecUtils.getSize(widthSpec)).isEqualTo(100)
    assertThat(MeasureSpecUtils.getMode(heightSpec)).isEqualTo(View.MeasureSpec.AT_MOST)
    assertThat(MeasureSpecUtils.getSize(heightSpec)).isEqualTo(200)
  }

  @Test
  fun `convert - size constraints to at most width and at most height measure specs - is lossy`() {
    val c = SizeConstraints(minWidth = 50, maxWidth = 100, minHeight = 120, maxHeight = 200)
    val widthSpec = c.toWidthSpec()
    val heightSpec = c.toHeightSpec()
    assertThat(MeasureSpecUtils.getMode(widthSpec)).isEqualTo(View.MeasureSpec.AT_MOST)
    assertThat(MeasureSpecUtils.getSize(widthSpec)).isEqualTo(100)
    assertThat(MeasureSpecUtils.getMode(heightSpec)).isEqualTo(View.MeasureSpec.AT_MOST)
    assertThat(MeasureSpecUtils.getSize(heightSpec)).isEqualTo(200)
  }

  @Test
  fun `convert - size constraints to unspecified width and unspecified height measure specs - is lossy`() {
    val c =
        SizeConstraints(
            minWidth = 50,
            maxWidth = SizeConstraints.Infinity,
            minHeight = 120,
            maxHeight = SizeConstraints.Infinity)
    val widthSpec = c.toWidthSpec()
    val heightSpec = c.toHeightSpec()
    assertThat(MeasureSpecUtils.getMode(widthSpec)).isEqualTo(View.MeasureSpec.UNSPECIFIED)
    assertThat(MeasureSpecUtils.getSize(widthSpec)).isEqualTo(0)
    assertThat(MeasureSpecUtils.getMode(heightSpec)).isEqualTo(View.MeasureSpec.UNSPECIFIED)
    assertThat(MeasureSpecUtils.getSize(heightSpec)).isEqualTo(0)
  }

  @Test
  fun `subtract - one from infinite maxWidth - should throw`() {
    val constraints =
        SizeConstraints(
            minWidth = 0,
            maxWidth = SizeConstraints.Infinity,
            minHeight = 0,
            maxHeight = SizeConstraints.Infinity)
    assertThatThrownBy { constraints.copy(maxWidth = constraints.maxWidth - 1) }
        .isInstanceOf(IllegalArgumentException::class.java)
        .hasMessageContaining("maxWidth must be < 65535")
  }

  @Test
  fun `add - one to infinite maxWidth - should throw`() {
    val constraints =
        SizeConstraints(
            minWidth = 0,
            maxWidth = SizeConstraints.Infinity,
            minHeight = 0,
            maxHeight = SizeConstraints.Infinity)
    assertThatThrownBy { constraints.copy(maxWidth = constraints.maxWidth + 1) }
        .isInstanceOf(IllegalArgumentException::class.java)
        .hasMessageContaining("maxWidth must be < 65535")
  }

  @Test
  fun `subtract - one from infinite maxHeight - should throw`() {
    val constraints =
        SizeConstraints(
            minWidth = 0,
            maxWidth = SizeConstraints.Infinity,
            minHeight = 0,
            maxHeight = SizeConstraints.Infinity)
    assertThatThrownBy { constraints.copy(maxHeight = constraints.maxHeight - 1) }
        .isInstanceOf(IllegalArgumentException::class.java)
        .hasMessageContaining("maxHeight must be < 65535")
  }

  @Test
  fun `add - one to infinite maxHeight - should throw`() {
    val constraints =
        SizeConstraints(
            minWidth = 0,
            maxWidth = SizeConstraints.Infinity,
            minHeight = 0,
            maxHeight = SizeConstraints.Infinity)
    assertThatThrownBy { constraints.copy(maxHeight = constraints.maxHeight + 1) }
        .isInstanceOf(IllegalArgumentException::class.java)
        .hasMessageContaining("maxHeight must be < 65535")
  }
}


/*
 * 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.rendercore.debug

import com.facebook.rendercore.LogLevel
import com.facebook.rendercore.debug.DebugEvent.Companion.All
import org.assertj.core.api.Assertions.assertThat
import org.junit.After
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner

@RunWith(RobolectricTestRunner::class)
class DebugEventsTest {

  companion object {
    const val TestEvent = "TestEvent"
    const val TestRenderStateId = "1"
    const val TestAttr = "attr"
    const val TestAttrValue = 1
  }

  @Before
  fun before() {
    DebugEventDispatcher.minLogLevel = LogLevel.DEBUG
  }

  @After
  fun after() {
    DebugEventBus.unsubscribeAll()
  }

  @Test
  fun `test event should be dispatched to subscriber`() {
    var event: DebugEvent? = null
    DebugEventBus.subscribe(TestEventSubscriber { e -> event = e })

    val timestamp = System.currentTimeMillis()
    DebugEventDispatcher.dispatch(
        type = TestEvent,
        renderStateId = { TestRenderStateId },
        timestamp = timestamp,
    ) { map ->
      map[TestAttr] = TestAttrValue
    }

    assertThat(event).isNotNull
    assertThat(event?.type).isEqualTo(TestEvent)
    assertThat(event?.renderStateId).isEqualTo(TestRenderStateId)
    assertThat(event?.attribute<Long>(DebugEventAttribute.timestamp)).isEqualTo(timestamp)
    assertThat(event?.attribute<Int>(TestAttr)).isEqualTo(TestAttrValue)
  }

  @Test
  fun `test event should be dispatched to multiple subscribers`() {
    var eventToSub1: DebugEvent? = null
    var eventToSub2: DebugEvent? = null
    DebugEventBus.subscribe(TestEventSubscriber { e -> eventToSub1 = e })
    DebugEventBus.subscribe(TestEventSubscriber { e -> eventToSub2 = e })

    val timestamp = System.currentTimeMillis()
    DebugEventDispatcher.dispatch(
        type = TestEvent,
        renderStateId = { TestRenderStateId },
        timestamp = timestamp,
    ) { map ->
      map[TestAttr] = TestAttrValue
    }

    assertThat(eventToSub1).isNotNull
    assertThat(eventToSub1?.type).isEqualTo(TestEvent)
    assertThat(eventToSub1?.renderStateId).isEqualTo(TestRenderStateId)
    assertThat(eventToSub1?.attribute<Long>(DebugEventAttribute.timestamp)).isEqualTo(timestamp)
    assertThat(eventToSub1?.attribute<Int>(TestAttr)).isEqualTo(TestAttrValue)

    assertThat(eventToSub2).isNotNull
    assertThat(eventToSub2?.type).isEqualTo(TestEvent)
    assertThat(eventToSub2?.renderStateId).isEqualTo(TestRenderStateId)
    assertThat(eventToSub2?.attribute<Long>(DebugEventAttribute.timestamp)).isEqualTo(timestamp)
    assertThat(eventToSub2?.attribute<Int>(TestAttr)).isEqualTo(TestAttrValue)

    assertThat(eventToSub1).isSameAs(eventToSub2)
  }

  @Test
  fun `test event should not be created on dispatch if there are no subscribers`() {
    var createdDebugEvent = false

    fun dispatchDebugEvent() {
      DebugEventDispatcher.dispatch(
          type = TestEvent,
          renderStateId = {
            createdDebugEvent = true
            TestRenderStateId
          })
    }

    dispatchDebugEvent()
    assertThat(createdDebugEvent).isFalse

    DebugEventBus.subscribe(TestEventSubscriber { /* do nothing */})
    dispatchDebugEvent()
    assertThat(createdDebugEvent).isTrue
  }

  @Test
  fun `test event should not be created on trace if there are no subscribers`() {
    var createdDebugEvent = false
    var traceBlockRan = false

    fun runTrace() {
      DebugEventDispatcher.trace(
          type = TestEvent,
          renderStateId = {
            createdDebugEvent = true
            TestRenderStateId
          }) {
            traceBlockRan = true
          }
    }

    runTrace()
    assertThat(createdDebugEvent).isFalse
    assertThat(traceBlockRan).isTrue

    DebugEventBus.subscribe(TestEventSubscriber { /* do nothing */})
    runTrace()
    assertThat(createdDebugEvent).isTrue
    assertThat(traceBlockRan).isTrue
  }

  @Test
  fun `test event should not be dispatched to subscriber after unsubscribing`() {
    var event: DebugEvent? = null
    val subscriber = TestEventSubscriber { e -> event = e }
    DebugEventBus.subscribe(subscriber)

    DebugEventDispatcher.dispatch(type = TestEvent, renderStateId = { TestRenderStateId })

    assertThat(event).isNotNull
    event = null

    DebugEventBus.unsubscribe(subscriber)

    DebugEventDispatcher.dispatch(type = TestEvent, renderStateId = { TestRenderStateId })

    assertThat(event).isNull()
  }

  @Test
  fun `multiple test events should be dispatched to subscriber`() {
    val events = mutableListOf<DebugEvent>()

    DebugEventBus.subscribe(TestEventSubscriber { e -> events.add(e) })

    val timestamp = System.currentTimeMillis() - 1 // note: time in the past
    DebugEventDispatcher.dispatch(
        type = TestEvent,
        renderStateId = { TestRenderStateId },
        timestamp = timestamp,
    ) { map ->
      map[TestAttr] = TestAttrValue
    }
    DebugEventDispatcher.dispatch(
        type = TestEvent,
        renderStateId = { TestRenderStateId }, // note: no timestamp passed
    ) { map ->
      map[TestAttr] = "{$TestAttrValue}_1" // note: different attr value
    }

    assertThat(events.size).isEqualTo(2)
    assertThat(events[0].type).isEqualTo(TestEvent)
    assertThat(events[0].renderStateId).isEqualTo(TestRenderStateId)
    assertThat(events[0].attribute<Long>(DebugEventAttribute.timestamp)).isEqualTo(timestamp)
    assertThat(events[0].attribute<Int>(TestAttr)).isEqualTo(TestAttrValue)

    assertThat(events).isNotEmpty
    assertThat(events[1].type).isEqualTo(TestEvent)
    assertThat(events[1].renderStateId).isEqualTo(TestRenderStateId)
    assertThat(events[1].attribute<Long>(DebugEventAttribute.timestamp)).isGreaterThan(timestamp)
    assertThat(events[1].attribute<String>(TestAttr)).isEqualTo("{$TestAttrValue}_1")
  }

  @Test
  fun `test events should be dispatched to correct subscribers`() {
    var event: DebugEvent? = null
    DebugEventBus.subscribe(TestEventSubscriber { e -> event = e })

    val notTestEvent = "Not${TestEvent}"
    var otherEvent: DebugEvent? = null

    DebugEventBus.subscribe(TestEventsSubscriber(notTestEvent, listener = { e -> otherEvent = e }))

    DebugEventDispatcher.dispatch(type = TestEvent, renderStateId = { TestRenderStateId })

    DebugEventDispatcher.dispatch(type = notTestEvent, renderStateId = { TestRenderStateId })

    assertThat(event).isNotNull
    assertThat(otherEvent).isNotNull
    assertThat(event).isNotEqualTo(otherEvent)
  }

  @Test
  fun `test event should be dispatched to all event subscriber`() {
    val events = mutableListOf<DebugEvent>()

    DebugEventBus.subscribe(TestEventsSubscriber(All, listener = { e -> events.add(e) }))

    DebugEventDispatcher.dispatch(type = "a", renderStateId = { TestRenderStateId })
    DebugEventDispatcher.dispatch(type = "b", renderStateId = { TestRenderStateId })
    DebugEventDispatcher.dispatch(type = "c", renderStateId = { TestRenderStateId })
    DebugEventDispatcher.dispatch(type = "d", renderStateId = { TestRenderStateId })

    assertThat(events.size).isEqualTo(4)
  }

  @Test
  fun `trace event should be dispatched to subscriber`() {
    val traceTestAttr = "otherTestAttr"
    val traceTestAttrValue = "2"

    var event: DebugEvent? = null
    DebugEventBus.subscribe(TestEventSubscriber { e -> event = e })

    val timestamp = System.currentTimeMillis() - 1 // note: time in the past

    DebugEventDispatcher.trace(
        type = TestEvent,
        renderStateId = { TestRenderStateId },
        attributesAccumulator = { attributes -> attributes[TestAttr] = TestAttrValue },
    ) { scope ->
      scope?.attribute(traceTestAttr, "2")
    }

    assertThat(event).isNotNull
    assertThat(event?.type).isEqualTo(TestEvent)
    assertThat(event?.renderStateId).isEqualTo(TestRenderStateId)
    assertThat(event?.attribute<Long>(DebugEventAttribute.timestamp)).isGreaterThan(timestamp)
    assertThat(event?.attribute<Duration>(DebugEventAttribute.duration)?.value).isGreaterThan(0)
    assertThat(event?.attribute<Int>(TestAttr)).isEqualTo(TestAttrValue)
    assertThat(event?.attribute<String>(traceTestAttr)).isEqualTo(traceTestAttrValue)
  }

  @Test
  fun `test event should be dispatched only if min log level is smaller than event log level`() {
    var event: DebugEvent? = null
    DebugEventBus.subscribe(TestEventSubscriber { e -> event = e })

    DebugEventDispatcher.minLogLevel = LogLevel.VERBOSE
    DebugEventDispatcher.dispatch(
        type = TestEvent,
        renderStateId = { TestRenderStateId },
        timestamp = System.currentTimeMillis(),
        logLevel = LogLevel.VERBOSE) {
          mapOf<String, Any?>(TestAttr to TestAttrValue)
        }

    assertThat(event).isNotNull
    event = null

    DebugEventDispatcher.minLogLevel = LogLevel.VERBOSE
    DebugEventDispatcher.dispatch(
        type = TestEvent,
        renderStateId = { TestRenderStateId },
        timestamp = System.currentTimeMillis(),
        logLevel = LogLevel.DEBUG) {
          mapOf<String, Any?>(TestAttr to TestAttrValue)
        }

    assertThat(event).isNotNull
    event = null

    DebugEventDispatcher.minLogLevel = LogLevel.WARNING
    DebugEventDispatcher.dispatch(
        type = TestEvent,
        renderStateId = { TestRenderStateId },
        timestamp = System.currentTimeMillis(),
        logLevel = LogLevel.DEBUG) {
          mapOf<String, Any?>(TestAttr to TestAttrValue)
        }

    assertThat(event).isNull()
    event = null

    DebugEventDispatcher.minLogLevel = LogLevel.WARNING
    DebugEventDispatcher.dispatch(
        type = TestEvent,
        renderStateId = { TestRenderStateId },
        timestamp = System.currentTimeMillis(),
        logLevel = LogLevel.ERROR) {
          mapOf<String, Any?>(TestAttr to TestAttrValue)
        }

    assertThat(event).isNotNull
    event = null

    DebugEventDispatcher.minLogLevel = LogLevel.ERROR
    DebugEventDispatcher.dispatch(
        type = TestEvent,
        renderStateId = { TestRenderStateId },
        timestamp = System.currentTimeMillis(),
        logLevel = LogLevel.ERROR) {
          mapOf<String, Any?>(TestAttr to TestAttrValue)
        }

    assertThat(event).isNotNull
    event = null

    DebugEventDispatcher.minLogLevel = LogLevel.FATAL
    DebugEventDispatcher.dispatch(
        type = TestEvent,
        renderStateId = { TestRenderStateId },
        timestamp = System.currentTimeMillis(),
        logLevel = LogLevel.ERROR) {
          mapOf<String, Any?>(TestAttr to TestAttrValue)
        }

    assertThat(event).isNull()
    event = null
  }

  @Test
  fun `generateTraceIdentifier should generate a valid id if there is at least one subscriber listening to all events`() {
    assertThat(DebugEventDispatcher.generateTraceIdentifier(TestEvent)).isNull()
    DebugEventDispatcher.subscribe(TestEventsSubscriber(All, listener = { /* nothing */}))
    assertThat(DebugEventDispatcher.generateTraceIdentifier(TestEvent)).isNotNull()
  }

  @Test
  fun `generateTraceIdentifier should generate a valid id if there is at least one subscriber listens to that event`() {
    assertThat(DebugEventDispatcher.generateTraceIdentifier(TestEvent)).isNull()
    DebugEventDispatcher.subscribe(TestEventsSubscriber(TestEvent, listener = { /* nothing */}))
    assertThat(DebugEventDispatcher.generateTraceIdentifier(TestEvent)).isNotNull()
  }

  @Test
  fun `generateTraceIdentifier should generate different ids for each invocation as long as there are valid subscribers`() {
    assertThat(DebugEventDispatcher.generateTraceIdentifier(TestEvent)).isNull()
    DebugEventDispatcher.subscribe(TestEventsSubscriber(TestEvent, listener = { /* nothing */}))
    assertThat(DebugEventDispatcher.generateTraceIdentifier(TestEvent)).isEqualTo(1)
    assertThat(DebugEventDispatcher.generateTraceIdentifier(TestEvent)).isEqualTo(2)
  }

  @Test
  fun `java trace APIs work correctly`() {
    val events = mutableListOf<DebugEvent>()
    DebugEventDispatcher.subscribe(TestEventsSubscriber(All, listener = { events.add(it) }))

    val traceId = DebugEventDispatcher.generateTraceIdentifier(TestEvent)
    DebugEventDispatcher.beginTrace(
        traceId!!, TestEvent, TestRenderStateId, mutableMapOf(TestAttr to 1))
    assertThat(events).isEmpty()

    DebugEventDispatcher.endTrace(traceId)
    assertThat(events).hasSize(1)

    val event = events.last()
    assertThat(event.type).isEqualTo(TestEvent)
    assertThat(event.renderStateId).isEqualTo(TestRenderStateId)
    assertThat(event.attribute<Int>(TestAttr)).isEqualTo(1)
  }

  class TestEventSubscriber(val listener: (DebugEvent) -> Unit) : DebugEventSubscriber(TestEvent) {
    override fun onEvent(event: DebugEvent) {
      listener(event)
    }
  }

  class TestEventsSubscriber(
      vararg events: String,
      val listener: (DebugEvent) -> Unit,
  ) : DebugEventSubscriber(*events) {
    override fun onEvent(event: DebugEvent) {
      listener(event)
    }
  }
}


/*
 * 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.rendercore.debug

import org.assertj.core.api.Assertions.assertThat
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner

@RunWith(RobolectricTestRunner::class)
class DurationValueTest {

  @Test
  fun `to string duration less than 1000 correctly`() {
    val duration = Duration(999)
    assertThat(duration.value).isEqualTo(999)
    assertThat(duration.toString()).isEqualTo("999 ns")
  }

  @Test
  fun `to string less than 1_000_000 correctly`() {
    val duration = Duration(999_999)
    assertThat(duration.value).isEqualTo(999_999)
    assertThat(duration.toString()).isEqualTo("999 Âµs")
  }

  @Test
  fun `to string more than 1_000_000 correctly`() {
    val duration = Duration(999_999_999)
    assertThat(duration.value).isEqualTo(999_999_999)
    assertThat(duration.toString()).isEqualTo("999 ms")
  }
}


// (c) Meta Platforms, Inc. and affiliates. Confidential and proprietary.

package com.facebook.rendercore.utils

import com.facebook.rendercore.Size
import com.facebook.rendercore.SizeConstraints
import com.facebook.rendercore.getOrElse
import org.assertj.core.api.Assertions.assertThat
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner

@RunWith(RobolectricTestRunner::class)
class SizeUtilsTest {

  @Test
  fun `exact size - with exact width and exact height - returns exact size`() {
    assertThat(
            Size.exact(
                SizeConstraints(minWidth = 10, maxWidth = 10, minHeight = 10, maxHeight = 10)))
        .isEqualTo(Size(width = 10, height = 10))
  }

  @Test
  fun `exact size - with exact width and unbounded height - returns Invalid size`() {
    assertThat(
            Size.exact(
                SizeConstraints(
                    minWidth = 10,
                    maxWidth = 10,
                    minHeight = 0,
                    maxHeight = SizeConstraints.Infinity)))
        .isEqualTo(Size.Invalid)
  }

  @Test
  fun `exact size - with exact width and bounded, non-exact height - returns Invalid size`() {
    assertThat(
            Size.exact(
                SizeConstraints(minWidth = 10, maxWidth = 10, minHeight = 10, maxHeight = 20)))
        .isEqualTo(Size.Invalid)
  }

  @Test
  fun `exact size - with unbounded width and exact height - returns Invalid size`() {
    assertThat(
            Size.exact(
                SizeConstraints(
                    minWidth = 0,
                    maxWidth = SizeConstraints.Infinity,
                    minHeight = 10,
                    maxHeight = 10)))
        .isEqualTo(Size.Invalid)
  }

  @Test
  fun `exact size - with bounded, non-exact width and exact height - returns Invalid size`() {
    assertThat(
            Size.exact(
                SizeConstraints(minWidth = 10, maxWidth = 20, minHeight = 10, maxHeight = 10)))
        .isEqualTo(Size.Invalid)
  }

  @Test
  fun `fill space - with bounded, non-exact width and bounded, non-exact height - returns size with maxWidth and maxHeight`() {
    assertThat(
            Size.fillSpace(
                SizeConstraints(minWidth = 10, maxWidth = 20, minHeight = 30, maxHeight = 40),
                fallbackWidth = 100,
                fallbackHeight = 200))
        .isEqualTo(Size(width = 20, height = 40))
  }

  @Test
  fun `fill space - with unbounded width and bounded, non-exact height - returns size with fallbackWidth and maxHeight`() {
    assertThat(
            Size.fillSpace(
                SizeConstraints(
                    minWidth = 10,
                    maxWidth = SizeConstraints.Infinity,
                    minHeight = 30,
                    maxHeight = 40),
                fallbackWidth = 100,
                fallbackHeight = 200))
        .isEqualTo(Size(width = 100, height = 40))
  }

  @Test
  fun `fill space - with bounded, non-exact width and unbounded height - returns size with maxWidth and fallbackHeight`() {
    assertThat(
            Size.fillSpace(
                SizeConstraints(
                    minWidth = 10,
                    maxWidth = 20,
                    minHeight = 30,
                    maxHeight = SizeConstraints.Infinity),
                fallbackWidth = 100,
                fallbackHeight = 200))
        .isEqualTo(Size(width = 20, height = 200))
  }

  @Test
  fun `fill space - with unbounded width and unbounded height - returns size with fallbackWidth and fallbackHeight`() {
    assertThat(
            Size.fillSpace(
                SizeConstraints(
                    minWidth = 10,
                    maxWidth = SizeConstraints.Infinity,
                    minHeight = 30,
                    maxHeight = SizeConstraints.Infinity),
                fallbackWidth = 100,
                fallbackHeight = 200))
        .isEqualTo(Size(width = 100, height = 200))
  }

  @Test
  fun `fill space - with unbounded width and unbounded height and min values larger than fallback values - returns size with minWidth and minHeight`() {
    assertThat(
            Size.fillSpace(
                SizeConstraints(
                    minWidth = 1000,
                    maxWidth = SizeConstraints.Infinity,
                    minHeight = 2000,
                    maxHeight = SizeConstraints.Infinity),
                fallbackWidth = 100,
                fallbackHeight = 200))
        .isEqualTo(Size(width = 1000, height = 2000))
  }

  @Test
  fun `aspect ratio - with bounded width and bounded height - returns size with maxWidth and correct height`() {
    assertThat(
            Size.withAspectRatio(
                SizeConstraints(minWidth = 0, maxWidth = 30, minHeight = 0, maxHeight = 40),
                aspectRatio = 2f))
        .isEqualTo(Size(width = 30, height = 15))
  }

  @Test
  fun `aspect ratio - with bounded width, bounded height, and intrinsic size - returns size with intrinsicWidth and correct height`() {
    assertThat(
            Size.withAspectRatio(
                SizeConstraints(minWidth = 0, maxWidth = 30, minHeight = 0, maxHeight = 40),
                aspectRatio = 2f,
                intrinsicWidth = 6,
                intrinsicHeight = 8))
        .isEqualTo(Size(width = 6, height = 3))
  }

  @Test
  fun `aspect ratio - with exact width and bounded height smaller than necessary for keeping aspect ratio - returns size with width set to maxWidth and height clamped to maxHeight`() {
    assertThat(
            Size.withAspectRatio(
                SizeConstraints(minWidth = 30, maxWidth = 30, minHeight = 0, maxHeight = 10),
                aspectRatio = 2f))
        .isEqualTo(Size(width = 30, height = 10))
  }

  @Test
  fun `aspect ratio - with exact width and unbounded height - returns size with maxWidth and correct height`() {
    assertThat(
            Size.withAspectRatio(
                SizeConstraints(
                    minWidth = 30,
                    maxWidth = 30,
                    minHeight = 0,
                    maxHeight = SizeConstraints.Infinity),
                aspectRatio = 2f))
        .isEqualTo(Size(width = 30, height = 15))
  }

  @Test
  fun `aspect ratio - with bounded width smaller than necessary for keeping aspect ratio and exact height - returns size with width clamped to maxWidth and height set to maxHeight`() {
    assertThat(
            Size.withAspectRatio(
                SizeConstraints(minWidth = 0, maxWidth = 10, minHeight = 30, maxHeight = 30),
                aspectRatio = 2f))
        .isEqualTo(Size(width = 10, height = 30))
  }

  @Test
  fun `aspect ratio - with unbounded width and exact height - returns size with correct width and maxHeight`() {
    assertThat(
            Size.withAspectRatio(
                SizeConstraints(
                    minWidth = 0,
                    maxWidth = SizeConstraints.Infinity,
                    minHeight = 30,
                    maxHeight = 30),
                aspectRatio = 2f))
        .isEqualTo(Size(width = 60, height = 30))
  }

  @Test
  fun `aspect ratio - with bounded width unbounded height - returns size with maxWidth and correct height`() {
    assertThat(
            Size.withAspectRatio(
                SizeConstraints(
                    minWidth = 0,
                    maxWidth = 30,
                    minHeight = 0,
                    maxHeight = SizeConstraints.Infinity),
                aspectRatio = 2f))
        .isEqualTo(Size(width = 30, height = 15))
  }

  @Test
  fun `aspect ratio - with unbounded width and bounded height - returns size with correct width and height set to maxHeight`() {
    assertThat(
            Size.withAspectRatio(
                SizeConstraints(
                    minWidth = 0,
                    maxWidth = SizeConstraints.Infinity,
                    minHeight = 0,
                    maxHeight = 30),
                aspectRatio = 2f))
        .isEqualTo(Size(width = 60, height = 30))
  }

  @Test
  fun `aspect ratio - with unbounded width and unbounded height - returns size with minWidth and minHeight`() {
    assertThat(
            Size.withAspectRatio(
                SizeConstraints(
                    minWidth = 10,
                    maxWidth = SizeConstraints.Infinity,
                    minHeight = 30,
                    maxHeight = SizeConstraints.Infinity),
                aspectRatio = 2f))
        .isEqualTo(Size(width = 10, height = 30))
  }

  @Test
  fun `aspect ratio - with zero aspect ratio - returns Invalid size`() {
    assertThat(
            Size.withAspectRatio(
                SizeConstraints(minWidth = 0, maxWidth = 10, minHeight = 0, maxHeight = 10),
                aspectRatio = 0f))
        .isEqualTo(Size.Invalid)
  }

  @Test
  fun `aspect ratio - with negative aspect ratio - returns Invalid size`() {
    assertThat(
            Size.withAspectRatio(
                SizeConstraints(minWidth = 0, maxWidth = 10, minHeight = 0, maxHeight = 10),
                aspectRatio = -42f))
        .isEqualTo(Size.Invalid)
  }

  @Test
  fun `aspect ratio - with width bounded to MaxValue - 1 and unbounded height - returns size with values clamped to MaxValue - 1`() {
    assertThat(
            Size.withAspectRatio(
                SizeConstraints(
                    minWidth = 0,
                    maxWidth = SizeConstraints.MaxValue - 1,
                    minHeight = 0,
                    maxHeight = SizeConstraints.Infinity),
                aspectRatio = 0.2f))
        .isEqualTo(
            Size(width = SizeConstraints.MaxValue - 1, height = SizeConstraints.MaxValue - 1))
  }

  @Test
  fun `aspect ratio - with unbounded width and height bounded to MaxValue - 1 - returns size with values clamped to MaxValue - 1`() {
    assertThat(
            Size.withAspectRatio(
                SizeConstraints(
                    minWidth = 0,
                    maxWidth = SizeConstraints.Infinity,
                    minHeight = 0,
                    maxHeight = SizeConstraints.MaxValue - 1),
                aspectRatio = 2.0f))
        .isEqualTo(
            Size(width = SizeConstraints.MaxValue - 1, height = SizeConstraints.MaxValue - 1))
  }

  @Test
  fun `equal dimensions - with bounded width and bounded height - returns size with equal dimensions`() {
    assertThat(
            Size.withEqualDimensions(
                SizeConstraints(minWidth = 0, maxWidth = 30, minHeight = 0, maxHeight = 40)))
        .isEqualTo(Size(width = 30, height = 30))
  }

  @Test
  fun `equal dimensions - with exact width and bounded height smaller than width - returns size with maxWidth and maxHeight`() {
    assertThat(
            Size.withEqualDimensions(
                SizeConstraints(minWidth = 30, maxWidth = 30, minHeight = 0, maxHeight = 10)))
        .isEqualTo(Size(width = 30, height = 10))
  }

  @Test
  fun `equal dimensions - with exact width and unbounded height - returns size with equal dimensions`() {
    assertThat(
            Size.withEqualDimensions(
                SizeConstraints(
                    minWidth = 30,
                    maxWidth = 30,
                    minHeight = 0,
                    maxHeight = SizeConstraints.Infinity)))
        .isEqualTo(Size(width = 30, height = 30))
  }

  @Test
  fun `equal dimensions - with bounded width smaller than height and exact height - returns size with maxWidth and maxHeight`() {
    assertThat(
            Size.withEqualDimensions(
                SizeConstraints(minWidth = 0, maxWidth = 10, minHeight = 30, maxHeight = 30)))
        .isEqualTo(Size(width = 10, height = 30))
  }

  @Test
  fun `equal dimensions - with unbounded width and exact height - returns size with equal dimensions`() {
    assertThat(
            Size.withEqualDimensions(
                SizeConstraints(
                    minWidth = 0,
                    maxWidth = SizeConstraints.Infinity,
                    minHeight = 30,
                    maxHeight = 30)))
        .isEqualTo(Size(width = 30, height = 30))
  }

  @Test
  fun `equal dimensions - with bounded width and unbounded height - returns size with equal dimensions`() {
    assertThat(
            Size.withEqualDimensions(
                SizeConstraints(
                    minWidth = 0,
                    maxWidth = 30,
                    minHeight = 0,
                    maxHeight = SizeConstraints.Infinity)))
        .isEqualTo(Size(width = 30, height = 30))
  }

  @Test
  fun `equal dimensions - with unbounded width and bounded - returns size with equal dimensions`() {
    assertThat(
            Size.withEqualDimensions(
                SizeConstraints(
                    minWidth = 0,
                    maxWidth = SizeConstraints.Infinity,
                    minHeight = 0,
                    maxHeight = 30)))
        .isEqualTo(Size(width = 30, height = 30))
  }

  @Test
  fun `equal dimensions - with unbounded width and unbounded height - returns size with minWidth and minHeight`() {
    assertThat(
            Size.withEqualDimensions(
                SizeConstraints(
                    minWidth = 10,
                    maxWidth = SizeConstraints.Infinity,
                    minHeight = 30,
                    maxHeight = SizeConstraints.Infinity)))
        .isEqualTo(Size(width = 10, height = 30))
  }

  @Test
  fun `preferred width and height - with exact width and exact height - returns size with maxWidth and maxHeight`() {
    assertThat(
            Size.withPreferredSize(
                SizeConstraints(minWidth = 30, maxWidth = 30, minHeight = 40, maxHeight = 40),
                preferredWidth = 100,
                preferredHeight = 200))
        .isEqualTo(Size(width = 30, height = 40))
  }

  @Test
  fun `preferred width and height - with exact width and bounded height - returns size with maxWidth and maxHeight`() {
    assertThat(
            Size.withPreferredSize(
                SizeConstraints(minWidth = 30, maxWidth = 30, minHeight = 20, maxHeight = 40),
                preferredWidth = 100,
                preferredHeight = 200))
        .isEqualTo(Size(width = 30, height = 40))
  }

  @Test
  fun `preferred width and height - with exact width and unbounded height - returns size with maxWidth and preferredHeight`() {
    assertThat(
            Size.withPreferredSize(
                SizeConstraints(
                    minWidth = 30,
                    maxWidth = 30,
                    minHeight = 0,
                    maxHeight = SizeConstraints.Infinity),
                preferredWidth = 100,
                preferredHeight = 200))
        .isEqualTo(Size(width = 30, height = 200))
  }

  @Test
  fun `preferred width and height - with bounded width and exact height - returns size with maxWidth and maxHeight`() {
    assertThat(
            Size.withPreferredSize(
                SizeConstraints(minWidth = 10, maxWidth = 30, minHeight = 40, maxHeight = 40),
                preferredWidth = 100,
                preferredHeight = 200))
        .isEqualTo(Size(width = 30, height = 40))
  }

  @Test
  fun `preferred width and height - with bounded width and bounded height - returns size with maxWidth and maxHeight`() {
    assertThat(
            Size.withPreferredSize(
                SizeConstraints(minWidth = 10, maxWidth = 30, minHeight = 20, maxHeight = 40),
                preferredWidth = 100,
                preferredHeight = 200))
        .isEqualTo(Size(width = 30, height = 40))
  }

  @Test
  fun `preferred width and height - with bounded width and unbounded height - returns size with maxWidth and preferredHeight`() {
    assertThat(
            Size.withPreferredSize(
                SizeConstraints(
                    minWidth = 10,
                    maxWidth = 30,
                    minHeight = 0,
                    maxHeight = SizeConstraints.Infinity),
                preferredWidth = 100,
                preferredHeight = 200))
        .isEqualTo(Size(width = 30, height = 200))
  }

  @Test
  fun `preferred width and height - with unbounded width and exact height - returns size with preferredWidth and maxHeight`() {
    assertThat(
            Size.withPreferredSize(
                SizeConstraints(
                    minWidth = 0,
                    maxWidth = SizeConstraints.Infinity,
                    minHeight = 40,
                    maxHeight = 40),
                preferredWidth = 100,
                preferredHeight = 200))
        .isEqualTo(Size(width = 100, height = 40))
  }

  @Test
  fun `preferred width and height - with unbounded width and bounded height - returns size with preferredWidth and maxHeight`() {
    assertThat(
            Size.withPreferredSize(
                SizeConstraints(
                    minWidth = 0,
                    maxWidth = SizeConstraints.Infinity,
                    minHeight = 20,
                    maxHeight = 40),
                preferredWidth = 100,
                preferredHeight = 200))
        .isEqualTo(Size(width = 100, height = 40))
  }

  @Test
  fun `preferred width and height - with unbounded width and unbounded height - returns size with preferredWidth and preferredHeight`() {
    assertThat(
            Size.withPreferredSize(
                SizeConstraints(
                    minWidth = 0,
                    maxWidth = SizeConstraints.Infinity,
                    minHeight = 0,
                    maxHeight = SizeConstraints.Infinity),
                preferredWidth = 100,
                preferredHeight = 200))
        .isEqualTo(Size(width = 100, height = 200))
  }

  @Test
  fun `getOrElse - with valid size - doesn't call provided lambda`() {
    var called = false
    val size =
        Size(10, 20).getOrElse {
          called = true
          Size(100, 200)
        }
    assertThat(size.width).isEqualTo(10)
    assertThat(size.height).isEqualTo(20)
    assertThat(called).isFalse
  }

  @Test
  fun `getOrElse - with invalid size - calls provided lambda`() {
    var called = false
    val size =
        Size.Invalid.getOrElse {
          called = true
          Size(100, 200)
        }
    assertThat(size.width).isEqualTo(100)
    assertThat(size.height).isEqualTo(200)
    assertThat(called).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.rendercore.extensions

import com.facebook.rendercore.primitives.MountConfigurationScope

/**
 * If true, the nested tree hierarchy (if present) will be notified about parent's bounds changes.
 * It will ensure that visibility events and incremental mount works correctly for the nested tree
 * hierarchy.
 *
 * Default is false.
 */
var <ContentType : Any> MountConfigurationScope<ContentType>.doesMountRenderTreeHosts: Boolean
  get() {
    throw IllegalStateException(
        "Getting the current value of doesMountRenderTreeHosts is not supported.")
  }
  set(value) {
    addExtra(R.id.does_mount_render_tree_hosts, value)
  }