/**
 * Per-user daily LLM usage ledger (llm_usage_daily) repository tests.
 *
 * Coverage:
 *  - incrementLlmUsage UPSERTs on first call, accumulates on second
 *  - requests defaults to +1; a usage-less call still bumps requests
 *  - negative deltas clamp to zero
 *  - distinct (model) and (route) produce distinct rows on the same day
 *  - 'system' / 'local' sentinels aggregate as single rows (NULL trap avoided)
 *  - queryLlmUsageDaily collapses model/route, groups by (day, user, source)
 *  - day filter is an inclusive range; userId filter scopes a single user
 *  - UTC day boundary splits into separate buckets
 *
 * Spec: docs/superpowers/specs/2026-06-11-llm-usage-aggregation-design.md
 */
import { describe, expect, it, beforeEach } from 'vitest';
import { Repository } from './repository.js';

function makeRepo(): Repository {
  return new Repository(':memory:');
}

describe('llm_usage_daily repository', () => {
  let repo: Repository;
  beforeEach(() => {
    repo = makeRepo();
  });

  it('UPSERTs first call and accumulates on the same grain', () => {
    const grain = { day: '2026-06-11', userId: 'u1', source: 'direct' as const, model: 'm', route: 'r' };
    repo.incrementLlmUsage({ ...grain, tokensIn: 100, tokensOut: 40 });
    repo.incrementLlmUsage({ ...grain, tokensIn: 10, tokensOut: 5 });
    const rows = repo.queryLlmUsageDaily({ from: '2026-06-11', to: '2026-06-11' });
    expect(rows).toHaveLength(1);
    expect(rows[0]).toMatchObject({
      day: '2026-06-11', userId: 'u1', source: 'direct',
      tokensIn: 110, tokensOut: 45, requests: 2,
    });
  });

  it('a usage-less call still bumps requests (0-token request != failure)', () => {
    repo.incrementLlmUsage({ day: '2026-06-11', userId: 'u1', source: 'gateway', model: 'm', route: 'r' });
    const rows = repo.queryLlmUsageDaily({ from: '2026-06-11', to: '2026-06-11' });
    expect(rows[0]).toMatchObject({ tokensIn: 0, tokensOut: 0, requests: 1 });
  });

  it('clamps negative deltas to zero', () => {
    repo.incrementLlmUsage({ day: '2026-06-11', userId: 'u1', source: 'direct', model: 'm', route: 'r', tokensIn: -5, tokensOut: -9, requests: -3 });
    const rows = repo.queryLlmUsageDaily({ from: '2026-06-11', to: '2026-06-11' });
    expect(rows[0]).toMatchObject({ tokensIn: 0, tokensOut: 0, requests: 0 });
  });

  it('distinct model and route are separate rows but collapse in the query', () => {
    const base = { day: '2026-06-11', userId: 'u1', source: 'direct' as const, tokensIn: 10, tokensOut: 5 };
    repo.incrementLlmUsage({ ...base, model: 'big', route: 'host-a' });
    repo.incrementLlmUsage({ ...base, model: 'small', route: 'host-a' });
    repo.incrementLlmUsage({ ...base, model: 'big', route: 'host-b' });
    // 3 distinct (model,route) rows underneath, collapsed to one (day,user,source).
    const rows = repo.queryLlmUsageDaily({ from: '2026-06-11', to: '2026-06-11' });
    expect(rows).toHaveLength(1);
    expect(rows[0]).toMatchObject({ tokensIn: 30, tokensOut: 15, requests: 3 });
  });

  it("'system' and 'local' sentinels each aggregate as a single row", () => {
    const g = { day: '2026-06-11', source: 'direct' as const, model: 'm', route: 'r', tokensIn: 1, tokensOut: 1 };
    repo.incrementLlmUsage({ ...g, userId: 'system' });
    repo.incrementLlmUsage({ ...g, userId: 'system' });
    repo.incrementLlmUsage({ ...g, userId: 'local' });
    const rows = repo.queryLlmUsageDaily({ from: '2026-06-11', to: '2026-06-11' });
    const byUser = Object.fromEntries(rows.map((r) => [r.userId, r.requests]));
    expect(byUser).toEqual({ system: 2, local: 1 });
  });

  it('gateway and direct are distinct rows for the same user/day', () => {
    repo.incrementLlmUsage({ day: '2026-06-11', userId: 'u1', source: 'gateway', model: 'm', route: 'r', tokensIn: 7, tokensOut: 3 });
    repo.incrementLlmUsage({ day: '2026-06-11', userId: 'u1', source: 'direct', model: 'm', route: 'r', tokensIn: 2, tokensOut: 1 });
    const rows = repo.queryLlmUsageDaily({ from: '2026-06-11', to: '2026-06-11' });
    expect(rows).toHaveLength(2);
    expect(rows.map((r) => r.source).sort()).toEqual(['direct', 'gateway']);
  });

  it('queryLlmUsageDaily honours an inclusive day range', () => {
    for (const day of ['2026-06-09', '2026-06-10', '2026-06-11', '2026-06-12']) {
      repo.incrementLlmUsage({ day, userId: 'u1', source: 'direct', model: 'm', route: 'r', tokensIn: 1, tokensOut: 0 });
    }
    const rows = repo.queryLlmUsageDaily({ from: '2026-06-10', to: '2026-06-11' });
    expect(rows.map((r) => r.day)).toEqual(['2026-06-10', '2026-06-11']);
  });

  it('userId filter scopes the query to one user', () => {
    repo.incrementLlmUsage({ day: '2026-06-11', userId: 'u1', source: 'direct', model: 'm', route: 'r', tokensIn: 1, tokensOut: 0 });
    repo.incrementLlmUsage({ day: '2026-06-11', userId: 'u2', source: 'direct', model: 'm', route: 'r', tokensIn: 9, tokensOut: 0 });
    const mine = repo.queryLlmUsageDaily({ from: '2026-06-11', to: '2026-06-11', userId: 'u1' });
    expect(mine).toHaveLength(1);
    expect(mine[0]).toMatchObject({ userId: 'u1', tokensIn: 1 });
  });

  it('derives the UTC day from `at` when day is omitted; boundary splits buckets', () => {
    const grain = { userId: 'u1', source: 'direct' as const, model: 'm', route: 'r', tokensIn: 1, tokensOut: 0 };
    repo.incrementLlmUsage({ ...grain, at: '2026-06-11T23:59:59.000Z' });
    repo.incrementLlmUsage({ ...grain, at: '2026-06-12T00:00:01.000Z' });
    const rows = repo.queryLlmUsageDaily({ from: '2026-06-01', to: '2026-06-30' });
    expect(rows.map((r) => r.day)).toEqual(['2026-06-11', '2026-06-12']);
  });
});
