package com.fabledsword.minstrel.api

import com.fabledsword.minstrel.auth.AuthStore
import com.fabledsword.minstrel.cache.db.dao.AuthSessionDao
import io.mockk.coEvery
import io.mockk.every
import io.mockk.mockk
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.UnconfinedTestDispatcher
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.mockwebserver.MockResponse
import okhttp3.mockwebserver.MockWebServer
import org.junit.jupiter.api.AfterEach
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import kotlin.test.assertEquals
import kotlin.test.assertNull

@OptIn(ExperimentalCoroutinesApi::class)
class AuthCookieInterceptorTest {
    private lateinit var server: MockWebServer
    private lateinit var authStore: AuthStore
    private lateinit var client: OkHttpClient

    @BeforeEach
    fun setup() {
        server = MockWebServer().apply { start() }

        // AuthStore is Room-backed in production (slice 10). The test only
        // exercises the in-memory state mutations of set*; the DAO writes
        // happen on the scope and don't affect the synchronous read path
        // the interceptor uses. UnconfinedTestDispatcher executes the
        // scope.launch blocks immediately so the mock DAO records the
        // calls if we ever want to verify persistence.
        val dao =
            mockk<AuthSessionDao> {
                every { observe() } returns flowOf(null)
                coEvery { get() } returns null
                coEvery { upsert(any()) } returns Unit
                coEvery { setSessionCookie(any()) } returns Unit
                coEvery { setBaseUrl(any()) } returns Unit
            }
        authStore = AuthStore(dao, TestScope(UnconfinedTestDispatcher()))

        // BaseUrlInterceptor rewrites placeholder.invalid → mock server.
        // AuthCookieInterceptor scopes its attach + clear behavior to
        // the placeholder host so external Coil image fetches don't get
        // the Minstrel session cookie attached and don't trigger a
        // session-clear on 401. Tests issue requests to
        // http://placeholder.invalid/... so the auth interceptor sees
        // the in-scope host, and BaseUrlInterceptor (running AFTER, so
        // the auth interceptor sees the original host) rewrites the URL
        // before transport.
        authStore.setBaseUrl(server.url("/").toString())
        client =
            OkHttpClient.Builder()
                .addInterceptor(AuthCookieInterceptor(authStore))
                .addInterceptor(BaseUrlInterceptor(authStore))
                .build()
    }

    private fun placeholderUrl(path: String): String =
        "http://${BaseUrlInterceptor.PLACEHOLDER_HOST}$path"

    @AfterEach
    fun teardown() {
        server.shutdown()
    }

    @Test
    fun `attaches stored cookie to outgoing requests`() {
        authStore.setSessionCookie("session=abc123")
        server.enqueue(MockResponse().setResponseCode(200))

        client.newCall(Request.Builder().url(placeholderUrl("/api/test")).build()).execute()

        val recorded = server.takeRequest()
        assertEquals("session=abc123", recorded.getHeader("Cookie"))
    }

    @Test
    fun `clears cookie on 401 response`() {
        authStore.setSessionCookie("session=expired")
        server.enqueue(MockResponse().setResponseCode(401))

        client.newCall(Request.Builder().url(placeholderUrl("/api/test")).build()).execute()

        assertNull(authStore.sessionCookie.value)
    }

    @Test
    fun `captures Set-Cookie on successful response`() {
        server.enqueue(
            MockResponse()
                .setResponseCode(200)
                .addHeader("Set-Cookie", "session=newvalue; Path=/; HttpOnly"),
        )

        client.newCall(Request.Builder().url(placeholderUrl("/api/login")).build()).execute()

        assertEquals("session=newvalue", authStore.sessionCookie.value)
    }

    @Test
    fun `does not overwrite cookie on a successful response without Set-Cookie`() {
        authStore.setSessionCookie("session=existing")
        server.enqueue(MockResponse().setResponseCode(200))

        client.newCall(Request.Builder().url(placeholderUrl("/api/anything")).build()).execute()

        assertEquals("session=existing", authStore.sessionCookie.value)
    }

    // Drift #568 regression guard: requests to external hosts must NOT
    // carry the Minstrel session cookie even when the auth store has
    // one. The shared OkHttpClient is also used by Coil for image
    // fetches to artwork.musicbrainz.org and the like; leaking the
    // session cookie to those hosts is a posture violation.
    @Test
    fun `does NOT attach cookie to non-placeholder host`() {
        authStore.setSessionCookie("session=secret")
        server.enqueue(MockResponse().setResponseCode(200))

        // Request goes directly to the mock server's host:port — NOT
        // through the placeholder sentinel — so the auth interceptor
        // should pass it through untouched.
        client.newCall(Request.Builder().url(server.url("/external/image.jpg")).build()).execute()

        val recorded = server.takeRequest()
        assertNull(recorded.getHeader("Cookie"))
    }

    // Drift #569 regression guard: a 401 from an external host must
    // NOT wipe the user's Minstrel session. A misbehaving CDN or a
    // Lidarr that's 401-ing on /MediaCover URLs used to silently sign
    // the user out.
    @Test
    fun `does NOT clear cookie on 401 from non-placeholder host`() {
        authStore.setSessionCookie("session=keep")
        server.enqueue(MockResponse().setResponseCode(401))

        client.newCall(Request.Builder().url(server.url("/external/image.jpg")).build()).execute()

        assertEquals("session=keep", authStore.sessionCookie.value)
    }
}
