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

import { kiro, createKiroProvider } from '../../src/providers/kiro.js'
import type { ParsedProviderCall } from '../../src/providers/types.js'

let tmpDir: string

function makeChatFile(opts: {
  executionId?: string
  modelId?: string
  workflowId?: string
  startTime?: number
  endTime?: number
  userPrompt?: string
  botResponses?: string[]
}) {
  const chat = [
    { role: 'human', content: '<identity>\nYou are Kiro.\n</identity>' },
    { role: 'bot', content: '' },
    { role: 'tool', content: 'workspace tree...' },
    { role: 'bot', content: 'I will follow these instructions.' },
  ]

  if (opts.userPrompt) {
    chat.push({ role: 'human', content: opts.userPrompt })
  }

  for (const resp of opts.botResponses ?? ['Done.']) {
    chat.push({ role: 'bot', content: resp })
  }

  return JSON.stringify({
    executionId: opts.executionId ?? 'exec-001',
    actionId: 'act',
    context: [],
    validations: {},
    chat,
    metadata: {
      modelId: opts.modelId ?? 'claude-haiku-4-5',
      modelProvider: 'qdev',
      workflow: 'act',
      workflowId: opts.workflowId ?? 'wf-001',
      startTime: opts.startTime ?? 1777333000000,
      endTime: opts.endTime ?? 1777333010000,
    },
  })
}

function makeModernExecutionFile(opts: {
  executionId?: string
  sessionId?: string
  modelId?: string
  startTime?: number | string
  userPrompt?: string
  assistantResponse?: string
}) {
  const startTime = opts.startTime ?? 1777333000000
  return JSON.stringify({
    executionId: opts.executionId ?? 'exec-modern-001',
    sessionId: opts.sessionId ?? 'session-modern-001',
    workflowType: 'chat-agent',
    status: 'succeed',
    startTime,
    endTime: typeof startTime === 'number' ? startTime + 10000 : 1777333010000,
    modelId: opts.modelId ?? 'claude-sonnet-4.5',
    messages: [
      { role: 'user', content: opts.userPrompt ?? 'explain the new kiro storage layout' },
      {
        role: 'assistant',
        content: opts.assistantResponse ?? 'Done. <tool_use><name>runCommand</name></tool_use>',
        toolCalls: [{ name: 'readFile' }],
      },
    ],
  })
}

