import { mkdtemp, mkdir, rm, writeFile } from 'fs/promises'
import { delimiter as pathDelimiter, join } from 'path'
import { tmpdir, homedir } from 'os'

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

import { claude } from '../../src/providers/claude.js'
import { clearSessionCache, filterProjectsByClaudeConfigSource, parseAllSessions } from '../../src/parser.js'

let tmpRoot: string
const savedEnv = {
  CLAUDE_CONFIG_DIR: process.env['CLAUDE_CONFIG_DIR'],
  CLAUDE_CONFIG_DIRS: process.env['CLAUDE_CONFIG_DIRS'],
  HOME: process.env['HOME'],
}

beforeEach(async () => {
  clearSessionCache()
  tmpRoot = await mkdtemp(join(tmpdir(), 'codeburn-claude-multi-'))
  // Point HOME at a scratch dir so the default `~/.claude` fallback resolves
  // somewhere we control. Without this, a stray `~/.claude` on the test
  // machine could leak into discovery.
  process.env['HOME'] = join(tmpRoot, 'home')
  await mkdir(process.env['HOME'], { recursive: true })
  delete process.env['CLAUDE_CONFIG_DIR']
  delete process.env['CLAUDE_CONFIG_DIRS']
})

afterEach(async () => {
  clearSessionCache()
  for (const [k, v] of Object.entries(savedEnv)) {
    if (v === undefined) delete process.env[k]
    else process.env[k] = v
  }
  await rm(tmpRoot, { recursive: true, force: true })
})

async function makeConfigDir(name: string, projectSlugs: string[]): Promise<string> {
  const dir = join(tmpRoot, name)
  for (const slug of projectSlugs) {
    const projectDir = join(dir, 'projects', slug)
    await mkdir(projectDir, { recursive: true })
    // Discovery only checks for the project subdirectory. A real session
    // file is not required; the parser is exercised separately below.
  }
  return dir
}

async function writeSession(configDir: string, slug: string, sessionId: string, lines: string[]): Promise<void> {
  const dir = join(configDir, 'projects', slug)
  await mkdir(dir, { recursive: true })
  await writeFile(join(dir, `${sessionId}.jsonl`), lines.join('\n'))
}

function summaryLine(sessionId: string, cwd: string): string {
  return JSON.stringify({
    type: 'summary',
    summary: 'test',
    leafUuid: 'l',
    sessionId,
    cwd,
    timestamp: '2026-05-09T00:00:00.000Z',
  })
}

function userLine(uuid: string, sessionId: string, cwd: string, text: string): string {
  return JSON.stringify({
    type: 'user',
    uuid,
    sessionId,
    cwd,
    timestamp: '2026-05-09T00:00:01.000Z',
    message: { role: 'user', content: text },
  })
}

function assistantLine(uuid: string, parentUuid: string, sessionId: string, cwd: string): string {
  return JSON.stringify({
    type: 'assistant',
    uuid,
    parentUuid,
    sessionId,
    cwd,
    timestamp: '2026-05-09T00:00:02.000Z',
    message: {
      id: `msg_${uuid}`,
      type: 'message',
      role: 'assistant',
      model: 'claude-sonnet-4-6',
      content: [{ type: 'text', text: 'reply' }],
      usage: { input_tokens: 100, output_tokens: 50 },
    },
  })
}

