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

// ── mock node:fs/promises + node:os ──────────────────────────────────────────
// Hoisted by vitest; history.ts binds to these mocks.
const mockReaddir = vi.fn()
const mockStat = vi.fn()
const mockOpen = vi.fn()
const mockHomedir = vi.fn(() => '/home/tester')

vi.mock('node:fs/promises', () => ({
  default: {
    readdir: (...a: unknown[]) => mockReaddir(...a),
    stat: (...a: unknown[]) => mockStat(...a),
    open: (...a: unknown[]) => mockOpen(...a),
  },
}))

vi.mock('node:os', async (importOriginal) => {
  const actual = await importOriginal<typeof import('node:os')>()
  return { ...actual, default: { ...actual.default, homedir: () => mockHomedir() } }
})

const { parseSessionMeta, listSessions } = await import('../src/http/history.js')

// ── parseSessionMeta (pure) ───────────────────────────────────────────────────
describe('parseSessionMeta', () => {
  it('extracts cwd and the first user prompt (string content)', () => {
    const jsonl = [
      JSON.stringify({ type: 'last-prompt', sessionId: 'x' }),
      JSON.stringify({ type: 'attachment', cwd: '/Users/me/proj', sessionId: 'x' }),
      JSON.stringify({ type: 'user', message: { role: 'user', content: 'Add monitoring please' } }),
    ].join('\n')
    expect(parseSessionMeta(jsonl)).toEqual({ cwd: '/Users/me/proj', preview: 'Add monitoring please' })
  })

  it('reads the first text block when content is an array', () => {
    const jsonl = [
      JSON.stringify({ type: 'user', cwd: '/p', message: { role: 'user', content: [{ type: 'text', text: 'hello there' }] } }),
    ].join('\n')
    expect(parseSessionMeta(jsonl)).toEqual({ cwd: '/p', preview: 'hello there' })
  })

  it('collapses whitespace and truncates long previews to 120 chars', () => {
    const long = 'a'.repeat(200)
    const jsonl = JSON.stringify({ type: 'user', cwd: '/p', message: { role: 'user', content: `line1\n   line2   ${long}` } })
    const { preview } = parseSessionMeta(jsonl)
    expect(preview.length).toBe(120)
    expect(preview.startsWith('line1 line2 ')).toBe(true)
  })

  it('returns nulls/empty when there is no cwd or user message', () => {
    expect(parseSessionMeta('')).toEqual({ cwd: null, preview: '' })
    expect(parseSessionMeta('not json\n{"type":"summary"}')).toEqual({ cwd: null, preview: '' })
  })
})

// ── listSessions (fs traversal, async) ────────────────────────────────────────
describe('listSessions', () => {
  function mockFile(text: string) {
    const buf = Buffer.from(text, 'utf8')
    return {
      read: vi.fn(async (b: Buffer, off: number, len: number) => {
        const n = buf.copy(b, off, 0, Math.min(len, buf.length))
        return { bytesRead: n }
      }),
      close: vi.fn(async () => undefined),
    }
  }

  beforeEach(() => {
    mockReaddir.mockReset()
    mockStat.mockReset()
    mockOpen.mockReset()
    mockHomedir.mockReturnValue('/home/tester')
  })

  it('returns [] when the projects root is missing', async () => {
    mockReaddir.mockRejectedValueOnce(new Error('ENOENT'))
    expect(await listSessions()).toEqual([])
  })

  it('lists .jsonl sessions newest-first with parsed cwd/preview/project', async () => {
    mockReaddir
      .mockResolvedValueOnce(['projA']) // root
      .mockResolvedValueOnce(['old.jsonl', 'new.jsonl', 'ignore.txt']) // projA
    mockStat.mockImplementation(async (p: string) => ({
      mtimeMs: p.endsWith('new.jsonl') ? 2000 : 1000,
    }))
    mockOpen.mockImplementation(async (p: string) =>
      p.endsWith('new.jsonl')
        ? mockFile(JSON.stringify({ type: 'user', cwd: '/work/newdir', message: { role: 'user', content: 'newest task' } }))
        : mockFile(JSON.stringify({ type: 'user', cwd: '/work/olddir', message: { role: 'user', content: 'older task' } })),
    )

    const out = await listSessions()
    expect(out.map((s) => s.id)).toEqual(['new', 'old']) // newest first
    expect(out[0]).toMatchObject({ id: 'new', cwd: '/work/newdir', project: 'newdir', preview: 'newest task' })
    // .txt is skipped
    expect(out).toHaveLength(2)
  })

  it('skips a project subdir that cannot be read', async () => {
    mockReaddir
      .mockResolvedValueOnce(['good', 'bad'])
      .mockResolvedValueOnce(['s.jsonl']) // good
      .mockRejectedValueOnce(new Error('EACCES')) // bad
    mockStat.mockResolvedValue({ mtimeMs: 1000 })
    mockOpen.mockResolvedValue(mockFile(JSON.stringify({ type: 'user', cwd: '/g', message: { role: 'user', content: 'hi' } })))

    const out = await listSessions()
    expect(out).toHaveLength(1)
    expect(out[0]!.id).toBe('s')
  })

  it('respects the limit', async () => {
    mockReaddir.mockResolvedValueOnce(['p']).mockResolvedValueOnce(['a.jsonl', 'b.jsonl', 'c.jsonl'])
    mockStat.mockImplementation(async (p: string) => ({ mtimeMs: p.charCodeAt(p.length - 7) }))
    mockOpen.mockResolvedValue(mockFile(JSON.stringify({ type: 'user', cwd: '/p', message: { role: 'user', content: 'x' } })))

    const out = await listSessions(2)
    expect(out).toHaveLength(2)
  })
})
