import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { mkdtemp, mkdir, readFile, rm, writeFile } from 'fs/promises'
import { existsSync } from 'fs'
import { join } from 'path'
import { tmpdir } from 'os'

import { getAllProviders } from '../../src/providers/index.js'
import { createCursorAgentProvider } from '../../src/providers/cursor-agent.js'
import type { ParsedProviderCall, Provider, SessionSource } from '../../src/providers/types.js'
import { isSqliteAvailable } from '../../src/sqlite.js'

const CHARS_PER_TOKEN = 4
const CURSOR_AGENT_DEFAULT_MODEL = 'cursor-agent-auto'
const FIXED_UUID = '123e4567-e89b-12d3-a456-426614174000'

const skipUnlessSqlite = isSqliteAvailable() ? describe : describe.skip

type TestDb = {
  exec(sql: string): void
  prepare(sql: string): { run(...params: unknown[]): void }
  close(): void
}

let tempRoots: string[] = []

beforeEach(() => {
  tempRoots = []
})

afterEach(async () => {
  await Promise.all(tempRoots.filter(existsSync).map((dir) => rm(dir, { recursive: true, force: true })))
})

async function makeBaseDir(): Promise<string> {
  const dir = await mkdtemp(join(tmpdir(), 'cursor-agent-test-'))
  tempRoots.push(dir)
  return dir
}

async function collectCalls(provider: Provider, source: SessionSource): Promise<ParsedProviderCall[]> {
  const calls: ParsedProviderCall[] = []
  for await (const call of provider.createSessionParser(source, new Set()).parse()) {
    calls.push(call)
  }
  return calls
}

function withTestDb(dbPath: string, fn: (db: TestDb) => void): void {
  const { DatabaseSync: Database } = require('node:sqlite')
  const db = new Database(dbPath)
  fn(db)
  db.close()
}

