/**
 * test/editor.test.ts — openInEditor validation + spawn wiring (v0.6).
 *
 * Uses editorCmd='true' (the POSIX no-op that exits 0, ignoring args) so the
 * success path actually spawns a harmless process — never opening a real editor.
 */

import { describe, it, expect, beforeAll, afterAll } from 'vitest'
import fs from 'node:fs/promises'
import os from 'node:os'
import path from 'node:path'
import { openInEditor, openFileInEditor, isGotoEditor } from '../src/http/editor.js'
import type { Config } from '../src/types.js'

function cfg(editorCmd: string): Config {
  // Only editorCmd matters here; the rest is filler to satisfy the type.
  return { editorCmd } as unknown as Config
}

let tmpDir: string
let tmpFile: string

beforeAll(async () => {
  tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'editor-test-'))
  tmpFile = path.join(tmpDir, 'a-file.txt')
  await fs.writeFile(tmpFile, 'hi')
})

afterAll(async () => {
  await fs.rm(tmpDir, { recursive: true, force: true })
})

describe('openInEditor — validation', () => {
  it('rejects a missing/empty path with 400', async () => {
    expect((await openInEditor(cfg('true'), undefined)).status).toBe(400)
    expect((await openInEditor(cfg('true'), '')).status).toBe(400)
    expect((await openInEditor(cfg('true'), '   ')).status).toBe(400)
  })

  it('rejects a non-string path with 400', async () => {
    expect((await openInEditor(cfg('true'), 123)).status).toBe(400)
    expect((await openInEditor(cfg('true'), { path: '/x' })).status).toBe(400)
  })

  it('rejects a relative path with 400', async () => {
    const r = await openInEditor(cfg('true'), 'relative/dir')
    expect(r.ok).toBe(false)
    expect(r.status).toBe(400)
  })

  it('returns 404 for a non-existent absolute path', async () => {
    const r = await openInEditor(cfg('true'), path.join(tmpDir, 'does-not-exist'))
    expect(r.status).toBe(404)
  })

  it('returns 400 when the path is a file, not a directory', async () => {
    const r = await openInEditor(cfg('true'), tmpFile)
    expect(r.status).toBe(400)
  })
})

describe('openInEditor — launch', () => {
  it('spawns the editor for a valid directory (204)', async () => {
    const r = await openInEditor(cfg('true'), tmpDir)
    expect(r.ok).toBe(true)
    expect(r.status).toBe(204)
  })
})

// ── W1: openFileInEditor (open a FILE at a line) ──────────────────────────────

/** Poll for a file to exist + be non-empty (the editor spawn is fire-and-forget). */
async function waitForFile(p: string, timeoutMs = 3_000): Promise<string> {
  const start = Date.now()
  for (;;) {
    try {
      const content = await fs.readFile(p, 'utf8')
      if (content.length > 0) return content
    } catch {
      // not written yet
    }
    if (Date.now() - start > timeoutMs) throw new Error(`timeout waiting for ${p}`)
    await new Promise((r) => setTimeout(r, 25))
  }
}

describe('isGotoEditor', () => {
  it('is true for the VS Code family (code/cursor/windsurf/codium)', () => {
    expect(isGotoEditor('code')).toBe(true)
    expect(isGotoEditor('/usr/local/bin/cursor')).toBe(true)
    expect(isGotoEditor('windsurf')).toBe(true)
    expect(isGotoEditor('codium')).toBe(true)
    expect(isGotoEditor('code.cmd')).toBe(true) // Windows shim
  })

  it('is false for editors without --goto (vim/nano/true)', () => {
    expect(isGotoEditor('vim')).toBe(false)
    expect(isGotoEditor('nano')).toBe(false)
    expect(isGotoEditor('true')).toBe(false)
  })
})

describe('openFileInEditor — validation', () => {
  it('rejects a missing / empty / non-string file with 400', async () => {
    expect((await openFileInEditor(cfg('true'), undefined)).status).toBe(400)
    expect((await openFileInEditor(cfg('true'), '')).status).toBe(400)
    expect((await openFileInEditor(cfg('true'), '   ')).status).toBe(400)
    expect((await openFileInEditor(cfg('true'), 123)).status).toBe(400)
  })

  it('rejects a relative file with 400', async () => {
    expect((await openFileInEditor(cfg('true'), 'rel/a.ts')).status).toBe(400)
  })

  it('returns 404 for a non-existent absolute file', async () => {
    const r = await openFileInEditor(cfg('true'), path.join(tmpDir, 'nope.ts'))
    expect(r.status).toBe(404)
  })

  it('returns 400 when the path is a directory, not a file', async () => {
    const r = await openFileInEditor(cfg('true'), tmpDir)
    expect(r.status).toBe(400)
    expect(r.error).toContain('not a file')
  })

  it('rejects a non-integer / zero / out-of-range / non-number line with 400', async () => {
    expect((await openFileInEditor(cfg('true'), tmpFile, 1.5)).status).toBe(400)
    expect((await openFileInEditor(cfg('true'), tmpFile, 0)).status).toBe(400)
    expect((await openFileInEditor(cfg('true'), tmpFile, 1_000_000_001)).status).toBe(400)
    expect((await openFileInEditor(cfg('true'), tmpFile, 'x')).status).toBe(400)
  })
})

describe('openFileInEditor — launch', () => {
  it('spawns the editor for a valid file with no line (204)', async () => {
    const r = await openFileInEditor(cfg('true'), tmpFile)
    expect(r.ok).toBe(true)
    expect(r.status).toBe(204)
  })

  it('spawns the editor for a valid file + line (204)', async () => {
    const r = await openFileInEditor(cfg('true'), tmpFile, 42)
    expect(r.status).toBe(204)
  })

  it('passes `--goto <file>:<line>` for a goto-capable editor (basename code)', async () => {
    const argsOut = path.join(tmpDir, 'goto-args.txt')
    const recorder = path.join(tmpDir, 'code') // basename 'code' → isGotoEditor true
    await fs.writeFile(recorder, `#!/bin/sh\nprintf '%s\\n' "$@" > "${argsOut}"\n`)
    await fs.chmod(recorder, 0o755)

    const r = await openFileInEditor(cfg(recorder), tmpFile, 42)
    expect(r.status).toBe(204)
    const recorded = (await waitForFile(argsOut)).trim().split('\n')
    expect(recorded).toEqual(['--goto', `${tmpFile}:42`])
  })

  it('passes only the bare file for a non-goto editor (drops the line)', async () => {
    const argsOut = path.join(tmpDir, 'bare-args.txt')
    const recorder = path.join(tmpDir, 'myeditor') // basename not in the goto set
    await fs.writeFile(recorder, `#!/bin/sh\nprintf '%s\\n' "$@" > "${argsOut}"\n`)
    await fs.chmod(recorder, 0o755)

    const r = await openFileInEditor(cfg(recorder), tmpFile, 42)
    expect(r.status).toBe(204)
    const recorded = (await waitForFile(argsOut)).trim().split('\n')
    expect(recorded).toEqual([tmpFile])
  })
})
