package org.thoughtcrime.securesms.database

import androidx.core.content.contentValuesOf
import androidx.test.ext.junit.runners.AndroidJUnit4
import org.junit.Assert.assertEquals
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.thoughtcrime.securesms.recipients.RecipientId
import org.thoughtcrime.securesms.testing.SignalDatabaseRule
import org.thoughtcrime.securesms.util.Util
import org.whispersystems.signalservice.api.push.ACI
import org.whispersystems.signalservice.api.push.PNI
import org.whispersystems.signalservice.api.push.ServiceId
import java.lang.AssertionError
import java.lang.IllegalArgumentException
import java.lang.IllegalStateException
import java.util.UUID

@RunWith(AndroidJUnit4::class)
class RecipientDatabaseTest_processPnpTupleToChangeSet {

  @Rule
  @JvmField
  val databaseRule = SignalDatabaseRule(deleteAllThreadsOnEachRun = false)

  private lateinit var db: RecipientDatabase

  @Before
  fun setup() {
    db = SignalDatabase.recipients
  }

  @Test
  fun noMatch_e164Only() {
    val changeSet = db.processPnpTupleToChangeSet(E164_A, null, null, pniVerified = false)

    assertEquals(
      PnpChangeSet(
        id = PnpIdResolver.PnpInsert(E164_A, null, null)
      ),
      changeSet
    )
  }

  @Test
  fun noMatch_e164AndPni() {
    val changeSet = db.processPnpTupleToChangeSet(E164_A, PNI_A, null, pniVerified = false)

    assertEquals(
      PnpChangeSet(
        id = PnpIdResolver.PnpInsert(E164_A, PNI_A, null)
      ),
      changeSet
    )
  }

  @Test
  fun noMatch_aciOnly() {
    val changeSet = db.processPnpTupleToChangeSet(null, null, ACI_A, pniVerified = false)

    assertEquals(
      PnpChangeSet(
        id = PnpIdResolver.PnpInsert(null, null, ACI_A)
      ),
      changeSet
    )
  }

  @Test(expected = IllegalArgumentException::class)
  fun noMatch_pniOnly() {
    db.processPnpTupleToChangeSet(null, PNI_A, null, pniVerified = false)
  }

  @Test(expected = IllegalArgumentException::class)
  fun noMatch_noData() {
    db.processPnpTupleToChangeSet(null, null, null, pniVerified = false)
  }

  @Test
  fun noMatch_allFields() {
    val changeSet = db.processPnpTupleToChangeSet(E164_A, PNI_A, ACI_A, pniVerified = false)

    assertEquals(
      PnpChangeSet(
        id = PnpIdResolver.PnpInsert(E164_A, PNI_A, ACI_A)
      ),
      changeSet
    )
  }

  @Test
  fun fullMatch() {
    val result = applyAndAssert(
      Input(E164_A, PNI_A, ACI_A),
      Update(E164_A, PNI_A, ACI_A),
      Output(E164_A, PNI_A, ACI_A)
    )

    assertEquals(
      PnpChangeSet(
        id = PnpIdResolver.PnpNoopId(result.id)
      ),
      result.changeSet
    )
  }

  @Test
  fun onlyE164Matches() {
    val result = applyAndAssert(
      Input(E164_A, null, null),
      Update(E164_A, PNI_A, ACI_A),
      Output(E164_A, PNI_A, ACI_A)
    )

    assertEquals(
      PnpChangeSet(
        id = PnpIdResolver.PnpNoopId(result.id),
        operations = listOf(
          PnpOperation.SetPni(result.id, PNI_A),
          PnpOperation.SetAci(result.id, ACI_A)
        )
      ),
      result.changeSet
    )
  }

  @Test
  fun onlyE164Matches_pniChanges_noAciProvided_existingPniSession() {
    val result = applyAndAssert(
      Input(E164_A, PNI_B, null, pniSession = true),
      Update(E164_A, PNI_A, null),
      Output(E164_A, PNI_A, null)
    )

    assertEquals(
      PnpChangeSet(
        id = PnpIdResolver.PnpNoopId(result.id),
        operations = listOf(
          PnpOperation.SetPni(result.id, PNI_A),
          PnpOperation.SessionSwitchoverInsert(result.id)
        )
      ),
      result.changeSet
    )
  }

