import { describe, expect, it } from 'vitest'
import { mountTerminalView, type TerminalLike } from '../src/terminal-view'
import { decodeServerMessage, type ClientMessage } from '../src/protocol'
import type { TerminalTransport } from '../src/ws-transport'

const decoder = new TextDecoder()
function decodeClient(bytes: Uint8Array): ClientMessage {
  return JSON.parse(decoder.decode(bytes)) as ClientMessage
}

/** In-memory transport capturing sent frames and exposing a way to push incoming bytes. */
function fakeTransport(): TerminalTransport & { sent: ClientMessage[]; push(m: object): void; closed: boolean } {
  let msgCb: ((b: Uint8Array) => void) | null = null
  const sent: ClientMessage[] = []
  return {
    sent,
    closed: false,
    open: async () => {},
    send: (bytes) => sent.push(decodeClient(bytes)),
    onMessage: (cb) => {
      msgCb = cb
    },
    onClose: () => {},
    close() {
      this.closed = true
    },
    push(m: object) {
      msgCb?.(new TextEncoder().encode(JSON.stringify(m)))
    },
  }
}

/** Mock xterm; `fitCalls` records fit() invocations, `dataCb` fires simulated keystrokes. */
function fakeTerminal(): TerminalLike & {
  written: string[]
  fitCalls: number
  typeInto(data: string): void
} {
  let onDataCb: ((d: string) => void) | null = null
  return {
    cols: 80,
    rows: 24,
    written: [],
    fitCalls: 0,
    open: () => {},
    write(data: string) {
      this.written.push(data)
    },
    onData: (cb) => {
      onDataCb = cb
    },
    fit() {
      this.fitCalls++
    },
    dispose: () => {},
    typeInto(data: string) {
      onDataCb?.(data)
    },
  }
}

function realDims(el: HTMLElement): void {
  Object.defineProperty(el, 'clientWidth', { value: 640, configurable: true })
  Object.defineProperty(el, 'clientHeight', { value: 480, configurable: true })
}

describe('mountTerminalView (T4)', () => {
  it('sends attach FIRST, then a resize frame once the container has real dimensions', () => {
    const root = document.createElement('div')
    realDims(root)
    const transport = fakeTransport()
    const term = fakeTerminal()
    mountTerminalView(root, transport, { createTerminal: () => term })

    expect(transport.sent[0]).toEqual({ type: 'attach', sessionId: null })
    const resize = transport.sent.find((m) => m.type === 'resize')
    expect(resize).toEqual({ type: 'resize', cols: 80, rows: 24 })
    expect(term.fitCalls).toBeGreaterThan(0)
  })

  it('keypress → input frame; Enter carries "\\r" verbatim (not "\\n")', () => {
    const root = document.createElement('div')
    realDims(root)
    const transport = fakeTransport()
    const term = fakeTerminal()
    mountTerminalView(root, transport, { createTerminal: () => term })

    term.typeInto('\r')
    const input = transport.sent.find((m) => m.type === 'input')
    expect(input).toEqual({ type: 'input', data: '\r' })
    expect(JSON.stringify(input)).not.toContain('\\n')
  })

  it('incoming output frame → xterm.write; attached/exit are not written as output', () => {
    const root = document.createElement('div')
    realDims(root)
    const transport = fakeTransport()
    const term = fakeTerminal()
    mountTerminalView(root, transport, { createTerminal: () => term })

    transport.push({ type: 'output', data: 'hello' })
    transport.push({ type: 'attached', sessionId: 'x' })
    expect(term.written).toEqual(['hello'])
  })

  it('does NOT call fit() while the container is display:none (guards NaN dims)', () => {
    const root = document.createElement('div') // clientWidth/Height default to 0 in jsdom
    const transport = fakeTransport()
    const term = fakeTerminal()
    mountTerminalView(root, transport, { createTerminal: () => term })
    expect(term.fitCalls).toBe(0)
    expect(transport.sent.some((m) => m.type === 'resize')).toBe(false)
  })

  it('the same seam mounts identically over any TerminalTransport (drop-in proof)', () => {
    const root = document.createElement('div')
    realDims(root)
    const transport = fakeTransport()
    const view = mountTerminalView(root, transport, { createTerminal: fakeTerminal })
    view.dispose()
    expect(transport.closed).toBe(true)
  })

  it('server messages decode via the shared codec (round-trip sanity)', () => {
    const bytes = new TextEncoder().encode(JSON.stringify({ type: 'output', data: 'z' }))
    expect(decodeServerMessage(bytes)).toEqual({ type: 'output', data: 'z' })
    expect(decodeServerMessage(new TextEncoder().encode('not json'))).toBeNull()
  })
})
