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

import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { calculateCost } from '../../src/models.js'
import { createHermesProvider } from '../../src/providers/hermes.js'
import { isSqliteAvailable } from '../../src/sqlite.js'
import type { ParsedProviderCall } from '../../src/providers/types.js'
import type { DateRange } from '../../src/types.js'

const requireForTest = createRequire(import.meta.url)

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

let tmpDir: string
let cacheDir: string
let originalHermesHome: string | undefined
let originalCodeburnCacheDir: string | undefined

beforeEach(async () => {
  tmpDir = await mkdtemp(join(tmpdir(), 'hermes-provider-test-'))
  cacheDir = await mkdtemp(join(tmpdir(), 'hermes-provider-cache-'))
  originalHermesHome = process.env['HERMES_HOME']
  originalCodeburnCacheDir = process.env['CODEBURN_CACHE_DIR']
})

afterEach(async () => {
  if (originalHermesHome === undefined) delete process.env['HERMES_HOME']
  else process.env['HERMES_HOME'] = originalHermesHome
  if (originalCodeburnCacheDir === undefined) delete process.env['CODEBURN_CACHE_DIR']
  else process.env['CODEBURN_CACHE_DIR'] = originalCodeburnCacheDir
  await rm(tmpDir, { recursive: true, force: true })
  await rm(cacheDir, { recursive: true, force: true })
})

function createHermesDb(homeDir: string): string {
  const { DatabaseSync: Database } = requireForTest('node:sqlite')
  const dbPath = join(homeDir, 'state.db')
  const db = new Database(dbPath)
  db.exec(`
    CREATE TABLE sessions (
      id TEXT PRIMARY KEY,
      source TEXT,
      model TEXT,
      cwd TEXT,
      billing_provider TEXT,
      billing_base_url TEXT,
      billing_mode TEXT,
      input_tokens INTEGER DEFAULT 0,
      output_tokens INTEGER DEFAULT 0,
      cache_read_tokens INTEGER DEFAULT 0,
      cache_write_tokens INTEGER DEFAULT 0,
      reasoning_tokens INTEGER DEFAULT 0,
      estimated_cost_usd REAL,
      actual_cost_usd REAL,
      cost_status TEXT,
      api_call_count INTEGER DEFAULT 0,
      message_count INTEGER DEFAULT 0,
      tool_call_count INTEGER DEFAULT 0,
      started_at REAL,
      ended_at REAL,
      title TEXT
    )
  `)
  db.exec(`
    CREATE TABLE messages (
      id INTEGER PRIMARY KEY AUTOINCREMENT,
      session_id TEXT NOT NULL,
      role TEXT NOT NULL,
      content TEXT,
      tool_call_id TEXT,
      tool_calls TEXT,
      tool_name TEXT,
      timestamp REAL NOT NULL
    )
  `)
  db.close()
  return dbPath
}

function createLegacyHermesDb(homeDir: string): string {
  const { DatabaseSync: Database } = requireForTest('node:sqlite')
  const dbPath = join(homeDir, 'state.db')
  const db = new Database(dbPath)
  db.exec(`
    CREATE TABLE sessions (
      id TEXT PRIMARY KEY,
      model TEXT,
      input_tokens INTEGER DEFAULT 0,
      output_tokens INTEGER DEFAULT 0,
      started_at REAL
    )
  `)
  db.exec(`
    CREATE TABLE messages (
      id INTEGER PRIMARY KEY AUTOINCREMENT,
      session_id TEXT NOT NULL,
      role TEXT NOT NULL,
      content TEXT,
      tool_calls TEXT,
      timestamp REAL NOT NULL
    )
  `)
  db.close()
  return dbPath
}

async function createProfileHermesDb(hermesHome: string, profile: string): Promise<string> {
  const profileDir = join(hermesHome, 'profiles', profile)
  await mkdir(profileDir, { recursive: true })
  return createHermesDb(profileDir)
}

