/**
 * Unit tests for Data Aggregator
 */

import { describe, expect, test } from 'bun:test';
import {
  aggregateDailyUsage,
  aggregateMonthlyUsage,
  aggregateSessionUsage,
} from '../../src/web-server/data-aggregator';
import { type RawUsageEntry } from '../../src/web-server/jsonl-parser';

// ============================================================================
// TEST FIXTURES
// ============================================================================

const createEntry = (overrides: Partial<RawUsageEntry> = {}): RawUsageEntry => ({
  inputTokens: 1000,
  outputTokens: 500,
  cacheCreationTokens: 100,
  cacheReadTokens: 50,
  model: 'claude-sonnet-4-5',
  sessionId: 'session-123',
  timestamp: '2025-12-09T10:00:00.000Z',
  projectPath: '/home/user/project',
  version: '2.0.60',
  ...overrides,
});

// ============================================================================
// aggregateDailyUsage Tests
// ============================================================================

describe('aggregateDailyUsage', () => {
  test('aggregates entries by date', () => {
    const entries: RawUsageEntry[] = [
      createEntry({ timestamp: '2025-12-09T10:00:00.000Z', inputTokens: 1000 }),
      createEntry({ timestamp: '2025-12-09T14:00:00.000Z', inputTokens: 2000 }),
      createEntry({ timestamp: '2025-12-08T10:00:00.000Z', inputTokens: 500 }),
    ];

    const result = aggregateDailyUsage(entries);

    expect(result.length).toBe(2);
    // Most recent first
    expect(result[0].date).toBe('2025-12-09');
    expect(result[0].inputTokens).toBe(3000); // 1000 + 2000
    expect(result[1].date).toBe('2025-12-08');
    expect(result[1].inputTokens).toBe(500);
  });

  test('groups by model within each day', () => {
    const entries: RawUsageEntry[] = [
      createEntry({ model: 'claude-sonnet-4-5', inputTokens: 1000 }),
      createEntry({ model: 'claude-opus-4-5-20251101', inputTokens: 2000 }),
      createEntry({ model: 'claude-sonnet-4-5', inputTokens: 500 }),
    ];

    const result = aggregateDailyUsage(entries);

    expect(result.length).toBe(1);
    expect(result[0].modelBreakdowns.length).toBe(2);
    expect(result[0].modelsUsed).toContain('claude-sonnet-4-5');
    expect(result[0].modelsUsed).toContain('claude-opus-4-5-20251101');

    // Find sonnet breakdown
    const sonnet = result[0].modelBreakdowns.find((b) => b.modelName === 'claude-sonnet-4-5');
    expect(sonnet!.inputTokens).toBe(1500); // 1000 + 500
  });

  test('calculates costs correctly', () => {
    const entries: RawUsageEntry[] = [
      createEntry({
        model: 'claude-sonnet-4-5',
        inputTokens: 1_000_000, // $3.00
        outputTokens: 1_000_000, // $15.00
        cacheCreationTokens: 0,
        cacheReadTokens: 0,
      }),
    ];

    const result = aggregateDailyUsage(entries);

    expect(result[0].totalCost).toBeCloseTo(18.0, 2);
    expect(result[0].modelBreakdowns[0].cost).toBeCloseTo(18.0, 2);
  });

  test('returns empty array for no entries', () => {
    const result = aggregateDailyUsage([]);
    expect(result.length).toBe(0);
  });

  test('sorts model breakdowns by cost descending', () => {
    const entries: RawUsageEntry[] = [
      createEntry({ model: 'claude-haiku-4-5-20251001', inputTokens: 1000 }), // cheap
      createEntry({ model: 'claude-opus-4-5-20251101', inputTokens: 1000 }), // expensive
    ];

    const result = aggregateDailyUsage(entries);

    // Opus should be first (higher cost)
    expect(result[0].modelBreakdowns[0].modelName).toBe('claude-opus-4-5-20251101');
  });

  test('sets source field', () => {
    const entries: RawUsageEntry[] = [createEntry()];
    const result = aggregateDailyUsage(entries, 'test-source');
    expect(result[0].source).toBe('test-source');
  });
});

// ============================================================================
// aggregateMonthlyUsage Tests
// ============================================================================

describe('aggregateMonthlyUsage', () => {
  test('aggregates entries by month', () => {
    const entries: RawUsageEntry[] = [
      createEntry({ timestamp: '2025-12-09T10:00:00.000Z', inputTokens: 1000 }),
      createEntry({ timestamp: '2025-12-15T10:00:00.000Z', inputTokens: 2000 }),
      createEntry({ timestamp: '2025-11-20T10:00:00.000Z', inputTokens: 500 }),
    ];

    const result = aggregateMonthlyUsage(entries);

    expect(result.length).toBe(2);
    // Most recent first
    expect(result[0].month).toBe('2025-12');
    expect(result[0].inputTokens).toBe(3000); // 1000 + 2000
    expect(result[1].month).toBe('2025-11');
    expect(result[1].inputTokens).toBe(500);
  });

  test('groups by model within each month', () => {
    const entries: RawUsageEntry[] = [
      createEntry({ model: 'claude-sonnet-4-5', inputTokens: 1000 }),
      createEntry({ model: 'gemini-2.5-pro', inputTokens: 2000 }),
    ];

    const result = aggregateMonthlyUsage(entries);

    expect(result[0].modelBreakdowns.length).toBe(2);
    expect(result[0].modelsUsed).toContain('claude-sonnet-4-5');
    expect(result[0].modelsUsed).toContain('gemini-2.5-pro');
  });

  test('returns empty array for no entries', () => {
    const result = aggregateMonthlyUsage([]);
    expect(result.length).toBe(0);
  });
});

