import Foundation

@testable import NucleicCore

/// An in-process `AgentBackend` that emits a scripted `AgentEvent` sequence — no
/// subprocess, no wire protocol. The component-test analogue of `FakeBackend`
/// (OBSERVABILITY Part B): it drives the `SessionController` pipeline directly so
/// tests can script turns, approvals, and failures deterministically.
final class ScriptedBackend: AgentBackend, @unchecked Sendable {
    static let id = BackendID.claudeCode

    let capabilities = BackendCapabilities(
        interactiveApprovals: true,
        allowAlwaysScopes: [.session, .toolName],
        canModifyToolInput: true,
        partialMessageStreaming: false,
        emitsThinking: true,
        emitsFileChangeEvents: false,
        nativeResume: true,
        sandboxModes: [],
        followUpWhileRunning: false)

    /// The session's approval waiter — shared with the run body, resolved by `respond`.
    let approvals = ApprovalCoordinator()

    /// Emits canonical-shaped events into the live stream.
    final class Emitter: @unchecked Sendable {
        let sessionID: SessionID
        private let backend: BackendID
        private let continuation: AsyncThrowingStream<AgentEvent, Error>.Continuation
        private let now: @Sendable () -> Date
        private var seq: UInt64 = 0

        init(
            sessionID: SessionID, backend: BackendID,
            continuation: AsyncThrowingStream<AgentEvent, Error>.Continuation,
            now: @escaping @Sendable () -> Date
        ) {
            self.sessionID = sessionID
            self.backend = backend
            self.continuation = continuation
            self.now = now
        }

        func emit(_ kind: AgentEvent.Kind, nativeType: String? = nil) {
            seq += 1
            continuation.yield(
                AgentEvent(
                    sessionID: sessionID, seq: seq, at: now(),
                    backend: backend, nativeType: nativeType, kind: kind))
        }

        func fail(_ error: Error) { continuation.finish(throwing: error) }
    }

    /// Follow-up user turns delivered via `send(_:)`, for multi-turn scripts.
    let inputs: AsyncStream<AgentInput>
    private let inputContinuation: AsyncStream<AgentInput>.Continuation

    typealias Body = @Sendable (Emitter, ApprovalCoordinator) async -> Void
    typealias MultiTurnBody = @Sendable (Emitter, ApprovalCoordinator, AsyncStream<AgentInput>) async -> Void

    private let body: MultiTurnBody
    private let now: @Sendable () -> Date
    private let sendHandler: (@Sendable (AgentInput) async -> Void)?

    /// The most recent `RunSpec` handed to `start`, captured so tests can assert on the flags
    /// the controller derived from the session (effort, appendSystemPrompt, …).
    private let runLock = NSLock()
    private var capturedRun: RunSpec?
    var lastRun: RunSpec? {
        runLock.lock(); defer { runLock.unlock() }
        return capturedRun
    }

    convenience init(
        now: @escaping @Sendable () -> Date = { Date(timeIntervalSince1970: 1_700_000_000) },
        onSend: (@Sendable (AgentInput) async -> Void)? = nil,
        body: @escaping Body
    ) {
        self.init(now: now, onSend: onSend) { emitter, approvals, _ in
            await body(emitter, approvals)
        }
    }

    init(
        now: @escaping @Sendable () -> Date = { Date(timeIntervalSince1970: 1_700_000_000) },
        onSend: (@Sendable (AgentInput) async -> Void)? = nil,
        multiTurn body: @escaping MultiTurnBody
    ) {
        self.now = now
        self.sendHandler = onSend
        self.body = body
        (inputs, inputContinuation) = AsyncStream.makeStream(of: AgentInput.self)
    }

    func start(_ run: RunSpec) -> AsyncThrowingStream<AgentEvent, Error> {
        runLock.lock(); capturedRun = run; runLock.unlock()
        return AsyncThrowingStream { continuation in
            let emitter = Emitter(
                sessionID: run.sessionID, backend: Self.id, continuation: continuation, now: now)
            let task = Task {
                await body(emitter, approvals, inputs)
                continuation.finish()
            }
            continuation.onTermination = { _ in task.cancel() }
        }
    }

    func resume(_ resume: ResumeSpec) -> AsyncThrowingStream<AgentEvent, Error> {
        AsyncThrowingStream { continuation in
            let emitter = Emitter(
                sessionID: resume.sessionID, backend: Self.id, continuation: continuation, now: now)
            let task = Task {
                await body(emitter, approvals, inputs)
                continuation.finish()
            }
            continuation.onTermination = { _ in task.cancel() }
        }
    }

    func send(_ input: AgentInput) async throws {
        inputContinuation.yield(input)
        await sendHandler?(input)
    }

    func respond(to approvalID: ApprovalID, _ decision: Decision) async throws {
        let outcome = await approvals.resolve(approvalID, decision, by: "test-ui")
        if outcome == .unknownID {
            throw BackendError.protocolViolation("no outstanding approval \(approvalID)")
        }
    }

    func interrupt() async {}
    func shutdown() async {
        inputContinuation.finish()
        await approvals.cancelOutstanding(reason: "shutdown")
    }
}