describe('kiro provider - chat file parsing', () => {
  beforeEach(async () => {
    tmpDir = await mkdtemp(join(tmpdir(), 'kiro-test-'))
  })

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

  it('parses a basic chat file', async () => {
    const wsHash = 'a'.repeat(32)
    const wsDir = join(tmpDir, wsHash)
    await mkdir(wsDir, { recursive: true })
    const chatPath = join(wsDir, 'abc123.chat')
    await writeFile(chatPath, makeChatFile({
      modelId: 'claude-haiku-4-5',
      userPrompt: 'explain the code',
      botResponses: ['Here is an explanation of the code structure.'],
    }))

    const source = { path: chatPath, project: 'myproject', provider: 'kiro' }
    const calls: ParsedProviderCall[] = []
    for await (const call of kiro.createSessionParser(source, new Set()).parse()) calls.push(call)

    expect(calls).toHaveLength(1)
    const call = calls[0]!
    expect(call.provider).toBe('kiro')
    expect(call.model).toBe('claude-haiku-4-5')
    expect(call.outputTokens).toBeGreaterThan(0)
    expect(call.userMessage).toBe('explain the code')
    expect(call.bashCommands).toEqual([])
    expect(call.costUSD).toBeGreaterThan(0)
  })

  it('stores kiro-auto when model is auto', async () => {
    const wsHash = 'b'.repeat(32)
    const wsDir = join(tmpDir, wsHash)
    await mkdir(wsDir, { recursive: true })
    const chatPath = join(wsDir, 'abc.chat')
    await writeFile(chatPath, makeChatFile({
      modelId: 'auto',
      botResponses: ['some output'],
    }))

    const source = { path: chatPath, project: 'test', provider: 'kiro' }
    const calls: ParsedProviderCall[] = []
    for await (const call of kiro.createSessionParser(source, new Set()).parse()) calls.push(call)

    expect(calls).toHaveLength(1)
    expect(calls[0]!.model).toBe('kiro-auto')
    expect(calls[0]!.costUSD).toBeGreaterThan(0)
  })

  it('skips chat files with no bot output', async () => {
    const wsHash = 'c'.repeat(32)
    const wsDir = join(tmpDir, wsHash)
    await mkdir(wsDir, { recursive: true })
    const chatPath = join(wsDir, 'empty.chat')
    await writeFile(chatPath, JSON.stringify({
      executionId: 'exec-empty',
      actionId: 'act',
      context: [],
      validations: {},
      chat: [
        { role: 'human', content: '<identity>\nYou are Kiro.\n</identity>' },
        { role: 'bot', content: '' },
        { role: 'human', content: 'do something' },
        { role: 'bot', content: '' },
      ],
      metadata: {
        modelId: 'claude-haiku-4-5',
        modelProvider: 'qdev',
        workflow: 'act',
        workflowId: 'wf-empty',
        startTime: 1777333000000,
        endTime: 1777333010000,
      },
    }))

    const source = { path: chatPath, project: 'test', provider: 'kiro' }
    const calls: ParsedProviderCall[] = []
    for await (const call of kiro.createSessionParser(source, new Set()).parse()) calls.push(call)

    expect(calls).toHaveLength(0)
  })

  it('deduplicates across parser runs', async () => {
    const wsHash = 'd'.repeat(32)
    const wsDir = join(tmpDir, wsHash)
    await mkdir(wsDir, { recursive: true })
    const chatPath = join(wsDir, 'dup.chat')
    await writeFile(chatPath, makeChatFile({ botResponses: ['hello'] }))

    const source = { path: chatPath, project: 'test', provider: 'kiro' }
    const seenKeys = new Set<string>()

    const calls1: ParsedProviderCall[] = []
    for await (const call of kiro.createSessionParser(source, seenKeys).parse()) calls1.push(call)

    const calls2: ParsedProviderCall[] = []
    for await (const call of kiro.createSessionParser(source, seenKeys).parse()) calls2.push(call)

    expect(calls1).toHaveLength(1)
    expect(calls2).toHaveLength(0)
  })

  it('returns empty for missing file', async () => {
    const source = { path: '/nonexistent/test.chat', project: 'test', provider: 'kiro' }
    const calls: ParsedProviderCall[] = []
    for await (const call of kiro.createSessionParser(source, new Set()).parse()) calls.push(call)
    expect(calls).toHaveLength(0)
  })

  it('returns empty for invalid JSON', async () => {
    const wsHash = 'e'.repeat(32)
    const wsDir = join(tmpDir, wsHash)
    await mkdir(wsDir, { recursive: true })
    const chatPath = join(wsDir, 'bad.chat')
    await writeFile(chatPath, 'not json at all')

    const source = { path: chatPath, project: 'test', provider: 'kiro' }
    const calls: ParsedProviderCall[] = []
    for await (const call of kiro.createSessionParser(source, new Set()).parse()) calls.push(call)
    expect(calls).toHaveLength(0)
  })

  it('estimates tokens from text length', async () => {
    const wsHash = 'f'.repeat(32)
    const wsDir = join(tmpDir, wsHash)
    await mkdir(wsDir, { recursive: true })
    const chatPath = join(wsDir, 'tokens.chat')
    const longResponse = 'x'.repeat(400)
    await writeFile(chatPath, makeChatFile({ botResponses: [longResponse] }))

    const source = { path: chatPath, project: 'test', provider: 'kiro' }
    const calls: ParsedProviderCall[] = []
    for await (const call of kiro.createSessionParser(source, new Set()).parse()) calls.push(call)

    expect(calls).toHaveLength(1)
    expect(calls[0]!.outputTokens).toBe(109)
  })

  it('normalizes dot-versioned model IDs to dashes', async () => {
    const wsHash = 'h'.repeat(32)
    const wsDir = join(tmpDir, wsHash)
    await mkdir(wsDir, { recursive: true })
    const chatPath = join(wsDir, 'dot.chat')
    await writeFile(chatPath, makeChatFile({
      modelId: 'claude-haiku-4.5',
      botResponses: ['response text here'],
    }))

    const source = { path: chatPath, project: 'test', provider: 'kiro' }
    const calls: ParsedProviderCall[] = []
    for await (const call of kiro.createSessionParser(source, new Set()).parse()) calls.push(call)

    expect(calls).toHaveLength(1)
    expect(calls[0]!.model).toBe('claude-haiku-4-5')
    expect(calls[0]!.costUSD).toBeGreaterThan(0)
  })

  it('uses workflowId as sessionId', async () => {
    const wsHash = 'g'.repeat(32)
    const wsDir = join(tmpDir, wsHash)
    await mkdir(wsDir, { recursive: true })
    const chatPath = join(wsDir, 'sess.chat')
    await writeFile(chatPath, makeChatFile({
      workflowId: 'my-workflow-id',
      botResponses: ['ok'],
    }))

    const source = { path: chatPath, project: 'test', provider: 'kiro' }
    const calls: ParsedProviderCall[] = []
    for await (const call of kiro.createSessionParser(source, new Set()).parse()) calls.push(call)

    expect(calls).toHaveLength(1)
    expect(calls[0]!.sessionId).toBe('my-workflow-id')
  })

  it('parses a post-February extensionless execution file', async () => {
    const wsHash = 'i'.repeat(32)
    const sessionHash = 'session-modern'
    const wsDir = join(tmpDir, wsHash, sessionHash)
    await mkdir(wsDir, { recursive: true })
    const executionPath = join(wsDir, 'execution-modern')
    await writeFile(executionPath, makeModernExecutionFile({
      executionId: 'exec-modern',
      sessionId: 'session-modern',
      modelId: 'claude-sonnet-4.5',
      userPrompt: 'summarize this workspace',
      assistantResponse: 'I reviewed it. <tool_use><name>runCommand</name></tool_use>',
    }))

    const source = { path: executionPath, project: 'test', provider: 'kiro' }
    const calls: ParsedProviderCall[] = []
    for await (const call of kiro.createSessionParser(source, new Set()).parse()) calls.push(call)

    expect(calls).toHaveLength(1)
    const call = calls[0]!
    expect(call.provider).toBe('kiro')
    expect(call.model).toBe('claude-sonnet-4-5')
    expect(call.sessionId).toBe('session-modern')
    expect(call.userMessage).toBe('summarize this workspace')
    expect(call.inputTokens).toBeGreaterThan(0)
    expect(call.outputTokens).toBeGreaterThan(0)
    expect(call.tools).toEqual(['Bash', 'Read'])
    expect(call.costUSD).toBeGreaterThan(0)
  })

  it('skips session index files without conversation content', async () => {
    const wsHash = 'j'.repeat(32)
    const wsDir = join(tmpDir, wsHash)
    await mkdir(wsDir, { recursive: true })
    const indexPath = join(wsDir, 'session-index')
    await writeFile(indexPath, JSON.stringify({
      executions: [{
        executionId: 'exec-indexed',
        type: 'chat-agent',
        status: 'succeed',
        startTime: 1777333000000,
        endTime: 1777333010000,
      }],
    }))

    const source = { path: indexPath, project: 'test', provider: 'kiro' }
    const calls: ParsedProviderCall[] = []
    for await (const call of kiro.createSessionParser(source, new Set()).parse()) calls.push(call)

    expect(calls).toHaveLength(0)
  })

  it('parses direct prompt and response fields from modern execution files', async () => {
    const wsHash = 'k'.repeat(32)
    const wsDir = join(tmpDir, wsHash)
    await mkdir(wsDir, { recursive: true })
    const executionPath = join(wsDir, 'execution-direct')
    await writeFile(executionPath, JSON.stringify({
      executionId: 'exec-direct',
      workflowType: 'chat-agent',
      status: 'succeed',
      startTime: 1777333000000,
      model: { id: 'auto' },
      prompt: 'make a small change',
      response: 'Changed it. <tool_use><name>writeFile</name></tool_use>',
    }))

    const source = { path: executionPath, project: 'test', provider: 'kiro' }
    const calls: ParsedProviderCall[] = []
    for await (const call of kiro.createSessionParser(source, new Set()).parse()) calls.push(call)

    expect(calls).toHaveLength(1)
    expect(calls[0]!.model).toBe('kiro-auto')
    expect(calls[0]!.userMessage).toBe('make a small change')
    expect(calls[0]!.tools).toEqual(['Edit'])
  })

  it('accepts second-based modern timestamps', async () => {
    const wsHash = 'n'.repeat(32)
    const wsDir = join(tmpDir, wsHash)
    await mkdir(wsDir, { recursive: true })
    const executionPath = join(wsDir, 'execution-seconds')
    await writeFile(executionPath, makeModernExecutionFile({
      executionId: 'exec-seconds',
      startTime: 1777333000,
    }))

    const source = { path: executionPath, project: 'test', provider: 'kiro' }
    const calls: ParsedProviderCall[] = []
    for await (const call of kiro.createSessionParser(source, new Set()).parse()) calls.push(call)

    expect(calls).toHaveLength(1)
    expect(calls[0]!.timestamp).toBe('2026-04-27T23:36:40.000Z')
  })

  it('accepts numeric-string modern timestamps', async () => {
    const wsHash = 'o'.repeat(32)
    const wsDir = join(tmpDir, wsHash)
    await mkdir(wsDir, { recursive: true })
    const executionPath = join(wsDir, 'execution-string-time')
    await writeFile(executionPath, makeModernExecutionFile({
      executionId: 'exec-string-time',
      startTime: '1777333000000',
    }))

    const source = { path: executionPath, project: 'test', provider: 'kiro' }
    const calls: ParsedProviderCall[] = []
    for await (const call of kiro.createSessionParser(source, new Set()).parse()) calls.push(call)

    expect(calls).toHaveLength(1)
    expect(calls[0]!.timestamp).toBe('2026-04-27T23:36:40.000Z')
  })

  it('does not poison dedup keys when a modern execution has an invalid timestamp', async () => {
    const wsHash = 'p'.repeat(32)
    const wsDir = join(tmpDir, wsHash)
    await mkdir(wsDir, { recursive: true })
    const invalidPath = join(wsDir, 'execution-invalid-time')
    const validPath = join(wsDir, 'execution-valid-time')
    const shared = {
      executionId: 'exec-recovered',
      sessionId: 'session-recovered',
    }
    await writeFile(invalidPath, makeModernExecutionFile({
      ...shared,
      startTime: 'not-a-timestamp',
    }))
    await writeFile(validPath, makeModernExecutionFile({
      ...shared,
      startTime: 1777333000000,
    }))

    const seenKeys = new Set<string>()
    const invalidCalls: ParsedProviderCall[] = []
    for await (const call of kiro.createSessionParser({ path: invalidPath, project: 'test', provider: 'kiro' }, seenKeys).parse()) {
      invalidCalls.push(call)
    }
    const validCalls: ParsedProviderCall[] = []
    for await (const call of kiro.createSessionParser({ path: validPath, project: 'test', provider: 'kiro' }, seenKeys).parse()) {
      validCalls.push(call)
    }

    expect(invalidCalls).toHaveLength(0)
    expect(validCalls).toHaveLength(1)
  })

  it.each(['conversation', 'chat', 'transcript', 'entries', 'events'])('parses modern execution conversation arrays from %s', async (key) => {
    const wsHash = 'q'.repeat(32)
    const wsDir = join(tmpDir, wsHash)
    await mkdir(wsDir, { recursive: true })
    const executionPath = join(wsDir, `execution-${key}`)
    await writeFile(executionPath, JSON.stringify({
      executionId: `exec-${key}`,
      workflowType: 'chat-agent',
      status: 'succeed',
      startTime: 1777333000000,
      modelId: 'claude-sonnet-4.5',
      [key]: [
        { role: 'user', content: `request from ${key}` },
        { role: 'assistant', content: `response from ${key}`, toolCalls: [{ name: 'readFile' }] },
      ],
    }))

    const source = { path: executionPath, project: 'test', provider: 'kiro' }
    const calls: ParsedProviderCall[] = []
    for await (const call of kiro.createSessionParser(source, new Set()).parse()) calls.push(call)

    expect(calls).toHaveLength(1)
    expect(calls[0]!.userMessage).toBe(`request from ${key}`)
    expect(calls[0]!.tools).toEqual(['Read'])
  })

  it('keeps modern executions with structured assistant tool calls and no assistant text', async () => {
    const wsHash = 'l'.repeat(32)
    const wsDir = join(tmpDir, wsHash, 'session-tools')
    await mkdir(wsDir, { recursive: true })
    const executionPath = join(wsDir, 'execution-tools')
    await writeFile(executionPath, JSON.stringify({
      executionId: 'exec-tools',
      sessionId: 'session-tools',
      workflowType: 'chat-agent',
      status: 'succeed',
      startTime: 1777333000000,
      modelId: 'claude-sonnet-4.5',
      messages: [
        { role: 'user', content: 'run the test suite' },
        { role: 'assistant', toolCalls: [{ name: 'runCommand' }] },
      ],
    }))

    const source = { path: executionPath, project: 'test', provider: 'kiro' }
    const calls: ParsedProviderCall[] = []
    for await (const call of kiro.createSessionParser(source, new Set()).parse()) calls.push(call)

    expect(calls).toHaveLength(1)
    expect(calls[0]!.tools).toEqual(['Bash'])
    expect(calls[0]!.inputTokens).toBeGreaterThan(0)
    expect(calls[0]!.outputTokens).toBe(0)
  })

  it('keeps direct modern executions with root tool calls and no response text', async () => {
    const wsHash = 'm'.repeat(32)
    const wsDir = join(tmpDir, wsHash)
    await mkdir(wsDir, { recursive: true })
    const executionPath = join(wsDir, 'execution-root-tools')
    await writeFile(executionPath, JSON.stringify({
      executionId: 'exec-root-tools',
      workflowType: 'chat-agent',
      status: 'succeed',
      startTime: 1777333000000,
      model: { id: 'auto' },
      name: 'workflow-name',
      prompt: 'edit a file',
      toolCalls: [{ name: 'writeFile' }],
    }))

    const source = { path: executionPath, project: 'test', provider: 'kiro' }
    const calls: ParsedProviderCall[] = []
    for await (const call of kiro.createSessionParser(source, new Set()).parse()) calls.push(call)

    expect(calls).toHaveLength(1)
    expect(calls[0]!.tools).toEqual(['Edit'])
    expect(calls[0]!.tools).not.toContain('workflow-name')
    expect(calls[0]!.outputTokens).toBe(0)
  })
})

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

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

  it('discovers chat files from workspace hash directories', async () => {
    const wsHash = 'a1b2c3d4e5f6'.padEnd(32, '0')
    const wsDir = join(tmpDir, wsHash)
    await mkdir(wsDir, { recursive: true })
    await writeFile(join(wsDir, 'session1.chat'), makeChatFile({}))
    await writeFile(join(wsDir, 'session2.chat'), makeChatFile({}))

    const provider = createKiroProvider(tmpDir, '/nonexistent/ws')
    const sessions = await provider.discoverSessions()

    expect(sessions).toHaveLength(2)
    expect(sessions.every(s => s.provider === 'kiro')).toBe(true)
    expect(sessions.every(s => s.path.endsWith('.chat'))).toBe(true)
  })

  it('discovers extensionless session index files and nested execution files', async () => {
    const wsHash = 'd'.repeat(32)
    const wsDir = join(tmpDir, wsHash)
    const sessionDir = join(wsDir, 'session-dir')
    await mkdir(sessionDir, { recursive: true })
    await writeFile(join(wsDir, 'session-index'), JSON.stringify({ executions: [] }))
    await writeFile(join(wsDir, 'legacy.chat'), makeChatFile({}))
    await writeFile(join(wsDir, 'ignored.json'), '{}')
    await writeFile(join(wsDir, '.DS_Store'), 'ignored')
    await writeFile(join(sessionDir, 'execution-1'), makeModernExecutionFile({}))
    await writeFile(join(sessionDir, '.hidden'), 'ignored')
    await writeFile(join(sessionDir, 'ignored.txt'), 'hello')

    const provider = createKiroProvider(tmpDir, '/nonexistent/ws')
    const sessions = await provider.discoverSessions()
    const paths = sessions.map(s => s.path).sort()

    expect(paths).toEqual([
      join(sessionDir, 'execution-1'),
      join(wsDir, 'legacy.chat'),
      join(wsDir, 'session-index'),
    ].sort())
  })

  it('reads project name from workspace.json', async () => {
    const wsHash = 'b'.repeat(32)
    const agentWsDir = join(tmpDir, wsHash)
    await mkdir(agentWsDir, { recursive: true })
    await writeFile(join(agentWsDir, 'test.chat'), makeChatFile({}))

    const workspaceStorageDir = join(tmpDir, 'ws-storage')
    const wsStorageEntry = join(workspaceStorageDir, wsHash)
    await mkdir(wsStorageEntry, { recursive: true })
    await writeFile(join(wsStorageEntry, 'workspace.json'), JSON.stringify({ folder: 'file:///home/user/myapp' }))

    const provider = createKiroProvider(tmpDir, workspaceStorageDir)
    const sessions = await provider.discoverSessions()

    expect(sessions).toHaveLength(1)
    expect(sessions[0]!.project).toBe('myapp')
  })

  it('returns empty when directory does not exist', async () => {
    const provider = createKiroProvider('/nonexistent/agent', '/nonexistent/ws')
    const sessions = await provider.discoverSessions()
    expect(sessions).toHaveLength(0)
  })

  it('skips non-32-char directories', async () => {
    const shortDir = join(tmpDir, 'short')
    await mkdir(shortDir, { recursive: true })
    await writeFile(join(shortDir, 'test.chat'), makeChatFile({}))

    const provider = createKiroProvider(tmpDir, '/nonexistent/ws')
    const sessions = await provider.discoverSessions()
    expect(sessions).toHaveLength(0)
  })

  it('skips files with unsupported extensions', async () => {
    const wsHash = 'c'.repeat(32)
    const wsDir = join(tmpDir, wsHash)
    await mkdir(wsDir, { recursive: true })
    await writeFile(join(wsDir, 'index.json'), '{}')
    await writeFile(join(wsDir, 'notes.txt'), 'hello')

    const provider = createKiroProvider(tmpDir, '/nonexistent/ws')
    const sessions = await provider.discoverSessions()
    expect(sessions).toHaveLength(0)
  })
})

