import Foundation
import WireProtocol

/// Errors `FakeHTTPTransport` produces on its own (Equatable for exact
/// `#expect(throws:)` assertions).
public enum FakeHTTPTransportError: Error, Equatable, Sendable {
    /// `send` was called for a URL/method with no queued response — the test
    /// forgot to script it. Loud and identifying, never a silent hang.
    case noQueuedResponse(method: String, url: URL)
    /// The request had no URL at all (malformed test input).
    case requestMissingURL
    /// `HTTPURLResponse` refused the scripted status/headers (should not
    /// happen with sane inputs; surfaced explicitly rather than force-unwrapped).
    case responseConstructionFailed(url: URL)
}

/// In-memory `HTTPTransport` double (T-iOS-4). `APIClient` cannot tell it
/// apart from the `URLSession`-backed implementation (plan §3.4).
///
/// - **Queue responses per URL/method** (FIFO per route): `queueSuccess` /
///   `queueFailure`. An unqueued route throws `.noQueuedResponse` immediately.
/// - **Record requests verbatim, headers included** — so Origin-iff-G tests
///   (plan §3.4 铁律) can assert exactly which requests carried `Origin`.
public actor FakeHTTPTransport: HTTPTransport {
    /// Route identity: HTTP method (uppercased) + exact URL.
    public struct RouteKey: Hashable, Sendable {
        public let method: String
        public let url: URL

        public init(method: String, url: URL) {
            self.method = method.uppercased()
            self.url = url
        }
    }

    private enum QueuedResult {
        case success(status: Int, headers: [String: String], body: Data)
        case failure(any Error)
    }

    /// Default HTTP method for queueing/matching when a request omits one.
    public static let defaultMethod = "GET"
    /// Default scripted success status.
    public static let defaultOKStatus = 200
    private static let httpVersion = "HTTP/1.1"

    private var queues: [RouteKey: [QueuedResult]] = [:]

    /// Every request passed to `send`, in order, verbatim (URL, method,
    /// headers, body) — including ones that found no queued response.
    public private(set) var recordedRequests: [URLRequest] = []

    public init() {}

    // MARK: - Scripting

    /// Queue one successful response for `method url` (FIFO per route).
    public func queueSuccess(
        method: String = FakeHTTPTransport.defaultMethod,
        url: URL,
        status: Int = FakeHTTPTransport.defaultOKStatus,
        headers: [String: String] = [:],
        body: Data = Data()
    ) {
        enqueue(.success(status: status, headers: headers, body: body),
                for: RouteKey(method: method, url: url))
    }

    /// Queue one transport-level failure for `method url` (e.g. connection
    /// refused → `PairingError.hostUnreachable` classification tests).
    public func queueFailure(
        method: String = FakeHTTPTransport.defaultMethod,
        url: URL,
        error: any Error
    ) {
        enqueue(.failure(error), for: RouteKey(method: method, url: url))
    }

    // MARK: - HTTPTransport

    public func send(_ request: URLRequest) async throws -> (Data, HTTPURLResponse) {
        recordedRequests.append(request)
        guard let url = request.url else {
            throw FakeHTTPTransportError.requestMissingURL
        }
        let key = RouteKey(method: request.httpMethod ?? Self.defaultMethod, url: url)
        guard var queue = queues[key], !queue.isEmpty else {
            throw FakeHTTPTransportError.noQueuedResponse(method: key.method, url: url)
        }
        let next = queue.removeFirst()
        queues[key] = queue
        switch next {
        case .failure(let error):
            throw error
        case .success(let status, let headers, let body):
            return (body, try Self.makeResponse(url: url, status: status, headers: headers))
        }
    }

    // MARK: - Internals

    private func enqueue(_ result: QueuedResult, for key: RouteKey) {
        queues[key, default: []].append(result)
    }

    private static func makeResponse(
        url: URL,
        status: Int,
        headers: [String: String]
    ) throws -> HTTPURLResponse {
        guard let response = HTTPURLResponse(
            url: url, statusCode: status, httpVersion: httpVersion, headerFields: headers
        ) else {
            throw FakeHTTPTransportError.responseConstructionFailed(url: url)
        }
        return response
    }
}
