package wang.yaojia.webterm.api.routes

import kotlinx.coroutines.test.runTest
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertFalse
import org.junit.jupiter.api.Assertions.assertTrue
import org.junit.jupiter.api.Test
import wang.yaojia.webterm.testsupport.FakeHttpTransport
import wang.yaojia.webterm.wire.ClaudeStatus
import wang.yaojia.webterm.wire.HostEndpoint
import java.util.UUID

/** Tolerant decode at the UNTRUSTED server boundary (plan §4): drop bad elements, never crash. */
class TolerantDecodeTest {
    private companion object {
        const val BASE = "http://h:3000"
        val ID: UUID = UUID.fromString("11111111-2222-4333-8444-555555555555")
        const val ID_STR = "11111111-2222-4333-8444-555555555555"
    }

    private val transport = FakeHttpTransport()
    private val client = ApiClient(HostEndpoint.fromBaseUrl(BASE)!!, transport)

    @Test
    fun liveSessionsDropsMalformedEntriesAndMapsUnknownStatusToUnknown() = runTest {
        // Entry 1: valid, unknown status string. Entry 2: missing required clientCount → dropped.
        // Entry 3: non-UUID id → dropped. Entry 4: valid, big-ms createdAt (Long), no telemetry.
        val body = """
            [
              {"id":"$ID_STR","createdAt":1700000000000,"clientCount":2,"status":"reticulating","exited":false,"cols":80,"rows":24},
              {"id":"22222222-2222-4222-8222-222222222222","createdAt":1,"status":"idle","exited":false,"cols":80,"rows":24},
              {"id":"not-a-uuid","createdAt":1,"clientCount":1,"status":"idle","exited":false,"cols":80,"rows":24},
              {"id":"33333333-3333-4333-8333-333333333333","createdAt":1700000000001,"clientCount":0,"status":"working","exited":true,"cwd":"/w","cols":120,"rows":40,"lastOutputAt":1700000000005}
            ]
        """.trimIndent()
        transport.queueSuccess(url = "$BASE/live-sessions", body = body.toByteArray())

        val sessions = client.liveSessions()

        assertEquals(2, sessions.size)
        assertEquals(ID, sessions[0].id)
        assertEquals(ClaudeStatus.UNKNOWN, sessions[0].status) // unknown wire value → UNKNOWN, not dropped
        assertEquals(1_700_000_000_000L, sessions[0].createdAt) // ms fits Long, would overflow Int
        assertEquals(ClaudeStatus.WORKING, sessions[1].status)
        assertEquals(1_700_000_000_005L, sessions[1].lastOutputAt)
    }

    @Test
    fun liveSessionsNonArrayBodyThrowsInvalidResponseBody() = runTest {
        transport.queueSuccess(url = "$BASE/live-sessions", body = "{}".toByteArray())
        assertEquals(
            ApiClientError.InvalidResponseBody,
            runCatching { client.liveSessions() }.exceptionOrNull(),
        )
    }

    @Test
    fun projectsKeepsProjectButDropsOnlyItsMalformedNestedSession() = runTest {
        val body = """
            [
              {"name":"repo","path":"/r","isGit":true,"branch":"main","sessions":[
                {"id":"$ID_STR","status":"working","clientCount":1,"createdAt":1,"exited":false},
                {"id":"bad","status":"working","clientCount":1,"createdAt":1,"exited":false}
              ]}
            ]
        """.trimIndent()
        transport.queueSuccess(url = "$BASE/projects", body = body.toByteArray())

        val projects = client.projects()

        assertEquals(1, projects.size) // project survives its bad nested session
        assertEquals(1, projects[0].sessions.size)
        assertEquals(ID, projects[0].sessions[0].id)
    }

    @Test
    fun eventsNonArrayBodyDegradesToEmptyAndUnknownClassSurvives() = runTest {
        // A non-array body (timeline capture disabled) → [] rather than an error.
        transport.queueSuccess(url = "$BASE/live-sessions/$ID_STR/events", body = "{}".toByteArray())
        assertTrue(client.events(ID).isEmpty())

        // Unknown class decodes fine (shape ok); hasKnownClass=false for downstream filtering.
        transport.queueSuccess(
            url = "$BASE/live-sessions/$ID_STR/events",
            body = """[{"at":5,"class":"weird","label":"did a thing"}]""".toByteArray(),
        )
        val events = client.events(ID)
        assertEquals(1, events.size)
        assertFalse(events[0].hasKnownClass)
        assertEquals("did a thing", events[0].label)
    }

    @Test
    fun singleObjectRoutesThrowInvalidResponseBodyOnGarbage() = runTest {
        transport.queueSuccess(url = "$BASE/config/ui", body = "not json".toByteArray())
        assertEquals(
            ApiClientError.InvalidResponseBody,
            runCatching { client.uiConfig() }.exceptionOrNull(),
        )
    }
}
