import { mkdtemp, rm } from 'fs/promises'
import { mkdirSync } from 'fs'
import { join } from 'path'
import { tmpdir } from 'os'
import { createRequire } from 'node:module'

import { afterEach, beforeEach, describe, expect, it } from 'vitest'
import { createWarpProvider } from '../../src/providers/warp.js'
import { isSqliteAvailable } from '../../src/sqlite.js'
import type { ParsedProviderCall } from '../../src/providers/types.js'

const requireForTest = createRequire(import.meta.url)

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

type QueryFixture = {
  exchangeId: string
  conversationId: string
  startTs: string
  input: string
  outputStatus?: string
  modelId?: string
  workingDirectory?: string | null
}

type BlockFixture = {
  blockId: string
  conversationId: string
  startTs: string
  completedTs: string
  exitCode: number
  command: string
}

let tmpDir: string

beforeEach(async () => {
  tmpDir = await mkdtemp(join(tmpdir(), 'warp-provider-test-'))
})

afterEach(async () => {
  await rm(tmpDir, { recursive: true, force: true })
})

function createWarpDb(dir: string): string {
  mkdirSync(dir, { recursive: true })
  const dbPath = join(dir, 'warp.sqlite')
  const { DatabaseSync: Database } = requireForTest('node:sqlite')
  const db = new Database(dbPath)
  db.exec(`
    CREATE TABLE IF NOT EXISTS agent_conversations (
      id INTEGER PRIMARY KEY AUTOINCREMENT,
      conversation_id TEXT NOT NULL,
      conversation_data TEXT NOT NULL,
      last_modified_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
    )
  `)
  db.exec(`
    CREATE TABLE IF NOT EXISTS ai_queries (
      id INTEGER PRIMARY KEY AUTOINCREMENT,
      exchange_id TEXT NOT NULL,
      conversation_id TEXT NOT NULL,
      start_ts DATETIME NOT NULL,
      input TEXT NOT NULL,
      working_directory TEXT,
      output_status TEXT NOT NULL,
      model_id TEXT NOT NULL DEFAULT '',
      planning_model_id TEXT NOT NULL DEFAULT '',
      coding_model_id TEXT NOT NULL DEFAULT ''
    )
  `)
  db.exec(`
    CREATE TABLE IF NOT EXISTS blocks (
      id INTEGER PRIMARY KEY AUTOINCREMENT,
      pane_leaf_uuid BLOB NOT NULL,
      stylized_command BLOB NOT NULL,
      stylized_output BLOB NOT NULL,
      pwd TEXT,
      git_branch TEXT,
      virtual_env TEXT,
      conda_env TEXT,
      exit_code INTEGER NOT NULL,
      did_execute BOOLEAN NOT NULL,
      completed_ts DATETIME,
      start_ts DATETIME,
      ps1 TEXT,
      honor_ps1 BOOLEAN NOT NULL DEFAULT 0,
      shell TEXT,
      user TEXT,
      host TEXT,
      is_background BOOLEAN NOT NULL DEFAULT 0,
      rprompt TEXT,
      prompt_snapshot TEXT,
      block_id TEXT NOT NULL DEFAULT '',
      ai_metadata TEXT,
      is_local BOOLEAN,
      agent_view_visibility TEXT,
      git_branch_name TEXT
    )
  `)
  db.close()
  return dbPath
}

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

function insertConversation(
  db: TestDb,
  conversationId: string,
  conversationData: unknown,
  lastModifiedAt = '2026-05-18 10:10:00',
): void {
  db.prepare(
    'INSERT INTO agent_conversations (conversation_id, conversation_data, last_modified_at) VALUES (?, ?, ?)',
  ).run(conversationId, JSON.stringify(conversationData), lastModifiedAt)
}

function insertQuery(db: TestDb, q: QueryFixture): void {
  db.prepare(
    `INSERT INTO ai_queries (
      exchange_id, conversation_id, start_ts, input, working_directory, output_status, model_id, planning_model_id, coding_model_id
    ) VALUES (?, ?, ?, ?, ?, ?, ?, '', '')`,
  ).run(
    q.exchangeId,
    q.conversationId,
    q.startTs,
    q.input,
    q.workingDirectory ?? null,
    q.outputStatus ?? '"Completed"',
    q.modelId ?? 'auto-efficient',
  )
}

