import Foundation
import Testing

@testable import NucleicCore

@Suite struct ApprovalCoordinatorTests {
    func makeRequest(
        id: ApprovalID = .generate(),
        toolName: String = "Bash",
        input: JSONValue = ["command": "ls"]
    ) -> ApprovalRequest {
        ApprovalRequest(
            id: id,
            sessionID: SessionID(rawValue: "test-session"),
            toolCallID: "toolu_1",
            toolName: toolName,
            input: input,
            title: "Run: ls",
            risk: .execute,
            createdAt: Date(timeIntervalSince1970: 0))
    }

    @Test func suspendsUntilResolved() async {
        let coordinator = ApprovalCoordinator()
        let request = makeRequest()

        async let resolved = coordinator.waitForDecision(request)
        // Let the waiter park, then resolve.
        try? await Task.sleep(for: .milliseconds(20))
        let outcome = await coordinator.resolve(request.id, .allow(), by: "mac-ui")
        #expect(outcome == .resolved)

        let result = await resolved
        #expect(result.decision == .allow())
        #expect(result.decidedBy == "mac-ui")
    }

    @Test func firstResponderWins() async {
        let coordinator = ApprovalCoordinator()
        let request = makeRequest()

        async let resolved = coordinator.waitForDecision(request)
        try? await Task.sleep(for: .milliseconds(20))

        let first = await coordinator.resolve(request.id, .allow(), by: "mac-ui")
        let second = await coordinator.resolve(request.id, .deny(reason: "late"), by: "iphone:abc")
        #expect(first == .resolved)
        #expect(second == .alreadyResolved)

        let result = await resolved
        #expect(result.decision == .allow())
        #expect(result.decidedBy == "mac-ui")
    }

    @Test func unknownIDReported() async {
        let coordinator = ApprovalCoordinator()
        let outcome = await coordinator.resolve(.generate(), .allow(), by: "mac-ui")
        #expect(outcome == .unknownID)
    }

    @Test func allowAlwaysToolNameShortCircuitsSubsequentCalls() async {
        let coordinator = ApprovalCoordinator()
        let request = makeRequest(toolName: "Bash", input: ["command": "git status"])

        async let resolved = coordinator.waitForDecision(request)
        try? await Task.sleep(for: .milliseconds(20))
        await coordinator.resolve(request.id, .allowAlways(.toolName), by: "mac-ui")
        _ = await resolved

        // Any Bash call now auto-resolves without surfacing…
        let cached = await coordinator.cachedDecision(
            toolName: "Bash", input: ["command": "git diff"])
        #expect(cached?.decidedBy == "always-rule")
        #expect(cached?.decision == .allow())

        // …but other tools still ask.
        let other = await coordinator.cachedDecision(toolName: "Write", input: [:])
        #expect(other == nil)
    }

    @Test func sessionScopeOnlyMatchesIdenticalInput() async {
        let coordinator = ApprovalCoordinator()
        let input: JSONValue = ["command": "make test"]
        let request = makeRequest(toolName: "Bash", input: input)

        async let resolved = coordinator.waitForDecision(request)
        try? await Task.sleep(for: .milliseconds(20))
        await coordinator.resolve(request.id, .allowAlways(.session), by: "mac-ui")
        _ = await resolved

        let same = await coordinator.cachedDecision(toolName: "Bash", input: input)
        #expect(same != nil)
        let different = await coordinator.cachedDecision(
            toolName: "Bash", input: ["command": "make deploy"])
        #expect(different == nil)
    }

    @Test func cancelOutstandingDeniesAllPending() async {
        let coordinator = ApprovalCoordinator()
        let a = makeRequest()
        let b = makeRequest()

        async let resolvedA = coordinator.waitForDecision(a)
        async let resolvedB = coordinator.waitForDecision(b)
        try? await Task.sleep(for: .milliseconds(20))
        await coordinator.cancelOutstanding(reason: "Session terminated")

        let (ra, rb) = await (resolvedA, resolvedB)
        #expect(ra.decision == .deny(reason: "Session terminated"))
        #expect(rb.decision == .deny(reason: "Session terminated"))
        #expect(ra.decidedBy == "system")
    }
}
