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

import { parseAllSessions } from '../src/parser.js'
import type { DateRange } from '../src/types.js'

let tmpDir: string

beforeEach(async () => {
  tmpDir = await mkdtemp(join(tmpdir(), 'claude-cwd-test-'))
  process.env['CLAUDE_CONFIG_DIR'] = tmpDir
  // Point desktop sessions at an empty subdir by default so real sessions
  // on the developer's machine do not bleed into the unit tests.
  process.env['CODEBURN_DESKTOP_SESSIONS_DIR'] = join(tmpDir, 'desktop-sessions')
})

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

function dayRange(day: string): DateRange {
  return {
    start: new Date(`${day}T00:00:00.000Z`),
    end: new Date(`${day}T23:59:59.999Z`),
  }
}

async function writeClaudeSession(
  projectSlug: string,
  sessionId: string,
  cwd: string,
  timestamp: string,
  usage: Record<string, unknown> = { input_tokens: 100, output_tokens: 50 },
  model = 'claude-sonnet-4-5',
): Promise<void> {
  const projectDir = join(tmpDir, 'projects', projectSlug)
  await mkdir(projectDir, { recursive: true })
  const filePath = join(projectDir, `${sessionId}.jsonl`)
  await writeFile(filePath, JSON.stringify({
    type: 'assistant',
    sessionId,
    timestamp,
    cwd,
    message: {
      id: `msg-${sessionId}`,
      type: 'message',
      role: 'assistant',
      model,
      content: [],
      usage,
    },
  }) + '\n')

  const mtime = new Date(timestamp)
  await utimes(filePath, mtime, mtime)
}

