package audio.funkwhale.ffa.utils

import android.app.Activity
import android.content.Context
import android.content.Intent
import com.github.kittinunf.fuel.core.Client
import com.github.kittinunf.fuel.core.FuelManager
import com.github.kittinunf.fuel.core.Request
import com.google.gson.Gson
import com.google.gson.reflect.TypeToken
import com.preference.PowerPreference
import com.preference.Preference
import io.mockk.MockKAnnotations
import io.mockk.coVerify
import io.mockk.every
import io.mockk.impl.annotations.InjectMockKs
import io.mockk.impl.annotations.MockK
import io.mockk.mockk
import io.mockk.mockkStatic
import io.mockk.slot
import io.mockk.verify
import net.openid.appauth.AuthState
import net.openid.appauth.AuthorizationService
import net.openid.appauth.AuthorizationServiceConfiguration
import net.openid.appauth.ClientSecretPost
import org.junit.Before
import org.junit.Test
import strikt.api.expectThat
import strikt.api.expectThrows
import strikt.assertions.isA
import strikt.assertions.isEqualTo
import strikt.assertions.isFalse
import strikt.assertions.isNotNull
import strikt.assertions.isNull
import strikt.assertions.isTrue

class OAuthTest {

  @InjectMockKs
  private lateinit var oAuth: OAuth

  @MockK
  private lateinit var authServiceFactory: AuthorizationServiceFactory

  @MockK
  private lateinit var authService: AuthorizationService

  @MockK
  private lateinit var mockPreference: Preference

  @MockK
  private lateinit var context: Context

  @Before
  fun setup() {
    MockKAnnotations.init(this, relaxUnitFun = true)
  }

  @Test
  fun `tryState() should return null if saved state is missing`() {
    mockkStatic(PowerPreference::class)
    every { PowerPreference.getFileByName(any()) } returns mockPreference
    every { mockPreference.getString(any()) } returns null
    expectThat(oAuth.tryState()).isNull()
  }

  @Test
  fun `tryState() should return null if saved state is empty`() {
    mockkStatic(PowerPreference::class)
    every { PowerPreference.getFileByName(any()) } returns mockPreference
    every { mockPreference.getString(any()) } returns ""
    expectThat(oAuth.tryState()).isNull()
  }

  @Test
  fun `tryState() should return deserialized object if saved state is present`() {
    mockkStatic(PowerPreference::class)
    every { PowerPreference.getFileByName(any()) } returns mockPreference
    every { mockPreference.getString(any()) } returns "{}"
    expectThat(oAuth.tryState()).isNotNull()
  }

  @Test
  fun `state() should return deserialized object if saved state is present`() {

    mockkStatic(PowerPreference::class)
    mockkStatic(AuthState::class)

    val authState = AuthState()
    every { AuthState.jsonDeserialize(any<String>()) } returns authState

    every { PowerPreference.getFileByName(any()) } returns mockPreference
    every { mockPreference.getString(any()) } returns "{}"

    val result = oAuth.state()
    expectThat(result).isEqualTo(authState)
  }

  @Test
  fun `state() should throw error if saved state is missing`() {
    mockkStatic(PowerPreference::class)
    every { PowerPreference.getFileByName(any()) } returns mockPreference
    every { mockPreference.getString(any()) } returns null

    expectThrows<IllegalStateException> { oAuth.state() }
  }

  @Test
  fun `isAuthorized() should return false if no state exists`() {
    mockkStatic(PowerPreference::class)
    every { PowerPreference.getFileByName(any()) } returns mockPreference
    every { mockPreference.getString(any()) } returns null

    expectThat(oAuth.isAuthorized(context)).isFalse()
  }

  @Test
  fun `isAuthorized() should return false if existing state is not authorized and refreshToken is missing`() {
    mockkStatic(PowerPreference::class)
    mockkStatic(AuthState::class)

    val authState = mockk<AuthState>()
    every { AuthState.jsonDeserialize(any<String>()) } returns authState
    every { authState.isAuthorized } returns false
    every { authState.needsTokenRefresh } returns false
    every { authState.refreshToken } returns null

    every { PowerPreference.getFileByName(any()) } returns mockPreference
    every { mockPreference.getString(any()) } returns "{}"

    expectThat(oAuth.isAuthorized(context)).isFalse()
  }

  @Test
  fun `isAuthorized() should return true if existing state is authorized and token needs no refresh`() {
    mockkStatic(PowerPreference::class)
    mockkStatic(AuthState::class)

    val authState = mockk<AuthState>()
    every { AuthState.jsonDeserialize(any<String>()) } returns authState
    every { authState.isAuthorized } returns true
    every { authState.needsTokenRefresh } returns false

    val mockPref = mockk<Preference>()
    every { PowerPreference.getFileByName(any()) } returns mockPref
    every { mockPref.getString(any()) } returns "{}"

    expectThat(oAuth.isAuthorized(context)).isTrue()
  }

  @Test
  fun `isAuthorized() should return true if existing state is authorized and token needs refresh`() {
    mockkStatic(PowerPreference::class)
    mockkStatic(AuthState::class)

    val authState = mockk<AuthState>()
    every { AuthState.jsonDeserialize(any<String>()) } returns authState
    every { authState.isAuthorized } returns true
    every { authState.needsTokenRefresh } returns true
    every { authState.refreshToken } returns "refreshToken"
    every { authState.createTokenRefreshRequest() } returns mockk()
    every { authState.clientSecret } returns "clientSecret"
    every { authServiceFactory.create(any()) } returns authService

    val mockPref = mockk<Preference>()
    every { PowerPreference.getFileByName(any()) } returns mockPref
    every { mockPref.getString(any()) } returns "{}"

    expectThat(oAuth.isAuthorized(context)).isTrue()
  }

