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

import { buildMenubarPayload, type CombinedUsage, type PeriodData, type ProviderCost } from '../src/menubar-json.js'
import type { OptimizeResult } from '../src/optimize.js'

function emptyPeriod(label: string): PeriodData {
  return {
    label,
    cost: 0,
    calls: 0,
    sessions: 0,
    inputTokens: 0,
    outputTokens: 0,
    cacheReadTokens: 0,
    cacheWriteTokens: 0,
    categories: [],
    models: [],
  }
}

describe('buildMenubarPayload', () => {
  it('emits the full schema with current-period metrics and iso timestamp', () => {
    const period: PeriodData = {
      label: '7 Days',
      cost: 1248.01,
      calls: 11231,
      sessions: 97,
      inputTokens: 19100,
      outputTokens: 675600,
      cacheReadTokens: 0,
      cacheWriteTokens: 0,
      categories: [],
      models: [],
    }
    const payload = buildMenubarPayload(period, [], null)

    expect(payload.generated).toMatch(/^\d{4}-\d{2}-\d{2}T/)
    expect(payload.current.label).toBe('7 Days')
    expect(payload.current.cost).toBe(1248.01)
    expect(payload.current.calls).toBe(11231)
    expect(payload.current.sessions).toBe(97)
    expect(payload.current.inputTokens).toBe(19100)
    expect(payload.current.outputTokens).toBe(675600)
  })

  it('exposes period-scoped cache tokens on current, decoupled from the 365-day history backfill (#583)', () => {
    const period: PeriodData = {
      label: '30 Days',
      cost: 5, calls: 10, sessions: 2,
      inputTokens: 1000, outputTokens: 2000,
      // What `report -p 30days` shows in the terminal for the same window.
      cacheReadTokens: 1_391_100_000,
      cacheWriteTokens: 42_000_000,
      categories: [], models: [],
    }
    // history.daily is the full BACKFILL_DAYS (365) trend, whose cache totals are
    // far larger than the selected period. The web cards used to sum these, which
    // is the bug in #583. current must mirror the period, not the backfill.
    const dailyHistory = [
      { date: '2025-07-15', cost: 0, savingsUSD: 0, calls: 0, inputTokens: 0, outputTokens: 0, cacheReadTokens: 1_800_000_000, cacheWriteTokens: 60_000_000, topModels: [] },
      { date: '2026-06-30', cost: 0, savingsUSD: 0, calls: 0, inputTokens: 0, outputTokens: 0, cacheReadTokens: 270_000_000, cacheWriteTokens: 12_000_000, topModels: [] },
    ]
    const payload = buildMenubarPayload(period, [], null, dailyHistory)

    // current carries the period totals verbatim ...
    expect(payload.current.cacheReadTokens).toBe(1_391_100_000)
    expect(payload.current.cacheWriteTokens).toBe(42_000_000)
    // ... and is independent of the larger history-backfill sum the cards used before.
    const historyReadSum = dailyHistory.reduce((s, d) => s + d.cacheReadTokens, 0)
    const historyWriteSum = dailyHistory.reduce((s, d) => s + d.cacheWriteTokens, 0)
    expect(historyReadSum).toBeGreaterThan(payload.current.cacheReadTokens)
    expect(historyWriteSum).toBeGreaterThan(payload.current.cacheWriteTokens)
  })

  it('computes per-category oneShotRate from editTurns and skips categories without edits', () => {
    const period: PeriodData = {
      label: 'Today',
      cost: 0, calls: 0, sessions: 0,
      inputTokens: 0, outputTokens: 0, cacheReadTokens: 0, cacheWriteTokens: 0,
      categories: [
        { name: 'Coding', cost: 15.83, turns: 7, editTurns: 7, oneShotTurns: 6 },
        { name: 'Conversation', cost: 16.69, turns: 47, editTurns: 0, oneShotTurns: 0 },
      ],
      models: [],
    }
    const payload = buildMenubarPayload(period, [], null)

    const coding = payload.current.topActivities.find(a => a.name === 'Coding')!
    expect(coding.oneShotRate).toBeCloseTo(6 / 7)

    const conv = payload.current.topActivities.find(a => a.name === 'Conversation')!
    expect(conv.oneShotRate).toBeNull()
  })

  it('computes aggregate oneShotRate across categories with edits', () => {
    const period: PeriodData = {
      label: 'Today',
      cost: 0, calls: 0, sessions: 0,
      inputTokens: 0, outputTokens: 0, cacheReadTokens: 0, cacheWriteTokens: 0,
      categories: [
        { name: 'Coding', cost: 1, turns: 7, editTurns: 10, oneShotTurns: 8 },
        { name: 'Debugging', cost: 1, turns: 5, editTurns: 10, oneShotTurns: 6 },
        { name: 'Conversation', cost: 1, turns: 40, editTurns: 0, oneShotTurns: 0 },
      ],
      models: [],
    }
    const payload = buildMenubarPayload(period, [], null)
    expect(payload.current.oneShotRate).toBeCloseTo((8 + 6) / (10 + 10))
  })

  it('returns null aggregate oneShotRate when no categories have editTurns', () => {
    const period: PeriodData = {
      label: 'Today',
      cost: 0, calls: 0, sessions: 0,
      inputTokens: 0, outputTokens: 0, cacheReadTokens: 0, cacheWriteTokens: 0,
      categories: [{ name: 'Conversation', cost: 1, turns: 5, editTurns: 0, oneShotTurns: 0 }],
      models: [],
    }
    const payload = buildMenubarPayload(period, [], null)
    expect(payload.current.oneShotRate).toBeNull()
  })

  it('filters out the synthetic model and caps topModels at 20 so multi-model users see all their models', () => {
    const models = Array.from({ length: 30 }, (_, i) => ({
      name: `Model${i}`, cost: 30 - i, calls: 100,
    }))
    const period: PeriodData = {
      label: 'Today',
      cost: 0, calls: 0, sessions: 0,
      inputTokens: 0, outputTokens: 0, cacheReadTokens: 0, cacheWriteTokens: 0,
      categories: [],
      models: [{ name: '<synthetic>', cost: 99, calls: 0 }, ...models],
    }
    const payload = buildMenubarPayload(period, [], null)
    expect(payload.current.topModels.find(m => m.name === '<synthetic>')).toBeUndefined()
    expect(payload.current.topModels).toHaveLength(20)
    expect(payload.current.topModels[0].name).toBe('Model0')
  })

  it('caps topActivities at 20 so all task categories can surface', () => {
    const period: PeriodData = {
      label: 'Today',
      cost: 0, calls: 0, sessions: 0,
      inputTokens: 0, outputTokens: 0, cacheReadTokens: 0, cacheWriteTokens: 0,
      categories: Array.from({ length: 25 }, (_, i) => ({
        name: `Cat${i}`, cost: 1, turns: 1, editTurns: 1, oneShotTurns: 1,
      })),
      models: [],
    }
    const payload = buildMenubarPayload(period, [], null)
    expect(payload.current.topActivities).toHaveLength(20)
  })

  it('computes cacheHitPercent from cache reads over input plus cache reads', () => {
    const period: PeriodData = {
      label: 'Today',
      cost: 0, calls: 0, sessions: 0,
      inputTokens: 100,
      outputTokens: 200,
      cacheReadTokens: 900,
      cacheWriteTokens: 0,
      categories: [],
      models: [],
    }
    const payload = buildMenubarPayload(period, [], null)
    expect(payload.current.cacheHitPercent).toBeCloseTo(90)
  })

  it('returns zero cacheHitPercent when there is no input or cache traffic', () => {
    const payload = buildMenubarPayload(emptyPeriod('Today'), [], null)
    expect(payload.current.cacheHitPercent).toBe(0)
  })

  it('handles null optimize as empty findings block', () => {
    const payload = buildMenubarPayload(emptyPeriod('Today'), [], null)
    expect(payload.optimize).toEqual({ findingCount: 0, savingsUSD: 0, topFindings: [] })
  })

  it('converts tokensSaved to savingsUSD via costRate and caps topFindings at 10', () => {
    const findings = Array.from({ length: 15 }, (_, i) => ({
      title: `F${i}`, explanation: '', impact: 'low' as const, tokensSaved: 1000,
      fix: { type: 'paste' as const, label: '', text: '' },
    }))
    const optimize: OptimizeResult = {
      findings,
      costRate: 0.00002,
      healthScore: 60,
      healthGrade: 'C',
    }
    const payload = buildMenubarPayload(emptyPeriod('Today'), [], optimize)

    expect(payload.optimize.findingCount).toBe(15)
    expect(payload.optimize.topFindings).toHaveLength(10)
    expect(payload.optimize.topFindings[0].title).toBe('F0')
    expect(payload.optimize.topFindings[0].savingsUSD).toBeCloseTo(1000 * 0.00002)
    expect(payload.optimize.savingsUSD).toBeCloseTo(15 * 1000 * 0.00002)
  })

  it('maps providers into a lowercased dict inside the current-period block', () => {
    const providers: ProviderCost[] = [
      { name: 'Claude Code', cost: 76.45 },
      { name: 'Cursor', cost: 2.18 },
      { name: 'Codex', cost: 1.5 },
    ]
    const payload = buildMenubarPayload(emptyPeriod('Today'), providers, null)
    expect(payload.current.providers).toEqual({ 'claude code': 76.45, cursor: 2.18, codex: 1.5 })
  })

  it('keeps zero-cost providers in the dict so installed-but-unused providers still render as tabs', () => {
    const providers: ProviderCost[] = [
      { name: 'Claude', cost: 76.45 },
      { name: 'Codex', cost: 0 },
      { name: 'Cursor', cost: 2.18 },
    ]
    const payload = buildMenubarPayload(emptyPeriod('Today'), providers, null)
    expect(payload.current.providers).toEqual({ claude: 76.45, codex: 0, cursor: 2.18 })
  })

  it('includes up to 365 daily history entries sorted ascending by date', () => {
    const history = Array.from({ length: 400 }, (_, i) => {
      const d = new Date(2025, 0, 1)
      d.setDate(d.getDate() + i)
      return {
        date: d.toISOString().slice(0, 10),
        cost: i,
        calls: i * 10,
        inputTokens: 0,
        outputTokens: 0,
        cacheReadTokens: 0,
        cacheWriteTokens: 0,
        topModels: [],
      }
    })
    const payload = buildMenubarPayload(emptyPeriod('Today'), [], null, history)
    expect(payload.history.daily).toHaveLength(365)
    expect(payload.history.daily[0]!.date < payload.history.daily[364]!.date).toBe(true)
    expect(payload.history.daily[364]!.date).toBe(history[399]!.date)
  })

  it('preserves token fields in dailyHistory entries', () => {
    const history = [
      { date: '2026-04-15', cost: 10, calls: 50, inputTokens: 100, outputTokens: 200, cacheReadTokens: 5000, cacheWriteTokens: 800, topModels: [{ name: 'Opus 4.7', cost: 8, calls: 40, inputTokens: 80, outputTokens: 160 }] },
      { date: '2026-04-16', cost: 20, calls: 75, inputTokens: 150, outputTokens: 350, cacheReadTokens: 8000, cacheWriteTokens: 1200, topModels: [] },
    ]
    const payload = buildMenubarPayload(emptyPeriod('Today'), [], null, history)
    expect(payload.history.daily[0]).toEqual(history[0])
    expect(payload.history.daily[1]).toEqual(history[1])
  })

  it('returns empty history when none supplied', () => {
    const payload = buildMenubarPayload(emptyPeriod('Today'), [], null)
    expect(payload.history.daily).toEqual([])
  })

  it('drops providers with negative cost defensively', () => {
    const providers: ProviderCost[] = [
      { name: 'Claude', cost: 76.45 },
      { name: 'Broken', cost: -1 },
    ]
    const payload = buildMenubarPayload(emptyPeriod('Today'), providers, null)
    expect(payload.current.providers).toEqual({ claude: 76.45 })
  })

  it('omits combined usage by default and accepts the documented combined shape when attached', () => {
    const payload = buildMenubarPayload(emptyPeriod('Today'), [], null)
    expect(payload).not.toHaveProperty('combined')

    const combined: CombinedUsage = {
      perDevice: [
        {
          id: 'local',
          name: 'Mac Studio',
          local: true,
          cost: 1,
          calls: 2,
          sessions: 1,
          inputTokens: 100,
          outputTokens: 50,
          cacheCreateTokens: 10,
          cacheReadTokens: 20,
          totalTokens: 180,
        },
      ],
      combined: {
        cost: 1,
        calls: 2,
        sessions: 1,
        inputTokens: 100,
        outputTokens: 50,
        cacheCreateTokens: 10,
        cacheReadTokens: 20,
        totalTokens: 180,
        deviceCount: 1,
        reachableCount: 1,
      },
    }
    payload.combined = combined

    expect(payload.combined).toEqual(combined)
  })

  it('emits Claude config selector metadata only when multiple configs are available', () => {
    const oneConfig = buildMenubarPayload(emptyPeriod('Today'), [], null, undefined, undefined, undefined, undefined, {
      selectedId: null,
      options: [{ id: 'claude-config:a', label: 'claude-work', path: '/tmp/claude-work' }],
    })
    expect(oneConfig).not.toHaveProperty('claudeConfigs')

    const twoConfigs = buildMenubarPayload(emptyPeriod('Today'), [], null, undefined, undefined, undefined, undefined, {
      selectedId: 'claude-config:b',
      options: [
        { id: 'claude-config:a', label: 'claude-work', path: '/tmp/claude-work' },
        { id: 'claude-config:b', label: 'claude-personal', path: '/tmp/claude-personal' },
      ],
    })

    expect(twoConfigs.claudeConfigs).toEqual({
      selectedId: 'claude-config:b',
      options: [
        { id: 'claude-config:a', label: 'claude-work', path: '/tmp/claude-work' },
        { id: 'claude-config:b', label: 'claude-personal', path: '/tmp/claude-personal' },
      ],
    })
  })
})