describe('Claude cwd project paths', () => {
  it('uses the JSONL cwd as the canonical project path instead of the lossy directory slug', async () => {
    await writeClaudeSession(
      'c--AI-LAB-OPENCLAW',
      'windows-session',
      'C:\\AI_LAB\\OPENCLAW',
      '2099-05-01T12:00:00.000Z',
    )

    const projects = await parseAllSessions(dayRange('2099-05-01'), 'claude')

    expect(projects).toHaveLength(1)
    expect(projects[0]!.projectPath).toBe('C:\\AI_LAB\\OPENCLAW')
    expect(projects[0]!.projectPath).not.toBe('c//AI/LAB/OPENCLAW')
    expect(projects[0]!.totalApiCalls).toBe(1)
  })

  it('groups Windows cwd case and slash variants into one project', async () => {
    await writeClaudeSession(
      'windows-openclaw-a',
      'upper-backslash',
      'C:\\AI_LAB\\OPENCLAW',
      '2099-05-02T10:00:00.000Z',
    )
    await writeClaudeSession(
      'windows-openclaw-b',
      'lower-forward-slash',
      'c:/AI_LAB/OPENCLAW/',
      '2099-05-02T11:00:00.000Z',
    )

    const projects = await parseAllSessions(dayRange('2099-05-02'), 'claude')

    expect(projects).toHaveLength(1)
    expect(projects[0]!.sessions).toHaveLength(2)
    expect(projects[0]!.totalApiCalls).toBe(2)
    expect(projects[0]!.sessions.map(s => s.sessionId).sort()).toEqual([
      'lower-forward-slash',
      'upper-backslash',
    ])
  })

  it('prefers the canonical cwd path even when mixed with slug-only sessions in the same directory', async () => {
    const slug = 'c--AI-LAB-OPENCLAW'
    const projectDir = join(tmpDir, 'projects', slug)
    await mkdir(projectDir, { recursive: true })
    const noCwdPath = join(projectDir, 'a-no-cwd.jsonl')
    await writeFile(noCwdPath, JSON.stringify({
      type: 'assistant',
      sessionId: 'no-cwd',
      timestamp: '2099-05-03T10:00:00.000Z',
      message: {
        id: 'msg-no-cwd', type: 'message', role: 'assistant',
        model: 'claude-sonnet-4-5', content: [],
        usage: { input_tokens: 100, output_tokens: 50 },
      },
    }) + '\n')
    await utimes(noCwdPath, new Date('2099-05-03T10:00:00.000Z'), new Date('2099-05-03T10:00:00.000Z'))

    await writeClaudeSession(slug, 'b-with-cwd', 'C:\\AI_LAB\\OPENCLAW', '2099-05-03T11:00:00.000Z')

    const projects = await parseAllSessions(dayRange('2099-05-03'), 'claude')

    expect(projects).toHaveLength(1)
    expect(projects[0]!.sessions).toHaveLength(2)
    expect(projects[0]!.projectPath).toBe('C:\\AI_LAB\\OPENCLAW')
    expect(projects[0]!.projectPath).not.toBe('c//AI/LAB/OPENCLAW')
  })

  it('falls back to the slug-derived path when cwd is null, missing, or empty', async () => {
    const slug = 'fallback-slug'
    const projectDir = join(tmpDir, 'projects', slug)
    await mkdir(projectDir, { recursive: true })

    async function writeWith(name: string, sessionId: string, cwdField: unknown, ts: string) {
      const filePath = join(projectDir, `${name}.jsonl`)
      const obj: Record<string, unknown> = {
        type: 'assistant', sessionId, timestamp: ts,
        message: {
          id: `msg-${sessionId}`, type: 'message', role: 'assistant',
          model: 'claude-sonnet-4-5', content: [],
          usage: { input_tokens: 100, output_tokens: 50 },
        },
      }
      if (cwdField !== undefined) obj.cwd = cwdField
      await writeFile(filePath, JSON.stringify(obj) + '\n')
      await utimes(filePath, new Date(ts), new Date(ts))
    }

    await writeWith('null-cwd', 's-null', null, '2099-05-04T10:00:00.000Z')
    await writeWith('empty-cwd', 's-empty', '', '2099-05-04T10:30:00.000Z')
    await writeWith('whitespace-cwd', 's-ws', '   ', '2099-05-04T11:00:00.000Z')
    await writeWith('missing-cwd', 's-miss', undefined, '2099-05-04T11:30:00.000Z')

    const projects = await parseAllSessions(dayRange('2099-05-04'), 'claude')

    expect(projects).toHaveLength(1)
    expect(projects[0]!.sessions).toHaveLength(4)
    expect(projects[0]!.projectPath).toBe('fallback-slug')
  })

  it('keeps a hyphenated slug intact when cwd is absent', async () => {
    const slug = 'Projects-Content-OS'
    const projectDir = join(tmpDir, 'projects', slug)
    await mkdir(projectDir, { recursive: true })
    const filePath = join(projectDir, 'no-cwd.jsonl')
    await writeFile(filePath, JSON.stringify({
      type: 'assistant',
      sessionId: 'no-cwd',
      timestamp: '2099-05-09T10:00:00.000Z',
      message: {
        id: 'msg-no-cwd',
        type: 'message',
        role: 'assistant',
        model: 'claude-sonnet-4-5',
        content: [],
        usage: { input_tokens: 100, output_tokens: 50 },
      },
    }) + '\n')
    await utimes(filePath, new Date('2099-05-09T10:00:00.000Z'), new Date('2099-05-09T10:00:00.000Z'))

    const projects = await parseAllSessions(dayRange('2099-05-09'), 'claude')

    expect(projects).toHaveLength(1)
    expect(projects[0]!.project).toBe(slug)
    expect(projects[0]!.projectPath).toBe(slug)
    expect(projects[0]!.projectPath).not.toBe('Projects/Content/OS')
  })

  it('does not group sibling projects under a parent directory that merely contains .git', async () => {
    const projectsRoot = join(tmpDir, 'Projects')
    const swiftbar = join(projectsRoot, 'Swiftbar')
    const contentOs = join(projectsRoot, 'Content-OS')
    await mkdir(join(projectsRoot, '.git'), { recursive: true })
    await mkdir(swiftbar, { recursive: true })
    await mkdir(contentOs, { recursive: true })

    await writeClaudeSession(
      'tmp-Projects-Swiftbar',
      'swiftbar-session',
      swiftbar,
      '2099-05-10T10:00:00.000Z',
    )
    await writeClaudeSession(
      'tmp-Projects-Content-OS',
      'content-os-session',
      contentOs,
      '2099-05-10T11:00:00.000Z',
    )

    const projects = await parseAllSessions(dayRange('2099-05-10'), 'claude')
    const projectPaths = projects.map(project => project.projectPath).sort()

    expect(projects).toHaveLength(2)
    expect(projectPaths).toEqual([contentOs, swiftbar].sort())
    expect(projectPaths).not.toContain(projectsRoot)
  })

  it('groups git worktrees under the main repository project', async () => {
    const mainRepo = join(tmpDir, 'repos', 'codeburn')
    const worktreeA = join(tmpDir, 'worktrees', 'codeburn-feature-a')
    const worktreeB = join(tmpDir, 'worktrees', 'codeburn-feature-b')
    await mkdir(join(mainRepo, '.git', 'worktrees', 'feature-a'), { recursive: true })
    await mkdir(join(mainRepo, '.git', 'worktrees', 'feature-b'), { recursive: true })
    await mkdir(worktreeA, { recursive: true })
    await mkdir(worktreeB, { recursive: true })
    await writeFile(join(worktreeA, '.git'), `gitdir: ${join(mainRepo, '.git', 'worktrees', 'feature-a')}\n`)
    await writeFile(join(worktreeB, '.git'), `gitdir: ${relative(worktreeB, join(mainRepo, '.git', 'worktrees', 'feature-b'))}\n`)

    await writeClaudeSession(
      'tmp-worktrees-codeburn-feature-a',
      'worktree-a-session',
      worktreeA,
      '2099-05-07T10:00:00.000Z',
    )
    await writeClaudeSession(
      'tmp-worktrees-codeburn-feature-b',
      'worktree-b-session',
      worktreeB,
      '2099-05-07T11:00:00.000Z',
    )

    const projects = await parseAllSessions(dayRange('2099-05-07'), 'claude')

    expect(projects).toHaveLength(1)
    expect(projects[0]!.project).toBe('codeburn')
    expect(projects[0]!.projectPath).toBe(mainRepo)
    expect(projects[0]!.sessions).toHaveLength(2)
    expect(projects[0]!.totalApiCalls).toBe(2)
    expect(projects[0]!.sessions.map(s => s.sessionId).sort()).toEqual([
      'worktree-a-session',
      'worktree-b-session',
    ])
  })

  it('does not group separate-git-dir projects that are not git worktrees', async () => {
    const externalGitDir = join(tmpDir, 'external-git-dirs', 'project.git')
    const projectDir = join(tmpDir, 'standalone', 'separate-git-dir')
    await mkdir(externalGitDir, { recursive: true })
    await mkdir(projectDir, { recursive: true })
    await writeFile(join(projectDir, '.git'), `gitdir: ${externalGitDir}\n`)

    await writeClaudeSession(
      'tmp-standalone-separate-git-dir',
      'separate-git-dir-session',
      projectDir,
      '2099-05-08T10:00:00.000Z',
    )

    const projects = await parseAllSessions(dayRange('2099-05-08'), 'claude')

    expect(projects).toHaveLength(1)
    expect(projects[0]!.projectPath).toBe(projectDir)
    expect(projects[0]!.project).toBe('tmp-standalone-separate-git-dir')
  })
})

