import Foundation
import Testing

import NucleicProtocol

@testable import NucleicCore

/// The remote sign-in coordinator (docs/REMOTE_AGENT_LOGIN.md §4), driven end to end through
/// the REAL Codex broker with an injected transport and a temp auth file — so these tests prove
/// the whole host half: begin → challenge (URL, capture spec, state) → callback → exchange →
/// 0600 persistence → result, plus the guardrails (port gate, per-provider busy gate, cancel,
/// state mismatch, unavailable provider). Keychain-free by construction: Claude is exercised
/// only where it fails before persistence.
@Suite struct AgentLoginCoordinatorTests {
    /// Collects the host messages the coordinator pushes at the "device".
    private actor Outbox {
        private(set) var messages: [HostMsg] = []

        func append(_ msg: HostMsg) {
            messages.append(msg)
        }

        /// Poll until `count` messages arrived (10 s cap so a hang fails, not wedges, the test).
        func waitFor(count: Int) async -> [HostMsg] {
            let deadline = ContinuousClock.now.advanced(by: .seconds(10))
            while messages.count < count, ContinuousClock.now < deadline {
                try? await Task.sleep(for: .milliseconds(10))
            }
            return messages
        }

        var challenge: WireAgentLoginChallenge? {
            for case .agentLoginChallenge(let c) in messages { return c }
            return nil
        }
        var result: WireAgentLoginResult? {
            for case .agentLoginResult(let r) in messages { return r }
            return nil
        }
    }

