/**
 * Integration test for the orphan-session routes (A):
 *   GET    /orphan-sessions
 *   GET    /orphan-sessions/:id/preview
 *   DELETE /orphan-sessions/:id
 *
 * SAFETY: this file NEVER issues `DELETE /orphan-sessions` (the bulk cleanup),
 * because these tests run against the host's real tmux server and would end the
 * developer's own long-running sessions. Bulk cleanup is covered with mocks in
 * test/manager-orphans.test.ts. The only session touched here is one this file
 * creates under a fresh UUID and tears down itself.
 */

import net from 'node:net'
import { execFileSync } from 'node:child_process'
import { randomUUID } from 'node:crypto'

import { afterAll, beforeAll, describe, expect, it } from 'vitest'

import { loadConfig } from '../../src/config.js'
import { startServer } from '../../src/server.js'
import type { OrphanSessionInfo } from '../../src/types.js'

const TMUX_AVAILABLE = (() => {
  try {
    execFileSync('tmux', ['-V'], { stdio: 'ignore' })
    return true
  } catch {
    return false
  }
})()
const itTmux = TMUX_AVAILABLE ? it : it.skip

function getFreePort(): Promise<number> {
  return new Promise((resolve, reject) => {
    const srv = net.createServer()
    srv.listen(0, '127.0.0.1', () => {
      const addr = srv.address()
      if (addr === null || typeof addr === 'string') {
        srv.close()
        reject(new Error('unexpected address type'))
        return
      }
      const port = addr.port
      srv.close(() => resolve(port))
    })
    srv.on('error', reject)
  })
}

/* ── tmux off: the whole feature must be inert ──────────────────────────────── */

describe('orphan sessions — tmux disabled', () => {
  let port: number
  let handle: { close(): Promise<void> }
  const origin = (): string => `http://127.0.0.1:${port}`

  beforeAll(async () => {
    port = await getFreePort()
    handle = startServer(
      loadConfig({
        PORT: String(port),
        BIND_HOST: '127.0.0.1',
        SHELL_PATH: process.env['SHELL'] ?? '/bin/zsh',
        ALLOWED_ORIGINS: `http://127.0.0.1:${port}`,
        USE_TMUX: '0',
        IDLE_TTL: '86400',
      }),
    )
    await new Promise<void>((r) => setTimeout(r, 100))
  })

  afterAll(async () => {
    await handle.close()
  })

  it('lists nothing, and does not require an Origin header (read-only)', async () => {
    const res = await fetch(`${origin()}/orphan-sessions`)
    expect(res.status).toBe(200)
    expect(await res.json()).toEqual([])
  })

  it('404s a preview instead of shelling out to tmux', async () => {
    const res = await fetch(`${origin()}/orphan-sessions/${randomUUID()}/preview`)
    expect(res.status).toBe(404)
  })

  it('404s a kill', async () => {
    const res = await fetch(`${origin()}/orphan-sessions/${randomUUID()}`, {
      method: 'DELETE',
      headers: { Origin: origin() },
    })
    expect(res.status).toBe(404)
  })
})

/* ── tmux on: real session, real capture, real kill ─────────────────────────── */

describe('orphan sessions — tmux enabled', () => {
  let port: number
  let handle: { close(): Promise<void> }
  const origin = (): string => `http://127.0.0.1:${port}`

  /** Our throwaway session. Never any other. */
  const id = randomUUID()
  const name = `web_${id}`
  let created = false

  beforeAll(async () => {
    port = await getFreePort()
    handle = startServer(
      loadConfig({
        PORT: String(port),
        BIND_HOST: '127.0.0.1',
        SHELL_PATH: process.env['SHELL'] ?? '/bin/zsh',
        ALLOWED_ORIGINS: `http://127.0.0.1:${port}`,
        USE_TMUX: '1',
        IDLE_TTL: '86400',
      }),
    )
    await new Promise<void>((r) => setTimeout(r, 100))

    if (TMUX_AVAILABLE) {
      // Detached, printing a known marker so capture-pane has something to find.
      execFileSync(
        'tmux',
        ['new-session', '-d', '-s', name, `printf 'ORPHAN_MARKER_%s' OK; sleep 300`],
        { stdio: 'ignore' },
      )
      created = true
      await new Promise<void>((r) => setTimeout(r, 400))
    }
  })

  afterAll(async () => {
    if (created) {
      try {
        execFileSync('tmux', ['kill-session', '-t', name], { stdio: 'ignore' })
      } catch {
        // the kill test already removed it — fine
      }
    }
    await handle.close()
  })

  itTmux('lists our untracked tmux session as an orphan', async () => {
    const res = await fetch(`${origin()}/orphan-sessions`)
    expect(res.status).toBe(200)
    const body = (await res.json()) as OrphanSessionInfo[]
    const mine = body.find((o) => o.id === id)
    expect(mine).toBeDefined()
    expect(mine!.attached).toBe(false) // created with -d, nobody attached
    expect(mine!.cols).toBeGreaterThan(0)
    expect(mine!.lastActivityAt).toBeGreaterThan(0)
  })

  itTmux('previews the live screen without attaching to it', async () => {
    const res = await fetch(`${origin()}/orphan-sessions/${id}/preview`)
    expect(res.status).toBe(200)
    const body = (await res.json()) as { id: string; data: string }
    expect(body.id).toBe(id)
    expect(body.data).toContain('ORPHAN_MARKER_OK')

    // The capture must not have left a client behind: still unattached.
    const after = execFileSync('tmux', ['ls', '-F', '#{session_name} #{session_attached}'], {
      encoding: 'utf8',
    })
    expect(after).toContain(`${name} 0`)
  })

  itTmux('rejects a non-UUID id with 400, never reaching tmux', async () => {
    for (const bad of ['-t', 'not-a-uuid', '../../etc/passwd']) {
      const res = await fetch(`${origin()}/orphan-sessions/${encodeURIComponent(bad)}/preview`)
      expect(res.status).toBe(400)
    }
  })

  itTmux('refuses a kill with a foreign Origin (CSRF guard)', async () => {
    const res = await fetch(`${origin()}/orphan-sessions/${id}`, {
      method: 'DELETE',
      headers: { Origin: 'http://evil.example' },
    })
    expect(res.status).toBe(403)
    // and it is still alive
    expect(
      execFileSync('tmux', ['ls', '-F', '#{session_name}'], { encoding: 'utf8' }),
    ).toContain(name)
  })

  itTmux('kills it, and 404s the second time', async () => {
    const first = await fetch(`${origin()}/orphan-sessions/${id}`, {
      method: 'DELETE',
      headers: { Origin: origin() },
    })
    expect(first.status).toBe(204)

    const listing = execFileSync('tmux', ['ls', '-F', '#{session_name}'], {
      encoding: 'utf8',
    }).split('\n')
    expect(listing).not.toContain(name)

    const second = await fetch(`${origin()}/orphan-sessions/${id}`, {
      method: 'DELETE',
      headers: { Origin: origin() },
    })
    expect(second.status).toBe(404)
  })
})
