/**
 * test/protocol.test.ts — TDD tests for src/protocol.ts (T5)
 *
 * Coverage:
 *   - SESSION_ID_RE: accepts valid UUID v4, rejects "abc123" and other non-UUIDs
 *   - parseClientMessage: all valid paths + every invalid branch (never throws)
 *   - serialize: all three ServerMessage types
 */

import { describe, it, expect } from 'vitest'
import { SESSION_ID_RE, parseClientMessage, serialize } from '../src/protocol'

// ─── Helpers ──────────────────────────────────────────────────────────────────

const VALID_UUID = 'f47ac10b-58cc-4372-a567-0e02b2c3d479'

// ─── SESSION_ID_RE ─────────────────────────────────────────────────────────────

describe('SESSION_ID_RE', () => {
  it('accepts a valid UUID v4', () => {
    expect(SESSION_ID_RE.test(VALID_UUID)).toBe(true)
  })

  it('accepts UUID v4 with uppercase hex digits', () => {
    expect(SESSION_ID_RE.test('F47AC10B-58CC-4372-A567-0E02B2C3D479')).toBe(true)
  })

  it('accepts another valid UUID v4 (variant bits 8, version 4)', () => {
    // 41d4 → version nibble = 4 ✓; 8716 → variant nibble = 8 ✓
    expect(SESSION_ID_RE.test('550e8400-e29b-41d4-8716-446655440000')).toBe(true)
  })

  it('rejects "abc123" (M7)', () => {
    expect(SESSION_ID_RE.test('abc123')).toBe(false)
  })

  it('rejects empty string', () => {
    expect(SESSION_ID_RE.test('')).toBe(false)
  })

  it('rejects UUID v1 (version bit = 1)', () => {
    expect(SESSION_ID_RE.test('550e8400-e29b-11d4-a716-446655440000')).toBe(false)
  })

  it('rejects UUID v4 with wrong variant (not 8/9/a/b)', () => {
    // variant bits should be 10xx → 8,9,a,b; 'c' is 1100 = wrong
    expect(SESSION_ID_RE.test('f47ac10b-58cc-4372-c567-0e02b2c3d479')).toBe(false)
  })
})

// ─── parseClientMessage — valid paths ─────────────────────────────────────────

describe('parseClientMessage — valid messages', () => {
  it('parses a valid attach message with null sessionId', () => {
    const result = parseClientMessage(JSON.stringify({ type: 'attach', sessionId: null }))
    expect(result.ok).toBe(true)
    if (result.ok) {
      expect(result.message).toEqual({ type: 'attach', sessionId: null })
    }
  })

  it('parses a valid attach message with a UUID v4 sessionId', () => {
    const result = parseClientMessage(JSON.stringify({ type: 'attach', sessionId: VALID_UUID }))
    expect(result.ok).toBe(true)
    if (result.ok) {
      expect(result.message).toEqual({ type: 'attach', sessionId: VALID_UUID })
    }
  })

  it('parses a valid input message', () => {
    const result = parseClientMessage(JSON.stringify({ type: 'input', data: 'ls -la\r' }))
    expect(result.ok).toBe(true)
    if (result.ok) {
      expect(result.message).toEqual({ type: 'input', data: 'ls -la\r' })
    }
  })

  it('passes input.data through verbatim (raw keyboard bytes)', () => {
    const raw = '\x1b[A\x03\r\t\x1b[Z'
    const result = parseClientMessage(JSON.stringify({ type: 'input', data: raw }))
    expect(result.ok).toBe(true)
    if (result.ok && result.message.type === 'input') {
      expect(result.message.data).toBe(raw)
    }
  })

  it('parses a valid resize message with boundary values (1)', () => {
    const result = parseClientMessage(JSON.stringify({ type: 'resize', cols: 1, rows: 1 }))
    expect(result.ok).toBe(true)
    if (result.ok) {
      expect(result.message).toEqual({ type: 'resize', cols: 1, rows: 1 })
    }
  })

  it('parses a valid resize message with boundary values (1000)', () => {
    const result = parseClientMessage(JSON.stringify({ type: 'resize', cols: 1000, rows: 1000 }))
    expect(result.ok).toBe(true)
    if (result.ok) {
      expect(result.message).toEqual({ type: 'resize', cols: 1000, rows: 1000 })
    }
  })

  it('parses a valid resize message with typical values', () => {
    const result = parseClientMessage(JSON.stringify({ type: 'resize', cols: 120, rows: 40 }))
    expect(result.ok).toBe(true)
    if (result.ok) {
      expect(result.message).toEqual({ type: 'resize', cols: 120, rows: 40 })
    }
  })

  it('parses approve / reject (H3, no payload)', () => {
    const a = parseClientMessage(JSON.stringify({ type: 'approve' }))
    expect(a.ok).toBe(true)
    if (a.ok) expect(a.message).toEqual({ type: 'approve' })
    const r = parseClientMessage(JSON.stringify({ type: 'reject' }))
    expect(r.ok).toBe(true)
    if (r.ok) expect(r.message).toEqual({ type: 'reject' })
  })

  it('rejects blur (removed in the min→latest-writer pivot)', () => {
    const b = parseClientMessage(JSON.stringify({ type: 'blur' }))
    expect(b.ok).toBe(false)
  })
})