describe('Claude cache creation pricing', () => {
  it('prices 1-hour cache writes from usage.cache_creation at the 2x input rate', async () => {
    await writeClaudeSession(
      'cache-pricing',
      'one-hour-cache',
      '/tmp/cache-pricing',
      '2099-05-05T10:00:00.000Z',
      {
        input_tokens: 0,
        output_tokens: 0,
        cache_creation_input_tokens: 60_120,
        cache_creation: {
          ephemeral_5m_input_tokens: 0,
          ephemeral_1h_input_tokens: 60_120,
        },
      },
      'claude-opus-4-7',
    )

    const projects = await parseAllSessions(dayRange('2099-05-05'), 'claude')

    expect(projects).toHaveLength(1)
    expect(projects[0]!.sessions[0]!.totalCacheWriteTokens).toBe(60_120)
    expect(projects[0]!.totalCostUSD).toBeCloseTo(0.6012, 6)
  })

  it('falls back to the legacy 5-minute cache write rate when split fields are absent', async () => {
    await writeClaudeSession(
      'legacy-cache-pricing',
      'legacy-cache',
      '/tmp/legacy-cache-pricing',
      '2099-05-06T10:00:00.000Z',
      {
        input_tokens: 0,
        output_tokens: 0,
        cache_creation_input_tokens: 60_120,
      },
      'claude-opus-4-7',
    )

    const projects = await parseAllSessions(dayRange('2099-05-06'), 'claude')

    expect(projects).toHaveLength(1)
    expect(projects[0]!.sessions[0]!.totalCacheWriteTokens).toBe(60_120)
    expect(projects[0]!.totalCostUSD).toBeCloseTo(0.37575, 6)
  })
})