describe('kiro provider - metadata', () => {
  it('has correct name and displayName', () => {
    expect(kiro.name).toBe('kiro')
    expect(kiro.displayName).toBe('Kiro')
  })

  it('normalizes model display names', () => {
    expect(kiro.modelDisplayName('claude-haiku-4-5')).toBe('Haiku 4.5')
    expect(kiro.modelDisplayName('claude-sonnet-4-5')).toBe('Sonnet 4.5')
    expect(kiro.modelDisplayName('claude-sonnet-4-6')).toBe('Sonnet 4.6')
    expect(kiro.modelDisplayName('unknown-model')).toBe('unknown-model')
  })

  it('normalizes tool display names', () => {
    expect(kiro.toolDisplayName('readFile')).toBe('Read')
    expect(kiro.toolDisplayName('writeFile')).toBe('Edit')
    expect(kiro.toolDisplayName('runCommand')).toBe('Bash')
    expect(kiro.toolDisplayName('searchFiles')).toBe('Grep')
    expect(kiro.toolDisplayName('unknown_tool')).toBe('unknown_tool')
  })

  it('normalizes CLI-specific tool names', () => {
    expect(kiro.toolDisplayName('code')).toBe('Read')
    expect(kiro.toolDisplayName('subagent')).toBe('Agent')
    expect(kiro.toolDisplayName('web_fetch')).toBe('WebFetch')
  })

  it('passes through MCP tool names unchanged', () => {
    expect(kiro.toolDisplayName('mcp__server__searchJira')).toBe('mcp__server__searchJira')
    expect(kiro.toolDisplayName('mcp__atlassian__getIssue')).toBe('mcp__atlassian__getIssue')
  })

  it('longest-prefix match for versioned model IDs', () => {
    expect(kiro.modelDisplayName('claude-sonnet-4-5-20260101')).toBe('Sonnet 4.5')
    expect(kiro.modelDisplayName('claude-haiku-4-5-20260101')).toBe('Haiku 4.5')
  })
})

