import Foundation
import Testing
@testable import OpenClaw
@testable import OpenClawKit

private final class ControlUIEndpointSource: @unchecked Sendable {
    private let lock = NSLock()
    private var endpoint: GatewayConnection.EndpointSnapshot

    init(_ endpoint: GatewayConnection.EndpointSnapshot) {
        self.endpoint = endpoint
    }

    func set(_ endpoint: GatewayConnection.EndpointSnapshot) {
        self.lock.lock()
        self.endpoint = endpoint
        self.lock.unlock()
    }

    func snapshot() -> GatewayConnection.EndpointSnapshot {
        self.lock.lock()
        defer { self.lock.unlock() }
        return self.endpoint
    }
}

private func makeControlUIAuthSession(
    issuedDeviceToken: String? = nil) -> GatewayTestWebSocketSession
{
    GatewayTestWebSocketSession(taskFactory: {
        GatewayTestWebSocketTask(
            sendHook: { task, message, sendIndex in
                guard sendIndex > 0,
                      let id = GatewayWebSocketTestSupport.requestID(from: message)
                else { return }
                task.emitReceiveSuccess(.data(GatewayWebSocketTestSupport.okResponseData(id: id)))
            },
            receiveHook: { task, receiveIndex in
                if receiveIndex == 0 {
                    return .data(GatewayWebSocketTestSupport.connectChallengeData())
                }
                let id = task.snapshotConnectRequestID() ?? "connect"
                return .data(GatewayWebSocketTestSupport.connectOkData(
                    id: id,
                    deviceToken: issuedDeviceToken))
            })
    })
}

@Suite(.serialized)
struct GatewayConnectionControlUIAuthTests {
    @Test func `shared token requires the current live route and socket`() async throws {
        let routeA: GatewayConnection.Config = (
            url: try #require(URL(string: "ws://route-a.invalid")),
            token: " shared-token ",
            password: nil)
        let source = ControlUIEndpointSource(.init(
            config: routeA,
            routeAuthority: 1,
            deviceAuthGatewayID: "route-a"))
        let connection = GatewayConnection(
            endpointProvider: { source.snapshot() },
            sessionBox: WebSocketSessionBox(session: makeControlUIAuthSession()))

        #expect(await connection.controlUiAutoAuthToken(config: routeA) == nil)
        _ = try await connection.request(
            method: "health",
            params: nil,
            retryTransportFailures: false)
        #expect(await connection.controlUiAutoAuthToken(config: routeA) == "shared-token")

        let routeB: GatewayConnection.Config = (
            url: try #require(URL(string: "ws://route-b.invalid")),
            token: routeA.token,
            password: nil)
        source.set(.init(
            config: routeB,
            routeAuthority: 2,
            deviceAuthGatewayID: "route-b"))

        // The old socket is still physically alive, but neither the old nor
        // the newly selected route may borrow its credential.
        #expect(await connection.controlUiAutoAuthToken(config: routeA) == nil)
        #expect(await connection.controlUiAutoAuthToken(config: routeB) == nil)
        await connection.shutdown()
    }

    @Test func `device auto auth reads only the live route scoped token`() async throws {
        let stateDir = FileManager.default.temporaryDirectory
            .appendingPathComponent(UUID().uuidString, isDirectory: true)
        try FileManager.default.createDirectory(at: stateDir, withIntermediateDirectories: true)
        defer { try? FileManager.default.removeItem(at: stateDir) }

        try await DeviceIdentityStore.withStateDirectory(stateDir) {
            let identity = DeviceIdentityStore.loadOrCreate()
            _ = DeviceAuthStore.storeToken(
                deviceId: identity.deviceId,
                role: "operator",
                token: "legacy-unscoped-token")
            _ = DeviceAuthStore.storeToken(
                deviceId: identity.deviceId,
                role: "operator",
                token: "route-a-device-token",
                gatewayID: "route-a")

            let routeA: GatewayConnection.Config = (
                url: try #require(URL(string: "ws://route-a.invalid")),
                token: nil,
                password: nil)
            let routeAConnection = GatewayConnection(
                endpointProvider: {
                    .init(
                        config: routeA,
                        routeAuthority: 1,
                        deviceAuthGatewayID: "route-a")
                },
                sessionBox: WebSocketSessionBox(session: makeControlUIAuthSession()))
            _ = try await routeAConnection.request(
                method: "health",
                params: nil,
                retryTransportFailures: false)
            #expect(
                await routeAConnection.controlUiAutoAuthToken(config: routeA) ==
                    "route-a-device-token")
            await routeAConnection.shutdown()

            let routeB: GatewayConnection.Config = (
                url: try #require(URL(string: "ws://route-b.invalid")),
                token: nil,
                password: nil)
            let routeBConnection = GatewayConnection(
                endpointProvider: {
                    .init(
                        config: routeB,
                        routeAuthority: 2,
                        deviceAuthGatewayID: "route-b")
                },
                sessionBox: WebSocketSessionBox(session: makeControlUIAuthSession()))
            _ = try await routeBConnection.request(
                method: "health",
                params: nil,
                retryTransportFailures: false)
            #expect(await routeBConnection.controlUiAutoAuthToken(config: routeB) == nil)
            await routeBConnection.shutdown()
        }
    }

    @Test func `hello token cannot cross to a newly selected route`() async throws {
        let stateDir = FileManager.default.temporaryDirectory
            .appendingPathComponent(UUID().uuidString, isDirectory: true)
        try FileManager.default.createDirectory(at: stateDir, withIntermediateDirectories: true)
        defer { try? FileManager.default.removeItem(at: stateDir) }

        try await DeviceIdentityStore.withStateDirectory(stateDir) {
            let identity = DeviceIdentityStore.loadOrCreate()
            _ = DeviceAuthStore.storeToken(
                deviceId: identity.deviceId,
                role: "operator",
                token: "route-a-device-token",
                gatewayID: "route-a")
            let routeA: GatewayConnection.Config = (
                url: try #require(URL(string: "ws://route-a.invalid")),
                token: nil,
                password: nil)
            let source = ControlUIEndpointSource(.init(
                config: routeA,
                routeAuthority: 1,
                deviceAuthGatewayID: "route-a"))
            let connection = GatewayConnection(
                endpointProvider: { source.snapshot() },
                sessionBox: WebSocketSessionBox(session: makeControlUIAuthSession(
                    issuedDeviceToken: "route-a-issued-token")))

            _ = try await connection.request(
                method: "health",
                params: nil,
                retryTransportFailures: false)
            #expect(
                await connection.controlUiAutoAuthToken(config: routeA) ==
                    "route-a-issued-token")

            source.set(.init(
                config: routeA,
                routeAuthority: 1,
                deviceAuthGatewayID: "route-b"))
            #expect(await connection.controlUiAutoAuthToken(config: routeA) == nil)

            let routeB: GatewayConnection.Config = (
                url: try #require(URL(string: "ws://route-b.invalid")),
                token: nil,
                password: nil)
            source.set(.init(
                config: routeB,
                routeAuthority: 2,
                deviceAuthGatewayID: "route-b"))
            #expect(await connection.controlUiAutoAuthToken(config: routeA) == nil)
            #expect(await connection.controlUiAutoAuthToken(config: routeB) == nil)
            await connection.shutdown()
        }
    }
}
