import Foundation
import Testing

@testable import NucleicCore

@Suite("Backend diagnostics")
struct BackendDiagnosticsTests {

    // MARK: - SIGKILL classification

    @Test func recognizesSIGKILLExitCodes() {
        #expect(BackendDiagnostics.isSIGKILL(137))
        #expect(BackendDiagnostics.isSIGKILL(-9))  // Foundation's signalled-process form
        #expect(!BackendDiagnostics.isSIGKILL(1))
        #expect(!BackendDiagnostics.isSIGKILL(0))
    }

    // MARK: - abnormalExitMessage (fallback, no post-mortem)

    @Test func fallbackSIGKILLDoesNotAssertOOM() {
        // The fallback must not claim OOM outright — a SIGKILL has several causes.
        let host = BackendDiagnostics.abnormalExitMessage(
            tool: "codex exec", exitCode: 137, stderrTail: "", containerized: false)
        #expect(host.contains("SIGKILL"))
        #expect(host.contains("possibly out of memory"))
        #expect(!host.contains("Settings → Sandbox"))

        let containerized = BackendDiagnostics.abnormalExitMessage(
            tool: "claude", exitCode: 137, stderrTail: "", containerized: true)
        #expect(containerized.contains("SIGKILL"))
        // Hedged: "may have run out of memory or been stopped" — not a definitive OOM claim.
        #expect(containerized.localizedCaseInsensitiveContains("stopped"))
    }

    @Test func ordinaryNonZeroExitKeepsStatusAndStderrTail() {
        let msg = BackendDiagnostics.abnormalExitMessage(
            tool: "claude", exitCode: 1, stderrTail: "boom: bad flag", containerized: true)
        #expect(msg == "claude exited with status 1: boom: bad flag")
    }

    @Test func emptyStderrTailIsOmittedCleanly() {
        let msg = BackendDiagnostics.abnormalExitMessage(
            tool: "claude", exitCode: 2, stderrTail: "   \n", containerized: false)
        #expect(msg == "claude exited with status 2")
    }

    // MARK: - containerKillMessage (evidence-based)

    private func sample(usedGB: Double, totalGB: Double) -> ContainerResourceSample {
        ContainerResourceSample(
            cpuPercent: 0,
            memoryUsedBytes: UInt64(usedGB * 1_073_741_824),
            memoryTotalBytes: UInt64(totalGB * 1_073_741_824))
    }

    @Test func containerDownIsReportedAsNotMemory() {
        let msg = BackendDiagnostics.containerKillMessage(
            tool: "claude",
            diagnosis: ContainerKillDiagnosis(containerRunning: false, oomKills: nil, sample: nil),
            stderrTail: "")
        #expect(msg.localizedCaseInsensitiveContains("no longer running"))
        #expect(msg.localizedCaseInsensitiveContains("every session"))
        // It must steer AWAY from the memory lever — the container went down, not OOM.
        #expect(!msg.contains("Settings → Sandbox"))
    }

    @Test func realOOMIsTrustedEvenWhenMemoryReadsLow() {
        // The persistent oom_kill counter is the OOM authority — true even though memory is low
        // now (the kill freed it). This is exactly the user's "OOM error but memory was low" case.
        let msg = BackendDiagnostics.containerKillMessage(
            tool: "claude",
            diagnosis: ContainerKillDiagnosis(
                containerRunning: true, oomKills: 3, sample: sample(usedGB: 1, totalGB: 8)),
            stderrTail: "")
        #expect(msg.localizedCaseInsensitiveContains("oom-killed"))
        #expect(msg.localizedCaseInsensitiveContains("freed"))
        #expect(msg.contains("Settings → Sandbox"))
    }

    @Test func healthyContainerWithLowMemoryIsNotBlamedOnMemory() {
        let msg = BackendDiagnostics.containerKillMessage(
            tool: "claude",
            diagnosis: ContainerKillDiagnosis(
                containerRunning: true, oomKills: 0, sample: sample(usedGB: 1, totalGB: 8)),
            stderrTail: "")
        #expect(msg.localizedCaseInsensitiveContains("not an out-of-memory"))
        #expect(msg.localizedCaseInsensitiveContains("healthy"))
        #expect(!msg.contains("Settings → Sandbox"))
    }

    @Test func nearlyFullMemoryReadsAsOOMEvenWithoutCounter() {
        let msg = BackendDiagnostics.containerKillMessage(
            tool: "claude",
            diagnosis: ContainerKillDiagnosis(
                containerRunning: true, oomKills: 0, sample: sample(usedGB: 7.6, totalGB: 8)),
            stderrTail: "out of memory")
        #expect(msg.localizedCaseInsensitiveContains("nearly full"))
        #expect(msg.contains("Settings → Sandbox"))
        // The stderr tail rides along.
        #expect(msg.contains("out of memory"))
    }
}
