import Foundation
import Testing

@testable import NucleicCore

/// `CodexExecDecoder`: the `codex exec --json` snake_case/dotted surface (ADAPTERS §3), a
/// distinct schema from the app-server. The simple-turn case replays a real codex-cli 0.141.0
/// capture; the rest are source-shaped synthetic inputs for the tool/file/error items.
@Suite struct CodexExecDecoderTests {
    private func decodeLines(_ s: String) -> [CodexExecDecoder.Decoded] {
        let decoder = CodexExecDecoder()
        return s.split(separator: "\n").flatMap { decoder.decode(line: Data($0.utf8)) }
    }
    private func tags(_ d: [CodexExecDecoder.Decoded]) -> [String] { d.map(\.kind.tag) }

    @Test func realSimpleTurnCapture() throws {
        let repoRoot = URL(fileURLWithPath: #filePath)
            .deletingLastPathComponent().deletingLastPathComponent().deletingLastPathComponent()
        let fixture = repoRoot.appendingPathComponent("fixtures/codex-exec/0.141.0/01-simple-turn.jsonl")
        let out = decodeLines(try String(contentsOf: fixture, encoding: .utf8))

        #expect(tags(out) == ["sessionStarted", "assistantText", "usage", "turnCompleted"])
        guard case .sessionStarted(let s) = out[0].kind else { Issue.record("not sessionStarted"); return }
        #expect(s.backendSessionID == "019ee754-b04c-7d83-94b2-97cb7f137666")
        guard case .assistantText(let chunk) = out[1].kind else { Issue.record("not assistantText"); return }
        #expect(chunk.text == "pong")
        guard case .usage(let usage) = out[2].kind else { Issue.record("not usage"); return }
        #expect(usage.inputTokens == 14746)
        #expect(usage.cachedInputTokens == 14592)
        #expect(usage.outputTokens == 5)
    }

    @Test func commandExecutionWithNonZeroExitIsError() {
        let out = decodeLines("""
            {"type":"item.started","item":{"id":"c1","type":"command_execution","command":"ls"}}
            {"type":"item.completed","item":{"id":"c1","type":"command_execution","command":"ls","status":"failed","exit_code":2,"aggregated_output":"boom"}}
            """)
        #expect(tags(out) == ["toolCallStarted", "toolCallCompleted", "toolResult"])
        guard case .toolCallStarted(let call) = out[0].kind else { Issue.record("not toolCallStarted"); return }
        #expect(call.name == "Bash")
        #expect(call.input["command"]?.stringValue == "ls")
        guard case .toolResult(let result) = out[2].kind else { Issue.record("not toolResult"); return }
        #expect(result.isError == true)
        #expect(result.content.stringValue == "boom")
    }

    @Test func fileChangeUsesStringKind() {
        // The exec schema's `kind` is a plain string (not the app-server's tagged object).
        let out = decodeLines("""
            {"type":"item.completed","item":{"id":"f1","type":"file_change","status":"completed","changes":[{"path":"a.swift","kind":"add"},{"path":"b.swift","kind":"delete"},{"path":"c.swift","kind":"update"}]}}
            """)
        let kinds: [(String, FileChange.ChangeKind)] = out.compactMap {
            guard case .fileChange(let fc) = $0.kind else { return nil }
            return (fc.path, fc.kind)
        }
        #expect(kinds.count == 3)
        #expect(kinds[0] == ("a.swift", .add))
        #expect(kinds[1] == ("b.swift", .delete))
        #expect(kinds[2] == ("c.swift", .update))
    }

    @Test func turnFailedBecomesError() {
        let out = decodeLines(#"{"type":"turn.failed","error":{"message":"sandbox denied"}}"#)
        #expect(tags(out) == ["error"])
        guard case .error(let err) = out[0].kind else { Issue.record("not error"); return }
        #expect(err.message == "sandbox denied")
    }

    @Test func reasoningBecomesThinking() {
        let out = decodeLines(
            #"{"type":"item.completed","item":{"id":"r1","type":"reasoning","text":"thinking it through"}}"#)
        guard case .thinking(let chunk) = out.first?.kind else { Issue.record("not thinking"); return }
        #expect(chunk.text == "thinking it through")
    }
}
