/**
 * test/tmux.test.ts (H1) — thin tmux CLI wrappers.
 *
 * execFileSync is mocked so no real tmux binary runs: every wrapper is
 * best-effort and must NOT throw (a throwing exec → false / no-op).
 */

import { describe, it, expect, vi, beforeEach } from 'vitest'

const mockExec = vi.fn()
// promisify(execFile) reads the custom symbol, so give it one that returns our mock.
const mockExecAsync = vi.fn()
const fakeExecFile = Object.assign(
  (...a: unknown[]) => mockExecAsync(...a),
  { [Symbol.for('nodejs.util.promisify.custom')]: (...a: unknown[]) => mockExecAsync(...a) },
)
vi.mock('node:child_process', () => ({
  execFileSync: (...a: unknown[]) => mockExec(...a),
  execFile: fakeExecFile,
}))

const { tmuxAvailable, tmuxName, hasSession, killSession, parseSessionList, listSessions, capturePane } =
  await import('../src/session/tmux.js')

beforeEach(() => {
  mockExec.mockReset()
  mockExecAsync.mockReset()
})

describe('tmuxName', () => {
  it('prefixes the session id with web_', () => {
    expect(tmuxName('abc')).toBe('web_abc')
  })
})

describe('tmuxAvailable', () => {
  it('returns true when `tmux -V` succeeds', () => {
    mockExec.mockReturnValue(Buffer.from('tmux 3.4'))
    expect(tmuxAvailable()).toBe(true)
    expect(mockExec).toHaveBeenCalledWith('tmux', ['-V'], expect.objectContaining({ stdio: 'ignore' }))
  })

  it('returns false when tmux is missing (exec throws)', () => {
    mockExec.mockImplementation(() => {
      throw new Error('ENOENT')
    })
    expect(tmuxAvailable()).toBe(false)
  })
})

describe('hasSession', () => {
  it('returns true when has-session exits 0', () => {
    mockExec.mockReturnValue(Buffer.from(''))
    expect(hasSession('web_x')).toBe(true)
    expect(mockExec).toHaveBeenCalledWith(
      'tmux',
      ['has-session', '-t', 'web_x'],
      expect.objectContaining({ stdio: 'ignore' }),
    )
  })

  it('returns false when has-session throws (no such session)', () => {
    mockExec.mockImplementation(() => {
      throw new Error("can't find session")
    })
    expect(hasSession('web_gone')).toBe(false)
  })
})

describe('killSession', () => {
  it('invokes tmux kill-session for the name', () => {
    mockExec.mockReturnValue(Buffer.from(''))
    killSession('web_x')
    expect(mockExec).toHaveBeenCalledWith(
      'tmux',
      ['kill-session', '-t', 'web_x'],
      expect.objectContaining({ stdio: 'ignore' }),
    )
  })

  it('swallows errors when the session is already gone', () => {
    mockExec.mockImplementation(() => {
      throw new Error('no session')
    })
    expect(() => killSession('web_gone')).not.toThrow()
  })
})

/* ── A: enumerate + preview tmux sessions the server does not track ─────────── */

const U1 = '05caa88b-1f5f-45d9-bbbe-fc5955314870'
const U2 = '3a63f3c7-acfe-4081-992f-c4dbd55b3d6b'

