package io.sentry

import java.net.URI
import java.util.Calendar
import java.util.Currency
import java.util.Locale
import java.util.TimeZone
import java.util.concurrent.atomic.AtomicBoolean
import java.util.concurrent.atomic.AtomicIntegerArray
import kotlin.test.assertNotNull
import kotlin.test.assertNull
import org.junit.Test
import org.mockito.kotlin.inOrder
import org.mockito.kotlin.mock
import org.mockito.kotlin.times
import org.mockito.kotlin.verify

internal class JsonObjectSerializerTest {
  private inner class Fixture {
    val writer = mock<JsonObjectWriter>()
    val logger = mock<ILogger>()

    fun getSUT(): JsonObjectSerializer = JsonObjectSerializer(100)
  }

  private val fixture = Fixture()

  // region primitives

  @Test
  fun `serializing null`() {
    fixture.getSUT().serialize(fixture.writer, fixture.logger, null)
    verify(fixture.writer).nullValue()
  }

  @Test
  fun `serializing bool`() {
    fixture.getSUT().serialize(fixture.writer, fixture.logger, true)
    verify(fixture.writer).value(true)
  }

  @Test
  fun `serializing int`() {
    fixture.getSUT().serialize(fixture.writer, fixture.logger, 1337)
    verify(fixture.writer).value(1337 as Number)
  }

  @Test
  fun `serializing long`() {
    fixture.getSUT().serialize(fixture.writer, fixture.logger, 1337L)
    verify(fixture.writer).value(1337L as Number)
  }

  @Test
  fun `serializing double`() {
    fixture.getSUT().serialize(fixture.writer, fixture.logger, 9.9)
    verify(fixture.writer).value(9.9 as Number)
  }

  @Test
  fun `serializing char`() {
    fixture.getSUT().serialize(fixture.writer, fixture.logger, 'c')
    verify(fixture.writer).value("c")
  }

  // endregion

  @Test
  fun `serializing string`() {
    fixture.getSUT().serialize(fixture.writer, fixture.logger, "fixture")
    verify(fixture.writer).value("fixture")
  }

  @Test
  fun `serializing date`() {
    val dateIsoFormat = "2021-08-05T15:15:15.000Z"
    val date = DateUtils.getDateTime(dateIsoFormat)
    fixture.getSUT().serialize(fixture.writer, fixture.logger, date)
    verify(fixture.writer).value(dateIsoFormat)
  }

  @Test
  fun `serializing timezone`() {
    val id = "Europe/Vienna"
    val timezone = TimeZone.getTimeZone(id)
    fixture.getSUT().serialize(fixture.writer, fixture.logger, timezone)
    verify(fixture.writer).value(id)
  }

  @Test
  fun `serializing collection`() {
    fixture.getSUT().serialize(fixture.writer, fixture.logger, listOf("fixture"))
    verify(fixture.writer).beginArray()
    verify(fixture.writer).value("fixture")
    verify(fixture.writer).endArray()
  }

  @Test
  fun `serializing array`() {
    fixture.getSUT().serialize(fixture.writer, fixture.logger, arrayOf("fixture"))
    verify(fixture.writer).beginArray()
    verify(fixture.writer).value("fixture")
    verify(fixture.writer).endArray()
  }

  @Suppress("RemoveRedundantCallsOfConversionMethods")
  @Test
  fun `serializing primitive int array`() {
    fixture.getSUT().serialize(fixture.writer, fixture.logger, intArrayOf(1, 2))
    verify(fixture.writer).beginArray()
    verify(fixture.writer).value(1.toInt())
    verify(fixture.writer).value(2.toInt())
    verify(fixture.writer).endArray()
  }

  @Test
  fun `serializing primitive boolean array`() {
    fixture.getSUT().serialize(fixture.writer, fixture.logger, booleanArrayOf(true, false))
    verify(fixture.writer).beginArray()
    verify(fixture.writer).value(true)
    verify(fixture.writer).value(false)
    verify(fixture.writer).endArray()
  }

  @Test
  fun `serializing primitive byte array`() {
    fixture.getSUT().serialize(fixture.writer, fixture.logger, byteArrayOf(1, 2))
    verify(fixture.writer).beginArray()
    verify(fixture.writer).value(1.toByte())
    verify(fixture.writer).value(2.toByte())
    verify(fixture.writer).endArray()
  }

  @Test
  fun `serializing primitive short array`() {
    fixture.getSUT().serialize(fixture.writer, fixture.logger, shortArrayOf(1, 2))
    verify(fixture.writer).beginArray()
    verify(fixture.writer).value(1.toShort())
    verify(fixture.writer).value(2.toShort())
    verify(fixture.writer).endArray()
  }