describe('kiro provider - CLI session discovery', () => {
  let cliDir: string

  beforeEach(async () => {
    cliDir = await mkdtemp(join(tmpdir(), 'kiro-cli-test-'))
  })

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

  it('discovers .jsonl files from CLI sessions directory', async () => {
    const sessionId = '11111111-1111-1111-1111-111111111111'
    await writeFile(join(cliDir, `${sessionId}.jsonl`), '')
    await writeFile(join(cliDir, `${sessionId}.json`), JSON.stringify({
      session_id: sessionId,
      cwd: '/home/user/my-project',
      created_at: '2026-01-01T00:00:00Z',
      updated_at: '2026-01-01T00:01:00Z',
    }))

    const provider = createKiroProvider('/nonexistent', '/nonexistent', cliDir)
    const sessions = await provider.discoverSessions()
    expect(sessions).toHaveLength(1)
    expect(sessions[0]!.project).toBe('my-project')
    expect(sessions[0]!.path).toContain('.jsonl')
    expect(sessions[0]!.provider).toBe('kiro')
  })

  it('parses CLI session JSONL into calls', async () => {
    const sessionId = '22222222-2222-2222-2222-222222222222'
    const jsonl = [
      JSON.stringify({ version: '1', kind: 'Prompt', data: { message_id: 'm1', content: [{ kind: 'text', data: 'hello world' }], meta: { timestamp: 1700000000 } } }),
      JSON.stringify({ version: '1', kind: 'AssistantMessage', data: { message_id: 'm2', content: [{ kind: 'text', data: 'Hello! How can I help you today?' }, { kind: 'toolUse', data: { toolUseId: 't1', name: 'read', input: {} } }] } }),
      JSON.stringify({ version: '1', kind: 'ToolResults', data: { message_id: 'm3', content: [{ kind: 'text', data: 'file contents here' }], results: { t1: { output: 'ok' } } } }),
      JSON.stringify({ version: '1', kind: 'AssistantMessage', data: { message_id: 'm4', content: [{ kind: 'text', data: 'I read the file for you.' }] } }),
    ].join('\n')

    await writeFile(join(cliDir, `${sessionId}.jsonl`), jsonl)
    await writeFile(join(cliDir, `${sessionId}.json`), JSON.stringify({
      session_id: sessionId,
      cwd: '/tmp/test-project',
      created_at: '2026-01-01T00:00:00Z',
      updated_at: '2026-01-01T00:01:00Z',
      session_state: {
        rts_model_state: { model_info: { model_id: 'auto' } },
        conversation_metadata: {
          user_turn_metadatas: [{
            end_timestamp: '2026-01-01T00:00:30Z',
            metering_usage: [{ value: 0.05, unit: 'credit' }, { value: 0.08, unit: 'credit' }],
          }],
        },
      },
    }))

    const source = { path: join(cliDir, `${sessionId}.jsonl`), project: 'test-project', provider: 'kiro' }
    const calls: ParsedProviderCall[] = []
    for await (const call of kiro.createSessionParser(source, new Set()).parse()) calls.push(call)

    expect(calls).toHaveLength(1)
    const call = calls[0]!
    expect(call.provider).toBe('kiro')
    expect(call.model).toBe('kiro-auto')
    expect(call.tools).toContain('Read')
    expect(call.userMessage).toBe('hello world')
    expect(call.costUSD).toBeCloseTo(0.13, 2)
    expect(call.deduplicationKey).toBe(`kiro-cli:${sessionId}:0`)
    expect(call.timestamp).toBe('2026-01-01T00:00:30.000Z')
    expect(call.project).toBe('test-project')
  })

  it('parses multiple turns from a CLI session', async () => {
    const sessionId = '33333333-3333-3333-3333-333333333333'
    const jsonl = [
      JSON.stringify({ version: '1', kind: 'Prompt', data: { message_id: 'm1', content: [{ kind: 'text', data: 'first question' }], meta: { timestamp: 1700000000 } } }),
      JSON.stringify({ version: '1', kind: 'AssistantMessage', data: { message_id: 'm2', content: [{ kind: 'text', data: 'first answer' }] } }),
      JSON.stringify({ version: '1', kind: 'Prompt', data: { message_id: 'm3', content: [{ kind: 'text', data: 'second question' }], meta: { timestamp: 1700000060 } } }),
      JSON.stringify({ version: '1', kind: 'AssistantMessage', data: { message_id: 'm4', content: [{ kind: 'text', data: 'second answer' }] } }),
    ].join('\n')

    await writeFile(join(cliDir, `${sessionId}.jsonl`), jsonl)
    await writeFile(join(cliDir, `${sessionId}.json`), JSON.stringify({
      session_id: sessionId,
      cwd: '/tmp/multi',
      created_at: '2026-01-01T00:00:00Z',
      updated_at: '2026-01-01T00:02:00Z',
      session_state: {
        rts_model_state: { model_info: { model_id: 'claude-sonnet-4' } },
        conversation_metadata: {
          user_turn_metadatas: [
            { end_timestamp: '2026-01-01T00:00:30Z', metering_usage: [{ value: 0.04, unit: 'credit' }] },
            { end_timestamp: '2026-01-01T00:01:30Z', metering_usage: [{ value: 0.06, unit: 'credit' }] },
          ],
        },
      },
    }))

    const source = { path: join(cliDir, `${sessionId}.jsonl`), project: 'multi', provider: 'kiro' }
    const calls: ParsedProviderCall[] = []
    for await (const call of kiro.createSessionParser(source, new Set()).parse()) calls.push(call)

    expect(calls).toHaveLength(2)
    expect(calls[0]!.userMessage).toBe('first question')
    expect(calls[0]!.model).toBe('claude-sonnet-4')
    expect(calls[1]!.userMessage).toBe('second question')
    expect(calls[1]!.costUSD).toBeCloseTo(0.06, 2)
  })

  it('skips non-jsonl files in CLI directory', async () => {
    await writeFile(join(cliDir, 'something.json'), '{}')
    await writeFile(join(cliDir, 'something.lock'), '')

    const provider = createKiroProvider('/nonexistent', '/nonexistent', cliDir)
    const sessions = await provider.discoverSessions()
    expect(sessions).toHaveLength(0)
  })
})

