package com.svix.kotlin

import com.github.tomakehurst.wiremock.WireMockServer
import com.github.tomakehurst.wiremock.client.WireMock
import com.github.tomakehurst.wiremock.client.WireMock.*
import com.github.tomakehurst.wiremock.core.WireMockConfiguration.options
import com.svix.kotlin.SvixOptions
import com.svix.kotlin.exceptions.ApiException
import com.svix.kotlin.models.*
import kotlin.time.Instant
import kotlin.test.assertEquals
import kotlinx.coroutines.runBlocking
import org.junit.jupiter.api.*

@TestInstance(TestInstance.Lifecycle.PER_CLASS)
class WiremockTests {

    private val wireMockServer =
        WireMockServer(options().port(0).withRootDirectory("lib/src/test/resources"))

    @BeforeAll
    fun beforeAll() {
        wireMockServer.start()
    }

    @AfterAll
    fun afterAll() {
        wireMockServer.stop()
    }

    @BeforeEach
    fun beforeEach() {
        wireMockServer.resetAll()
    }

    private fun testClient(): Svix {
        return Svix("testtk_asd12.eu", SvixOptions(this.wireMockServer.baseUrl()))
    }

    @Test
    fun testEnumQueryParamEncodedCorrectly() {
        val svx = testClient()
        wireMockServer.stubFor(
            WireMock.get(urlMatching("/api/v1/app?.*"))
                .willReturn(WireMock.ok().withBodyFile("ListResponseApplicationOut.json"))
        )
        runBlocking {
            svx.application.list(
                ApplicationListOptions(
                    limit = 5UL,
                    order = Ordering.ASCENDING,
                    iterator = "cool-string-!@#$%^",
                )
            )
        }
        wireMockServer.verify(
            1,
            getRequestedFor(
                urlEqualTo(
                    "/api/v1/app?limit=5&iterator=cool-string-%21%40%23%24%25%5E&order=ascending"
                )
            ),
        )
    }

    @Test
    fun testSetQueryParamEncodedCorrectly() {
        val svx = testClient()
        wireMockServer.stubFor(
            WireMock.get(urlMatching("/api/v1/app/app_asd123/msg.*"))
                .willReturn(WireMock.ok().withBodyFile("ListResponseMessageOut.json"))
        )
        runBlocking {
            svx.message.list(
                "app_asd123",
                MessageListOptions(eventTypes = setOf("key3", "key4", "key1")),
            )
        }
        wireMockServer.verify(
            1,
            getRequestedFor(urlEqualTo("/api/v1/app/app_asd123/msg?with_content=false&event_types=key1%2Ckey3%2Ckey4")),
        )
    }

    @Test
    fun testInstantAndBoolQueryParamEncodedCorrectly() {
        val svx = testClient()
        wireMockServer.stubFor(
            WireMock.get(urlMatching("/api/v1/app/app_asd123/msg.*"))
                .willReturn(WireMock.ok().withBodyFile("ListResponseMessageOut.json"))
        )
        runBlocking {
            svx.message.list(
                "app_asd123",
                MessageListOptions(
                    before = Instant.fromEpochSeconds(1739399072, 864755000),
                    withContent = true,
                ),
            )
        }
        wireMockServer.verify(
            1,
            getRequestedFor(
                urlEqualTo(
                    "/api/v1/app/app_asd123/msg?before=2025-02-12T22%3A24%3A32.864755Z&with_content=true"
                )
            ),
        )
    }

    @Test
    fun maybeUnsetIsCorrectlySerialized() {
        val svx = testClient()
        wireMockServer.stubFor(
            WireMock.patch(urlMatching("/api/v1/app/ap/endpoint/endp"))
                .willReturn(WireMock.ok().withBodyFile("EndpointOut.json"))
        )
        runBlocking {
            // MaybeUnset.Present
            svx.endpoint.patch(
                "ap",
                "endp",
                EndpointPatch(eventTypes = MaybeUnset.Present(setOf("ft1", "ft2"))),
            )
            // MaybeUnset.Null
            svx.endpoint.patch("ap", "endp", EndpointPatch(eventTypes = MaybeUnset.Null))
            // MaybeUnset.Unset
            svx.endpoint.patch(
                "ap",
                "endp",
                EndpointPatch(eventTypes = MaybeUnset.Unset, disabled = false),
            )
        }
        // MaybeUnset.Present
        wireMockServer.verify(
            1,
            patchRequestedFor(urlEqualTo("/api/v1/app/ap/endpoint/endp"))
                .withRequestBody(equalTo("""{"eventTypes":["ft1","ft2"]}""")),
        )
        // MaybeUnset.Null
        wireMockServer.verify(
            1,
            patchRequestedFor(urlEqualTo("/api/v1/app/ap/endpoint/endp"))
                .withRequestBody(equalTo("""{"eventTypes":null}""")),
        )
        // MaybeUnset.Unset
        wireMockServer.verify(
            1,
            patchRequestedFor(urlEqualTo("/api/v1/app/ap/endpoint/endp"))
                .withRequestBody(equalTo("""{"disabled":false}""")),
        )
    }