  @Test
  fun `tryRefreshAccessToken() should perform token refresh request if accessToken needs refresh and refreshToken exists`() {
    mockkStatic(PowerPreference::class)
    mockkStatic(AuthState::class)

    val authState = mockk<AuthState>()
    every { AuthState.jsonDeserialize(any<String>()) } returns authState
    every { authState.isAuthorized } returns false
    every { authState.needsTokenRefresh } returns true
    every { authState.refreshToken } returns "refreshToken"
    every { authState.createTokenRefreshRequest() } returns mockk()
    every { authState.clientSecret } returns "clientSecret"
    every { authServiceFactory.create(any()) } returns authService
    every { authService.performTokenRequest(any(), any<ClientSecretPost>(), any()) } returns mockk()

    every { PowerPreference.getFileByName(any()) } returns mockPreference
    every { mockPreference.getString(any()) } returns "{}"

    oAuth.tryRefreshAccessToken(context)

    verify { authService.performTokenRequest(any(), any(), any()) }
  }

  @Test
  fun `tryRefreshAccessToken() should not perform token refresh request if accessToken doesn't need refresh`() {
    mockkStatic(PowerPreference::class)
    every { PowerPreference.getFileByName(any()) } returns mockPreference
    every { mockPreference.getString(any()) } returns "{}"

    mockkStatic(AuthState::class)

    val authState = mockk<AuthState>()
    every { AuthState.jsonDeserialize(any<String>()) } returns authState
    every { authState.isAuthorized } returns false
    every { authState.needsTokenRefresh } returns false

    oAuth.tryRefreshAccessToken(context)

    verify(exactly = 0) { authService.performTokenRequest(any(), any(), any()) }
  }

  @Test
  fun `init() should setup correct endpoints`() {
    mockkStatic(PowerPreference::class)
    every { PowerPreference.getFileByName(any()) } returns mockPreference
    every { mockPreference.setString(any(), any()) } returns true

    val result = oAuth.init("hostname")

    expectThat(result.authorizationServiceConfiguration?.authorizationEndpoint.toString())
      .isEqualTo("hostname/authorize")
    expectThat(result.authorizationServiceConfiguration?.tokenEndpoint.toString())
      .isEqualTo("hostname/api/v1/oauth/token/")
    expectThat(result.authorizationServiceConfiguration?.registrationEndpoint.toString())
      .isEqualTo("hostname/api/v1/oauth/apps/")
  }

  @Test
  fun `register() should not initiate http request if configuration is missing`() {
    mockkStatic(PowerPreference::class)
    every { PowerPreference.getFileByName(any()) } returns mockPreference
    every { mockPreference.getString(any()) } returns "{}"

    mockkStatic(AuthState::class)
    val authState = mockk<AuthState>()
    every { AuthState.jsonDeserialize(any<String>()) } returns authState
    every { authState.authorizationServiceConfiguration } returns null

    val mockkClient = mockk<Client>()
    FuelManager.instance.client = mockkClient

    oAuth.register {}

    verify(exactly = 0) { mockkClient.executeRequest(any()) }
  }

  @Test
  fun `register() should initiate correct HTTP request to registration endpoint`() {
    mockkStatic(PowerPreference::class)
    every { PowerPreference.getFileByName(any()) } returns mockPreference
    every { mockPreference.getString(any()) } returns "{}"
    every { mockPreference.setString(any(), any()) } returns true

    mockkStatic(AuthState::class)
    val authState = mockk<AuthState>()
    every { AuthState.jsonDeserialize(any<String>()) } returns authState
    val mockConfig = mockk<AuthorizationServiceConfiguration>()
    every { authState.authorizationServiceConfiguration } returns mockConfig

    val mockkClient = mockk<Client>()

    FuelManager.instance.client = mockkClient

    val state = oAuth.init("https://example.com")
    oAuth.register(state) { }

    val requestSlot = slot<Request>()

    coVerify { mockkClient.awaitRequest(capture(requestSlot)) }

    val capturedRequest = requestSlot.captured
    expectThat(capturedRequest.url.toString())
      .isEqualTo("https://example.com/api/v1/oauth/apps/")

    expectThat(deserializeJson<Map<String, String>>(capturedRequest)).isEqualTo(
      mapOf(
        "name" to "Funkwhale for Android (null)",
        "redirect_uris" to "urn:/audio.funkwhale.funkwhale-android/oauth/callback",
        "scopes" to "read write"
      )
    )
  }

  @Test
  fun `authorizeIntent() should return an Intent`() {

    mockkStatic(PowerPreference::class)
    every { PowerPreference.getFileByName(any()) } returns mockPreference
    every { mockPreference.getString(any()) } returns "{}"
    every { mockPreference.setString(any(), any()) } returns true

    mockkStatic(AuthState::class)
    val authState = mockk<AuthState>()
    every { AuthState.jsonDeserialize(any<String>()) } returns authState
    val mockConfig = mockk<AuthorizationServiceConfiguration>()
    every { authState.authorizationServiceConfiguration } returns mockConfig
    every { authState.lastRegistrationResponse } returns mockk()

    every { authServiceFactory.create(any()) } returns authService
    val mockkIntent = mockk<Intent>()
    every { authService.getAuthorizationRequestIntent(any()) } returns mockkIntent

    val activity = mockk<Activity>(relaxed = true)

    expectThat(oAuth.authorizeIntent(activity)).isNotNull().isA<Intent>()
  }

  private fun <T> deserializeJson(
    capturedRequest: Request
  ): T {
    return Gson().fromJson(
      capturedRequest.body.asString("application/json"),
      object : TypeToken<T>() {}.type
    )
  }
}