// ─── parseClientMessage — invalid / error paths ───────────────────────────────

describe('parseClientMessage — invalid input (ok=false, never throws)', () => {
  // Bad JSON
  it('returns ok=false for bad JSON', () => {
    const result = parseClientMessage('not json at all')
    expect(result.ok).toBe(false)
  })

  it('returns ok=false for empty string', () => {
    const result = parseClientMessage('')
    expect(result.ok).toBe(false)
  })

  it('returns ok=false for JSON null', () => {
    const result = parseClientMessage('null')
    expect(result.ok).toBe(false)
  })

  it('returns ok=false for JSON array', () => {
    const result = parseClientMessage('[]')
    expect(result.ok).toBe(false)
  })

  it('returns ok=false for JSON number', () => {
    const result = parseClientMessage('42')
    expect(result.ok).toBe(false)
  })

  // Unknown type
  it('returns ok=false for unknown type "ping"', () => {
    const result = parseClientMessage(JSON.stringify({ type: 'ping' }))
    expect(result.ok).toBe(false)
  })

  it('returns ok=false for missing type field', () => {
    const result = parseClientMessage(JSON.stringify({ data: 'hello' }))
    expect(result.ok).toBe(false)
  })

  it('returns ok=false for type=number', () => {
    const result = parseClientMessage(JSON.stringify({ type: 42 }))
    expect(result.ok).toBe(false)
  })

  // resize: cols out of range
  it('returns ok=false for resize cols=0 (below range)', () => {
    const result = parseClientMessage(JSON.stringify({ type: 'resize', cols: 0, rows: 40 }))
    expect(result.ok).toBe(false)
  })

  it('returns ok=false for resize cols=1001 (above range)', () => {
    const result = parseClientMessage(JSON.stringify({ type: 'resize', cols: 1001, rows: 40 }))
    expect(result.ok).toBe(false)
  })

  it('returns ok=false for resize rows=0 (below range)', () => {
    const result = parseClientMessage(JSON.stringify({ type: 'resize', cols: 80, rows: 0 }))
    expect(result.ok).toBe(false)
  })

  it('returns ok=false for resize rows=1001 (above range)', () => {
    const result = parseClientMessage(JSON.stringify({ type: 'resize', cols: 80, rows: 1001 }))
    expect(result.ok).toBe(false)
  })

  // resize: non-integer cols/rows
  it('returns ok=false for resize cols=80.5 (non-integer)', () => {
    const result = parseClientMessage(JSON.stringify({ type: 'resize', cols: 80.5, rows: 40 }))
    expect(result.ok).toBe(false)
  })

  it('returns ok=false for resize rows=24.9 (non-integer)', () => {
    const result = parseClientMessage(JSON.stringify({ type: 'resize', cols: 80, rows: 24.9 }))
    expect(result.ok).toBe(false)
  })

  it('returns ok=false for resize cols="80" (string, not number)', () => {
    const result = parseClientMessage(JSON.stringify({ type: 'resize', cols: '80', rows: 40 }))
    expect(result.ok).toBe(false)
  })

  it('returns ok=false for resize missing cols', () => {
    const result = parseClientMessage(JSON.stringify({ type: 'resize', rows: 40 }))
    expect(result.ok).toBe(false)
  })

  it('returns ok=false for resize missing rows', () => {
    const result = parseClientMessage(JSON.stringify({ type: 'resize', cols: 80 }))
    expect(result.ok).toBe(false)
  })

  // input.data non-string
  it('returns ok=false when input.data is a number', () => {
    const result = parseClientMessage(JSON.stringify({ type: 'input', data: 42 }))
    expect(result.ok).toBe(false)
  })

  it('returns ok=false when input.data is null', () => {
    const result = parseClientMessage(JSON.stringify({ type: 'input', data: null }))
    expect(result.ok).toBe(false)
  })

  it('returns ok=false when input.data is missing', () => {
    const result = parseClientMessage(JSON.stringify({ type: 'input' }))
    expect(result.ok).toBe(false)
  })

  it('returns ok=false when input.data is an object', () => {
    const result = parseClientMessage(JSON.stringify({ type: 'input', data: {} }))
    expect(result.ok).toBe(false)
  })

  // attach.sessionId: "abc123" rejected (M7)
  it('returns ok=false for attach.sessionId="abc123" (M7)', () => {
    const result = parseClientMessage(JSON.stringify({ type: 'attach', sessionId: 'abc123' }))
    expect(result.ok).toBe(false)
  })

  it('returns ok=false for attach.sessionId="" (empty string, not null or UUID)', () => {
    const result = parseClientMessage(JSON.stringify({ type: 'attach', sessionId: '' }))
    expect(result.ok).toBe(false)
  })

  it('returns ok=false for attach.sessionId=42 (not string or null)', () => {
    const result = parseClientMessage(JSON.stringify({ type: 'attach', sessionId: 42 }))
    expect(result.ok).toBe(false)
  })

  it('returns ok=false for attach.sessionId=undefined (missing field)', () => {
    // JSON.stringify omits undefined keys → { type: 'attach' } — no sessionId property
    const result = parseClientMessage(JSON.stringify({ type: 'attach' }))
    expect(result.ok).toBe(false)
  })

  // attach.sessionId: valid UUID passes (M7 positive case)
  it('returns ok=true for attach.sessionId = valid UUID v4 (M7 positive)', () => {
    const result = parseClientMessage(JSON.stringify({ type: 'attach', sessionId: VALID_UUID }))
    expect(result.ok).toBe(true)
  })
})