  @Test
  fun onlyE164Matches_pniChanges_noAciProvided_noPniSession() {
    val result = applyAndAssert(
      Input(E164_A, PNI_B, null),
      Update(E164_A, PNI_A, null),
      Output(E164_A, PNI_A, null)
    )

    assertEquals(
      PnpChangeSet(
        id = PnpIdResolver.PnpNoopId(result.id),
        operations = listOf(
          PnpOperation.SetPni(result.id, PNI_A)
        )
      ),
      result.changeSet
    )
  }

  @Test
  fun e164AndPniMatches_noExistingSession() {
    val result = applyAndAssert(
      Input(E164_A, PNI_A, null),
      Update(E164_A, PNI_A, ACI_A),
      Output(E164_A, PNI_A, ACI_A)
    )

    assertEquals(
      PnpChangeSet(
        id = PnpIdResolver.PnpNoopId(result.id),
        operations = listOf(
          PnpOperation.SetAci(result.id, ACI_A)
        )
      ),
      result.changeSet
    )
  }

  @Test
  fun e164AndPniMatches_existingPniSession() {
    val result = applyAndAssert(
      Input(E164_A, PNI_A, null, pniSession = true),
      Update(E164_A, PNI_A, ACI_A),
      Output(E164_A, PNI_A, ACI_A)
    )

    assertEquals(
      PnpChangeSet(
        id = PnpIdResolver.PnpNoopId(result.id),
        operations = listOf(
          PnpOperation.SetAci(result.id, ACI_A),
          PnpOperation.SessionSwitchoverInsert(result.id)
        )
      ),
      result.changeSet
    )
  }

  @Test
  fun e164AndAciMatches() {
    val result = applyAndAssert(
      Input(E164_A, null, ACI_A),
      Update(E164_A, PNI_A, ACI_A),
      Output(E164_A, PNI_A, ACI_A)
    )

    assertEquals(
      PnpChangeSet(
        id = PnpIdResolver.PnpNoopId(result.id),
        operations = listOf(
          PnpOperation.SetPni(result.id, PNI_A)
        )
      ),
      result.changeSet
    )
  }

  @Test
  fun onlyPniMatches_noExistingSession() {
    val result = applyAndAssert(
      Input(null, PNI_A, null),
      Update(E164_A, PNI_A, ACI_A),
      Output(E164_A, PNI_A, ACI_A)
    )

    assertEquals(
      PnpChangeSet(
        id = PnpIdResolver.PnpNoopId(result.id),
        operations = listOf(
          PnpOperation.SetE164(result.id, E164_A),
          PnpOperation.SetAci(result.id, ACI_A)
        )
      ),
      result.changeSet
    )
  }

  @Test
  fun onlyPniMatches_existingPniSession() {
    val result = applyAndAssert(
      Input(null, PNI_A, null, pniSession = true),
      Update(E164_A, PNI_A, ACI_A),
      Output(E164_A, PNI_A, ACI_A)
    )

    assertEquals(
      PnpChangeSet(
        id = PnpIdResolver.PnpNoopId(result.id),
        operations = listOf(
          PnpOperation.SetE164(result.id, E164_A),
          PnpOperation.SetAci(result.id, ACI_A),
          PnpOperation.SessionSwitchoverInsert(result.id)
        )
      ),
      result.changeSet
    )
  }

  @Test
  fun onlyPniMatches_existingPniSession_changeNumber() {
    val result = applyAndAssert(
      Input(E164_B, PNI_A, null, pniSession = true),
      Update(E164_A, PNI_A, ACI_A),
      Output(E164_A, PNI_A, ACI_A)
    )

    assertEquals(
      PnpChangeSet(
        id = PnpIdResolver.PnpNoopId(result.id),
        operations = listOf(
          PnpOperation.SetE164(result.id, E164_A),
          PnpOperation.SetAci(result.id, ACI_A),
          PnpOperation.ChangeNumberInsert(
            recipientId = result.id,
            oldE164 = E164_B,
            newE164 = E164_A
          ),
          PnpOperation.SessionSwitchoverInsert(result.id)
        )
      ),
      result.changeSet
    )
  }