  @Test
  fun `serializing primitive char array`() {
    fixture.getSUT().serialize(fixture.writer, fixture.logger, charArrayOf('a', 'b'))
    verify(fixture.writer).beginArray()
    verify(fixture.writer).value("a")
    verify(fixture.writer).value("b")
    verify(fixture.writer).endArray()
  }

  @Test
  fun `serializing primitive long array`() {
    fixture.getSUT().serialize(fixture.writer, fixture.logger, longArrayOf(1L, 2L))
    verify(fixture.writer).beginArray()
    verify(fixture.writer).value(1L as Number)
    verify(fixture.writer).value(2L as Number)
    verify(fixture.writer).endArray()
  }

  @Test
  fun `serializing primitive float array`() {
    fixture.getSUT().serialize(fixture.writer, fixture.logger, floatArrayOf(1.5f, 2.5f))
    verify(fixture.writer).beginArray()
    verify(fixture.writer).value(1.5f as Number)
    verify(fixture.writer).value(2.5f as Number)
    verify(fixture.writer).endArray()
  }

  @Test
  fun `serializing primitive double array`() {
    fixture.getSUT().serialize(fixture.writer, fixture.logger, doubleArrayOf(1.5, 2.5))
    verify(fixture.writer).beginArray()
    verify(fixture.writer).value(1.5 as Number)
    verify(fixture.writer).value(2.5 as Number)
    verify(fixture.writer).endArray()
  }

  @Test
  fun `serialize map`() {
    fixture
      .getSUT()
      .serialize(fixture.writer, fixture.logger, mapOf("fixture-key" to "fixture-value"))
    verify(fixture.writer).beginObject()
    verify(fixture.writer).name("fixture-key")
    verify(fixture.writer).value("fixture-value")
    verify(fixture.writer).endObject()
  }

  @Test
  fun `serialize json serializable`() {
    val jsonSerializable: JsonSerializable = mock()
    fixture.getSUT().serialize(fixture.writer, fixture.logger, jsonSerializable)
    verify(jsonSerializable).serialize(fixture.writer, fixture.logger)
  }

  @Test
  fun `serialize json serializable does not create reflection serializer`() {
    val serializer = fixture.getSUT()
    val jsonSerializable: JsonSerializable = mock()
    serializer.serialize(fixture.writer, fixture.logger, jsonSerializable)
    assertNull(serializer.reflectionObjectSerializer)
  }

  @Test
  fun `serialize unknown object creates reflection serializer`() {
    val serializer = fixture.getSUT()
    serializer.serialize(fixture.writer, fixture.logger, object {})
    assertNotNull(serializer.reflectionObjectSerializer)
  }

  @Test
  fun `serialize unknown object without data`() {
    val value = object {}
    fixture.getSUT().serialize(fixture.writer, fixture.logger, value)
    verify(fixture.writer).value(value.toString())
  }

  @Test
  fun `serialize enum`() {
    fixture.getSUT().serialize(fixture.writer, fixture.logger, DataCategory.Session)
    verify(fixture.writer).value("Session")
  }

  @Test
  fun `serialize list of enum`() {
    fixture.getSUT().serialize(fixture.writer, fixture.logger, listOf(DataCategory.Session))
    verify(fixture.writer).beginArray()
    verify(fixture.writer).value("Session")
    verify(fixture.writer).endArray()
  }

  @Test
  fun `serialize map of enum`() {
    fixture
      .getSUT()
      .serialize(fixture.writer, fixture.logger, mapOf("key" to DataCategory.Transaction))
    verify(fixture.writer).beginObject()
    verify(fixture.writer).name("key")
    verify(fixture.writer).value("Transaction")
    verify(fixture.writer).endObject()
  }

  @Test
  fun `serialize object with enum property`() {
    fixture
      .getSUT()
      .serialize(fixture.writer, fixture.logger, ClassWithEnumProperty(DataCategory.Attachment))
    verify(fixture.writer).beginObject()
    verify(fixture.writer).name("enumProperty")
    verify(fixture.writer).value("Attachment")
    verify(fixture.writer).endObject()
  }

  @Test
  fun `serialize unknown object with data`() {
    val objectWithPrimitiveFields = UnknownClassWithData(17, "fixtureString")
    fixture.getSUT().serialize(fixture.writer, fixture.logger, objectWithPrimitiveFields)
    verify(fixture.writer).beginObject()
    verify(fixture.writer).name("integer")
    verify(fixture.writer).value(17 as Number)
    verify(fixture.writer).name("string")
    verify(fixture.writer).value("fixtureString")
    verify(fixture.writer).endObject()
  }