describe('kiro provider - context.messages with entries', () => {
  beforeEach(async () => {
    tmpDir = await mkdtemp(join(tmpdir(), 'kiro-ctx-'))
  })

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

  it('parses context.messages using entries field', async () => {
    // Simulates the real Kiro IDE format where messages use "entries" not "content"
    const file = JSON.stringify({
      executionId: 'exec-ctx-001',
      workflowType: 'chat-agent',
      status: 'succeed',
      startTime: 1777333000000,
      chatSessionId: 'session-ctx-001',
      context: {
        messages: [
          { role: 'human', entries: ['What is the meaning of life?'] },
          { role: 'bot', entries: ['The meaning of life is 42, according to Douglas Adams.'] },
          { role: 'human', entries: ['Tell me more'] },
          { role: 'bot', entries: ['The answer comes from The Hitchhiker\'s Guide to the Galaxy.'] },
        ],
      },
    })

    const wsHash = 'a'.repeat(32)
    const subDir = 'b'.repeat(32)
    await mkdir(join(tmpDir, wsHash, subDir), { recursive: true })
    await writeFile(join(tmpDir, wsHash, subDir, 'exec-ctx-001'), file)

    const provider = createKiroProvider(tmpDir, tmpDir, '/nonexistent')
    const sessions = await provider.discoverSessions()
    expect(sessions.length).toBeGreaterThan(0)

    const calls: ParsedProviderCall[] = []
    for (const source of sessions) {
      for await (const call of provider.createSessionParser(source, new Set()).parse()) {
        calls.push(call)
      }
    }

    expect(calls.length).toBeGreaterThan(0)
    const call = calls[0]!
    expect(call.inputTokens).toBeGreaterThan(0)
    expect(call.outputTokens).toBeGreaterThan(0)
    expect(call.sessionId).toBe('session-ctx-001')
  })

  it('extracts tools from usageSummary', async () => {
    const file = JSON.stringify({
      executionId: 'exec-tools-001',
      workflowType: 'chat-agent',
      status: 'succeed',
      startTime: 1777333000000,
      chatSessionId: 'session-tools-001',
      context: {
        messages: [
          { role: 'human', entries: ['Search for accounts'] },
          { role: 'bot', entries: ['Found 5 accounts.'] },
        ],
      },
      usageSummary: [
        { usedTools: ['mcp_aws_sentral_mcp_search_accounts'], usage: 0.5, unit: 'credit' },
        { usedTools: ['executeBash', 'readFile'], usage: 1.0, unit: 'credit' },
      ],
    })

    const wsHash = 'c'.repeat(32)
    const subDir = 'd'.repeat(32)
    await mkdir(join(tmpDir, wsHash, subDir), { recursive: true })
    await writeFile(join(tmpDir, wsHash, subDir, 'exec-tools-001'), file)

    const provider = createKiroProvider(tmpDir, tmpDir, '/nonexistent')
    const sessions = await provider.discoverSessions()
    const calls: ParsedProviderCall[] = []
    for (const source of sessions) {
      for await (const call of provider.createSessionParser(source, new Set()).parse()) {
        calls.push(call)
      }
    }

    expect(calls.length).toBeGreaterThan(0)
    const call = calls[0]!
    expect(call.tools).toContain('aws_sentral_mcp_search_accounts')
    expect(call.tools).toContain('Bash')
    expect(call.tools).toContain('Read')
  })

  it('skips execution index files with executions array', async () => {
    // The session index file has {executions: [...], version: 2}
    const indexFile = JSON.stringify({
      executions: [
        { executionId: 'exec-001', type: 'chat-agent', status: 'succeed', startTime: 1777333000000 },
      ],
      version: 2,
    })

    const wsHash = 'e'.repeat(32)
    await mkdir(join(tmpDir, wsHash), { recursive: true })
    await writeFile(join(tmpDir, wsHash, 'f'.repeat(32)), indexFile)

    const provider = createKiroProvider(tmpDir, tmpDir, '/nonexistent')
    const sessions = await provider.discoverSessions()
    const calls: ParsedProviderCall[] = []
    for (const source of sessions) {
      for await (const call of provider.createSessionParser(source, new Set()).parse()) {
        calls.push(call)
      }
    }

    expect(calls).toHaveLength(0)
  })
})