    /// A JWT with OpenAI's account claim, as `id_token` carries it.
    private func idToken(accountID: String) -> String {
        let header = CodexOAuth.base64URL(Data(#"{"alg":"none"}"#.utf8))
        let payload = CodexOAuth.base64URL(try! JSONSerialization.data(
            withJSONObject: ["https://api.openai.com/auth": ["chatgpt_account_id": accountID]]))
        return "\(header).\(payload).sig"
    }

    private func tokenResponse() -> Data {
        try! JSONSerialization.data(withJSONObject: [
            "access_token": "acc", "refresh_token": "ref",
            "id_token": idToken(accountID: "acct_1"),
        ])
    }

    private func codexBroker(
        authFileURL: URL, timeoutNanos: UInt64 = 10 * 1_000_000_000
    ) -> CodexCredentialBroker {
        let body = tokenResponse()
        return CodexCredentialBroker(
            oauth: CodexOAuth(transport: { request in
                (body, HTTPURLResponse(
                    url: request.url!, statusCode: 200, httpVersion: nil, headerFields: nil)!)
            }),
            loginTimeoutNanos: timeoutNanos,
            authFileURL: authFileURL)
    }

    private func tempAuthFile() -> URL {
        FileManager.default.temporaryDirectory
            .appendingPathComponent("agent-login-\(UUID().uuidString)")
            .appendingPathComponent("auth.json")
    }

    private func state(fromAuthorizeURL url: String) -> String? {
        URLComponents(string: url)?.queryItems?.first { $0.name == "state" }?.value
    }

    @Test func codexFlowEndToEnd() async throws {
        let authFile = tempAuthFile()
        let coordinator = AgentLoginCoordinator(
            claude: nil, codex: codexBroker(authFileURL: authFile))
        let outbox = Outbox()

        let run = Task {
            await coordinator.run(
                WireAgentLoginBegin(requestID: "r1", provider: .codex, boundLoopbackPort: 1455),
                send: { await outbox.append($0) })
        }

        _ = await outbox.waitFor(count: 1)
        let challenge = try #require(await outbox.challenge)
        #expect(challenge.requestID == "r1")
        #expect(challenge.capture == .loopback(port: 1455, path: "/auth/callback"))
        #expect(challenge.authorizeURL.hasPrefix("https://auth.openai.com/oauth/authorize"))
        let state = try #require(state(fromAuthorizeURL: challenge.authorizeURL))

        await coordinator.deliverCallback(WireAgentLoginCallback(
            requestID: "r1", code: "authcode", state: state))

        #expect(await run.value)
        let result = try #require(await outbox.result)
        #expect(result.succeeded)
        // The credential landed exactly where every codex session and the mesh read it, 0600.
        let json = try String(contentsOf: authFile, encoding: .utf8)
        #expect(CodexAuthFile.isValidLogin(json))
        let mode = try FileManager.default.attributesOfItem(
            atPath: authFile.path)[.posixPermissions] as? NSNumber
        #expect(mode?.uint16Value == 0o600)
    }

    @Test func codexRequiresTheFixedPort() async throws {
        let coordinator = AgentLoginCoordinator(
            claude: nil, codex: codexBroker(authFileURL: tempAuthFile()))
        let outbox = Outbox()
        let ok = await coordinator.run(
            WireAgentLoginBegin(requestID: "r1", provider: .codex, boundLoopbackPort: nil),
            send: { await outbox.append($0) })
        #expect(!ok)
        let result = try #require(await outbox.result)
        #expect(result.error?.contains("1455") == true)
        // Never pushed a challenge it can't complete.
        #expect(await outbox.challenge == nil)
    }

    @Test func stateMismatchFailsWithoutExchanging() async throws {
        let authFile = tempAuthFile()
        let coordinator = AgentLoginCoordinator(
            claude: nil, codex: codexBroker(authFileURL: authFile))
        let outbox = Outbox()
        let run = Task {
            await coordinator.run(
                WireAgentLoginBegin(requestID: "r1", provider: .codex, boundLoopbackPort: 1455),
                send: { await outbox.append($0) })
        }
        _ = await outbox.waitFor(count: 1)
        await coordinator.deliverCallback(WireAgentLoginCallback(
            requestID: "r1", code: "authcode", state: "forged"))
        #expect(!(await run.value))
        let result = try #require(await outbox.result)
        #expect(result.error?.contains("did not match") == true)
        #expect(!FileManager.default.fileExists(atPath: authFile.path))
    }

    @Test func cancelFailsThePendingAttempt() async throws {
        let coordinator = AgentLoginCoordinator(
            claude: nil, codex: codexBroker(authFileURL: tempAuthFile()))
        let outbox = Outbox()
        let run = Task {
            await coordinator.run(
                WireAgentLoginBegin(requestID: "r1", provider: .codex, boundLoopbackPort: 1455),
                send: { await outbox.append($0) })
        }
        _ = await outbox.waitFor(count: 1)
        await coordinator.cancel(requestID: "r1")
        #expect(!(await run.value))
        let result = try #require(await outbox.result)
        #expect(result.succeeded == false)
    }

    @Test func secondAttemptForBusyProviderFailsFast() async throws {
        let coordinator = AgentLoginCoordinator(
            claude: nil, codex: codexBroker(authFileURL: tempAuthFile()))
        let outbox = Outbox()
        let first = Task {
            await coordinator.run(
                WireAgentLoginBegin(requestID: "r1", provider: .codex, boundLoopbackPort: 1455),
                send: { await outbox.append($0) })
        }
        _ = await outbox.waitFor(count: 1)

        let secondOutbox = Outbox()
        let ok = await coordinator.run(
            WireAgentLoginBegin(requestID: "r2", provider: .codex, boundLoopbackPort: 1455),
            send: { await secondOutbox.append($0) })
        #expect(!ok)
        let rejection = try #require(await secondOutbox.result)
        #expect(rejection.requestID == "r2")
        #expect(rejection.error?.contains("already in progress") == true)

        await coordinator.cancel(requestID: "r1")
        _ = await first.value
    }

    @Test func unavailableProviderAnswersImmediately() async throws {
        let coordinator = AgentLoginCoordinator(claude: nil, codex: nil)
        let outbox = Outbox()
        let ok = await coordinator.run(
            WireAgentLoginBegin(requestID: "r1", provider: .claude),
            send: { await outbox.append($0) })
        #expect(!ok)
        #expect(await outbox.result?.error?.contains("unavailable") == true)

        let unknownOutbox = Outbox()
        _ = await coordinator.run(
            WireAgentLoginBegin(
                requestID: "r2", provider: AgentLoginProvider(rawValue: "hal9000")),
            send: { await unknownOutbox.append($0) })
        #expect(await unknownOutbox.result?.succeeded == false)
    }

    @Test func brokerDeadlineBackstopsAnAbandonedAttempt() async throws {
        // 50 ms deadline, no callback ever: the flow must resolve (failed), not wedge — the
        // race in `remoteCaptureWithDeadline` + the cancellation handler in `present` is what
        // unblocks the suspended capture.
        let coordinator = AgentLoginCoordinator(
            claude: nil,
            codex: codexBroker(authFileURL: tempAuthFile(), timeoutNanos: 50_000_000))
        let outbox = Outbox()
        let ok = await coordinator.run(
            WireAgentLoginBegin(requestID: "r1", provider: .codex, boundLoopbackPort: 1455),
            send: { await outbox.append($0) })
        #expect(!ok)
        #expect(await outbox.result?.succeeded == false)
    }
}