// ─── parseClientMessage — fuzzing: must never throw ───────────────────────────

describe('parseClientMessage — fuzzy inputs never throw', () => {
  const fuzzInputs: unknown[] = [
    undefined,
    null,
    0,
    true,
    {},
    [],
    '\x00\x01\xFF',
    '{"type":"attach","sessionId":{"$ne":null}}',
    '{"type":"resize","cols":null,"rows":null}',
    '{"type":"input","data":["a","b"]}',
    '{"type":"attach","sessionId":' + 'x'.repeat(10000) + '}',
    // valid JSON but with extra fields
    '{"type":"resize","cols":80,"rows":40,"extra":"ignored"}',
  ]

  for (const input of fuzzInputs) {
    it(`does not throw for input: ${JSON.stringify(input)?.slice(0, 60) ?? String(input)}`, () => {
      expect(() => parseClientMessage(input as string)).not.toThrow()
    })
  }
})

// ─── serialize ────────────────────────────────────────────────────────────────

describe('serialize', () => {
  it('serializes "attached" message', () => {
    const msg = { type: 'attached' as const, sessionId: VALID_UUID }
    const json = serialize(msg)
    expect(JSON.parse(json)).toEqual(msg)
  })

  it('serializes "output" message', () => {
    const msg = { type: 'output' as const, data: '\x1b[1;32mHello\x1b[0m' }
    const json = serialize(msg)
    expect(JSON.parse(json)).toEqual(msg)
  })

  it('serializes "exit" message with code only', () => {
    const msg = { type: 'exit' as const, code: 0 }
    const json = serialize(msg)
    expect(JSON.parse(json)).toEqual(msg)
  })

  it('serializes "exit" message with code and reason', () => {
    const msg = { type: 'exit' as const, code: -1, reason: 'spawn failed: /bin/badshell' }
    const json = serialize(msg)
    expect(JSON.parse(json)).toEqual(msg)
  })

  it('serialize output is a string', () => {
    expect(typeof serialize({ type: 'output', data: 'x' })).toBe('string')
  })

  it('round-trip: parseClientMessage(JSON) produces the right attach message', () => {
    const original: { type: 'attach'; sessionId: string | null } = {
      type: 'attach',
      sessionId: VALID_UUID,
    }
    const result = parseClientMessage(JSON.stringify(original))
    expect(result.ok).toBe(true)
    if (result.ok) {
      expect(result.message).toEqual(original)
    }
  })

  it('round-trip: parseClientMessage(JSON) produces the right input message', () => {
    const original: { type: 'input'; data: string } = { type: 'input', data: 'echo hi\r' }
    const result = parseClientMessage(JSON.stringify(original))
    expect(result.ok).toBe(true)
    if (result.ok) {
      expect(result.message).toEqual(original)
    }
  })

  it('round-trip: parseClientMessage(JSON) produces the right resize message', () => {
    const original: { type: 'resize'; cols: number; rows: number } = {
      type: 'resize',
      cols: 80,
      rows: 24,
    }
    const result = parseClientMessage(JSON.stringify(original))
    expect(result.ok).toBe(true)
    if (result.ok) {
      expect(result.message).toEqual(original)
    }
  })
})