describe('kiro provider - workspace-sessions format', () => {
  beforeEach(async () => {
    tmpDir = await mkdtemp(join(tmpdir(), 'kiro-wss-'))
  })

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

  it('discovers and parses workspace-sessions files', async () => {
    // Create workspace-sessions/<base64>/<sessionId>.json
    const wsSessionsDir = join(tmpDir, 'workspace-sessions', 'L3RtcC90ZXN0')
    await mkdir(wsSessionsDir, { recursive: true })

    const sessionFile = JSON.stringify({
      sessionId: 'ws-session-001',
      title: 'Test session',
      selectedModel: 'claude-opus-4.8',
      workspaceDirectory: '/tmp/test',
      history: [
        { message: { role: 'user', content: [{ type: 'text', text: 'What is TypeScript?' }] } },
        { message: { role: 'assistant', content: 'TypeScript is a typed superset of JavaScript.' } },
        { message: { role: 'user', content: [{ type: 'text', text: 'How do I use generics?' }] } },
        { message: { role: 'assistant', content: 'Generics allow you to create reusable components.' } },
      ],
    })

    await writeFile(join(wsSessionsDir, 'ws-session-001.json'), sessionFile)
    // Also need sessions.json (should be skipped)
    await writeFile(join(wsSessionsDir, 'sessions.json'), '[]')

    const provider = createKiroProvider(tmpDir, tmpDir, '/nonexistent')
    const sessions = await provider.discoverSessions()

    const wsSessions = sessions.filter(s => s.path.includes('workspace-sessions'))
    expect(wsSessions).toHaveLength(1)
    expect(wsSessions[0]!.path).toContain('ws-session-001.json')

    const calls: ParsedProviderCall[] = []
    for (const source of wsSessions) {
      for await (const call of provider.createSessionParser(source, new Set()).parse()) {
        calls.push(call)
      }
    }

    expect(calls).toHaveLength(1)
    const call = calls[0]!
    expect(call.model).toBe('claude-opus-4-8')
    expect(call.sessionId).toBe('ws-session-001')
    expect(call.inputTokens).toBeGreaterThan(0)
    expect(call.outputTokens).toBeGreaterThan(0)
    expect(call.deduplicationKey).toBe('kiro:ws-session:ws-session-001')
  })

  it('skips workspace-sessions with only stub assistant replies referencing execution files', async () => {
    const wsSessionsDir = join(tmpDir, 'workspace-sessions', 'L3RtcC90ZXN0')
    await mkdir(wsSessionsDir, { recursive: true })

    // Session where assistant only says "On it." with executionId refs
    // (real output is in execution files — skip to avoid double-counting)
    const sessionFile = JSON.stringify({
      sessionId: 'ws-session-stub',
      selectedModel: 'auto',
      workspaceDirectory: '/tmp/test',
      history: [
        { message: { role: 'user', content: [{ type: 'text', text: 'Deploy the stack' }] } },
        { message: { role: 'assistant', content: 'On it.' }, executionId: 'exec-ref-001' },
      ],
    })

    await writeFile(join(wsSessionsDir, 'ws-session-stub.json'), sessionFile)

    const provider = createKiroProvider(tmpDir, tmpDir, '/nonexistent')
    const sessions = await provider.discoverSessions()
    const calls: ParsedProviderCall[] = []
    for (const source of sessions) {
      for await (const call of provider.createSessionParser(source, new Set()).parse()) {
        calls.push(call)
      }
    }

    // Should be skipped: has executionId refs but no real assistant content
    expect(calls).toHaveLength(0)
  })

  it('skips sessions.json file in workspace-sessions', async () => {
    const wsSessionsDir = join(tmpDir, 'workspace-sessions', 'L3RtcC90ZXN0')
    await mkdir(wsSessionsDir, { recursive: true })
    await writeFile(join(wsSessionsDir, 'sessions.json'), '[]')

    const provider = createKiroProvider(tmpDir, tmpDir, '/nonexistent')
    const sessions = await provider.discoverSessions()
    const wsSessions = sessions.filter(s => s.path.includes('workspace-sessions'))
    expect(wsSessions).toHaveLength(0)
  })
})
