import Foundation
import Testing

@testable import NucleicCore

/// End-to-end tests for the app-scoped `/shell-event` listener (docs/NASH.md §7.7.3, §10.6):
/// events reach the sink tagged with the registering spawn's synthetic origin id, and the
/// listener refuses a wrong or retired bearer token. Real loopback HTTP — the wire nash speaks.
@Suite struct HostShellEventListenerTests {

    private func post(
        _ body: [String: Any], to url: String, token: String
    ) async throws -> Int {
        var request = URLRequest(url: URL(string: url)!)
        request.httpMethod = "POST"
        request.httpBody = try JSONSerialization.data(withJSONObject: body)
        request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
        request.setValue("application/json", forHTTPHeaderField: "Content-Type")
        let (_, response) = try await URLSession.shared.data(for: request)
        return (response as? HTTPURLResponse)?.statusCode ?? -1
    }

    private func execBatch() -> [String: Any] {
        [
            "type": "shell-batch", "source": "nash", "shellId": "ab-12",
            "events": [
                [
                    "kind": "exec", "seq": 1, "ts": 1,
                    "argv": ["swift", "build"], "cwd": "/proj", "exitCode": 0, "durationMs": 5,
                ]
            ],
        ]
    }

    @Test func deliversEventsTaggedWithOriginAndRefusesBadTokens() async throws {
        let listener = HostShellEventListener()

        // No sink installed yet → registration refuses (nothing binds without a consumer).
        #expect(await listener.register(origin: "buildrun:p1") == nil)

        // A tiny async inbox so the test can await delivery without polling shared state.
        let inbox = AsyncStream.makeStream(of: (String, [MCPApprovalServer.ShellReportCall]).self)
        await listener.install { origin, calls in
            inbox.continuation.yield((origin, calls))
        }

        let registration = try #require(await listener.register(origin: "buildrun:p1"))
        #expect(registration.hookURL.contains("127.0.0.1"))
        #expect(registration.hookURL.hasSuffix(MCPApprovalServer.shellEventPath))

        // A wrong token is refused outright (401) and delivers nothing.
        #expect(try await post(execBatch(), to: registration.hookURL, token: "forged") == 401)

        // The real token 202s and the event lands in the sink, tagged with the origin.
        #expect(
            try await post(execBatch(), to: registration.hookURL, token: registration.token) == 202)
        var iterator = inbox.stream.makeAsyncIterator()
        let delivered = try #require(await iterator.next())
        #expect(delivered.0 == "buildrun:p1")
        #expect(delivered.1.count == 1)
        #expect(
            delivered.1.first?.kind
                == .exec(
                    argv: ["swift", "build"], cwd: "/proj", exitCode: 0, durationMs: 5,
                    pipeline: nil))

        // Retiring the token closes the door: the same token now 401s (no spool — §7.7.3).
        await listener.unregister(token: registration.token)
        #expect(
            try await post(execBatch(), to: registration.hookURL, token: registration.token) == 401)
    }

    @Test func concurrentSpawnsKeepDistinctOrigins() async throws {
        let listener = HostShellEventListener()
        let inbox = AsyncStream.makeStream(of: (String, [MCPApprovalServer.ShellReportCall]).self)
        await listener.install { origin, calls in
            inbox.continuation.yield((origin, calls))
        }
        let a = try #require(await listener.register(origin: "buildrun:alpha"))
        let b = try #require(await listener.register(origin: "buildrun:beta"))
        #expect(a.token != b.token)  // per-spawn tokens, never shared

        #expect(try await post(execBatch(), to: b.hookURL, token: b.token) == 202)
        var iterator = inbox.stream.makeAsyncIterator()
        let delivered = try #require(await iterator.next())
        #expect(delivered.0 == "buildrun:beta")  // attribution follows the token, not arrival order
    }
}
