import Foundation
import Testing

@testable import NucleicCore

/// The wall-clock bound + kill semantics of `GitRunner.run` (LOCKING invariant 7's git half):
/// a child that never exits — a blocking hook, a wedged credential helper — must be killed at
/// the deadline (or on caller cancellation) instead of parking the caller and surviving until
/// app quit. `executable` is injectable, so the "hung git" is simply `sleep`.
@Suite("GitRunner — bounded execution")
struct GitRunnerTimeoutTests {
    @Test func hungCommandIsKilledAtTheDeadline() async {
        let started = ContinuousClock.now
        do {
            _ = try await GitRunner(executable: "/bin/sleep").run(["30"], in: "/tmp", timeout: 0.3)
            Issue.record("a hung command must throw, not return")
        } catch let GitError.timedOut(command, seconds) {
            #expect(command.contains("sleep"))
            #expect(seconds == 0.3)
        } catch {
            Issue.record("expected GitError.timedOut, got \(error)")
        }
        // Bounded promptly: deadline (0.3s) + at most the TERM→KILL grace, never the 30s sleep.
        #expect(ContinuousClock.now - started < .seconds(10))
    }

    @Test func cancellationKillsTheChildAndThrows() async {
        let started = ContinuousClock.now
        let run = Task {
            try await GitRunner(executable: "/bin/sleep").run(["30"], in: "/tmp", timeout: 60)
        }
        try? await Task.sleep(for: .milliseconds(200))
        run.cancel()
        do {
            _ = try await run.value
            Issue.record("a cancelled run must throw, not return")
        } catch is CancellationError {
            // expected
        } catch {
            Issue.record("expected CancellationError, got \(error)")
        }
        #expect(ContinuousClock.now - started < .seconds(10))
    }

    @Test func wellBehavedCommandIsUntouchedByTheBound() async throws {
        let result = try await GitRunner(executable: "/bin/echo").run(["ok"], in: "/tmp")
        #expect(result.ok)
        #expect(result.stdout.trimmingCharacters(in: .whitespacesAndNewlines) == "ok")
    }
}