describe('cursor-agent provider', () => {
  it('is registered', async () => {
    const all = await getAllProviders()
    const provider = all.find((p) => p.name === 'cursor-agent')

    expect(provider).toBeDefined()
    expect(provider?.displayName).toBe('Cursor Agent')
  })

  it('maps default model to Cursor (auto) label', () => {
    const provider = createCursorAgentProvider('/tmp/nonexistent-cursor-agent-fixture')
    expect(provider.modelDisplayName('cursor-agent-auto')).toBe('Cursor (auto)')
  })

  it('maps known models and appends estimation label', () => {
    const provider = createCursorAgentProvider('/tmp/nonexistent-cursor-agent-fixture')

    expect(provider.modelDisplayName('claude-4.5-opus-high-thinking')).toBe('Opus 4.5 (Thinking) (est.)')
    expect(provider.modelDisplayName('claude-4.6-sonnet')).toBe('Sonnet 4.6 (est.)')
    expect(provider.modelDisplayName('composer-1')).toBe('Composer 1 (est.)')
  })

  it('falls through to raw model name for unknown models with single est. suffix', () => {
    const provider = createCursorAgentProvider('/tmp/nonexistent-cursor-agent-fixture')

    expect(provider.modelDisplayName('claude-5-future-model')).toBe('claude-5-future-model (est.)')
    expect(provider.modelDisplayName('gpt-9')).toBe('gpt-9 (est.)')
  })

  it('returns identity for tool display name', () => {
    const provider = createCursorAgentProvider('/tmp/nonexistent-cursor-agent-fixture')
    expect(provider.toolDisplayName('cursor:edit')).toBe('cursor:edit')
  })

  it('returns empty discovery when projects dir is missing', async () => {
    const baseDir = await makeBaseDir()
    const provider = createCursorAgentProvider(baseDir)
    const sources = await provider.discoverSessions()

    expect(sources).toEqual([])
  })

  it('discovers a single transcript', async () => {
    const baseDir = await makeBaseDir()
    const transcriptDir = join(baseDir, 'projects', 'test-proj', 'agent-transcripts')
    await mkdir(transcriptDir, { recursive: true })
    const transcriptPath = join(transcriptDir, `${FIXED_UUID}.txt`)
    await writeFile(transcriptPath, 'user:\n<user_query>hello</user_query>\nA:\nworld\n')

    const provider = createCursorAgentProvider(baseDir)
    const sources = await provider.discoverSessions()

    expect(sources).toHaveLength(1)
    expect(sources[0]!.provider).toBe('cursor-agent')
    expect(sources[0]!.path).toBe(transcriptPath)
  })

  it('discovers transcripts across multiple projects', async () => {
    const baseDir = await makeBaseDir()
    const transcriptA = join(baseDir, 'projects', 'proj-one', 'agent-transcripts')
    const transcriptB = join(baseDir, 'projects', 'proj-two', 'agent-transcripts')
    await mkdir(transcriptA, { recursive: true })
    await mkdir(transcriptB, { recursive: true })
    await writeFile(join(transcriptA, `${FIXED_UUID}.txt`), 'user:\n<user_query>a</user_query>\nA:\na\n')
    await writeFile(join(transcriptB, `${FIXED_UUID}.txt`), 'user:\n<user_query>b</user_query>\nA:\nb\n')

    const provider = createCursorAgentProvider(baseDir)
    const sources = await provider.discoverSessions()

    expect(sources).toHaveLength(2)
    expect(sources.every((s) => s.provider === 'cursor-agent')).toBe(true)
  })

  it('does not scan a workspace root when agent-transcripts is missing', async () => {
    const baseDir = await makeBaseDir()
    const workspaceRoot = join(baseDir, 'projects', 'workspace-without-transcripts')
    await mkdir(workspaceRoot, { recursive: true })
    await writeFile(
      join(workspaceRoot, 'extension-state.txt'),
      'user:\n<user_query>not a transcript</user_query>\nA:\nnot a cursor-agent answer\n',
    )

    const provider = createCursorAgentProvider(baseDir)
    const sources = await provider.discoverSessions()

    expect(sources).toEqual([])
  })

  it('prefers jsonl over same-session txt inside UUID transcript dirs', async () => {
    const baseDir = await makeBaseDir()
    const sessionDir = join(baseDir, 'projects', 'proj-with-duplicates', 'agent-transcripts', FIXED_UUID)
    const jsonlPath = join(sessionDir, `${FIXED_UUID}.jsonl`)
    const txtPath = join(sessionDir, `${FIXED_UUID}.txt`)
    await mkdir(sessionDir, { recursive: true })
    await writeFile(
      jsonlPath,
      '{"role":"user","message":{"content":[{"type":"text","text":"<user_query>jsonl wins</user_query>"}]}}\n{"role":"assistant","message":{"content":[{"type":"text","text":"jsonl answer"}]}}\n',
    )
    await writeFile(txtPath, 'user:\n<user_query>txt duplicate</user_query>\nA:\ntxt answer\n')

    const provider = createCursorAgentProvider(baseDir)
    const sources = await provider.discoverSessions()

    expect(sources).toHaveLength(1)
    expect(sources[0]!.path).toBe(jsonlPath)
  })

  it('parses one user/assistant pair with estimated token counts', async () => {
    const baseDir = await makeBaseDir()
    const transcriptDir = join(baseDir, 'projects', 'my-proj', 'agent-transcripts')
    await mkdir(transcriptDir, { recursive: true })

    const userText = 'explain parser output'
    const assistantText = 'first line\nsecond line'
    const transcriptPath = join(transcriptDir, `${FIXED_UUID}.txt`)

    await writeFile(
      transcriptPath,
      `user:\n<user_query>${userText}</user_query>\nA:\n${assistantText}\n`
    )

    const provider = createCursorAgentProvider(baseDir)
    const source = (await provider.discoverSessions())[0]!
    const calls = await collectCalls(provider, source)

    expect(calls).toHaveLength(1)
    expect(calls[0]!.provider).toBe('cursor-agent')
    expect(calls[0]!.model).toBe(CURSOR_AGENT_DEFAULT_MODEL)
    expect(calls[0]!.inputTokens).toBe(Math.ceil(userText.length / CHARS_PER_TOKEN))
    expect(calls[0]!.outputTokens).toBe(Math.ceil(assistantText.length / CHARS_PER_TOKEN))
    expect(calls[0]!.reasoningTokens).toBe(0)
    expect(calls[0]!.deduplicationKey).toBe(`cursor-agent:${FIXED_UUID}:0`)
  })

  it('parses without sqlite db and defaults model', async () => {
    const baseDir = await makeBaseDir()
    const transcriptDir = join(baseDir, 'projects', 'fallback-proj', 'agent-transcripts')
    await mkdir(transcriptDir, { recursive: true })
    const transcriptPath = join(transcriptDir, `${FIXED_UUID}.txt`)

    await writeFile(transcriptPath, 'user:\n<user_query>hello world</user_query>\nA:\n[Thinking]private\nvisible\n')

    const provider = createCursorAgentProvider(baseDir)
    const source = (await provider.discoverSessions())[0]!
    const calls = await collectCalls(provider, source)

    expect(calls).toHaveLength(1)
    expect(calls[0]!.model).toBe(CURSOR_AGENT_DEFAULT_MODEL)
    expect(calls[0]!.reasoningTokens).toBe(2)
    expect(calls[0]!.outputTokens).toBe(2)
  })

  it('skips unrecognized transcript format and writes stderr message', async () => {
    const baseDir = await makeBaseDir()
    const transcriptDir = join(baseDir, 'projects', 'bad-proj', 'agent-transcripts')
    await mkdir(transcriptDir, { recursive: true })
    const transcriptPath = join(transcriptDir, `${FIXED_UUID}.txt`)
    await writeFile(transcriptPath, 'no markers in this transcript')

    const provider = createCursorAgentProvider(baseDir)
    const source = (await provider.discoverSessions())[0]!
    const stderrSpy = vi.spyOn(process.stderr, 'write').mockImplementation(() => true)

    const calls = await collectCalls(provider, source)

    expect(calls).toHaveLength(0)
    expect(stderrSpy).toHaveBeenCalled()
    expect(String(stderrSpy.mock.calls[0]?.[0] ?? '')).toContain('unrecognized cursor-agent transcript format')

    stderrSpy.mockRestore()
  })

  it('warns only once for the same unrecognized transcript', async () => {
    const baseDir = await makeBaseDir()
    const transcriptDir = join(baseDir, 'projects', 'bad-proj-repeat', 'agent-transcripts')
    await mkdir(transcriptDir, { recursive: true })
    const transcriptPath = join(transcriptDir, 'repeat-bad.txt')
    await writeFile(transcriptPath, 'no cursor-agent markers here')

    const provider = createCursorAgentProvider(baseDir)
    const source = (await provider.discoverSessions())[0]!
    const stderrSpy = vi.spyOn(process.stderr, 'write').mockImplementation(() => true)

    await collectCalls(provider, source)
    await collectCalls(provider, source)

    const warnings = stderrSpy.mock.calls
      .map(call => String(call[0] ?? ''))
      .filter(message => message.includes('unrecognized cursor-agent transcript format'))
    expect(warnings).toHaveLength(1)

    stderrSpy.mockRestore()
  })

  it('discovers jsonl transcripts stored directly under project dir (workspace-less layout)', async () => {
    const baseDir = await makeBaseDir()
    const fixtureRoot = join(import.meta.dirname, '../fixtures/cursor-agent/workspace-less')
    const sessionDir = join(baseDir, 'projects', 'agent-transcripts', '1031d227-0c67-4e17-8954-0b6e2b3322f0')
    await mkdir(sessionDir, { recursive: true })
    await writeFile(
      join(sessionDir, '1031d227-0c67-4e17-8954-0b6e2b3322f0.jsonl'),
      await readFile(
        join(
          fixtureRoot,
          'projects/agent-transcripts/1031d227-0c67-4e17-8954-0b6e2b3322f0/1031d227-0c67-4e17-8954-0b6e2b3322f0.jsonl',
        ),
        'utf-8',
      ),
    )

    const provider = createCursorAgentProvider(baseDir)
    const sources = await provider.discoverSessions()

    expect(sources).toHaveLength(1)
    expect(sources[0]!.project).toBe('transcripts')
    expect(sources[0]!.path.endsWith('.jsonl')).toBe(true)

    const calls = await collectCalls(provider, sources[0]!)
    expect(calls).toHaveLength(1)
    expect(calls[0]!.sessionId).toBe('1031d227-0c67-4e17-8954-0b6e2b3322f0')
    expect(calls[0]!.userMessage).toBe('Run a quick smoke test')
    expect(calls[0]!.costUSD).toBeGreaterThan(0)
  })

  it('falls back to stable sha1 conversation id for non-uuid filenames', async () => {
    const baseDir = await makeBaseDir()
    const transcriptDir = join(baseDir, 'projects', 'sha-proj', 'agent-transcripts')
    await mkdir(transcriptDir, { recursive: true })
    const transcriptPath = join(transcriptDir, 'not-a-uuid.txt')
    await writeFile(transcriptPath, 'user:\n<user_query>test</user_query>\nA:\nresult\n')

    const provider = createCursorAgentProvider(baseDir)
    const source = (await provider.discoverSessions())[0]!

    const callsFirst = await collectCalls(provider, source)
    const callsSecond = await collectCalls(provider, source)

    expect(callsFirst).toHaveLength(1)
    expect(callsSecond).toHaveLength(1)
    expect(callsFirst[0]!.sessionId).toHaveLength(16)
    expect(callsFirst[0]!.deduplicationKey.startsWith('cursor-agent:')).toBe(true)
    expect(callsFirst[0]!.sessionId).toBe(callsSecond[0]!.sessionId)
    expect(callsFirst[0]!.deduplicationKey).toBe(callsSecond[0]!.deduplicationKey)
  })
})