  @Test
  fun pniAndAciMatches() {
    val result = applyAndAssert(
      Input(null, PNI_A, ACI_A),
      Update(E164_A, PNI_A, ACI_A),
      Output(E164_A, PNI_A, ACI_A)
    )

    assertEquals(
      PnpChangeSet(
        id = PnpIdResolver.PnpNoopId(result.id),
        operations = listOf(
          PnpOperation.SetE164(result.id, E164_A),
        )
      ),
      result.changeSet
    )
  }

  @Test
  fun pniAndAciMatches_changeNumber() {
    val result = applyAndAssert(
      Input(E164_B, PNI_A, ACI_A),
      Update(E164_A, PNI_A, ACI_A),
      Output(E164_A, PNI_A, ACI_A)
    )

    assertEquals(
      PnpChangeSet(
        id = PnpIdResolver.PnpNoopId(result.id),
        operations = listOf(
          PnpOperation.SetE164(result.id, E164_A),
          PnpOperation.ChangeNumberInsert(
            recipientId = result.id,
            oldE164 = E164_B,
            newE164 = E164_A
          )
        )
      ),
      result.changeSet
    )
  }

  @Test
  fun onlyAciMatches() {
    val result = applyAndAssert(
      Input(null, null, ACI_A),
      Update(E164_A, PNI_A, ACI_A),
      Output(E164_A, PNI_A, ACI_A)
    )

    assertEquals(
      PnpChangeSet(
        id = PnpIdResolver.PnpNoopId(result.id),
        operations = listOf(
          PnpOperation.SetE164(result.id, E164_A),
          PnpOperation.SetPni(result.id, PNI_A)
        )
      ),
      result.changeSet
    )
  }

  @Test
  fun onlyAciMatches_changeNumber() {
    val result = applyAndAssert(
      Input(E164_B, null, ACI_A),
      Update(E164_A, PNI_A, ACI_A),
      Output(E164_A, PNI_A, ACI_A)
    )

    assertEquals(
      PnpChangeSet(
        id = PnpIdResolver.PnpNoopId(result.id),
        operations = listOf(
          PnpOperation.SetE164(result.id, E164_A),
          PnpOperation.SetPni(result.id, PNI_A),
          PnpOperation.ChangeNumberInsert(
            recipientId = result.id,
            oldE164 = E164_B,
            newE164 = E164_A
          )
        )
      ),
      result.changeSet
    )
  }

  @Test
  fun merge_e164Only_pniOnly_aciOnly() {
    val result = applyAndAssert(
      listOf(
        Input(E164_A, null, null),
        Input(null, PNI_A, null),
        Input(null, null, ACI_A)
      ),
      Update(E164_A, PNI_A, ACI_A),
      Output(E164_A, PNI_A, ACI_A)
    )

    assertEquals(
      PnpChangeSet(
        id = PnpIdResolver.PnpNoopId(result.thirdId),
        operations = listOf(
          PnpOperation.Merge(
            primaryId = result.firstId,
            secondaryId = result.secondId
          ),
          PnpOperation.Merge(
            primaryId = result.thirdId,
            secondaryId = result.firstId
          )
        )
      ),
      result.changeSet
    )
  }

  @Test
  fun merge_e164Only_pniOnly_noAciProvided() {
    val result = applyAndAssert(
      listOf(
        Input(E164_A, null, null),
        Input(null, PNI_A, null),
      ),
      Update(E164_A, PNI_A, null),
      Output(E164_A, PNI_A, null)
    )

    assertEquals(
      PnpChangeSet(
        id = PnpIdResolver.PnpNoopId(result.firstId),
        operations = listOf(
          PnpOperation.Merge(
            primaryId = result.firstId,
            secondaryId = result.secondId
          )
        )
      ),
      result.changeSet
    )
  }

