package org.thoughtcrime.securesms.jobs

import androidx.test.ext.junit.runners.AndroidJUnit4
import okhttp3.mockwebserver.MockResponse
import org.junit.After
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.signal.libsignal.protocol.ecc.Curve
import org.thoughtcrime.securesms.crypto.storage.PreKeyMetadataStore
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
import org.thoughtcrime.securesms.dependencies.InstrumentationApplicationDependencyProvider
import org.thoughtcrime.securesms.jobmanager.Job
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.testing.Get
import org.thoughtcrime.securesms.testing.Put
import org.thoughtcrime.securesms.testing.SignalActivityRule
import org.thoughtcrime.securesms.testing.assertIs
import org.thoughtcrime.securesms.testing.assertIsNot
import org.thoughtcrime.securesms.testing.parsedRequestBody
import org.thoughtcrime.securesms.testing.success
import org.whispersystems.signalservice.api.push.SignedPreKeyEntity
import org.whispersystems.signalservice.internal.push.PreKeyState
import org.whispersystems.signalservice.internal.push.PreKeyStatus

@RunWith(AndroidJUnit4::class)
class PreKeysSyncJobTest {

  @get:Rule
  val harness = SignalActivityRule()

  private val aciPreKeyMeta: PreKeyMetadataStore
    get() = SignalStore.account().aciPreKeys

  private val pniPreKeyMeta: PreKeyMetadataStore
    get() = SignalStore.account().pniPreKeys

  private lateinit var job: PreKeysSyncJob

  @Before
  fun setUp() {
    job = PreKeysSyncJob()
  }

  @After
  fun tearDown() {
    InstrumentationApplicationDependencyProvider.clearHandlers()
  }

  /**
   * Create signed prekeys for both identities when both do not have registered prekeys according
   * to our local state.
   */
  @Test
  fun runWithoutRegisteredKeysForBothIdentities() {
    // GIVEN
    aciPreKeyMeta.isSignedPreKeyRegistered = false
    pniPreKeyMeta.isSignedPreKeyRegistered = false

    lateinit var aciSignedPreKey: SignedPreKeyEntity
    lateinit var pniSignedPreKey: SignedPreKeyEntity

    InstrumentationApplicationDependencyProvider.addMockWebRequestHandlers(
      Put("/v2/keys/signed?identity=aci") { r ->
        aciSignedPreKey = r.parsedRequestBody()
        MockResponse().success()
      },
      Put("/v2/keys/signed?identity=pni") { r ->
        pniSignedPreKey = r.parsedRequestBody()
        MockResponse().success()
      },
    )

    // WHEN
    val result: Job.Result = job.run()

    // THEN
    result.isSuccess assertIs true

    aciPreKeyMeta.isSignedPreKeyRegistered assertIs true
    pniPreKeyMeta.isSignedPreKeyRegistered assertIs true

    val aciVerifySignatureResult = Curve.verifySignature(
      ApplicationDependencies.getProtocolStore().aci().identityKeyPair.publicKey.publicKey,
      aciSignedPreKey.publicKey.serialize(),
      aciSignedPreKey.signature
    )
    aciVerifySignatureResult assertIs true

    val pniVerifySignatureResult = Curve.verifySignature(
      ApplicationDependencies.getProtocolStore().pni().identityKeyPair.publicKey.publicKey,
      pniSignedPreKey.publicKey.serialize(),
      pniSignedPreKey.signature
    )
    pniVerifySignatureResult assertIs true
  }

  /**
   * With 100 prekeys registered for each identity, do nothing.
   */
  @Test
  fun runWithRegisteredKeysForBothIdentities() {
    // GIVEN
    val currentAciKeyId = aciPreKeyMeta.activeSignedPreKeyId
    val currentPniKeyId = pniPreKeyMeta.activeSignedPreKeyId

    InstrumentationApplicationDependencyProvider.addMockWebRequestHandlers(
      Get("/v2/keys?identity=aci") { MockResponse().success(PreKeyStatus(100)) },
      Get("/v2/keys?identity=pni") { MockResponse().success(PreKeyStatus(100)) },
    )

    // WHEN
    val result: Job.Result = job.run()

    // THEN
    result.isSuccess assertIs true

    aciPreKeyMeta.activeSignedPreKeyId assertIs currentAciKeyId
    pniPreKeyMeta.activeSignedPreKeyId assertIs currentPniKeyId
  }