function insertSession(db: TestDb, values: {
  id: string
  source?: string
  model?: string
  cwd?: string | null
  billingProvider?: string
  inputTokens: number
  outputTokens: number
  cacheReadTokens: number
  cacheWriteTokens: number
  reasoningTokens: number
  estimatedCost?: number | null
  actualCost?: number | null
  apiCalls?: number
  toolCalls?: number
  startedAt: number
  title?: string
}): void {
  db.prepare(
    `INSERT INTO sessions (
      id, source, model, cwd, billing_provider, input_tokens, output_tokens,
      cache_read_tokens, cache_write_tokens, reasoning_tokens, estimated_cost_usd,
      actual_cost_usd, api_call_count, tool_call_count, started_at, title
    ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
  ).run(
    values.id,
    values.source ?? 'cli',
    values.model ?? 'gpt-5.5',
    values.cwd ?? null,
    values.billingProvider ?? 'openai-codex',
    values.inputTokens,
    values.outputTokens,
    values.cacheReadTokens,
    values.cacheWriteTokens,
    values.reasoningTokens,
    values.estimatedCost ?? null,
    values.actualCost ?? null,
    values.apiCalls ?? 1,
    values.toolCalls ?? 0,
    values.startedAt,
    values.title ?? values.id,
  )
}

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 dayRange(): DateRange {
  return {
    start: new Date('2026-05-23T00:00:00.000Z'),
    end: new Date('2026-05-23T23:59:59.999Z'),
  }
}

async function loadParserWithHermesHome(hermesHome: string, codeburnCacheDir: string) {
  process.env['HERMES_HOME'] = hermesHome
  process.env['CODEBURN_CACHE_DIR'] = codeburnCacheDir
  vi.resetModules()
  const parser = await import('../../src/parser.js')
  return parser
}

async function collectCalls(hermesHome: string, sourcePath: string): Promise<ParsedProviderCall[]> {
  const provider = createHermesProvider(hermesHome)
  const calls: ParsedProviderCall[] = []
  for await (const call of provider.createSessionParser({ path: sourcePath, project: 'hermes', provider: 'hermes' }, new Set()).parse()) {
    calls.push(call)
  }
  return calls
}

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

skipUnlessSqlite('hermes provider', () => {
  it('discovers state.db sessions with token usage', async () => {
    const dbPath = createHermesDb(tmpDir)
    withTestDb(dbPath, (db) => {
      insertSession(db, {
        id: 'session-1',
        inputTokens: 100,
        outputTokens: 20,
        cacheReadTokens: 50,
        cacheWriteTokens: 0,
        reasoningTokens: 5,
        startedAt: 1779549200,
        title: 'Test Project',
      })
      db.prepare(
        `INSERT INTO sessions (id, source, model, input_tokens, output_tokens, started_at, title)
         VALUES (?, ?, ?, ?, ?, ?, ?)`,
      ).run('empty', 'cli', 'gpt-5.5', 0, 0, 1779549300, 'Empty')
    })

    const provider = createHermesProvider(tmpDir)
    const sessions = await provider.discoverSessions()
    expect(sessions).toHaveLength(1)
    expect(sessions[0]!.provider).toBe('hermes')
    expect(sessions[0]!.path).toBe(`${dbPath}#hermes-session=session-1`)
    expect(sessions[0]!.project).toBe('default')
  })

  it('parses session-level token usage and tool calls from messages', async () => {
    const dbPath = createHermesDb(tmpDir)
    withTestDb(dbPath, (db) => {
      insertSession(db, {
        id: 'session-1',
        source: 'tui',
        inputTokens: 1000,
        outputTokens: 200,
        cacheReadTokens: 300,
        cacheWriteTokens: 40,
        reasoningTokens: 25,
        estimatedCost: 0.12,
        apiCalls: 3,
        toolCalls: 2,
        startedAt: 1779549200,
        title: 'Provider Work',
      })
      db.prepare('INSERT INTO messages (session_id, role, content, timestamp) VALUES (?, ?, ?, ?)')
        .run('session-1', 'user', 'Add Hermes support', 1779549201)
      db.prepare('INSERT INTO messages (session_id, role, content, tool_calls, timestamp) VALUES (?, ?, ?, ?, ?)')
        .run(
          'session-1',
          'assistant',
          '',
          JSON.stringify([
            { function: { name: 'read_file', arguments: JSON.stringify({ path: '/tmp/file.ts' }) } },
            { function: { name: 'terminal', arguments: JSON.stringify({ command: 'npm test' }) } },
          ]),
          1779549202,
        )
    })

    const calls = await collectCalls(tmpDir, `${dbPath}#hermes-session=session-1`)
    expect(calls).toHaveLength(1)
    expect(calls[0]!).toMatchObject({
      provider: 'hermes',
      model: 'gpt-5.5',
      inputTokens: 1000,
      outputTokens: 200,
      cacheReadInputTokens: 300,
      cacheCreationInputTokens: 40,
      cachedInputTokens: 300,
      reasoningTokens: 25,
      costUSD: 0.12,
      userMessage: 'Add Hermes support',
      sessionId: 'session-1',
      deduplicationKey: 'hermes:default:session-1',
    })
    expect(calls[0]!.tools).toEqual(['Read', 'Bash'])
    expect(calls[0]!.bashCommands).toEqual(['npm test'])
    expect(calls[0]!.toolSequence).toEqual([
      [{ tool: 'Read', file: '/tmp/file.ts' }, { tool: 'Bash', command: 'npm test' }],
    ])
  })


  it('maps composio MCP tools before generic MCP prefixes', () => {
    const provider = createHermesProvider(tmpDir)
    expect(provider.toolDisplayName('mcp_composio_GMAIL_SEND_EMAIL')).toBe('MCP')
    expect(provider.toolDisplayName('mcp__github__create_issue')).toBe('mcp__github__create_issue')
  })

  it('falls back to calculateCost when no actual or estimated cost is recorded', async () => {
    const dbPath = createHermesDb(tmpDir)
    withTestDb(dbPath, (db) => {
      insertSession(db, {
        id: 'no-cost-session',
        model: 'claude-sonnet-4-20250514',
        inputTokens: 1000,
        outputTokens: 200,
        cacheReadTokens: 0,
        cacheWriteTokens: 0,
        reasoningTokens: 50,
        estimatedCost: null,
        actualCost: null,
        startedAt: 1779549200,
      })
      db.prepare('INSERT INTO messages (session_id, role, content, timestamp) VALUES (?, ?, ?, ?)')
        .run('no-cost-session', 'user', 'Test calculateCost fallback', 1779549201)
    })

    const calls = await collectCalls(tmpDir, `${dbPath}#hermes-session=no-cost-session`)
    expect(calls).toHaveLength(1)
    expect(calls[0]!.costUSD).toBe(calculateCost('claude-sonnet-4-20250514', 1000, 250, 0, 0, 0))
    expect(calls[0]!.reasoningTokens).toBe(50)
  })

  it('does not split multibyte characters when truncating the first user message', async () => {
    const dbPath = createHermesDb(tmpDir)
    const message = `${'a'.repeat(499)}😀truncated tail`
    withTestDb(dbPath, (db) => {
      insertSession(db, {
        id: 'emoji-session',
        inputTokens: 10,
        outputTokens: 5,
        cacheReadTokens: 0,
        cacheWriteTokens: 0,
        reasoningTokens: 0,
        estimatedCost: 0.01,
        startedAt: 1779549200,
      })
      db.prepare('INSERT INTO messages (session_id, role, content, timestamp) VALUES (?, ?, ?, ?)')
        .run('emoji-session', 'user', message, 1779549201)
    })

    const calls = await collectCalls(tmpDir, `${dbPath}#hermes-session=emoji-session`)
    expect(calls).toHaveLength(1)
    expect(calls[0]!.userMessage).toBe(`${'a'.repeat(499)}😀`)
  })

  it('parses legacy databases that predate optional accounting columns', async () => {
    const dbPath = createLegacyHermesDb(tmpDir)
    withTestDb(dbPath, (db) => {
      db.prepare(
        `INSERT INTO sessions (id, model, input_tokens, output_tokens, started_at)
         VALUES (?, ?, ?, ?, ?)`,
      ).run('legacy-session', 'gpt-5.5', 12, 34, 1779549200)
      db.prepare('INSERT INTO messages (session_id, role, content, timestamp) VALUES (?, ?, ?, ?)')
        .run('legacy-session', 'user', 'Legacy Hermes DB', 1779549201)
    })

    const provider = createHermesProvider(tmpDir)
    const discovered = await provider.discoverSessions()
    expect(discovered.map(s => s.path)).toEqual([`${dbPath}#hermes-session=legacy-session`])

    const calls = await collectCalls(tmpDir, `${dbPath}#hermes-session=legacy-session`)
    expect(calls).toHaveLength(1)
    expect(calls[0]).toMatchObject({
      inputTokens: 12,
      outputTokens: 34,
      cacheReadInputTokens: 0,
      cacheCreationInputTokens: 0,
      reasoningTokens: 0,
      userMessage: 'Legacy Hermes DB',
    })
  })

  it('discovers root and profile databases and preserves Hermes DB accounting through parser aggregation', async () => {
    const rootDbPath = createHermesDb(tmpDir)
    const profileDbPath = await createProfileHermesDb(tmpDir, 'coder')

    withTestDb(rootDbPath, (db) => {
      insertSession(db, {
        id: 'root-session',
        model: 'gpt-5.5',
        inputTokens: 100,
        outputTokens: 20,
        cacheReadTokens: 30,
        cacheWriteTokens: 40,
        reasoningTokens: 5,
        estimatedCost: 0.25,
        actualCost: 0.30,
        startedAt: 1779494400,
        title: 'Root session',
      })
      db.prepare('INSERT INTO messages (session_id, role, content, timestamp) VALUES (?, ?, ?, ?)')
        .run('root-session', 'user', 'Current working directory: /tmp/root-project\nImplement root support', 1779494401)
    })
    withTestDb(profileDbPath, (db) => {
      insertSession(db, {
        id: 'profile-session',
        model: 'gpt-5.5',
        inputTokens: 200,
        outputTokens: 70,
        cacheReadTokens: 11,
        cacheWriteTokens: 13,
        reasoningTokens: 17,
        estimatedCost: 0.42,
        actualCost: null,
        startedAt: 1779501600,
        title: 'Profile session',
      })
      db.prepare('INSERT INTO messages (session_id, role, content, timestamp) VALUES (?, ?, ?, ?)')
        .run('profile-session', 'user', 'Current working directory: /tmp/profile-project\nImplement profile support', 1779501601)
    })

    const provider = createHermesProvider(tmpDir)
    const discovered = await provider.discoverSessions()
    expect(discovered.map(s => s.path).sort()).toEqual([
      `${profileDbPath}#hermes-session=profile-session`,
      `${rootDbPath}#hermes-session=root-session`,
    ].sort())
    expect(discovered.map(s => s.project).sort()).toEqual(['coder', 'default'])

    const rootCalls = await collectCalls(tmpDir, `${rootDbPath}#hermes-session=root-session`)
    const profileCalls = await collectCalls(tmpDir, `${profileDbPath}#hermes-session=profile-session`)
    expect(rootCalls[0]).toMatchObject({ inputTokens: 100, outputTokens: 20, cacheReadInputTokens: 30, cacheCreationInputTokens: 40, reasoningTokens: 5, costUSD: 0.30 })
    expect(profileCalls[0]).toMatchObject({ inputTokens: 200, outputTokens: 70, cacheReadInputTokens: 11, cacheCreationInputTokens: 13, reasoningTokens: 17, costUSD: 0.42 })

    const { clearSessionCache, parseAllSessions } = await loadParserWithHermesHome(tmpDir, cacheDir)
    clearSessionCache()
    const projects = await parseAllSessions(dayRange(), 'hermes')
    const sessions = projects.flatMap(project => project.sessions)
    expect(sessions).toHaveLength(2)
    expect(sessions.reduce((sum, session) => sum + session.totalInputTokens, 0)).toBe(300)
    expect(sessions.reduce((sum, session) => sum + session.totalOutputTokens, 0)).toBe(90)
    expect(sessions.reduce((sum, session) => sum + session.totalReasoningTokens, 0)).toBe(22)
    expect(sessions.reduce((sum, session) => sum + session.totalCacheReadTokens, 0)).toBe(41)
    expect(sessions.reduce((sum, session) => sum + session.totalCacheWriteTokens, 0)).toBe(53)
    expect(sessions.reduce((sum, session) => sum + session.totalCostUSD, 0)).toBeCloseTo(0.72)
    expect(projects.map(project => project.project).sort()).toEqual(['tmp-profile-project', 'tmp-root-project'])

    const modelTokens = sessions.flatMap(session => Object.values(session.modelBreakdown).map(model => model.tokens))
    expect(modelTokens.reduce((sum, tokens) => sum + tokens.outputTokens, 0)).toBe(90)
    expect(modelTokens.reduce((sum, tokens) => sum + tokens.reasoningTokens, 0)).toBe(22)
  })

  it('treats sibling profile-like directories as default sessions', async () => {
    const profileLikeDir = join(dirname(tmpDir), `${basename(tmpDir)}-profiles_backup`, 'coder')
    await mkdir(profileLikeDir, { recursive: true })
    const dbPath = createHermesDb(profileLikeDir)

    withTestDb(dbPath, (db) => {
      insertSession(db, {
        id: 'sibling-session',
        inputTokens: 10,
        outputTokens: 5,
        cacheReadTokens: 0,
        cacheWriteTokens: 0,
        reasoningTokens: 0,
        startedAt: 1779549200,
      })
      db.prepare('INSERT INTO messages (session_id, role, content, timestamp) VALUES (?, ?, ?, ?)')
        .run('sibling-session', 'user', 'Sibling profile-like directory', 1779549201)
    })

    const calls = await collectCalls(tmpDir, `${dbPath}#hermes-session=sibling-session`)
    expect(calls[0]).toMatchObject({
      deduplicationKey: 'hermes:default:sibling-session',
      project: 'default',
    })
  })

  it('infers projects from Windows current working directory messages', async () => {
    const dbPath = createHermesDb(tmpDir)
    withTestDb(dbPath, (db) => {
      insertSession(db, {
        id: 'windows-cwd-session',
        inputTokens: 10,
        outputTokens: 5,
        cacheReadTokens: 0,
        cacheWriteTokens: 0,
        reasoningTokens: 0,
        startedAt: 1779549200,
      })
      db.prepare('INSERT INTO messages (session_id, role, content, timestamp) VALUES (?, ?, ?, ?)')
        .run('windows-cwd-session', 'user', 'Current working directory: C:\\AI_LAB\\OPENCLAW\nAdd Windows path support', 1779549201)
    })

    const calls = await collectCalls(tmpDir, `${dbPath}#hermes-session=windows-cwd-session`)
    expect(calls[0]).toMatchObject({
      project: 'C--AI_LAB-OPENCLAW',
      projectPath: 'C:\\AI_LAB\\OPENCLAW',
    })
  })

  it('groups by the sessions.cwd column when present, ahead of message scraping', async () => {
    const dbPath = createHermesDb(tmpDir)
    withTestDb(dbPath, (db) => {
      insertSession(db, {
        id: 'cwd-session',
        cwd: '/Users/me/projects/codeburn',
        inputTokens: 30,
        outputTokens: 10,
        cacheReadTokens: 0,
        cacheWriteTokens: 0,
        reasoningTokens: 0,
        startedAt: 1779549200,
      })
      db.prepare('INSERT INTO messages (session_id, role, content, timestamp) VALUES (?, ?, ?, ?)')
        .run('cwd-session', 'user', 'Current working directory: /tmp/decoy\nbuild it', 1779549201)
    })

    const calls = await collectCalls(tmpDir, `${dbPath}#hermes-session=cwd-session`)
    expect(calls[0]).toMatchObject({
      project: 'Users-me-projects-codeburn',
      projectPath: '/Users/me/projects/codeburn',
    })
  })

  it('flags estimated cost only when Hermes recorded none', async () => {
    const dbPath = createHermesDb(tmpDir)
    withTestDb(dbPath, (db) => {
      insertSession(db, {
        id: 'no-cost',
        inputTokens: 100, outputTokens: 20, cacheReadTokens: 0, cacheWriteTokens: 0, reasoningTokens: 0,
        startedAt: 1779549200,
      })
      insertSession(db, {
        id: 'recorded-cost',
        actualCost: 1.23,
        inputTokens: 100, outputTokens: 20, cacheReadTokens: 0, cacheWriteTokens: 0, reasoningTokens: 0,
        startedAt: 1779549300,
      })
    })

    const noCost = await collectCalls(tmpDir, `${dbPath}#hermes-session=no-cost`)
    expect(noCost[0]!.costIsEstimated).toBe(true)

    const recorded = await collectCalls(tmpDir, `${dbPath}#hermes-session=recorded-cost`)
    expect(recorded[0]).toMatchObject({ costUSD: 1.23, costIsEstimated: false })
  })

  it('counts tool-result messages by their tool_name', async () => {
    const dbPath = createHermesDb(tmpDir)
    withTestDb(dbPath, (db) => {
      insertSession(db, {
        id: 'tool-result-session',
        inputTokens: 10, outputTokens: 5, cacheReadTokens: 0, cacheWriteTokens: 0, reasoningTokens: 0,
        startedAt: 1779549200,
      })
      db.prepare('INSERT INTO messages (session_id, role, content, tool_name, timestamp) VALUES (?, ?, ?, ?, ?)')
        .run('tool-result-session', 'tool', null, 'read_file', 1779549201)
    })

    const calls = await collectCalls(tmpDir, `${dbPath}#hermes-session=tool-result-session`)
    expect(calls[0]!.tools).toContain('Read')
  })
})
