import CoreGraphics
import Foundation
import Testing

@testable import VMAgentCore

// MARK: - NDJSON line framing

@Test func lineSplitterYieldsCompleteLines() {
    var buffer = LineSplitBuffer()
    buffer.append(Data("{\"op\":\"pi".utf8))
    #expect(buffer.nextLine() == nil)
    buffer.append(Data("ng\"}\n{\"op\":".utf8))
    #expect(buffer.nextLine() == Data("{\"op\":\"ping\"}".utf8))
    #expect(buffer.nextLine() == nil)
    buffer.append(Data("\"click\"}\n".utf8))
    #expect(buffer.nextLine() == Data("{\"op\":\"click\"}".utf8))
    #expect(buffer.nextLine() == nil)
    #expect(buffer.pendingBytes == 0)
}

@Test func lineSplitterHandlesCRLFAndEmptyLines() {
    var buffer = LineSplitBuffer()
    buffer.append(Data("a\r\n\nb\n".utf8))
    #expect(buffer.nextLine() == Data("a".utf8))
    #expect(buffer.nextLine() == Data())
    #expect(buffer.nextLine() == Data("b".utf8))
    #expect(buffer.nextLine() == nil)
}

@Test func lineSplitterTracksPendingBytes() {
    var buffer = LineSplitBuffer()
    buffer.append(Data(repeating: UInt8(ascii: "x"), count: 100))
    #expect(buffer.pendingBytes == 100)
    #expect(buffer.nextLine() == nil)
}

// MARK: - Key chords

@Test func chordParsesModifiersAndKey() {
    let stroke = KeyMap.stroke(forChord: "cmd+shift+s")
    #expect(stroke == KeyStroke(keyCode: 0x01, flags: [.maskCommand, .maskShift]))
}

@Test func chordParsesBareSpecialKeys() {
    #expect(KeyMap.stroke(forChord: "return") == KeyStroke(keyCode: 0x24, flags: []))
    #expect(KeyMap.stroke(forChord: "enter") == KeyStroke(keyCode: 0x24, flags: []))
    #expect(KeyMap.stroke(forChord: "escape") == KeyStroke(keyCode: 0x35, flags: []))
    #expect(KeyMap.stroke(forChord: "backspace") == KeyStroke(keyCode: 0x33, flags: []))
    #expect(KeyMap.stroke(forChord: "tab") == KeyStroke(keyCode: 0x30, flags: []))
}

@Test func chordParsesArrowAliasesAndFKeys() {
    #expect(KeyMap.stroke(forChord: "arrowdown") == KeyMap.stroke(forChord: "down"))
    #expect(KeyMap.stroke(forChord: "f5") == KeyStroke(keyCode: 0x60, flags: []))
    #expect(KeyMap.stroke(forChord: "cmd+shift+4") == KeyStroke(
        keyCode: 0x15, flags: [.maskCommand, .maskShift]))
}

@Test func chordIsCaseInsensitiveAndToleratesUnknownModifiers() {
    #expect(KeyMap.stroke(forChord: "CMD+S") == KeyStroke(keyCode: 0x01, flags: [.maskCommand]))
    // Unknown modifier tokens are dropped (host-side leniency parity), the key still lands.
    #expect(KeyMap.stroke(forChord: "hyper+s") == KeyStroke(keyCode: 0x01, flags: []))
}

@Test func chordRejectsUnknownKey() {
    #expect(KeyMap.stroke(forChord: "cmd+definitely-not-a-key") == nil)
    #expect(KeyMap.stroke(forChord: "") == nil)
}

// MARK: - nash shell-event spool drain (docs/NASH.md §6.2, §7.5)

@Test func drainConcatenatesJSONLBatchesAndListsDrainedFiles() throws {
    let fm = FileManager.default
    let dir = (NSTemporaryDirectory() as NSString)
        .appendingPathComponent("nash-drain-\(UUID().uuidString)")
    try fm.createDirectory(atPath: dir, withIntermediateDirectories: true)
    defer { try? fm.removeItem(atPath: dir) }

    let b1 = #"{"type":"shell-batch","shellId":"aaa","events":[]}"# + "\n"
    let b2 = #"{"type":"shell-batch","shellId":"bbb","events":[]}"# + "\n"
    try b1.write(toFile: (dir as NSString).appendingPathComponent("aaa.jsonl"),
                 atomically: true, encoding: .utf8)
    try b2.write(toFile: (dir as NSString).appendingPathComponent("bbb.jsonl"),
                 atomically: true, encoding: .utf8)
    // A non-.jsonl file must be ignored, not drained.
    try "nope\n".write(toFile: (dir as NSString).appendingPathComponent("ignore.txt"),
                       atomically: true, encoding: .utf8)

    let result = ShellSpool.drain(dir: dir)
    #expect(result.files == 2)
    #expect(!result.truncated)
    #expect(result.drainedPaths.count == 2)
    let text = String(decoding: result.data, as: UTF8.self)
    #expect(text.contains(#""shellId":"aaa""#))
    #expect(text.contains(#""shellId":"bbb""#))
    #expect(!text.contains("nope"))
}

@Test func drainMissingDirIsEmptyNotAnError() {
    let result = ShellSpool.drain(dir: "/tmp/nash-does-not-exist-\(UUID().uuidString)")
    #expect(result.files == 0)
    #expect(result.data.isEmpty)
    #expect(result.drainedPaths.isEmpty)
    #expect(!result.truncated)
}

@Test func drainLeavesOversizeFileForNextPoll() throws {
    let fm = FileManager.default
    let dir = (NSTemporaryDirectory() as NSString)
        .appendingPathComponent("nash-drain-\(UUID().uuidString)")
    try fm.createDirectory(atPath: dir, withIntermediateDirectories: true)
    defer { try? fm.removeItem(atPath: dir) }
    // One small batch that fits, one file bigger than the tiny budget that must be left whole.
    try (#"{"shellId":"s"}"# + "\n").write(
        toFile: (dir as NSString).appendingPathComponent("a.jsonl"), atomically: true, encoding: .utf8)
    try String(repeating: "x", count: 100).write(
        toFile: (dir as NSString).appendingPathComponent("b.jsonl"), atomically: true, encoding: .utf8)

    let result = ShellSpool.drain(dir: dir, maxBytes: 20)
    #expect(result.files == 1)          // only the small one fit
    #expect(result.truncated)           // the oversize file signals "more remains"
    #expect(result.drainedPaths.count == 1)
}