  @Test
  fun merge_e164Only_pniOnly_aciProvidedButNoAciRecord() {
    val result = applyAndAssert(
      listOf(
        Input(E164_A, null, null),
        Input(null, PNI_A, null),
      ),
      Update(E164_A, PNI_A, ACI_A),
      Output(E164_A, PNI_A, ACI_A)
    )

    assertEquals(
      PnpChangeSet(
        id = PnpIdResolver.PnpNoopId(result.firstId),
        operations = listOf(
          PnpOperation.Merge(
            primaryId = result.firstId,
            secondaryId = result.secondId
          ),
          PnpOperation.SetAci(
            recipientId = result.firstId,
            aci = ACI_A
          )
        )
      ),
      result.changeSet
    )
  }

  @Test
  fun merge_e164Only_pniAndE164_noAciProvided() {
    val result = applyAndAssert(
      listOf(
        Input(E164_A, null, null),
        Input(E164_B, PNI_A, null),
      ),
      Update(E164_A, PNI_A, null),
      Output(E164_A, PNI_A, null)
    )

    assertEquals(
      PnpChangeSet(
        id = PnpIdResolver.PnpNoopId(result.firstId),
        operations = listOf(
          PnpOperation.RemovePni(result.secondId),
          PnpOperation.SetPni(
            recipientId = result.firstId,
            pni = PNI_A
          ),
        )
      ),
      result.changeSet
    )
  }

  @Test
  fun merge_e164AndPni_pniOnly_noAciProvided() {
    val result = applyAndAssert(
      listOf(
        Input(E164_A, PNI_B, null),
        Input(null, PNI_A, null),
      ),
      Update(E164_A, PNI_A, null),
      Output(E164_A, PNI_A, null)
    )

    assertEquals(
      PnpChangeSet(
        id = PnpIdResolver.PnpNoopId(result.firstId),
        operations = listOf(
          PnpOperation.RemovePni(result.firstId),
          PnpOperation.Merge(
            primaryId = result.firstId,
            secondaryId = result.secondId
          ),
        )
      ),
      result.changeSet
    )
  }

  @Test
  fun merge_e164AndPni_e164AndPni_noAciProvided_noSessions() {
    val result = applyAndAssert(
      listOf(
        Input(E164_A, PNI_B, null),
        Input(E164_B, PNI_A, null),
      ),
      Update(E164_A, PNI_A, null),
      Output(E164_A, PNI_A, null)
    )

    assertEquals(
      PnpChangeSet(
        id = PnpIdResolver.PnpNoopId(result.firstId),
        operations = listOf(
          PnpOperation.RemovePni(result.secondId),
          PnpOperation.SetPni(result.firstId, PNI_A)
        )
      ),
      result.changeSet
    )
  }

  @Test
  fun merge_e164AndPni_e164AndPni_noAciProvided_sessionsExist() {
    val result = applyAndAssert(
      listOf(
        Input(E164_A, PNI_B, null, pniSession = true),
        Input(E164_B, PNI_A, null, pniSession = true),
      ),
      Update(E164_A, PNI_A, null),
      Output(E164_A, PNI_A, null)
    )

    assertEquals(
      PnpChangeSet(
        id = PnpIdResolver.PnpNoopId(result.firstId),
        operations = listOf(
          PnpOperation.RemovePni(result.secondId),
          PnpOperation.SetPni(result.firstId, PNI_A),
          PnpOperation.SessionSwitchoverInsert(result.secondId),
          PnpOperation.SessionSwitchoverInsert(result.firstId)
        )
      ),
      result.changeSet
    )
  }

  @Test
  fun merge_e164AndPni_aciOnly() {
    val result = applyAndAssert(
      listOf(
        Input(E164_A, PNI_A, null),
        Input(null, null, ACI_A),
      ),
      Update(E164_A, PNI_A, ACI_A),
      Output(E164_A, PNI_A, ACI_A)
    )

    assertEquals(
      PnpChangeSet(
        id = PnpIdResolver.PnpNoopId(result.secondId),
        operations = listOf(
          PnpOperation.Merge(
            primaryId = result.secondId,
            secondaryId = result.firstId
          ),
        )
      ),
      result.changeSet
    )
  }