    @Test
    fun deeplyNestedMapStringAnyIsCorrectlySerialized() {
        val svx = testClient()
        wireMockServer.stubFor(
            WireMock.post(urlMatching("/api/v1/event-type"))
                .willReturn(WireMock.ok().withBodyFile("EventTypeOut.json"))
        )
        val nestedList = listOf("l1", listOf("l2", listOf("l3")))
        val sampleMap =
            mapOf(
                "str_key" to "val",
                "int_key" to 1,
                "bool_key" to false,
                "list_key" to listOf("val1", "val2", nestedList),
                "map_key" to mapOf("key" to "val", "list" to nestedList),
            )
        val nestedMap =
            mapOf(
                "l1" to mapOf("l2" to mapOf("l3" to sampleMap, "more_list" to nestedList)),
                "m2" to sampleMap,
            )

        runBlocking {
            svx.eventType.create(
                EventTypeIn(description = "", name = "", schemas = mapOf("nestedMap" to nestedMap))
            )
        }
        wireMockServer.verify(
            1,
            postRequestedFor(urlEqualTo("/api/v1/event-type"))
                .withRequestBody(
                    equalToJson(
                        wireMockServer.options.stores.filesBlobStore
                            .get("VeryNestedMap.json")
                            .get()
                            .toString(Charsets.UTF_8)
                    )
                ),
        )
    }

    @Test
    fun maybeUnsetOnMapStringAnyIsCorrectlySerialized() {
        val svx = testClient()
        wireMockServer.stubFor(
            WireMock.patch(urlMatching("/api/v1/event-type/event"))
                .willReturn(WireMock.ok().withBodyFile("EventTypeOut.json"))
        )
        runBlocking {
            // MaybeUnset.Present
            svx.eventType.patch(
                "event",
                EventTypePatch(
                    schemas =
                        MaybeUnset.Present(
                            mapOf(
                                "str_key" to "val",
                                "int_key" to 1,
                                "bool_key" to false,
                                "list_key" to listOf("val1", "val2"),
                                "map_key" to mapOf("key" to "val"),
                            )
                        )
                ),
            )
            // MaybeUnset.Null
            svx.eventType.patch("event", EventTypePatch(schemas = MaybeUnset.Null))
            // MaybeUnset.Unset
            svx.eventType.patch(
                "event",
                EventTypePatch(description = "42", schemas = MaybeUnset.Unset),
            )
        }
        // MaybeUnset.Present
        wireMockServer.verify(
            1,
            patchRequestedFor(urlEqualTo("/api/v1/event-type/event"))
                .withRequestBody(
                    equalTo(
                        """{"schemas":{"str_key":"val","int_key":1,"bool_key":false,"list_key":["val1","val2"],"map_key":{"key":"val"}}}"""
                    )
                ),
        )
        // MaybeUnset.Null
        wireMockServer.verify(
            1,
            patchRequestedFor(urlEqualTo("/api/v1/event-type/event"))
                .withRequestBody(equalTo("""{"schemas":null}""")),
        )
        // MaybeUnset.Unset
        wireMockServer.verify(
            1,
            patchRequestedFor(urlEqualTo("/api/v1/event-type/event"))
                .withRequestBody(equalTo("""{"description":"42"}""")),
        )
    }

    @Test
    fun optionsHeadersAreSent() {
        val svx = testClient()
        wireMockServer.stubFor(
            WireMock.post(urlPathEqualTo("/api/v1/app/ap/msg"))
                .willReturn(WireMock.ok().withBodyFile("MessageOut.json"))
        )
        runBlocking {
            val payload =
                svx.message.create(
                    "ap",
                    MessageIn(
                        eventType = "event.test",
                        payload =
                            kotlinx.serialization.json.Json.encodeToString(mapOf("key" to "val")),
                    ),
                    MessageCreateOptions(idempotencyKey = "key123"),
                )
        }
        wireMockServer.verify(
            1,
            postRequestedFor(urlEqualTo("/api/v1/app/ap/msg?with_content=false"))
                .withHeader("idempotency-key", equalTo("key123")),
        )
    }