  @Test
  fun `serialize locale`() {
    val inOrder = inOrder(fixture.writer)
    fixture.getSUT().serialize(fixture.writer, fixture.logger, Locale.US)

    inOrder.verify(fixture.writer).value("en_US")
  }

  @Test
  fun `serialize locale in map`() {
    val map = mapOf<String, Locale>("one" to Locale.US)
    val inOrder = inOrder(fixture.writer)
    fixture.getSUT().serialize(fixture.writer, fixture.logger, map)
    inOrder.verify(fixture.writer).beginObject()
    inOrder.verify(fixture.writer).name("one")
    inOrder.verify(fixture.writer).value("en_US")
    inOrder.verify(fixture.writer).endObject()
  }

  @Test
  fun `serialize locale in list`() {
    val list = listOf<Locale>(Locale.US, Locale.GERMAN)
    val inOrder = inOrder(fixture.writer)
    fixture.getSUT().serialize(fixture.writer, fixture.logger, list)
    inOrder.verify(fixture.writer).beginArray()
    inOrder.verify(fixture.writer).value("en_US")
    inOrder.verify(fixture.writer).value("de")
    inOrder.verify(fixture.writer).endArray()
  }

  @Test
  fun `serialize locale in object`() {
    val obj = ClassWithLocaleProperty(Locale.US)
    val inOrder = inOrder(fixture.writer)
    fixture.getSUT().serialize(fixture.writer, fixture.logger, obj)
    inOrder.verify(fixture.writer).beginObject()
    inOrder.verify(fixture.writer).name("localeProperty")
    inOrder.verify(fixture.writer).value("en_US")
    inOrder.verify(fixture.writer).endObject()
  }

  @Test
  fun `serializing AtomicIntegerArray`() {
    fixture
      .getSUT()
      .serialize(fixture.writer, fixture.logger, AtomicIntegerArray(arrayOf(1, 2, 3).toIntArray()))
    verify(fixture.writer).beginArray()
    verify(fixture.writer).value(1 as Number)
    verify(fixture.writer).value(2 as Number)
    verify(fixture.writer).value(3 as Number)
    verify(fixture.writer).endArray()
  }

  @Test
  fun `serializing AtomicBoolean`() {
    fixture.getSUT().serialize(fixture.writer, fixture.logger, AtomicBoolean(true))
    verify(fixture.writer).value(true)
  }

  @Test
  fun `serializing URI`() {
    fixture
      .getSUT()
      .serialize(fixture.writer, fixture.logger, URI("http://localhost:8081/api/product?id=99"))
    verify(fixture.writer).value("http://localhost:8081/api/product?id=99")
  }

  @Test
  fun `serializing UUID`() {
    fixture
      .getSUT()
      .serialize(fixture.writer, fixture.logger, "828900a5-15dc-413f-8c17-6ef04d74e074")
    verify(fixture.writer).value("828900a5-15dc-413f-8c17-6ef04d74e074")
  }

  @Test
  fun `serializing Currency`() {
    fixture.getSUT().serialize(fixture.writer, fixture.logger, Currency.getInstance("USD"))
    verify(fixture.writer).value("USD")
  }

  @Test
  fun `serializing Calendar`() {
    val calendar = Calendar.getInstance()
    calendar.set(2022, 0, 1, 11, 59, 58)
    fixture.getSUT().serialize(fixture.writer, fixture.logger, calendar)
    verify(fixture.writer).beginObject()
    verify(fixture.writer).name("year")
    verify(fixture.writer).value(2022 as java.lang.Integer)
    verify(fixture.writer).name("month")
    verify(fixture.writer).value(0 as java.lang.Integer)
    verify(fixture.writer).name("dayOfMonth")
    verify(fixture.writer).value(1 as java.lang.Integer)
    verify(fixture.writer).name("hourOfDay")
    verify(fixture.writer).value(11 as java.lang.Integer)
    verify(fixture.writer).name("minute")
    verify(fixture.writer).value(59 as java.lang.Integer)
    verify(fixture.writer).name("second")
    verify(fixture.writer).value(58 as java.lang.Integer)
    verify(fixture.writer).endObject()
  }

  class UnknownClassWithData(private val integer: Int, private val string: String)
}

data class ClassWithEnumProperty(val enumProperty: DataCategory)

data class ClassWithLocaleProperty(val localeProperty: Locale)

private val JsonObjectSerializer.reflectionObjectSerializer: JsonReflectionObjectSerializer?
  get() {
    val field = JsonObjectSerializer::class.java.getDeclaredField("jsonReflectionObjectSerializer")
    field.isAccessible = true
    return field.get(this) as JsonReflectionObjectSerializer?
  }