  @Test
  fun merge_e164AndPni_aciOnly_e164RecordHasSeparateE164() {
    val result = applyAndAssert(
      listOf(
        Input(E164_B, PNI_A, null),
        Input(null, null, ACI_A),
      ),
      Update(E164_A, PNI_A, ACI_A),
      Output(E164_A, PNI_A, ACI_A)
    )

    assertEquals(
      PnpChangeSet(
        id = PnpIdResolver.PnpNoopId(result.secondId),
        operations = listOf(
          PnpOperation.RemovePni(result.firstId),
          PnpOperation.Update(
            recipientId = result.secondId,
            e164 = E164_A,
            pni = PNI_A,
            aci = ACI_A
          )
        )
      ),
      result.changeSet
    )
  }

  @Test
  fun merge_e164AndPni_e164AndPniAndAci_changeNumber() {
    val result = applyAndAssert(
      listOf(
        Input(E164_A, PNI_A, null),
        Input(E164_B, PNI_B, ACI_A),
      ),
      Update(E164_A, PNI_A, ACI_A),
      Output(E164_A, PNI_A, ACI_A)
    )

    assertEquals(
      PnpChangeSet(
        id = PnpIdResolver.PnpNoopId(result.secondId),
        operations = listOf(
          PnpOperation.RemovePni(result.secondId),
          PnpOperation.RemoveE164(result.secondId),
          PnpOperation.Merge(
            primaryId = result.secondId,
            secondaryId = result.firstId
          ),
          PnpOperation.ChangeNumberInsert(
            recipientId = result.secondId,
            oldE164 = E164_B,
            newE164 = E164_A
          )
        )
      ),
      result.changeSet
    )
  }

  @Test
  fun merge_e164AndPni_e164Aci_changeNumber() {
    val result = applyAndAssert(
      listOf(
        Input(E164_A, PNI_A, null),
        Input(E164_B, null, ACI_A),
      ),
      Update(E164_A, PNI_A, ACI_A),
      Output(E164_A, PNI_A, ACI_A)
    )

    assertEquals(
      PnpChangeSet(
        id = PnpIdResolver.PnpNoopId(result.secondId),
        operations = listOf(
          PnpOperation.RemoveE164(result.secondId),
          PnpOperation.Merge(
            primaryId = result.secondId,
            secondaryId = result.firstId
          ),
          PnpOperation.ChangeNumberInsert(
            recipientId = result.secondId,
            oldE164 = E164_B,
            newE164 = E164_A
          )
        )
      ),
      result.changeSet
    )
  }

  private fun insert(e164: String?, pni: PNI?, aci: ACI?): RecipientId {
    val id: Long = SignalDatabase.rawDatabase.insert(
      RecipientDatabase.TABLE_NAME,
      null,
      contentValuesOf(
        RecipientDatabase.PHONE to e164,
        RecipientDatabase.SERVICE_ID to (aci ?: pni)?.toString(),
        RecipientDatabase.PNI_COLUMN to pni?.toString(),
        RecipientDatabase.REGISTERED to RecipientDatabase.RegisteredState.REGISTERED.id
      )
    )

    return RecipientId.from(id)
  }

  private fun insertMockSessionFor(account: ServiceId, address: ServiceId) {
    SignalDatabase.rawDatabase.insert(
      SessionDatabase.TABLE_NAME, null,
      contentValuesOf(
        SessionDatabase.ACCOUNT_ID to account.toString(),
        SessionDatabase.ADDRESS to address.toString(),
        SessionDatabase.DEVICE to 1,
        SessionDatabase.RECORD to Util.getSecretBytes(32)
      )
    )
  }