describe('claude provider — CLAUDE_CONFIG_DIRS discovery', () => {
  it('falls back to ~/.claude when no env var is set', async () => {
    const homeDir = process.env['HOME']!
    await mkdir(join(homeDir, '.claude', 'projects', '-Users-you-app'), { recursive: true })

    const sources = await claude.discoverSessions()
    const projectDirs = sources.map(s => s.path)
    expect(projectDirs).toContain(join(homeDir, '.claude', 'projects', '-Users-you-app'))
  })

  it('honors CLAUDE_CONFIG_DIR for a single override', async () => {
    const dir = await makeConfigDir('claude-work', ['-Users-you-app'])
    process.env['CLAUDE_CONFIG_DIR'] = dir

    const sources = await claude.discoverSessions()
    expect(sources.some(s => s.path === join(dir, 'projects', '-Users-you-app'))).toBe(true)
    // The default `~/.claude` should NOT also be scanned when the override is set.
    expect(sources.every(s => !s.path.startsWith(join(process.env['HOME']!, '.claude')))).toBe(true)
  })

  it('CLAUDE_CONFIG_DIRS overrides CLAUDE_CONFIG_DIR and walks every dir in the list', async () => {
    const work = await makeConfigDir('claude-work', ['-Users-you-app'])
    const personal = await makeConfigDir('claude-personal', ['-Users-you-app'])
    const single = await makeConfigDir('claude-other', ['-Users-you-other'])

    process.env['CLAUDE_CONFIG_DIR'] = single
    process.env['CLAUDE_CONFIG_DIRS'] = [work, personal].join(pathDelimiter)

    const sources = await claude.discoverSessions()
    const paths = sources.map(s => s.path)
    expect(paths).toContain(join(work, 'projects', '-Users-you-app'))
    expect(paths).toContain(join(personal, 'projects', '-Users-you-app'))
    // CLAUDE_CONFIG_DIR should be ignored once CLAUDE_CONFIG_DIRS is non-empty.
    expect(paths.some(p => p.startsWith(single))).toBe(false)
  })

  it('emits the same project name for the same slug across dirs (so parser merges)', async () => {
    const work = await makeConfigDir('claude-work', ['-Users-you-app'])
    const personal = await makeConfigDir('claude-personal', ['-Users-you-app'])
    process.env['CLAUDE_CONFIG_DIRS'] = [work, personal].join(pathDelimiter)

    const sources = await claude.discoverSessions()
    const ourSources = sources.filter(s =>
      s.path === join(work, 'projects', '-Users-you-app') ||
      s.path === join(personal, 'projects', '-Users-you-app'),
    )
    expect(ourSources).toHaveLength(2)
    expect(new Set(ourSources.map(s => s.project))).toEqual(new Set(['-Users-you-app']))
    expect(new Set(ourSources.map(s => s.sourceKind))).toEqual(new Set(['claude-config']))
    expect(new Set(ourSources.map(s => s.sourceLabel))).toEqual(new Set(['claude-work', 'claude-personal']))
    expect(ourSources.every(s => typeof s.sourceId === 'string' && s.sourceId.startsWith('claude-config:'))).toBe(true)
  })

  it('tolerates a non-existent dir in the list without dropping the real ones', async () => {
    const real = await makeConfigDir('claude-real', ['-Users-you-app'])
    const fake = join(tmpRoot, 'does-not-exist')
    process.env['CLAUDE_CONFIG_DIRS'] = [real, fake].join(pathDelimiter)

    const sources = await claude.discoverSessions()
    expect(sources.some(s => s.path === join(real, 'projects', '-Users-you-app'))).toBe(true)
  })

  it('dedupes when the same dir appears twice in CLAUDE_CONFIG_DIRS', async () => {
    const dir = await makeConfigDir('claude-once', ['-Users-you-app'])
    process.env['CLAUDE_CONFIG_DIRS'] = [dir, dir].join(pathDelimiter)

    const sources = await claude.discoverSessions()
    const ourSources = sources.filter(s => s.path === join(dir, 'projects', '-Users-you-app'))
    expect(ourSources).toHaveLength(1)
  })

  it('skips empty entries (leading, trailing, doubled delimiters)', async () => {
    const dir = await makeConfigDir('claude-only', ['-Users-you-app'])
    process.env['CLAUDE_CONFIG_DIRS'] = `${pathDelimiter}${dir}${pathDelimiter}${pathDelimiter}`

    const sources = await claude.discoverSessions()
    expect(sources.some(s => s.path === join(dir, 'projects', '-Users-you-app'))).toBe(true)
  })

  it('expands ~ in CLAUDE_CONFIG_DIR', async () => {
    const homeDir = process.env['HOME']!
    await mkdir(join(homeDir, 'custom-claude', 'projects', '-Users-you-app'), { recursive: true })
    process.env['CLAUDE_CONFIG_DIR'] = '~/custom-claude'

    const sources = await claude.discoverSessions()
    expect(sources.some(s => s.path === join(homeDir, 'custom-claude', 'projects', '-Users-you-app'))).toBe(true)
  })

  it('falls back to CLAUDE_CONFIG_DIR when CLAUDE_CONFIG_DIRS is set but empty', async () => {
    const single = await makeConfigDir('claude-fallback', ['-Users-you-app'])
    process.env['CLAUDE_CONFIG_DIR'] = single
    process.env['CLAUDE_CONFIG_DIRS'] = ''

    const sources = await claude.discoverSessions()
    expect(sources.some(s => s.path === join(single, 'projects', '-Users-you-app'))).toBe(true)
  })

  it('skips entries that point at a file rather than a directory', async () => {
    const real = await makeConfigDir('claude-real', ['-Users-you-app'])
    const filePath = join(tmpRoot, 'not-a-dir.txt')
    await writeFile(filePath, 'this is not a config dir')
    process.env['CLAUDE_CONFIG_DIRS'] = [real, filePath].join(pathDelimiter)

    const sources = await claude.discoverSessions()
    expect(sources.some(s => s.path === join(real, 'projects', '-Users-you-app'))).toBe(true)
    expect(sources.every(s => !s.path.startsWith(filePath))).toBe(true)
  })
})