// ============================================================================
// aggregateSessionUsage Tests
// ============================================================================

describe('aggregateSessionUsage', () => {
  test('aggregates entries by sessionId', () => {
    const entries: RawUsageEntry[] = [
      createEntry({ sessionId: 'session-A', inputTokens: 1000 }),
      createEntry({ sessionId: 'session-A', inputTokens: 2000 }),
      createEntry({ sessionId: 'session-B', inputTokens: 500 }),
    ];

    const result = aggregateSessionUsage(entries);

    expect(result.length).toBe(2);

    const sessionA = result.find((s) => s.sessionId === 'session-A');
    expect(sessionA!.inputTokens).toBe(3000);

    const sessionB = result.find((s) => s.sessionId === 'session-B');
    expect(sessionB!.inputTokens).toBe(500);
  });

  test('tracks last activity timestamp', () => {
    const entries: RawUsageEntry[] = [
      createEntry({ sessionId: 'session-A', timestamp: '2025-12-09T10:00:00.000Z' }),
      createEntry({ sessionId: 'session-A', timestamp: '2025-12-09T14:00:00.000Z' }),
      createEntry({ sessionId: 'session-A', timestamp: '2025-12-09T12:00:00.000Z' }),
    ];

    const result = aggregateSessionUsage(entries);

    expect(result[0].lastActivity).toBe('2025-12-09T14:00:00.000Z');
  });

  test('collects unique versions', () => {
    const entries: RawUsageEntry[] = [
      createEntry({ sessionId: 'session-A', version: '2.0.59' }),
      createEntry({ sessionId: 'session-A', version: '2.0.60' }),
      createEntry({ sessionId: 'session-A', version: '2.0.60' }), // duplicate
    ];

    const result = aggregateSessionUsage(entries);

    expect(result[0].versions.length).toBe(2);
    expect(result[0].versions).toContain('2.0.59');
    expect(result[0].versions).toContain('2.0.60');
  });

  test('includes project path', () => {
    const entries: RawUsageEntry[] = [
      createEntry({ sessionId: 'session-A', projectPath: '/home/user/my-project' }),
    ];

    const result = aggregateSessionUsage(entries);

    expect(result[0].projectPath).toBe('/home/user/my-project');
  });

  test('skips entries without sessionId', () => {
    const entries: RawUsageEntry[] = [
      createEntry({ sessionId: '', inputTokens: 1000 }),
      createEntry({ sessionId: 'valid-session', inputTokens: 500 }),
    ];

    const result = aggregateSessionUsage(entries);

    expect(result.length).toBe(1);
    expect(result[0].sessionId).toBe('valid-session');
  });

  test('sorts by last activity descending', () => {
    const entries: RawUsageEntry[] = [
      createEntry({ sessionId: 'old-session', timestamp: '2025-12-01T10:00:00.000Z' }),
      createEntry({ sessionId: 'new-session', timestamp: '2025-12-09T10:00:00.000Z' }),
    ];

    const result = aggregateSessionUsage(entries);

    expect(result[0].sessionId).toBe('new-session');
    expect(result[1].sessionId).toBe('old-session');
  });

  test('propagates target metadata into session usage', () => {
    const entries: RawUsageEntry[] = [
      createEntry({ sessionId: 'session-A', target: 'droid' }),
      createEntry({ sessionId: 'session-B' }),
    ];

    const result = aggregateSessionUsage(entries);
    const sessionA = result.find((s) => s.sessionId === 'session-A');
    const sessionB = result.find((s) => s.sessionId === 'session-B');

    expect(sessionA?.target).toBe('droid');
    expect(sessionB?.target).toBeUndefined();
  });

  test('uses latest timestamp target when a session has mixed targets', () => {
    const entries: RawUsageEntry[] = [
      createEntry({
        sessionId: 'session-A',
        timestamp: '2025-12-09T10:00:00.000Z',
        target: 'claude',
      }),
      createEntry({
        sessionId: 'session-A',
        timestamp: '2025-12-09T12:00:00.000Z',
        target: 'droid',
      }),
      createEntry({
        sessionId: 'session-A',
        timestamp: '2025-12-09T11:00:00.000Z',
      }),
    ];

    const result = aggregateSessionUsage(entries);
    expect(result[0].target).toBe('droid');
  });

  test('returns empty array for no entries', () => {
    const result = aggregateSessionUsage([]);
    expect(result.length).toBe(0);
  });
});

// ============================================================================
// Integration: All token types
// ============================================================================

describe('token aggregation completeness', () => {
  test('aggregates all token types correctly', () => {
    const entries: RawUsageEntry[] = [
      createEntry({
        inputTokens: 100,
        outputTokens: 200,
        cacheCreationTokens: 50,
        cacheReadTokens: 25,
      }),
      createEntry({
        inputTokens: 150,
        outputTokens: 100,
        cacheCreationTokens: 30,
        cacheReadTokens: 10,
      }),
    ];

    const daily = aggregateDailyUsage(entries);

    expect(daily[0].inputTokens).toBe(250);
    expect(daily[0].outputTokens).toBe(300);
    expect(daily[0].cacheCreationTokens).toBe(80);
    expect(daily[0].cacheReadTokens).toBe(35);

    // Model breakdown should also have correct totals
    expect(daily[0].modelBreakdowns[0].inputTokens).toBe(250);
    expect(daily[0].modelBreakdowns[0].outputTokens).toBe(300);
    expect(daily[0].modelBreakdowns[0].cacheCreationTokens).toBe(80);
    expect(daily[0].modelBreakdowns[0].cacheReadTokens).toBe(35);
  });
});