  /**
   * With 100 prekeys registered for ACI, but no PNI prekeys registered according to local state,
   * do nothing for ACI but create PNI prekeys and update local state.
   */
  @Test
  fun runWithRegisteredKeysForAciIdentityOnly() {
    // GIVEN
    pniPreKeyMeta.isSignedPreKeyRegistered = false

    val currentAciKeyId = aciPreKeyMeta.activeSignedPreKeyId
    val currentPniKeyId = pniPreKeyMeta.activeSignedPreKeyId

    InstrumentationApplicationDependencyProvider.addMockWebRequestHandlers(
      Get("/v2/keys?identity=aci") { MockResponse().success(PreKeyStatus(100)) },
      Put("/v2/keys/signed?identity=pni") { MockResponse().success() },
    )

    // WHEN
    val result: Job.Result = job.run()

    // THEN
    result.isSuccess assertIs true

    pniPreKeyMeta.isSignedPreKeyRegistered assertIs true
    aciPreKeyMeta.activeSignedPreKeyId assertIs currentAciKeyId
    pniPreKeyMeta.activeSignedPreKeyId assertIsNot currentPniKeyId
  }

  /**
   * With <10 prekeys registered for each identity, upload new.
   */
  @Test
  fun runWithLowNumberOfRegisteredKeysForBothIdentities() {
    // GIVEN
    val currentAciKeyId = aciPreKeyMeta.activeSignedPreKeyId
    val currentPniKeyId = pniPreKeyMeta.activeSignedPreKeyId

    val currentNextAciPreKeyId = aciPreKeyMeta.nextOneTimePreKeyId
    val currentNextPniPreKeyId = pniPreKeyMeta.nextOneTimePreKeyId

    lateinit var aciPreKeyStateRequest: PreKeyState
    lateinit var pniPreKeyStateRequest: PreKeyState

    InstrumentationApplicationDependencyProvider.addMockWebRequestHandlers(
      Get("/v2/keys?identity=aci") { MockResponse().success(PreKeyStatus(5)) },
      Get("/v2/keys?identity=pni") { MockResponse().success(PreKeyStatus(5)) },
      Put("/v2/keys/?identity=aci") { r ->
        aciPreKeyStateRequest = r.parsedRequestBody()
        MockResponse().success()
      },
      Put("/v2/keys/?identity=pni") { r ->
        pniPreKeyStateRequest = r.parsedRequestBody()
        MockResponse().success()
      },
    )

    // WHEN
    val result: Job.Result = job.run()

    // THEN
    result.isSuccess assertIs true
    aciPreKeyMeta.activeSignedPreKeyId assertIsNot currentAciKeyId
    pniPreKeyMeta.activeSignedPreKeyId assertIsNot currentPniKeyId

    aciPreKeyMeta.nextOneTimePreKeyId assertIsNot currentNextAciPreKeyId
    pniPreKeyMeta.nextOneTimePreKeyId assertIsNot currentNextPniPreKeyId

    ApplicationDependencies.getProtocolStore().aci().identityKeyPair.publicKey.let { aciIdentityKey ->
      aciPreKeyStateRequest.identityKey assertIs aciIdentityKey

      val verifySignatureResult = Curve.verifySignature(
        aciIdentityKey.publicKey,
        aciPreKeyStateRequest.signedPreKey.publicKey.serialize(),
        aciPreKeyStateRequest.signedPreKey.signature
      )
      verifySignatureResult assertIs true
    }

    ApplicationDependencies.getProtocolStore().pni().identityKeyPair.publicKey.let { pniIdentityKey ->
      pniPreKeyStateRequest.identityKey assertIs pniIdentityKey

      val verifySignatureResult = Curve.verifySignature(
        pniIdentityKey.publicKey,
        pniPreKeyStateRequest.signedPreKey.publicKey.serialize(),
        pniPreKeyStateRequest.signedPreKey.signature
      )
      verifySignatureResult assertIs true
    }
  }
}