// ── Helpers for Cowork local-agent-mode session fixtures ────────────────

async function writeCoworkSession(opts: {
  desktopSessionsDir: string
  appId: string
  workspaceId: string
  sessionId: string
  spaceName?: string
  spaceId?: string
  userSelectedFolders?: string[]
  title?: string
  claudeSessionId: string
  timestamp: string
  usage?: Record<string, unknown>
  model?: string
}): Promise<void> {
  const {
    desktopSessionsDir, appId, workspaceId, sessionId,
    spaceName, spaceId, userSelectedFolders, title,
    claudeSessionId, timestamp,
    usage = { input_tokens: 100, output_tokens: 50 },
    model = 'claude-sonnet-4-5',
  } = opts

  const workspaceDir = join(desktopSessionsDir, appId, workspaceId)

  // spaces.json — written only when a space is defined
  await mkdir(workspaceDir, { recursive: true })
  if (spaceId && spaceName) {
    await writeFile(join(workspaceDir, 'spaces.json'), JSON.stringify({
      spaces: [{ id: spaceId, name: spaceName, folders: [], projects: [] }],
    }))
  }

  // local_<sessionId>.json — session metadata
  const outputsDir = join(workspaceDir, sessionId, 'outputs')
  const sessionMeta: Record<string, unknown> = {
    sessionId,
    cwd: outputsDir,
    title: title ?? (spaceName ? `Test session for ${spaceName}` : 'Untitled session'),
  }
  if (spaceId) sessionMeta['spaceId'] = spaceId
  if (userSelectedFolders) sessionMeta['userSelectedFolders'] = userSelectedFolders
  await writeFile(join(workspaceDir, `${sessionId}.json`), JSON.stringify(sessionMeta))

  // .claude/projects/<slug>/session.jsonl — the actual token-bearing session
  const projectSlug = outputsDir.replace(/[/\\]/g, '-').replace(/^-/, '')
  const projectDir = join(workspaceDir, sessionId, '.claude', 'projects', projectSlug)
  await mkdir(projectDir, { recursive: true })
  const filePath = join(projectDir, `${claudeSessionId}.jsonl`)
  await writeFile(filePath, JSON.stringify({
    type: 'assistant',
    sessionId: claudeSessionId,
    timestamp,
    cwd: outputsDir,
    message: {
      id: `msg-${claudeSessionId}`,
      type: 'message',
      role: 'assistant',
      model,
      content: [],
      usage,
    },
  }) + '\n')
  const mtime = new Date(timestamp)
  await utimes(filePath, mtime, mtime)
}

