import Foundation
import Testing

@testable import NucleicCore

/// Exercises the in-process MCP server over real HTTP (URLSession ↔ NWListener),
/// the same path the claude CLI uses (ADAPTERS §1.2).
@Suite struct MCPApprovalServerTests {
    func post(_ body: JSONValue, port: UInt16, token: String?) async throws -> (
        status: Int, body: JSONValue?
    ) {
        var request = URLRequest(url: URL(string: "http://127.0.0.1:\(port)/mcp")!)
        request.httpMethod = "POST"
        request.setValue("application/json", forHTTPHeaderField: "Content-Type")
        if let token {
            request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
        }
        request.httpBody = try body.encodedData()
        let (data, response) = try await URLSession.shared.data(for: request)
        let status = (response as? HTTPURLResponse)?.statusCode ?? -1
        return (status, try? JSONValue(parsing: data))
    }

    /// POST to the dedicated `/git-event` route the in-container git shim reports to.
    func postGitEvent(_ body: JSONValue, port: UInt16, token: String?) async throws -> Int {
        var request = URLRequest(
            url: URL(string: "http://127.0.0.1:\(port)\(MCPApprovalServer.gitEventPath)")!)
        request.httpMethod = "POST"
        request.setValue("application/json", forHTTPHeaderField: "Content-Type")
        if let token {
            request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
        }
        request.httpBody = try body.encodedData()
        let (_, response) = try await URLSession.shared.data(for: request)
        return (response as? HTTPURLResponse)?.statusCode ?? -1
    }

    @Test func gitEventRouteRecordsReportAndRejectsBadToken() async throws {
        let server = MCPApprovalServer()
        let port = try await server.start()

        actor Captured {
            private(set) var argv: [String]?
            private(set) var exitCode: Int?
            func record(_ c: MCPApprovalServer.GitReportCall) { argv = c.argv; exitCode = c.exitCode }
        }
        let captured = Captured()
        await server.registerGitReport(token: "gtok") { call in
            Task { await captured.record(call) }
        }

        let body = JSONValue.object([
            "argv": .array([.string("merge"), .string("feature/x")]),
            "cwd": .string("/repo"),
            "exitCode": .number(0),
            "subcommand": .string("merge"),
            "sessionId": .string("s1"),
        ])
        // A registered token is accepted (202) and the report reaches the handler.
        #expect(try await postGitEvent(body, port: port, token: "gtok") == 202)
        var tries = 0
        while await captured.argv == nil && tries < 200 {
            try await Task.sleep(for: .milliseconds(10)); tries += 1
        }
        #expect(await captured.argv == ["merge", "feature/x"])
        #expect(await captured.exitCode == 0)

        // An unknown bearer token is rejected.
        #expect(try await postGitEvent(body, port: port, token: "wrong") == 401)
    }

    /// POST to the dedicated `/gh-event` route the in-container gh shim reports to.
    func postGhEvent(_ body: JSONValue, port: UInt16, token: String?) async throws -> Int {
        var request = URLRequest(
            url: URL(string: "http://127.0.0.1:\(port)\(MCPApprovalServer.ghEventPath)")!)
        request.httpMethod = "POST"
        request.setValue("application/json", forHTTPHeaderField: "Content-Type")
        if let token {
            request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
        }
        request.httpBody = try body.encodedData()
        let (_, response) = try await URLSession.shared.data(for: request)
        return (response as? HTTPURLResponse)?.statusCode ?? -1
    }

    /// POST to the dedicated `/command-event` route the non-git interceptor reports to.
    func postCommandEvent(_ body: JSONValue, port: UInt16, token: String?) async throws -> Int {
        var request = URLRequest(
            url: URL(string: "http://127.0.0.1:\(port)\(MCPApprovalServer.commandEventPath)")!)
        request.httpMethod = "POST"
        request.setValue("application/json", forHTTPHeaderField: "Content-Type")
        if let token {
            request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
        }
        request.httpBody = try body.encodedData()
        let (_, response) = try await URLSession.shared.data(for: request)
        return (response as? HTTPURLResponse)?.statusCode ?? -1
    }