    @Test
    fun userAgentHeaderIsSent() {
        val svx = testClient()
        wireMockServer.stubFor(
            WireMock.get(urlPathEqualTo("/api/v1/app/ap/msg"))
                .willReturn(WireMock.ok().withBodyFile("ListResponseMessageOut.json"))
        )
        runBlocking { svx.message.list("ap") }
        wireMockServer.verify(
            1,
            getRequestedFor(urlEqualTo("/api/v1/app/ap/msg?with_content=false"))
                .withHeader("User-Agent", matching("svix-libs/.*/kotlin kotlin/2.* jre/.*")),
        )
    }

    @Test
    fun defaultRetryStatusCode500() {
        val svx = testClient()
        wireMockServer.stubFor(
            WireMock.get(urlPathEqualTo("/api/v1/app/ap/msg"))
                .willReturn(WireMock.status(500).withBodyFile("ListResponseMessageOut.json"))
        )
        runBlocking {
            try {
                svx.message.list("ap")
            } catch (e: ApiException) {
                assertEquals(500, e.statusCode)
            }
        }

        wireMockServer.verify(
            1,
            getRequestedFor(urlEqualTo("/api/v1/app/ap/msg?with_content=false"))
                // first request does not have `svix-retry-count` header
                .withHeader("svix-retry-count", absent()),
        )

        // check the svix-retry-count are set correctly
        for (retryCount in 1..3) {
            wireMockServer.verify(
                1,
                getRequestedFor(urlEqualTo("/api/v1/app/ap/msg?with_content=false"))
                    .withHeader("svix-retry-count", equalTo("$retryCount")),
            )
        }
    }

    @Test
    fun instantSerializedCorrectly() {
        val svx = testClient()
        wireMockServer.stubFor(
            WireMock.get(urlPathEqualTo("/api/v1/app/ap/msg/msg_asd123"))
                // this file includes a string timestamp `2025-02-12T22:24:32.864755Z`
                .willReturn(WireMock.ok().withBodyFile("MessageOut.json"))
        )
        runBlocking {
            val res = svx.message.get("ap", "msg_asd123")
            assertEquals(Instant.fromEpochSeconds(1739399072, 864755000), res.timestamp)
        }

        wireMockServer.verify(1, getRequestedFor(urlEqualTo("/api/v1/app/ap/msg/msg_asd123?with_content=false")))
    }

    @Test
    fun instantDeserializedCorrectly() {
        val svx = testClient()
        wireMockServer.stubFor(
            WireMock.post(urlMatching("/api/v1/stats/usage/app"))
                .willReturn(
                    WireMock.ok()
                        .withBody(
                            """{"unresolvedAppIds":["unique-identifier"],"id":"qtask_1srOrx2ZWZBpBUvZwXKQmoEYga2","status":"running","task":"endpoint.replay","updatedAt":"2025-03-03T03:03:03.000000Z"}"""
                        )
                )
        )
        runBlocking {
            svx.statistics.aggregateAppStats(
                AppUsageStatsIn(
                    since = Instant.fromEpochSeconds(1739399072, 864755000),
                    until = Instant.fromEpochSeconds(1739399072, 864755000),
                )
            )
        }

        wireMockServer.verify(
            1,
            postRequestedFor(urlEqualTo("/api/v1/stats/usage/app"))
                .withRequestBody(
                    equalTo(
                        """{"since":"2025-02-12T22:24:32.864755Z","until":"2025-02-12T22:24:32.864755Z"}"""
                    )
                ),
        )
    }

    @Test
    fun listResponseOutCorrectlyDeserialized() {
        val svx = testClient()
        wireMockServer.stubFor(
            WireMock.get(urlMatching("/api/v1/app"))
                .willReturn(
                    WireMock.ok()
                        .withBody("""{"data":[],"iterator":null,"prevIterator":null,"done":true}""")
                )
        )
        runBlocking { svx.application.list() }
    }

    @Test
    fun octothorpeInUrlQuery() {
        val svx = testClient()
        wireMockServer.stubFor(
            get(urlPathEqualTo("/api/v1/app/app1/msg"))
                .willReturn(ok().withBodyFile("ListResponseMessageOut.json"))
        )

        runBlocking { svx.message.list("app1", MessageListOptions(tag = "test#test")) }

        wireMockServer.verify(
            1,
            getRequestedFor(urlEqualTo("/api/v1/app/app1/msg?with_content=false&tag=test%23test")),
        )
    }