describe('Claude Cowork local-agent-mode session grouping', () => {
  it('groups multiple Cowork sessions from the same space under the space name', async () => {
    const desktopSessionsDir = join(tmpDir, 'desktop-sessions')
    const spaceId = 'space-001'
    const spaceName = 'Project1'

    await writeCoworkSession({
      desktopSessionsDir,
      appId: 'app-abc',
      workspaceId: 'ws-001',
      sessionId: 'local_aaaa',
      spaceName,
      spaceId,
      claudeSessionId: 'session-a',
      timestamp: '2099-06-01T10:00:00.000Z',
    })

    await writeCoworkSession({
      desktopSessionsDir,
      appId: 'app-abc',
      workspaceId: 'ws-001',
      sessionId: 'local_bbbb',
      spaceName,
      spaceId,
      claudeSessionId: 'session-b',
      timestamp: '2099-06-01T11:00:00.000Z',
    })

    const projects = await parseAllSessions(dayRange('2099-06-01'), 'claude')

    expect(projects).toHaveLength(1)
    expect(projects[0]!.project).toBe(spaceName)
    expect(projects[0]!.sessions).toHaveLength(2)
    expect(projects[0]!.sessions.map(s => s.sessionId).sort()).toEqual([
      'session-a',
      'session-b',
    ])
  })

  it('keeps sessions from different spaces in separate projects', async () => {
    const desktopSessionsDir = join(tmpDir, 'desktop-sessions')

    // Each space gets its own workspace so their spaces.json files don't overwrite each other.
    await writeCoworkSession({
      desktopSessionsDir,
      appId: 'app-abc',
      workspaceId: 'ws-001',
      sessionId: 'local_cccc',
      spaceName: 'Project1',
      spaceId: 'space-001',
      claudeSessionId: 'session-c',
      timestamp: '2099-06-02T10:00:00.000Z',
    })

    await writeCoworkSession({
      desktopSessionsDir,
      appId: 'app-abc',
      workspaceId: 'ws-002',
      sessionId: 'local_dddd',
      spaceName: 'Project2',
      spaceId: 'space-002',
      claudeSessionId: 'session-d',
      timestamp: '2099-06-02T11:00:00.000Z',
    })

    const projects = await parseAllSessions(dayRange('2099-06-02'), 'claude')

    expect(projects).toHaveLength(2)
    const names = projects.map(p => p.project).sort()
    expect(names).toEqual(['Project1', 'Project2'])
  })

  it('falls back to userSelectedFolders[0] basename when no spaceId is set', async () => {
    const desktopSessionsDir = join(tmpDir, 'desktop-sessions')

    await writeCoworkSession({
      desktopSessionsDir,
      appId: 'app-abc',
      workspaceId: 'ws-003',
      sessionId: 'local_eeee',
      userSelectedFolders: ['/home/user/projects/ParentFolder/SubFolder'],
      title: 'Some session title',
      claudeSessionId: 'session-e',
      timestamp: '2099-06-03T10:00:00.000Z',
    })

    const projects = await parseAllSessions(dayRange('2099-06-03'), 'claude')

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

  it('falls back to title when no spaceId and no userSelectedFolders', async () => {
    const desktopSessionsDir = join(tmpDir, 'desktop-sessions')

    await writeCoworkSession({
      desktopSessionsDir,
      appId: 'app-abc',
      workspaceId: 'ws-004',
      sessionId: 'local_ffff',
      title: 'A standalone session task',
      claudeSessionId: 'session-f',
      timestamp: '2099-06-04T10:00:00.000Z',
    })

    const projects = await parseAllSessions(dayRange('2099-06-04'), 'claude')

    expect(projects).toHaveLength(1)
    expect(projects[0]!.project).toBe('A standalone session task')
    expect(projects[0]!.sessions).toHaveLength(1)
  })

  it('groups container-mode sessions (cwd=/sessions/<name>) under the space name', async () => {
    const desktopSessionsDir = join(tmpDir, 'desktop-sessions')
    const workspaceDir = join(desktopSessionsDir, 'app-abc', 'ws-006')
    const sessionId = 'local_hhhh'

    // Set up workspace metadata (spaces.json + session .json with spaceId)
    await mkdir(workspaceDir, { recursive: true })
    await writeFile(join(workspaceDir, 'spaces.json'), JSON.stringify({
      spaces: [{ id: 'space-001', name: 'Project1', folders: [], projects: [] }],
    }))
    const containerCwd = '/sessions/trusting-inspiring-ritchie'
    await writeFile(join(workspaceDir, `${sessionId}.json`), JSON.stringify({
      sessionId, spaceId: 'space-001', cwd: containerCwd, title: 'Container session',
    }))

    // Container-mode: project slug is derived from the container cwd, not outputs/
    const containerSlug = containerCwd.replace(/\//g, '-').replace(/^-/, '')
    const projectDir = join(workspaceDir, sessionId, '.claude', 'projects', containerSlug)
    await mkdir(projectDir, { recursive: true })
    const filePath = join(projectDir, 'container-session.jsonl')
    await writeFile(filePath, JSON.stringify({
      type: 'assistant',
      sessionId: 'container-session',
      timestamp: '2099-06-06T10:00:00.000Z',
      cwd: containerCwd,
      message: {
        id: 'msg-container', type: 'message', role: 'assistant',
        model: 'claude-sonnet-4-5', content: [],
        usage: { input_tokens: 100, output_tokens: 50 },
      },
    }) + '\n')
    await utimes(filePath, new Date('2099-06-06T10:00:00.000Z'), new Date('2099-06-06T10:00:00.000Z'))

    const projects = await parseAllSessions(dayRange('2099-06-06'), 'claude')

    expect(projects).toHaveLength(1)
    expect(projects[0]!.project).toBe('Project1')
    expect(projects[0]!.projectPath).toBe('Project1')
    expect(projects[0]!.sessions).toHaveLength(1)
  })

  it('falls back to sanitized directory slug when no session metadata exists', async () => {
    const desktopSessionsDir = join(tmpDir, 'desktop-sessions')
    const workspaceDir = join(desktopSessionsDir, 'app-abc', 'ws-005')
    const sessionId = 'local_gggg'
    const outputsDir = join(workspaceDir, sessionId, 'outputs')
    const projectSlug = outputsDir.replace(/[/\\]/g, '-').replace(/^-/, '')
    const projectDir = join(workspaceDir, sessionId, '.claude', 'projects', projectSlug)
    await mkdir(projectDir, { recursive: true })

    // No spaces.json or session .json at all
    const filePath = join(projectDir, 'no-meta-session.jsonl')
    await writeFile(filePath, JSON.stringify({
      type: 'assistant',
      sessionId: 'no-meta-session',
      timestamp: '2099-06-05T10:00:00.000Z',
      cwd: outputsDir,
      message: {
        id: 'msg-no-meta', type: 'message', role: 'assistant',
        model: 'claude-sonnet-4-5', content: [],
        usage: { input_tokens: 100, output_tokens: 50 },
      },
    }) + '\n')
    await utimes(filePath, new Date('2099-06-05T10:00:00.000Z'), new Date('2099-06-05T10:00:00.000Z'))

    const projects = await parseAllSessions(dayRange('2099-06-05'), 'claude')

    expect(projects).toHaveLength(1)
    expect(projects[0]!.project).toBe(projectSlug)
    expect(projects[0]!.sessions).toHaveLength(1)
  })
})
