import Foundation
import Testing
@testable import ClientTLS

// B3 · DeviceEnrollmentFlow composes the pinned bootstrap end-to-end:
//   login(password) → bearer  →  enroll(CSR, subdomain) with that bearer  → leaf.
// Driven by a path-routing stub transport (no network, no keychain): asserts the
// bearer minted at login is exactly what authenticates the enroll call.

private let flowBaseURL = URL(string: "https://cp.terminal.yaojia.wang")!

/// Routes by path: `/auth/login` → login body, `/device/enroll` → enroll body.
/// Records every request so the test can assert the enroll bearer. `@unchecked
/// Sendable`: mutable captures guarded by a lock.
private final class RoutingStubTransport: EnrollmentTransport, @unchecked Sendable {
    private let lock = NSLock()
    private var _requests: [URLRequest] = []
    private let loginStatus: Int
    private let loginBody: Data

    init(loginStatus: Int = 201, loginBody: Data) {
        self.loginStatus = loginStatus
        self.loginBody = loginBody
    }

    var requests: [URLRequest] { lock.withLock { _requests } }
    func request(forPathSuffix suffix: String) -> URLRequest? {
        lock.withLock { _requests.first { $0.url?.path.hasSuffix(suffix) == true } }
    }

    func send(_ request: URLRequest) async throws -> (Data, HTTPURLResponse) {
        lock.withLock { _requests.append(request) }
        let path = request.url?.path ?? ""
        let (status, body): (Int, Data)
        if path.hasSuffix("/auth/login") {
            (status, body) = (loginStatus, loginBody)
        } else {
            // /device/enroll → a minimal 201 leaf.
            (status, body) = (201, enrollLeafBody())
        }
        let response = HTTPURLResponse(
            url: request.url!, statusCode: status, httpVersion: "HTTP/1.1", headerFields: nil
        )!
        return (body, response)
    }
}

private func loginResponseBody(enrollToken: String) -> Data {
    try! JSONSerialization.data(withJSONObject: [
        "enrollToken": enrollToken,
        "accountId": "acct-123",
        "expiresIn": 600,
    ])
}

private func enrollLeafBody() -> Data {
    try! JSONSerialization.data(withJSONObject: [
        "deviceId": "dev-1",
        "cert": Data([0x30, 0x01]).base64EncodedString(),
        "caChain": [Data([0x30, 0x02]).base64EncodedString()],
        "notBefore": "2026-07-18T00:00:00Z",
        "notAfter": "2026-10-16T00:00:00Z",
        "renewAfter": "2026-09-15T00:00:00Z",
    ])
}

/// A fake installer that invokes the real `client.enroll` (so the transport sees
/// the enroll request with the login-minted bearer), then returns a summary.
/// Records the subdomain/deviceName it was handed. `@unchecked Sendable`: lock.
private final class SpyInstaller: @unchecked Sendable {
    private let lock = NSLock()
    private(set) var subdomain: String?
    private(set) var deviceName: String?

    func install(
        _ client: DeviceEnrollmentClient, _ subdomain: String, _ deviceName: String
    ) async throws -> ClientCertificateSummary? {
        lock.withLock { self.subdomain = subdomain; self.deviceName = deviceName }
        _ = try await client.enroll(csrDER: Data([0xAB]), subdomain: subdomain, deviceName: deviceName)
        return ClientCertificateSummary(
            subjectCommonName: deviceName, issuerCommonName: "webterm-device-ca", notAfter: nil
        )
    }
}

@Test("flow logs in then enrolls with the login-minted bearer (end to end)")
func flowEndToEnd() async throws {
    // Arrange
    let transport = RoutingStubTransport(loginBody: loginResponseBody(enrollToken: "TOKEN-XYZ"))
    let installer = SpyInstaller()
    let flow = DeviceEnrollmentFlow(
        baseURL: flowBaseURL,
        transport: transport,
        install: installer.install
    )

    // Act
    let summary = try await flow.run(password: "pw", subdomain: "alice", deviceName: "Alice iPhone")

    // Assert — login happened, THEN enroll with the exact bearer from login.
    #expect(transport.request(forPathSuffix: "/auth/login") != nil)
    let enroll = try #require(transport.request(forPathSuffix: "/device/enroll"))
    #expect(enroll.value(forHTTPHeaderField: "Authorization") == "Bearer TOKEN-XYZ")
    #expect(installer.subdomain == "alice")
    #expect(installer.deviceName == "Alice iPhone")
    #expect(summary?.subjectCommonName == "Alice iPhone")
}

@Test("a login failure short-circuits: enroll is never attempted")
func flowLoginFailureShortCircuits() async {
    // 401 login → the flow must NOT reach /device/enroll.
    let body = try! JSONSerialization.data(withJSONObject: ["error": "login rejected"])
    let transport = RoutingStubTransport(loginStatus: 401, loginBody: body)
    let installer = SpyInstaller()
    let flow = DeviceEnrollmentFlow(
        baseURL: flowBaseURL, transport: transport, install: installer.install
    )

    await #expect(throws: ControlPlaneLoginError.http(status: 401, code: "login rejected")) {
        _ = try await flow.run(password: "wrong", subdomain: "alice", deviceName: "d")
    }
    #expect(transport.request(forPathSuffix: "/device/enroll") == nil)
    #expect(installer.subdomain == nil) // installer never ran
}

@Test("an empty password is rejected before any network")
func flowEmptyPassword() async {
    let transport = RoutingStubTransport(loginBody: loginResponseBody(enrollToken: "t"))
    let flow = DeviceEnrollmentFlow(
        baseURL: flowBaseURL, transport: transport, install: { _, _, _ in nil }
    )
    await #expect(throws: ControlPlaneLoginError.emptyPassword) {
        _ = try await flow.run(password: "", subdomain: "alice", deviceName: "d")
    }
    #expect(transport.requests.isEmpty)
}