    @Test func ghEventRouteRecordsReportAndRejectsBadToken() async throws {
        let server = MCPApprovalServer()
        let port = try await server.start()

        actor Captured {
            private(set) var argv: [String]?
            func record(_ c: MCPApprovalServer.GhReportCall) { argv = c.argv }
        }
        let captured = Captured()
        await server.registerGhReport(token: "gtok") { call in
            Task { await captured.record(call) }
        }

        let body = JSONValue.object([
            "argv": .array([.string("pr"), .string("create"), .string("--title"), .string("Fix")]),
            "cwd": .string("/repo"),
            "exitCode": .number(0),
            "subcommand": .string("pr"),
            "sessionId": .string("s1"),
        ])
        #expect(try await postGhEvent(body, port: port, token: "gtok") == 202)
        var tries = 0
        while await captured.argv == nil && tries < 200 {
            try await Task.sleep(for: .milliseconds(10)); tries += 1
        }
        #expect(await captured.argv == ["pr", "create", "--title", "Fix"])

        #expect(try await postGhEvent(body, port: port, token: "wrong") == 401)
    }

    @Test func commandEventRouteRecordsShimAndBatchAndRejectsBadToken() async throws {
        let server = MCPApprovalServer()
        let port = try await server.start()
        defer { Task { await server.stop() } }

        actor Captured {
            private(set) var calls: [MCPApprovalServer.CommandReportCall] = []
            func record(_ c: MCPApprovalServer.CommandReportCall) { calls.append(c) }
        }
        let captured = Captured()
        await server.registerCommandReport(token: "ctok") { call in
            Task { await captured.record(call) }
        }

        // A single shim report carries argv + capped output and is fanned out to one call.
        let shim = JSONValue.object([
            "type": .string("command"), "source": .string("shim"),
            "command": .string("grep"),
            "argv": .array([.string("grep"), .string("-rn"), .string("TODO")]),
            "cwd": .string("/repo"), "exitCode": .number(0), "durationMs": .number(12),
            "stdout": .string("src/a.swift:3:// TODO\n"), "stderr": .string(""),
            "truncated": .bool(false), "sessionId": .string("s1"),
        ])
        #expect(try await postCommandEvent(shim, port: port, token: "ctok") == 202)

        // A tracer batch fans out to one call per event.
        let batch = JSONValue.object([
            "type": .string("command-batch"), "source": .string("tracer"),
            "sessionId": .string("s1"),
            "events": .array([
                .object(["line": .string("npm install"), "cwd": .string("/repo"),
                         "exitCode": .number(0), "durationMs": .number(900)]),
                .object(["line": .string("make build"), "cwd": .string("/repo"),
                         "exitCode": .number(2), "durationMs": .number(1500)]),
            ]),
        ])
        #expect(try await postCommandEvent(batch, port: port, token: "ctok") == 202)

        var tries = 0
        while await captured.calls.count < 3 && tries < 200 {
            try await Task.sleep(for: .milliseconds(10)); tries += 1
        }
        let calls = await captured.calls
        #expect(calls.count == 3)
        let shimCall = try #require(calls.first { $0.source == .shim })
        #expect(shimCall.argv == ["grep", "-rn", "TODO"])
        #expect(shimCall.stdout.contains("TODO"))
        #expect(shimCall.durationMs == 12)
        let tracerCalls = calls.filter { $0.source == .tracer }
        #expect(tracerCalls.count == 2)
        #expect(tracerCalls.contains { $0.commandLine == "make build" && $0.exitCode == 2 })

        // An unknown bearer token is rejected.
        #expect(try await postCommandEvent(shim, port: port, token: "wrong") == 401)
    }

    @Test func fullHandshakeAndApprovalCall() async throws {
        let server = MCPApprovalServer()
        let port = try await server.start()
        defer { Task { await server.stop() } }
        #expect(port > 0)

        await server.register(token: "tok-1") { call in
            #expect(call.toolName == "Bash")
            #expect(call.input == ["command": "rm -rf build/"])
            #expect(call.toolUseID == "toolu_42")
            return .allow(updatedInput: call.input)
        }

        let initialize = try await post(
            [
                "jsonrpc": "2.0", "id": 1, "method": "initialize",
                "params": ["protocolVersion": "2025-06-18"],
            ], port: port, token: "tok-1")
        #expect(initialize.status == 200)
        #expect(initialize.body?["result"]?["protocolVersion"]?.stringValue == "2025-06-18")
        #expect(initialize.body?["result"]?["serverInfo"]?["name"]?.stringValue == "nucleic-approval")

        let notification = try await post(
            ["jsonrpc": "2.0", "method": "notifications/initialized"], port: port, token: "tok-1")
        #expect(notification.status == 202)

        let toolsList = try await post(
            ["jsonrpc": "2.0", "id": 2, "method": "tools/list"], port: port, token: "tok-1")
        #expect(
            toolsList.body?["result"]?["tools"]?[0]?["name"]?.stringValue == "approve")

