package wang.yaojia.webterm.testsupport

import wang.yaojia.webterm.wire.HttpMethod
import wang.yaojia.webterm.wire.HttpRequest
import wang.yaojia.webterm.wire.HttpResponse
import wang.yaojia.webterm.wire.HttpTransport

/**
 * Errors [FakeHttpTransport] raises on its own. Android analogue of iOS `FakeHTTPTransportError`;
 * only the "you forgot to script this route" case survives the port — the Swift `requestMissingURL`
 * / `responseConstructionFailed` cases are unreachable here because [HttpRequest.url] is a non-null
 * `String` and [HttpResponse] is a plain data class (no `HTTPURLResponse` construction to fail).
 */
public sealed class FakeHttpTransportError(message: String) : Exception(message) {
    /**
     * [FakeHttpTransport.send] was called for a route with no queued response — the test forgot to
     * script it. Loud and identifying (data class → exact-value assertable), never a silent hang.
     */
    public data class NoQueuedResponse(val method: HttpMethod, val url: String) :
        FakeHttpTransportError("FakeHttpTransport: no queued response for ${method.name} $url")
}

/**
 * In-memory [HttpTransport] double (A3, mirrors iOS `FakeHTTPTransport`). `:api-client` (A8/A9)
 * cannot tell it apart from `:transport-okhttp`'s real impl (plan §3.4).
 *
 * - **Queue responses per route (method + exact url), FIFO**: [queueSuccess] / [queueFailure]. An
 *   unqueued route throws [FakeHttpTransportError.NoQueuedResponse] immediately.
 * - **Record every request verbatim, headers included** — so Origin-iff-guarded tests (plan §4.3
 *   铁律) can assert exactly which requests carried the `Origin` header, and query-string tests
 *   (`staged=1`) can assert the exact url.
 *
 * Thread-safe: one JVM monitor guards the queues and the recording; every section is synchronous.
 */
public class FakeHttpTransport : HttpTransport {
    private data class RouteKey(val method: HttpMethod, val url: String)

    private sealed interface QueuedResult {
        data class Success(
            val status: Int,
            val headers: Map<String, String>,
            val body: ByteArray,
        ) : QueuedResult

        data class Failure(val error: Throwable) : QueuedResult
    }

    private val lock = Any()
    private val queues = mutableMapOf<RouteKey, ArrayDeque<QueuedResult>>()
    private val recorded = mutableListOf<HttpRequest>()

    /**
     * Every request passed to [send], in order, verbatim (method, url, headers, body) — including
     * ones that found no queued response and threw.
     */
    public val recordedRequests: List<HttpRequest>
        get() = synchronized(lock) { recorded.toList() }

    // ── Scripting ────────────────────────────────────────────────────────────────────────

    /** Queue one successful response for `method url` (FIFO per route). */
    public fun queueSuccess(
        method: HttpMethod = HttpMethod.GET,
        url: String,
        status: Int = DEFAULT_OK_STATUS,
        headers: Map<String, String> = emptyMap(),
        body: ByteArray = EMPTY_BODY,
    ) {
        enqueue(RouteKey(method, url), QueuedResult.Success(status, headers, body))
    }

    /**
     * Queue one transport-level failure for `method url` (e.g. connection-refused →
     * `PairingError.hostUnreachable` classification tests, A9). The error is re-thrown by [send].
     */
    public fun queueFailure(
        method: HttpMethod = HttpMethod.GET,
        url: String,
        error: Throwable,
    ) {
        enqueue(RouteKey(method, url), QueuedResult.Failure(error))
    }

    // ── HttpTransport ────────────────────────────────────────────────────────────────────

    override suspend fun send(request: HttpRequest): HttpResponse {
        val next: QueuedResult =
            synchronized(lock) {
                recorded.add(request)
                val key = RouteKey(request.method, request.url)
                val queue = queues[key]
                if (queue.isNullOrEmpty()) {
                    throw FakeHttpTransportError.NoQueuedResponse(request.method, request.url)
                }
                queue.removeFirst()
            }
        return when (next) {
            is QueuedResult.Failure -> throw next.error
            is QueuedResult.Success -> HttpResponse(next.status, next.body, next.headers)
        }
    }

    // ── Internals ──────────────────────────────────────────────────────────────────────

    private fun enqueue(key: RouteKey, result: QueuedResult) {
        synchronized(lock) { queues.getOrPut(key) { ArrayDeque() }.addLast(result) }
    }

    public companion object {
        /** Default scripted success status (matches iOS `defaultOKStatus`). */
        public const val DEFAULT_OK_STATUS: Int = 200
        private val EMPTY_BODY: ByteArray = ByteArray(0)
    }
}
