import Foundation
import Testing

@testable import NucleicCore

@Suite("Orchestra worker coordinator")
struct OrchestraWorkerCoordinatorTests {
    private func worker(_ id: String) -> SessionID { SessionID(rawValue: id) }

    /// A completed worker surfaces as a `completed` event and drops out of `remaining_running`.
    @Test func superviseDrainsCompletedEvent() async {
        let coord = OrchestraWorkerCoordinator()
        let w = worker("w1")
        await coord.register(workerID: w, task: "scan")
        await coord.complete(workerID: w, model: "m", output: "done")

        let result = await coord.supervise()
        #expect(result.remainingRunning == 0)
        guard case .completed(let id, let task, let model, let output)? = result.events.first else {
            Issue.record("expected a completed event, got \(result.events)")
            return
        }
        #expect(id == w)
        #expect(task == "scan")
        #expect(model == "m")
        #expect(output == "done")
    }

    /// A failed worker surfaces as a `failed` event.
    @Test func superviseDrainsFailedEvent() async {
        let coord = OrchestraWorkerCoordinator()
        let w = worker("w1")
        await coord.register(workerID: w, task: "scan")
        await coord.fail(workerID: w, message: "boom")

        let result = await coord.supervise()
        #expect(result.remainingRunning == 0)
        guard case .failed(let id, _, let message)? = result.events.first else {
            Issue.record("expected a failed event, got \(result.events)")
            return
        }
        #expect(id == w)
        #expect(message == "boom")
    }

    /// With no workers registered, `supervise` returns at once — the supervisor learns it is done.
    @Test func superviseReturnsEmptyWhenNothingRunning() async {
        let coord = OrchestraWorkerCoordinator()
        let result = await coord.supervise()
        #expect(result.events.isEmpty)
        #expect(result.remainingRunning == 0)
    }

    /// A worker's question surfaces to the supervisor, and the supervisor's reply resumes that exact
    /// worker with the answer. `supervise` blocks until the question arrives, so no polling is needed.
    @Test func askSurfacesQuestionAndReplyResumesTheWorker() async {
        let coord = OrchestraWorkerCoordinator()
        let w = worker("w1")
        await coord.register(workerID: w, task: "scan")

        let asked = Task { await coord.ask(workerID: w, question: "which path?") }

        let result = await coord.supervise()  // blocks until the question is raised
        guard case .question(let id, let task, let question)? = result.events.first else {
            Issue.record("expected a question event, got \(result.events)")
            return
        }
        #expect(id == w)
        #expect(task == "scan")
        #expect(question == "which path?")
        #expect(result.remainingRunning == 1)  // still running — parked awaiting the reply

        let replyResult = await coord.reply(workerID: w, reply: "the left one")
        #expect(replyResult == .ok)

        let answer = await asked.value
        #expect(answer == .answered("the left one"))
    }

    /// Replying to a worker that isn't waiting is a no-op `unknown`, not a crash.
    @Test func replyToUnknownWorkerReportsUnknown() async {
        let coord = OrchestraWorkerCoordinator()
        let result = await coord.reply(workerID: worker("ghost"), reply: "hi")
        guard case .unknown = result else {
            Issue.record("expected .unknown, got \(result)")
            return
        }
    }

    /// Teardown force-drains: a worker parked in `ask` is answered "unavailable" (so its call can
    /// unwind rather than hang), and teardown reports the worker as live so its run task gets
    /// cancelled by the caller.
    @Test func teardownAnswersPendingQuestionUnavailable() async {
        let coord = OrchestraWorkerCoordinator()
        let w = worker("w1")
        await coord.register(workerID: w, task: "scan")

        let asked = Task { await coord.ask(workerID: w, question: "wait for me?") }
        _ = await coord.supervise()  // ensure the question is registered before teardown

        let live = await coord.teardown()
        #expect(live.contains(w))

        let answer = await asked.value
        guard case .unavailable = answer else {
            Issue.record("expected .unavailable after teardown, got \(answer)")
            return
        }
    }

    /// After teardown, a fresh `ask` short-circuits to unavailable rather than parking forever.
    @Test func askAfterTeardownIsImmediatelyUnavailable() async {
        let coord = OrchestraWorkerCoordinator()
        _ = await coord.teardown()
        let answer = await coord.ask(workerID: worker("w1"), question: "anyone?")
        guard case .unavailable = answer else {
            Issue.record("expected .unavailable, got \(answer)")
            return
        }
    }
}
