/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */

package com.facebook.react.bridge

import com.facebook.react.turbomodule.core.interfaces.TurboModule
import com.facebook.testutils.shadows.ShadowNativeLoader
import com.facebook.testutils.shadows.ShadowSoLoader
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.kotlin.mock
import org.mockito.kotlin.whenever
import org.robolectric.RobolectricTestRunner
import org.robolectric.annotation.Config

/** Tests for [BaseJavaModule] and [JavaModuleWrapper] */
@Suppress("DEPRECATION")
@Config(shadows = [ShadowSoLoader::class, ShadowNativeLoader::class])
@RunWith(RobolectricTestRunner::class)
class BaseJavaModuleTest {
  private lateinit var methods: List<JavaModuleWrapper.MethodDescriptor>
  private lateinit var moduleWrapper: JavaModuleWrapper
  private lateinit var generatedMethods: List<JavaModuleWrapper.MethodDescriptor>
  private lateinit var generatedModuleWrapper: JavaModuleWrapper
  private lateinit var arguments: ReadableNativeArray

  @Before
  fun setup() {
    val jsInstance = mock<JSInstance>()
    val moduleHolder = ModuleHolder(MethodsModule())
    moduleWrapper = JavaModuleWrapper(jsInstance, moduleHolder)
    methods = moduleWrapper.methodDescriptors
    val generatedModuleHolder = ModuleHolder(GeneratedMethodsModule())
    generatedModuleWrapper = JavaModuleWrapper(jsInstance, generatedModuleHolder)
    generatedMethods = generatedModuleWrapper.methodDescriptors
    arguments = mock<ReadableNativeArray>()
  }

  private fun findMethod(mname: String, methods: List<JavaModuleWrapper.MethodDescriptor>): Int =
      methods.indexOfFirst({ it.name === mname })

  @Test(expected = JSApplicationCausedNativeException::class)
  fun testCallMethodWithoutEnoughArgs() {
    val methodId = findMethod("regularMethod", methods)
    whenever(arguments.size()).thenReturn(1)
    moduleWrapper.invoke(methodId, arguments)
  }

  @Test
  fun testCallMethodWithEnoughArgs() {
    val methodId = findMethod("regularMethod", methods)
    whenever(arguments.size()).thenReturn(2)
    moduleWrapper.invoke(methodId, arguments)
  }

  @Test
  fun testCallAsyncMethodWithEnoughArgs() {
    // Promise block evaluates to 2 args needing to be passed from JS
    val methodId = findMethod("asyncMethod", methods)
    whenever(arguments.size()).thenReturn(3)
    moduleWrapper.invoke(methodId, arguments)
  }

  @Test
  fun testCallSyncMethod() {
    val methodId = findMethod("syncMethod", methods)
    whenever(arguments.size()).thenReturn(2)
    moduleWrapper.invoke(methodId, arguments)
  }

  @Test
  fun testCallGeneratedMethod() {
    val methodId = findMethod("generatedMethod", generatedMethods)
    whenever(arguments.size()).thenReturn(2)
    generatedModuleWrapper.invoke(methodId, arguments)
  }

  @Suppress("UNUSED_PARAMETER")
  private class MethodsModule : BaseJavaModule() {
    override fun getName(): String = "Methods"

    @ReactMethod fun regularMethod(a: String?, b: Int?) = Unit

    @ReactMethod fun asyncMethod(a: Int, p: Promise) = Unit

    @ReactMethod(isBlockingSynchronousMethod = true) fun syncMethod(a: Int, b: Int): Int = a + b
  }

  private abstract inner class NativeTestGeneratedModuleSpec : BaseJavaModule(), TurboModule {
    @ReactMethod abstract fun generatedMethod(a: String?, b: Int?)
  }

  private inner class GeneratedMethodsModule : NativeTestGeneratedModuleSpec() {
    override fun getName(): String = "GeneratedMethods"

    override fun generatedMethod(a: String?, b: Int?) = Unit
  }
}