  data class Input(val e164: String?, val pni: PNI?, val aci: ACI?, val pniSession: Boolean = false, val aciSession: Boolean = false)
  data class Update(val e164: String?, val pni: PNI?, val aci: ACI?, val pniVerified: Boolean = false)
  data class Output(val e164: String?, val pni: PNI?, val aci: ACI?)
  data class PnpMatchResult(val ids: List<RecipientId>, val changeSet: PnpChangeSet) {
    val id
      get() = if (ids.size == 1) {
        ids[0]
      } else {
        throw IllegalStateException("There are multiple IDs, but you assumed 1!")
      }

    val firstId
      get() = ids[0]

    val secondId
      get() = ids[1]

    val thirdId
      get() = ids[2]
  }

  private fun applyAndAssert(input: Input, update: Update, output: Output): PnpMatchResult {
    return applyAndAssert(listOf(input), update, output)
  }

  /**
   * Helper method that will call insert your recipients, call [RecipientDatabase.processPnpTupleToChangeSet] with your params,
   * and then verify your output matches what you expect.
   *
   * It results the inserted ID's and changeset for additional verification.
   *
   * But basically this is here to make the tests more readable. It gives you a clear list of:
   * - input
   * - update
   * - output
   *
   * that you can spot check easily.
   *
   * Important: The output will only include records that contain fields from the input. That means
   * for:
   *
   * Input:  E164_B, PNI_A, null
   * Update: E164_A, PNI_A, null
   *
   * You will get:
   * Output: E164_A, PNI_A, null
   *
   * Even though there was an update that will also result in the row (E164_B, null, null)
   */
  private fun applyAndAssert(input: List<Input>, update: Update, output: Output): PnpMatchResult {
    val ids = input.map { insert(it.e164, it.pni, it.aci) }

    input
      .filter { it.pniSession }
      .forEach { insertMockSessionFor(databaseRule.localAci, it.pni!!) }

    input
      .filter { it.aciSession }
      .forEach { insertMockSessionFor(databaseRule.localAci, it.aci!!) }

    val byE164 = update.e164?.let { db.getByE164(it).orElse(null) }
    val byPniSid = update.pni?.let { db.getByServiceId(it).orElse(null) }
    val byAciSid = update.aci?.let { db.getByServiceId(it).orElse(null) }

    val data = PnpDataSet(
      e164 = update.e164,
      pni = update.pni,
      aci = update.aci,
      byE164 = byE164,
      byPniSid = byPniSid,
      byPniOnly = update.pni?.let { db.getByPni(it).orElse(null) },
      byAciSid = byAciSid,
      e164Record = byE164?.let { db.getRecord(it) },
      pniSidRecord = byPniSid?.let { db.getRecord(it) },
      aciSidRecord = byAciSid?.let { db.getRecord(it) }
    )
    val changeSet = db.processPnpTupleToChangeSet(update.e164, update.pni, update.aci, pniVerified = update.pniVerified)

    val finalData = data.perform(changeSet.operations)

    val finalRecords = setOfNotNull(finalData.e164Record, finalData.pniSidRecord, finalData.aciSidRecord)
    assertEquals("There's still multiple records in the resulting record set! $finalRecords", 1, finalRecords.size)

    finalRecords.firstOrNull { record -> record.e164 == output.e164 && record.pni == output.pni && record.serviceId == (output.aci ?: output.pni) }
      ?: throw AssertionError("Expected output was not found in the result set! Expected: $output")

    return PnpMatchResult(
      ids = ids,
      changeSet = changeSet
    )
  }

  companion object {
    val ACI_A = ACI.from(UUID.fromString("3436efbe-5a76-47fa-a98a-7e72c948a82e"))
    val ACI_B = ACI.from(UUID.fromString("8de7f691-0b60-4a68-9cd9-ed2f8453f9ed"))

    val PNI_A = PNI.from(UUID.fromString("154b8d92-c960-4f6c-8385-671ad2ffb999"))
    val PNI_B = PNI.from(UUID.fromString("ba92b1fb-cd55-40bf-adda-c35a85375533"))

    const val E164_A = "+12221234567"
    const val E164_B = "+13331234567"
  }
}