describe('parseSessionList', () => {
  it('parses tmux rows into summaries, converting seconds to ms', () => {
    const out = `web_${U1}\t1783911991\t1783911992\t0\t80\t23\n`
    expect(parseSessionList(out)).toEqual([
      {
        id: U1,
        createdAtMs: 1783911991_000,
        lastActivityAtMs: 1783911992_000,
        attached: false,
        cols: 80,
        rows: 23,
      },
    ])
  })

  it('sorts most-recently-active first', () => {
    const out = [`web_${U1}\t100\t100\t0\t80\t24`, `web_${U2}\t100\t900\t1\t80\t24`].join('\n')
    expect(parseSessionList(out).map((s) => s.id)).toEqual([U2, U1])
  })

  it('reports a session attached from a real terminal', () => {
    const out = `web_${U1}\t100\t100\t1\t80\t24`
    expect(parseSessionList(out)[0]!.attached).toBe(true)
  })

  it('ignores sessions that are not ours — never touch the user’s own tmux', () => {
    const out = ['mywork\t100\t100\t0\t80\t24', `web_${U1}\t100\t100\t0\t80\t24`].join('\n')
    expect(parseSessionList(out).map((s) => s.id)).toEqual([U1])
  })

  it('ignores a web_-prefixed name whose suffix is not a UUID v4', () => {
    // Only ids the protocol could have produced are actionable: anything else is
    // someone else's session that merely starts with web_.
    const out = ['web_../../etc\t100\t100\t0\t80\t24', 'web_-t\t100\t100\t0\t80\t24'].join('\n')
    expect(parseSessionList(out)).toEqual([])
  })

  it('skips malformed and blank rows instead of throwing', () => {
    const out = ['', 'garbage', `web_${U1}\tNaN\t100\t0\t80\t24`, `web_${U2}\t100\t100\t0\t80\t24`].join('\n')
    expect(parseSessionList(out).map((s) => s.id)).toEqual([U2])
  })

  it('returns an empty list for empty output (no tmux server)', () => {
    expect(parseSessionList('')).toEqual([])
  })
})

describe('listSessions', () => {
  it('asks tmux for a machine-readable list', async () => {
    mockExecAsync.mockResolvedValue({ stdout: `web_${U1}\t100\t100\t0\t80\t24\n`, stderr: '' })
    expect((await listSessions()).map((s) => s.id)).toEqual([U1])
    const [file, args] = mockExecAsync.mock.calls[0] as [string, string[]]
    expect(file).toBe('tmux')
    expect(args[0]).toBe('list-sessions')
    expect(args).toContain('-F')
  })

  it('asks for window_activity, NOT session_activity', async () => {
    // session_activity is a CLIENT clock: it never advances for a detached session
    // however much its shell prints. Using it would make idle-cleanup kill exactly
    // the unattended, actively-working sessions this feature exists to protect.
    mockExecAsync.mockResolvedValue({ stdout: '', stderr: '' })
    await listSessions()
    const [, args] = mockExecAsync.mock.calls[0] as [string, string[]]
    const fmt = args[args.indexOf('-F') + 1]!
    expect(fmt).toContain('#{window_activity}')
    expect(fmt).not.toContain('#{session_activity}')
  })

  it('bounds the call so a wedged tmux cannot hang the server', async () => {
    mockExecAsync.mockResolvedValue({ stdout: '', stderr: '' })
    await listSessions()
    const opts = mockExecAsync.mock.calls[0]![2] as { timeout?: number; maxBuffer?: number }
    expect(opts.timeout).toBeGreaterThan(0)
    expect(opts.maxBuffer).toBeGreaterThan(0)
  })

  it('returns [] when there is no tmux server (exec rejects)', async () => {
    mockExecAsync.mockRejectedValue(new Error('no server running'))
    expect(await listSessions()).toEqual([])
  })
})

describe('capturePane', () => {
  it('captures the current screen WITHOUT attaching', async () => {
    mockExecAsync.mockResolvedValue({ stdout: 'hello\n', stderr: '' })
    expect(await capturePane('web_x')).toBe('hello\n')
    const [file, args] = mockExecAsync.mock.calls[0] as [string, string[]]
    expect(file).toBe('tmux')
    expect(args[0]).toBe('capture-pane')
    expect(args).toContain('-p') // print to stdout — read-only, no client
    expect(args).toContain('-e') // keep escape sequences so colour survives
    expect(args).toContain('web_x')
    // Attaching would resize the session and SIGWINCH whatever is running in it.
    expect(args).not.toContain('attach-session')
  })

  it('returns null when the session is gone', async () => {
    mockExecAsync.mockRejectedValue(new Error("can't find session"))
    expect(await capturePane('web_gone')).toBeNull()
  })

  it('bounds the call', async () => {
    mockExecAsync.mockResolvedValue({ stdout: '', stderr: '' })
    await capturePane('web_x')
    const opts = mockExecAsync.mock.calls[0]![2] as { timeout?: number }
    expect(opts.timeout).toBeGreaterThan(0)
  })
})