function insertBlock(db: TestDb, b: BlockFixture): void {
  db.prepare(
    `INSERT INTO blocks (
      pane_leaf_uuid, stylized_command, stylized_output, exit_code, did_execute,
      completed_ts, start_ts, block_id, ai_metadata
    ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
  ).run(
    Buffer.from([0]),
    b.command,
    '',
    b.exitCode,
    1,
    b.completedTs,
    b.startTs,
    b.blockId,
    JSON.stringify({
      requested_command_action_id: `call-${b.blockId}`,
      conversation_id: b.conversationId,
    }),
  )
}

async function collectCalls(
  dbPath: string,
  conversationId: string,
  seenKeys = new Set<string>(),
): Promise<ParsedProviderCall[]> {
  const provider = createWarpProvider(dbPath)
  const source = {
    path: `${dbPath}:${conversationId}`,
    project: 'warp',
    provider: 'warp',
  }
  const calls: ParsedProviderCall[] = []
  for await (const call of provider.createSessionParser(source, seenKeys).parse()) {
    calls.push(call)
  }
  return calls
}

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

skipUnlessSqlite('warp provider', () => {
  it('discovers sessions and sanitizes project names from working_directory', async () => {
    const dbPath = createWarpDb(tmpDir)
    withTestDb(dbPath, (db) => {
      insertConversation(db, 'conv-1', { conversation_usage_metadata: { token_usage: [] } })
      insertQuery(db, {
        exchangeId: 'ex-1',
        conversationId: 'conv-1',
        startTs: '2026-05-18 10:00:00.000000',
        input: '[]',
        workingDirectory: '/Users/test/project-a',
      })
    })

    const provider = createWarpProvider(dbPath)
    const sessions = await provider.discoverSessions()
    expect(sessions).toHaveLength(1)
    expect(sessions[0]!.provider).toBe('warp')
    expect(sessions[0]!.path).toBe(`${dbPath}:conv-1`)
    expect(sessions[0]!.project).toBe('Users-test-project-a')
  })

  it('parses one call per completed exchange and estimates tokens from primary-agent totals', async () => {
    const dbPath = createWarpDb(tmpDir)
    withTestDb(dbPath, (db) => {
      insertConversation(db, 'conv-1', {
        conversation_usage_metadata: {
          token_usage: [
            {
              model_id: 'GPT-5.3 Codex (medium reasoning)',
              warp_tokens: 300,
              byok_tokens: 0,
              warp_token_usage_by_category: { primary_agent: 300 },
              byok_token_usage_by_category: {},
            },
            {
              model_id: 'Claude Haiku 4.5',
              warp_tokens: 90,
              byok_tokens: 0,
              warp_token_usage_by_category: { full_terminal_use: 90 },
              byok_token_usage_by_category: {},
            },
          ],
        },
      })
      insertQuery(db, {
        exchangeId: 'ex-1',
        conversationId: 'conv-1',
        startTs: '2026-05-18 10:00:00.000000',
        input: JSON.stringify([{ Query: { text: 'short prompt' } }]),
        modelId: 'auto-efficient',
        workingDirectory: '/Users/test/project-a',
      })
      insertQuery(db, {
        exchangeId: 'ex-2',
        conversationId: 'conv-1',
        startTs: '2026-05-18 10:03:00.000000',
        input: JSON.stringify([{ Query: { text: 'longer prompt with substantially more detail for weighting' } }]),
        modelId: 'auto-efficient',
        workingDirectory: '/Users/test/project-a',
      })
    })

    const calls = await collectCalls(dbPath, 'conv-1')
    expect(calls).toHaveLength(2)
    expect(calls.map(call => call.deduplicationKey)).toEqual([
      'warp:conv-1:ex-1',
      'warp:conv-1:ex-2',
    ])
    expect(calls.map(call => call.userMessage)).toEqual([
      'short prompt',
      'longer prompt with substantially more detail for weighting',
    ])
    expect(calls.every(call => call.model === 'gpt-5.3-codex')).toBe(true)
    expect(calls.every(call => call.costIsEstimated === true)).toBe(true)
    expect(calls.reduce((sum, call) => sum + call.inputTokens, 0)).toBe(300)
    expect(calls[1]!.inputTokens).toBeGreaterThan(calls[0]!.inputTokens)
    expect(calls[0]!.projectPath).toBe('/Users/test/project-a')
    expect(calls[0]!.project).toBe('Users-test-project-a')
  })

  it('attributes command blocks to the nearest preceding exchange and extracts Bash commands', async () => {
    const dbPath = createWarpDb(tmpDir)
    withTestDb(dbPath, (db) => {
      insertConversation(db, 'conv-2', {
        conversation_usage_metadata: {
          token_usage: [
            {
              model_id: 'GPT-5.3 Codex (medium reasoning)',
              warp_tokens: 120,
              byok_tokens: 0,
              warp_token_usage_by_category: { primary_agent: 120 },
              byok_token_usage_by_category: {},
            },
          ],
        },
      })
      insertQuery(db, {
        exchangeId: 'ex-a',
        conversationId: 'conv-2',
        startTs: '2026-05-18 11:00:00.000000',
        input: JSON.stringify([{ Query: { text: 'run tests' } }]),
      })
      insertQuery(db, {
        exchangeId: 'ex-b',
        conversationId: 'conv-2',
        startTs: '2026-05-18 11:05:00.000000',
        input: JSON.stringify([{ Query: { text: 'summarize results' } }]),
      })
      insertBlock(db, {
        blockId: 'block-1',
        conversationId: 'conv-2',
        startTs: '2026-05-18 11:01:00.000000',
        completedTs: '2026-05-18 11:01:04.000000',
        exitCode: 0,
        command: 'npm test && git status',
      })
    })

    const calls = await collectCalls(dbPath, 'conv-2')
    expect(calls).toHaveLength(2)
    expect(calls[0]!.tools).toEqual(['Bash'])
    expect(calls[0]!.bashCommands).toEqual(['npm', 'git'])
    expect(calls[1]!.tools).toEqual([])
    expect(calls[1]!.bashCommands).toEqual([])
  })

  it('skips pending or invalid exchanges and does not poison seenKeys for skipped rows', async () => {
    const dbPath = createWarpDb(tmpDir)
    withTestDb(dbPath, (db) => {
      insertConversation(db, 'conv-3', {
        conversation_usage_metadata: {
          token_usage: [
            {
              model_id: 'GPT-5.3 Codex (medium reasoning)',
              warp_tokens: 42,
              byok_tokens: 0,
              warp_token_usage_by_category: { primary_agent: 42 },
              byok_token_usage_by_category: {},
            },
          ],
        },
      })
      insertQuery(db, {
        exchangeId: 'ex-skip-seen',
        conversationId: 'conv-3',
        startTs: '2026-05-18 12:00:00.000000',
        input: JSON.stringify([{ Query: { text: 'already seen' } }]),
      })
      insertQuery(db, {
        exchangeId: 'ex-pending',
        conversationId: 'conv-3',
        startTs: '2026-05-18 12:01:00.000000',
        input: JSON.stringify([{ Query: { text: 'still running' } }]),
        outputStatus: '"Pending"',
      })
      insertQuery(db, {
        exchangeId: 'ex-invalid-ts',
        conversationId: 'conv-3',
        startTs: 'not-a-timestamp',
        input: JSON.stringify([{ Query: { text: 'bad timestamp' } }]),
      })
    })

    const seen = new Set<string>(['warp:conv-3:ex-skip-seen'])
    const calls = await collectCalls(dbPath, 'conv-3', seen)
    expect(calls).toEqual([])
    expect(seen.has('warp:conv-3:ex-invalid-ts')).toBe(false)
    expect(seen.has('warp:conv-3:ex-pending')).toBe(false)
  })
})