skipUnlessSqlite('cursor-agent sqlite metadata', () => {
  it('uses model metadata from ai-code-tracking db when present', async () => {
    const baseDir = await makeBaseDir()
    const transcriptDir = join(baseDir, 'projects', 'proj-with-db', 'agent-transcripts')
    const aiTrackingDir = join(baseDir, 'ai-tracking')
    await mkdir(transcriptDir, { recursive: true })
    await mkdir(aiTrackingDir, { recursive: true })

    await writeFile(
      join(transcriptDir, `${FIXED_UUID}.txt`),
      'user:\n<user_query>estimate cost</user_query>\nA:\nanswer\n'
    )

    const dbPath = join(aiTrackingDir, 'ai-code-tracking.db')
    withTestDb(dbPath, (db) => {
      db.exec('CREATE TABLE conversation_summaries (conversationId TEXT, title TEXT, tldr TEXT, model TEXT, mode TEXT, updatedAt INTEGER)')
      db.prepare('INSERT INTO conversation_summaries (conversationId, title, tldr, model, mode, updatedAt) VALUES (?, ?, ?, ?, ?, ?)')
        .run(FIXED_UUID, 'Demo title', '', 'claude-4.6-sonnet', 'agent', 1735689600000)
    })

    const provider = createCursorAgentProvider(baseDir)
    const source = (await provider.discoverSessions())[0]!
    const calls = await collectCalls(provider, source)

    expect(calls).toHaveLength(1)
    expect(calls[0]!.model).toBe('claude-4.6-sonnet')
    expect(calls[0]!.timestamp).toBe('2025-01-01T00:00:00.000Z')
  })
})
