import Foundation
import Testing

@testable import NucleicCore

/// The `mac_vm_exec` wait must be BOUNDED: a guest command that never exits (a foregrounded server, a
/// stdin-blocked process, or the agent's own `… | tail` that buffers until an EOF that never comes)
/// used to leave `MacVMEngine.run` parked on `handle.wait()` forever — the tool call never returned and
/// the agent saw no stdout/stderr/crash at all. These tests pin the fix: on timeout the guest process is
/// signalled, the streams are force-closed, and the caller gets a `-1` exit plus a note — never a hang.
@Suite struct MacVMExecTimeoutTests {
    /// A `ProcessHandle` that (by default) never exits on its own — `wait()` only resolves once
    /// `forceCloseStreams()` is called, exactly as ``MacVMExecChannel`` behaves when its connection is
    /// torn down. Records the signals it received so a test can assert the guest process was reaped.
    final class FakeExecHandle: ProcessHandle, @unchecked Sendable {
        let stdoutLines: AsyncThrowingStream<Data, Error>
        let stderrLines: AsyncThrowingStream<Data, Error>
        var processID: Int32 { 7 }  // a positive "fd", like the real vsock handle — NOT a host pid

        private let lock = NSLock()
        private var exited: Int32?
        private var waiters: [CheckedContinuation<Int32, Never>] = []
        private(set) var signals: [Int32] = []
        private(set) var forceClosed = false

        /// `exitsWith == nil` → never exits until force-closed; non-nil → `wait()` returns it at once.
        init(exitsWith code: Int32? = nil) {
            stdoutLines = AsyncThrowingStream { $0.finish() }
            stderrLines = AsyncThrowingStream { $0.finish() }
            exited = code
        }

        func writeLine(_ data: Data) throws {}
        func closeStdin() {}
        func sendSignal(_ sig: Int32) { lock.withLock { signals.append(sig) } }

        func forceCloseStreams() {
            let toWake: [CheckedContinuation<Int32, Never>] = lock.withLock {
                forceClosed = true
                if exited == nil { exited = -1 }
                let w = waiters
                waiters.removeAll()
                return w
            }
            for c in toWake { c.resume(returning: -1) }
        }

        func wait() async -> Int32 {
            await withCheckedContinuation { (c: CheckedContinuation<Int32, Never>) in
                let immediate: Int32? = lock.withLock {
                    if let exited { return exited }
                    waiters.append(c)
                    return nil
                }
                if let immediate { c.resume(returning: immediate) }
            }
        }
    }

    @Test func timesOutInsteadOfHangingAndReapsTheGuestProcess() async {
        let handle = FakeExecHandle()  // never exits on its own
        let timeout: UInt64 = 50_000_000  // 50ms — the test must not wait out a real 30m bound
        // Without the fix this call would never return; the suite would hang rather than fail.
        let outcome = await MacVMEngine.awaitExit(handle, timeoutNanos: timeout)
        let result = MacVMEngine.finalize(
            outcome, stdout: "partial output", stderr: "", timeoutNanos: timeout)

        #expect(result.exitCode == -1)
        #expect(result.stdout == "partial output")  // whatever was captured still comes back
        #expect(result.stderr.contains("was terminated"))
        #expect(handle.signals.contains(SIGKILL))  // the guest process was signalled…
        #expect(handle.forceClosed)  // …and the streams released so the drains finish
    }

    @Test func passesThroughARealExitUntouched() async {
        let handle = FakeExecHandle(exitsWith: 3)
        let outcome = await MacVMEngine.awaitExit(handle, timeoutNanos: 5_000_000_000)
        let result = MacVMEngine.finalize(
            outcome, stdout: "ok", stderr: "a warning", timeoutNanos: 5_000_000_000)

        #expect(result.exitCode == 3)
        #expect(result.stdout == "ok")
        #expect(result.stderr == "a warning")  // no timeout note appended on a clean exit
        #expect(handle.signals.isEmpty)  // a command that exits on its own is never signalled
    }

    @Test func unboundedWaitPreservesInternalCallerBehavior() async {
        // Internal housekeeping runs pass `nil` — they must still wait for the real exit, unbounded.
        let handle = FakeExecHandle(exitsWith: 0)
        let outcome = await MacVMEngine.awaitExit(handle, timeoutNanos: nil)
        let result = MacVMEngine.finalize(outcome, stdout: "", stderr: "", timeoutNanos: nil)
        #expect(result.exitCode == 0)
        #expect(handle.signals.isEmpty)
    }

    @Test func timeoutNoteReportsTheMinutesAndTheLikelyCauses() {
        let note = MacVMEngine.timeoutNote(30 * 60 * 1_000_000_000)
        #expect(note.contains("30m"))
        #expect(note.contains("| tail"))  // steers the agent off the buffering-tail footgun
    }
}