        let call = try await post(
            [
                "jsonrpc": "2.0", "id": 3, "method": "tools/call",
                "params": [
                    "name": "approve",
                    "arguments": [
                        "tool_name": "Bash",
                        "input": ["command": "rm -rf build/"],
                        "tool_use_id": "toolu_42",
                    ],
                ],
            ], port: port, token: "tok-1")
        #expect(call.status == 200)
        let text = call.body?["result"]?["content"]?[0]?["text"]?.stringValue
        #expect(text == #"{"behavior":"allow","updatedInput":{"command":"rm -rf build/"}}"#)
    }

    @Test func advertisesAndServesCheckConflict() async throws {
        let server = MCPApprovalServer()
        let port = try await server.start()
        defer { Task { await server.stop() } }

        // Both handlers register under the same token (auth keys off the approve handler).
        await server.register(token: "tok-1") { _ in .deny(message: "n/a") }
        await server.registerConflict(token: "tok-1") { call in
            #expect(call.task == "rework auth")
            #expect(call.files == ["src/auth/Login.swift"])
            return MCPApprovalServer.ConflictReply(resolution: .deferred)
        }

        let toolsList = try await post(
            ["jsonrpc": "2.0", "id": 2, "method": "tools/list"], port: port, token: "tok-1")
        let names = (toolsList.body?["result"]?["tools"]?.arrayValue ?? [])
            .compactMap { $0["name"]?.stringValue }
        #expect(names.contains("approve"))
        #expect(names.contains("check_conflict"))

        let call = try await post(
            [
                "jsonrpc": "2.0", "id": 3, "method": "tools/call",
                "params": [
                    "name": "check_conflict",
                    "arguments": [
                        "task": "rework auth",
                        "files": ["src/auth/Login.swift"],
                    ],
                ],
            ], port: port, token: "tok-1")
        #expect(call.status == 200)
        let text = call.body?["result"]?["content"]?[0]?["text"]?.stringValue
        #expect(text == #"{"action":"deferred","conflict":true,"message":"This task was added to the Nucleic to-do list because it conflicts with another active agent. Stop now and do NOT work on it."}"#)
    }

    @Test func advertisesAndServesHostExecOnlyWhenRegistered() async throws {
        let server = MCPApprovalServer()
        let port = try await server.start()
        defer { Task { await server.stop() } }

        // Token without a host-exec handler: tool is hidden, and a direct call is refused.
        await server.register(token: "plain") { _ in .deny(message: "n/a") }
        let plainList = try await post(
            ["jsonrpc": "2.0", "id": 1, "method": "tools/list"], port: port, token: "plain")
        let plainNames = (plainList.body?["result"]?["tools"]?.arrayValue ?? [])
            .compactMap { $0["name"]?.stringValue }
        #expect(!plainNames.contains("host_exec"))

        let refused = try await post(
            [
                "jsonrpc": "2.0", "id": 2, "method": "tools/call",
                "params": ["name": "host_exec", "arguments": ["command": "echo hi"]],
            ], port: port, token: "plain")
        #expect(refused.body?["result"]?["content"]?[0]?["text"]?.stringValue
            == #"{"denied":true,"message":"Host execution is not enabled."}"#)

        // Token with a host-exec handler: tool is advertised and routed to the handler.
        await server.register(token: "host") { _ in .deny(message: "n/a") }
        await server.registerHostExec(token: "host") { call in
            #expect(call.command == "swift build && swift run Tool")
            return .ran(exitCode: 0, stdout: "built\n", stderr: "")
        }
        let hostList = try await post(
            ["jsonrpc": "2.0", "id": 3, "method": "tools/list"], port: port, token: "host")
        let hostNames = (hostList.body?["result"]?["tools"]?.arrayValue ?? [])
            .compactMap { $0["name"]?.stringValue }
        #expect(hostNames.contains("host_exec"))

        let call = try await post(
            [
                "jsonrpc": "2.0", "id": 4, "method": "tools/call",
                "params": [
                    "name": "host_exec",
                    "arguments": ["command": "swift build && swift run Tool"],
                ],
            ], port: port, token: "host")
        #expect(call.status == 200)
        let text = call.body?["result"]?["content"]?[0]?["text"]?.stringValue
        #expect(text == #"{"exit_code":0,"stderr":"","stdout":"built\n"}"#)
    }

    @Test func rejectsBadBearerToken() async throws {
        let server = MCPApprovalServer()
        let port = try await server.start()
        defer { Task { await server.stop() } }
        await server.register(token: "real-token") { _ in .deny(message: "n/a") }

        let wrong = try await post(
            ["jsonrpc": "2.0", "id": 1, "method": "initialize"], port: port, token: "wrong")
        #expect(wrong.status == 401)
        let missing = try await post(
            ["jsonrpc": "2.0", "id": 1, "method": "initialize"], port: port, token: nil)
        #expect(missing.status == 401)
    }

    @Test func unknownMethodIsRPCError() async throws {
        let server = MCPApprovalServer()
        let port = try await server.start()
        defer { Task { await server.stop() } }
        await server.register(token: "t") { _ in .deny(message: "n/a") }

        let response = try await post(
            ["jsonrpc": "2.0", "id": 9, "method": "resources/list"], port: port, token: "t")
        #expect(response.status == 200)
        #expect(response.body?["error"]?["code"]?.intValue == -32601)
    }

    // MARK: - Unix-domain-socket transport (vsock-relayed control plane)

    /// Raw HTTP/1.1 over a unix-domain socket via a plain `AF_UNIX` BSD client — what the framework's
    /// host-side relay (and an in-guest bridge) does once the control server is relayed over vsock.
    /// Sends one request with `Connection: close` so the server closes after responding and the
    /// client reads to EOF. Returns (status, parsed JSON body).
    func unixRequest(
        socketPath: String, method: String = "POST", route: String, token: String?, body: JSONValue
    ) async throws -> (status: Int, body: JSONValue?) {
        let bodyData = try body.encodedData()
        var head = "\(method) \(route) HTTP/1.1\r\nHost: nucleic\r\n"
        head += "Content-Type: application/json\r\n"
        if let token { head += "Authorization: Bearer \(token)\r\n" }
        head += "Content-Length: \(bodyData.count)\r\nConnection: close\r\n\r\n"
        var payload = Data(head.utf8)
        payload.append(bodyData)

        return try await withCheckedThrowingContinuation { cont in
            DispatchQueue.global(qos: .userInitiated).async {
                let fd = socket(AF_UNIX, SOCK_STREAM, 0)
                guard fd >= 0 else {
                    cont.resume(throwing: POSIXError(.EIO))
                    return
                }
                defer { close(fd) }

                var addr = sockaddr_un()
                addr.sun_family = sa_family_t(AF_UNIX)
                let pathBytes = Array(socketPath.utf8)
                let capacity = MemoryLayout.size(ofValue: addr.sun_path)
                withUnsafeMutablePointer(to: &addr.sun_path) { raw in
                    raw.withMemoryRebound(to: CChar.self, capacity: capacity) { dst in
                        for (i, b) in pathBytes.enumerated() { dst[i] = CChar(bitPattern: b) }
                        dst[pathBytes.count] = 0
                    }
                }
                let rc = withUnsafePointer(to: &addr) { ptr in
                    ptr.withMemoryRebound(to: sockaddr.self, capacity: 1) {
                        connect(fd, $0, socklen_t(MemoryLayout<sockaddr_un>.size))
                    }
                }
                guard rc == 0 else {
                    cont.resume(throwing: POSIXError(POSIXErrorCode(rawValue: errno) ?? .EIO))
                    return
                }

                payload.withUnsafeBytes { raw in
                    guard let base = raw.baseAddress else { return }
                    var off = 0
                    while off < raw.count {
                        let n = write(fd, base + off, raw.count - off)
                        if n > 0 { off += n } else { break }
                    }
                }

                var received = Data()
                var buf = [UInt8](repeating: 0, count: 1 << 16)
                while true {
                    let n = buf.withUnsafeMutableBytes { read(fd, $0.baseAddress, 1 << 16) }
                    if n > 0 { received.append(contentsOf: buf[0..<n]) } else { break }
                }

                guard let sep = received.range(of: Data("\r\n\r\n".utf8)),
                    let headText = String(
                        data: received.subdata(in: received.startIndex..<sep.lowerBound),
                        encoding: .utf8)
                else {
                    cont.resume(returning: (-1, nil))
                    return
                }
                let statusLine = headText.components(separatedBy: "\r\n").first ?? ""
                let status = Int(
                    statusLine.split(separator: " ").dropFirst().first.map(String.init) ?? "")
                let bodyBytes = received.subdata(in: sep.upperBound..<received.endIndex)
                cont.resume(returning: (status ?? -1, try? JSONValue(parsing: bodyBytes)))
            }
        }
    }

    @Test func unixSocketTransportServesMCPAndInterceptorRoutes() async throws {
        // AF_UNIX paths are capped (~104 bytes on macOS), so the control socket must live at a short
        // path — the long Application Support session path would overflow `sun_path`. (NB for the
        // real wiring: bind the per-session socket under a short dir like /tmp, not under ~/Library.)
        let socket = "/tmp/nuc-mcp-\(UUID().uuidString.prefix(8)).sock"

        let server = MCPApprovalServer()
        let bound = try await server.start(unixSocketPath: socket)
        defer { Task { await server.stop() } }
        #expect(bound == socket)
        #expect(await server.boundUnixSocketPath == socket)
        // A real on-disk AF_UNIX socket the framework relay (an ordinary socket client) can connect
        // to — not an in-process-only Network.framework endpoint.
        #expect(FileManager.default.fileExists(atPath: socket))
        var st = stat()
        #expect(stat(socket, &st) == 0)
        #expect((st.st_mode & S_IFMT) == S_IFSOCK)

        // MCP JSON-RPC over the UDS returns the same server info as the TCP path.
        await server.register(token: "mtok") { _ in .deny(message: "n/a") }
        let initResp = try await unixRequest(
            socketPath: socket, route: "/mcp", token: "mtok",
            body: ["jsonrpc": "2.0", "id": 1, "method": "initialize"])
        #expect(initResp.status == 200)
        #expect(initResp.body?["result"]?["serverInfo"]?["name"]?.stringValue == "nucleic-approval")

        // An unknown bearer token is still rejected over the UDS.
        let unauth = try await unixRequest(
            socketPath: socket, route: "/mcp", token: "wrong",
            body: ["jsonrpc": "2.0", "id": 1, "method": "initialize"])
        #expect(unauth.status == 401)

        // A dedicated interceptor route (`/git-event`) reaches its handler over the UDS and 202s.
        actor Captured {
            private(set) var argv: [String]?
            func record(_ c: MCPApprovalServer.GitReportCall) { argv = c.argv }
        }
        let captured = Captured()
        await server.registerGitReport(token: "gtok") { call in
            Task { await captured.record(call) }
        }
        let gitResp = try await unixRequest(
            socketPath: socket, route: MCPApprovalServer.gitEventPath, token: "gtok",
            body: [
                "argv": ["merge", "feature/x"], "cwd": "/repo", "exitCode": 0,
                "subcommand": "merge", "sessionId": "s1",
            ])
        #expect(gitResp.status == 202)
        var tries = 0
        while await captured.argv == nil && tries < 200 {
            try await Task.sleep(for: .milliseconds(10))
            tries += 1
        }
        #expect(await captured.argv == ["merge", "feature/x"])
    }

    // MARK: - ApprovalServerRegistry (container-scoped shared server)

    @Test func registrySharesOneServerPerContainer() async {
        let registry = ApprovalServerRegistry()
        let a1 = await registry.server(for: "nucleic-control")
        let a2 = await registry.server(for: "nucleic-control")
        let b = await registry.server(for: "nucleic-control-claude")
        #expect(a1 === a2)  // same container → the same shared, token-multiplexed server
        #expect(a1 !== b)  // different container → its own server
        await registry.teardown(containerName: "nucleic-control")
        let a3 = await registry.server(for: "nucleic-control")
        #expect(a3 !== a1)  // torn down → a fresh server next time
    }

    @Test func controlSocketPathIsShortAndStable() {
        let path = ApprovalServerRegistry.controlSocketPath(for: "nucleic-control-claude")
        #expect(path == "/tmp/nucleic/nucleic-control-claude.sock")
        #expect(path.utf8.count < 104)  // AF_UNIX sun_path cap
    }

    /// Concurrent `start(host:)` calls on the SAME shared server (the default once sessions share a
    /// control-container server) must all observe the real bound port — never the half-initialized
    /// `0` that the pre-fix reentrancy race could return, which baked an unreachable `:0` endpoint
    /// into a racing session's MCP config and stalled it. All callers must agree on one non-zero port.
    @Test func concurrentStartsAllReturnTheBoundPort() async throws {
        let server = MCPApprovalServer()
        let ports = await withTaskGroup(of: UInt16?.self) { group in
            for _ in 0..<8 {
                group.addTask { try? await server.start(host: "127.0.0.1") }
            }
            var seen: [UInt16] = []
            for await p in group { if let p { seen.append(p) } }
            return seen
        }
        await server.stop()
        #expect(ports.count == 8)  // every start resolved (none threw)
        #expect(!ports.contains(0))  // none got the racy port 0
        #expect(Set(ports).count == 1)  // and they all agree on the single bound port
    }
}