    @Test
    fun headersInTransformationParamsNotOverwritten() {
        val svx = testClient()
        wireMockServer.stubFor(
            post(urlPathEqualTo("/api/v1/app/app1/msg"))
                .willReturn(ok().withBodyFile("MessageOut.json"))
        )

        val jsonPayload = "{\"key\":\"val\",\"key1\":[\"list\"]}"
        val msg =
            MessageIn(
                payload = jsonPayload,
                eventType = "event.type",
                transformationsParams = mapOf("headers" to mapOf("header-key" to "header-val")),
            )
        runBlocking { svx.message.create("app1", msg) }

        val expectedBody =
            """{"eventType":"event.type","payload":{},"transformationsParams":{"headers":{"header-key":"header-val"},"rawPayload":"{\"key\":\"val\",\"key1\":[\"list\"]}"}}"""
        wireMockServer.verify(
            1,
            postRequestedFor(urlEqualTo("/api/v1/app/app1/msg?with_content=false"))
                .withRequestBody(equalTo(expectedBody)),
        )
    }

    @Test
    fun jsonEncodedMessageIn() {
        val svx = testClient()
        wireMockServer.stubFor(
            post(urlPathEqualTo("/api/v1/app/app1/msg"))
                .willReturn(ok().withBodyFile("MessageOut.json"))
        )

        val jsonPayload = "{\"key\":\"val\",\"key1\":[\"list\"]}"
        val msg = MessageIn(payload = jsonPayload, eventType = "event.type")
        runBlocking { svx.message.create("app1", msg) }

        val expectedBody =
            """{"eventType":"event.type","payload":{},"transformationsParams":{"rawPayload":"{\"key\":\"val\",\"key1\":[\"list\"]}"}}"""

        wireMockServer.verify(
            1,
            postRequestedFor(urlEqualTo("/api/v1/app/app1/msg?with_content=false"))
                .withRequestBody(equalTo(expectedBody)),
        )
    }


  @Test
  fun testIdempotencyKeyIsSentForCreateRequest() {
    val svx = testClient()
    wireMockServer.stubFor(
        post(urlEqualTo("/api/v1/app")).willReturn(ok().withBodyFile("ApplicationOut.json")))

    runBlocking { svx.application.create(ApplicationIn(name = "test app")) }

    wireMockServer.verify(
        1,
        postRequestedFor(urlEqualTo("/api/v1/app"))
            .withHeader("idempotency-key", matching("auto_.*")),
    )
  }

  @Test
  fun testClientProvidedIdempotencyKeyIsNotOverridden() {
    val svx = testClient()
    wireMockServer.stubFor(
        post(urlEqualTo("/api/v1/app")).willReturn(ok().withBodyFile("ApplicationOut.json")))

    val clientProvidedKey = "test-key-123"
    runBlocking {
      svx.application.create(
          ApplicationIn(name = "test app"),
          ApplicationCreateOptions(idempotencyKey = clientProvidedKey))
    }

    wireMockServer.verify(
        1,
        postRequestedFor(urlEqualTo("/api/v1/app"))
            .withHeader("idempotency-key", equalTo(clientProvidedKey)),
    )
  }

  @Test
  fun testUnknownKeysAreIgnored() {
    val res =
        """{"data": [],"iterator": "iterator","prevIterator": "-iterator","done": true,"extra-field": "ignored"}"""
    val svx = testClient()
    wireMockServer.stubFor(get(urlEqualTo("/api/v1/app")).willReturn(ok().withBody(res)))

    runBlocking { svx.application.list() }

    wireMockServer.verify(
        1,
        getRequestedFor(urlEqualTo("/api/v1/app")),
    )
  }

  @Test
  fun testQueryParamDefaults() {
    val svx = testClient()
    wireMockServer.stubFor(
      WireMock.get(urlPathEqualTo("/api/v1/app/ap/attempt/endpoint/endp"))
        .willReturn(ok().withBodyFile("ListResponseMessageAttemptOut.json"))
    )
    runBlocking {
      svx.messageAttempt.listByEndpoint("ap", "endp")
    }

    wireMockServer.verify(
      1,
      getRequestedFor(urlEqualTo("/api/v1/app/ap/attempt/endpoint/endp?with_content=false&expanded_statuses=true"))
    )
  }
}