describe('claude provider — config.json claudeConfigDirs (menubar-driven)', () => {
  async function writeConfigJson(value: unknown): Promise<void> {
    const dir = join(process.env['HOME']!, '.config', 'codeburn')
    await mkdir(dir, { recursive: true })
    await writeFile(join(dir, 'config.json'), JSON.stringify({ claudeConfigDirs: value }))
  }

  it('honors claudeConfigDirs from config.json when no env var is set', async () => {
    const work = await makeConfigDir('claude-work', ['-Users-you-app'])
    const personal = await makeConfigDir('claude-personal', ['-Users-you-app'])
    await writeConfigJson([work, personal])

    const sources = await claude.discoverSessions()
    const paths = sources.map(s => s.path)
    expect(paths).toContain(join(work, 'projects', '-Users-you-app'))
    expect(paths).toContain(join(personal, 'projects', '-Users-you-app'))
  })

  it('lets env CLAUDE_CONFIG_DIRS override config.json', async () => {
    const fromEnv = await makeConfigDir('claude-env', ['-Users-you-app'])
    const fromFile = await makeConfigDir('claude-file', ['-Users-you-app'])
    await writeConfigJson([fromFile])
    process.env['CLAUDE_CONFIG_DIRS'] = fromEnv

    const sources = await claude.discoverSessions()
    expect(sources.some(s => s.path === join(fromEnv, 'projects', '-Users-you-app'))).toBe(true)
    expect(sources.every(s => !s.path.startsWith(fromFile))).toBe(true)
  })

  it('lets env CLAUDE_CONFIG_DIR override config.json', async () => {
    const fromEnv = await makeConfigDir('claude-env', ['-Users-you-app'])
    const fromFile = await makeConfigDir('claude-file', ['-Users-you-app'])
    await writeConfigJson([fromFile])
    process.env['CLAUDE_CONFIG_DIR'] = fromEnv

    const sources = await claude.discoverSessions()
    expect(sources.some(s => s.path === join(fromEnv, 'projects', '-Users-you-app'))).toBe(true)
    expect(sources.every(s => !s.path.startsWith(fromFile))).toBe(true)
  })

  it('falls back to ~/.claude when config.json claudeConfigDirs is empty', async () => {
    const homeDir = process.env['HOME']!
    await mkdir(join(homeDir, '.claude', 'projects', '-Users-you-app'), { recursive: true })
    await writeConfigJson([])

    const sources = await claude.discoverSessions()
    expect(sources.some(s => s.path === join(homeDir, '.claude', 'projects', '-Users-you-app'))).toBe(true)
  })

  it('expands ~ in config.json entries', async () => {
    const homeDir = process.env['HOME']!
    await mkdir(join(homeDir, 'cfg-claude', 'projects', '-Users-you-app'), { recursive: true })
    await writeConfigJson(['~/cfg-claude'])

    const sources = await claude.discoverSessions()
    expect(sources.some(s => s.path === join(homeDir, 'cfg-claude', 'projects', '-Users-you-app'))).toBe(true)
  })

  it('ignores non-string and blank entries in config.json', async () => {
    const real = await makeConfigDir('claude-real', ['-Users-you-app'])
    await writeConfigJson([real, 42, '', '   ', null])

    const sources = await claude.discoverSessions()
    expect(sources.some(s => s.path === join(real, 'projects', '-Users-you-app'))).toBe(true)
  })

  it('falls back to ~/.claude when claudeConfigDirs is not an array', async () => {
    const homeDir = process.env['HOME']!
    await mkdir(join(homeDir, '.claude', 'projects', '-Users-you-app'), { recursive: true })
    await writeConfigJson('not-an-array')

    const sources = await claude.discoverSessions()
    expect(sources.some(s => s.path === join(homeDir, '.claude', 'projects', '-Users-you-app'))).toBe(true)
  })
})

describe('claude parser — multi-dir aggregation (issue #208 option 1)', () => {
  it('merges sessions from two config dirs into a single ProjectSummary when the canonical cwd matches', async () => {
    const work = await makeConfigDir('claude-work', [])
    const personal = await makeConfigDir('claude-personal', [])
    process.env['CLAUDE_CONFIG_DIRS'] = [work, personal].join(pathDelimiter)

    // Both accounts touch the same real project path. Same cwd -> same merge key.
    const slug = '-Users-you-shared-app'
    const cwd = '/Users/you/shared-app'
    await writeSession(work, slug, 'sess-work', [
      summaryLine('sess-work', cwd),
      userLine('u1', 'sess-work', cwd, 'hi from work'),
      assistantLine('a1', 'u1', 'sess-work', cwd),
    ])
    await writeSession(personal, slug, 'sess-personal', [
      summaryLine('sess-personal', cwd),
      userLine('u2', 'sess-personal', cwd, 'hi from personal'),
      assistantLine('a2', 'u2', 'sess-personal', cwd),
    ])

    const projects = await parseAllSessions(undefined, 'claude')
    const matches = projects.filter(p => p.project === slug)
    expect(matches).toHaveLength(1)
    expect(matches[0]!.totalApiCalls).toBe(2)
    // Two sessions, one from each dir, both rolled up.
    expect(matches[0]!.sessions.map(s => s.sessionId).sort()).toEqual(['sess-personal', 'sess-work'])
    // No `account` or `accountPath` field should appear on the ProjectSummary
    // — option 1 explicitly avoids attribution.
    expect((matches[0]! as Record<string, unknown>)['account']).toBeUndefined()
    expect((matches[0]! as Record<string, unknown>)['accountPath']).toBeUndefined()
  })

  it('keeps source metadata on merged sessions so one Claude config can be selected', async () => {
    const work = await makeConfigDir('claude-work', [])
    const personal = await makeConfigDir('claude-personal', [])
    process.env['CLAUDE_CONFIG_DIRS'] = [work, personal].join(pathDelimiter)

    const slug = '-Users-you-shared-app'
    const cwd = '/Users/you/shared-app'
    await writeSession(work, slug, 'sess-work', [
      summaryLine('sess-work', cwd),
      userLine('u1', 'sess-work', cwd, 'hi from work'),
      assistantLine('a1', 'u1', 'sess-work', cwd),
    ])
    await writeSession(personal, slug, 'sess-personal', [
      summaryLine('sess-personal', cwd),
      userLine('u2', 'sess-personal', cwd, 'hi from personal'),
      assistantLine('a2', 'u2', 'sess-personal', cwd),
    ])

    const projects = await parseAllSessions(undefined, 'claude')
    const merged = projects.find(p => p.project === slug)
    expect(merged).toBeDefined()
    expect(merged!.sessions).toHaveLength(2)

    const sourceIds = new Map(merged!.sessions.map(s => [s.source?.label, s.source?.id]))
    const workSourceId = sourceIds.get('claude-work')
    expect(workSourceId).toBeDefined()

    const workOnly = filterProjectsByClaudeConfigSource(projects, workSourceId!)
    expect(workOnly).toHaveLength(1)
    expect(workOnly[0]!.project).toBe(slug)
    expect(workOnly[0]!.totalApiCalls).toBe(1)
    expect(workOnly[0]!.sessions.map(s => s.sessionId)).toEqual(['sess-work'])
    expect(workOnly[0]!.sessions[0]!.source?.label).toBe('claude-work')
  })

  // Documents the path-aware merge behavior: the mergedMap in parseAllSessions
  // now keys by normalized cwd path (crossProviderKey), not by slug. Two dirs
  // can share the same slug but have different underlying cwds — those stay
  // separate because they represent genuinely different repositories. In real
  // Claude usage different cwds always produce different slugs anyway, so this
  // scenario is contrived, but the test pins the new behavior explicitly.
  it('keeps sessions with the same slug but different cwds as separate projects', async () => {
    const work = await makeConfigDir('claude-work', [])
    const personal = await makeConfigDir('claude-personal', [])
    process.env['CLAUDE_CONFIG_DIRS'] = [work, personal].join(pathDelimiter)

    const slug = '-Users-you-app'
    await writeSession(work, slug, 'sess-work', [
      summaryLine('sess-work', '/Users/you/work-app'),
      userLine('u1', 'sess-work', '/Users/you/work-app', 'work'),
      assistantLine('a1', 'u1', 'sess-work', '/Users/you/work-app'),
    ])
    await writeSession(personal, slug, 'sess-personal', [
      summaryLine('sess-personal', '/Users/you/personal-app'),
      userLine('u2', 'sess-personal', '/Users/you/personal-app', 'personal'),
      assistantLine('a2', 'u2', 'sess-personal', '/Users/you/personal-app'),
    ])

    const projects = await parseAllSessions(undefined, 'claude')
    const matches = projects.filter(p => p.project === slug)
    // Different cwds → different crossProviderKey → two separate project rows.
    expect(matches).toHaveLength(2)
    expect(matches.every(m => m.totalApiCalls === 1)).toBe(true)
  })
})
