import { assert, createLogicSuite } from '@genesislcap/foundation-testing';
import type { CostSessionRecord } from './cost-session-history';
import {
  clearCostSessionHistory,
  costHistoryStorageKey,
  formatCostSessionDate,
  loadCostSessionHistory,
  resolveBankedUsage,
  saveCostSessionHistory,
  sortRecordsByRecency,
  upsertRecord,
} from './cost-session-history';
import { emptyUsage } from './sum-usage';

const scope = `test-${Date.now()}`;

const usage = (costUsd: number): CostSessionRecord['usage'] => ({
  costUsd,
  uncachedInputTokens: 100_000,
  cacheReadTokens: 1_200_000,
  cacheWriteTokens: 60_000,
  outputTokens: 40_000,
});

const sampleRecord = (overrides: Partial<CostSessionRecord> = {}): CostSessionRecord => ({
  id: 'rec-1',
  projectKey: 'proj-1',
  title: 'Equity Options Pricer',
  updatedAt: '2026-06-14T12:00:00.000Z',
  usage: usage(1.86),
  // Present by default so `delete record.banked` in the legacy-row cases actually removes
  // something, and so the storage round-trip covers it.
  banked: usage(0.5),
  models: [{ model: 'claude-sonnet-4-6', provider: 'anthropic' }],
  ...overrides,
});

const suite = createLogicSuite('costSessionHistory');

suite('uses a scoped storage key', () => {
  assert.is(costHistoryStorageKey('my-app'), 'genesis-ai-assistant:cost-history:my-app');
});

suite('upsertRecord prepends a project it has not seen', () => {
  const first = upsertRecord([], sampleRecord({ id: 'a', projectKey: 'a', title: 'First' }));
  const both = upsertRecord(first, sampleRecord({ id: 'b', projectKey: 'b', title: 'Second' }));
  assert.is(both.length, 2);
  // Insertion order, not recency — display ordering is sortRecordsByRecency's job.
  assert.is(both[0]?.id, 'b', 'most recently added is prepended');
  assert.is(both[1]?.id, 'a');
});

suite('upsertRecord replaces a project in place rather than stacking a second row', () => {
  const seeded = [
    sampleRecord({ id: 'b', projectKey: 'b', usage: usage(2) }),
    sampleRecord({ id: 'a', projectKey: 'a', usage: usage(1) }),
  ];
  // Re-reporting a project must REPLACE its row. Appending here is what made a single
  // project's spend count once per refresh in any total summed over the list.
  const next = upsertRecord(seeded, sampleRecord({ id: 'a', projectKey: 'a', usage: usage(9.5) }));
  assert.is(next.length, 2);
  // Replaced in place rather than moved — the stored array is not a recency ordering.
  assert.is(next[1]?.projectKey, 'a');
  assert.is(next[1]?.usage.costUsd, 9.5);
  assert.is(
    next.reduce((sum, r) => sum + r.usage.costUsd, 0),
    11.5,
  );
});

suite('round-trips records through storage', () => {
  clearCostSessionHistory(scope);
  const records = upsertRecord([], sampleRecord({ id: 'a', projectKey: 'a', usage: usage(3.5) }));
  saveCostSessionHistory(scope, records);
  const loaded = loadCostSessionHistory(scope);
  assert.is(loaded.length, 1);
  assert.is(loaded[0]?.usage.costUsd, 3.5);
  assert.is(loaded[0]?.usage.cacheReadTokens, 1_200_000, 'buckets survive serialization');
  assert.is(loaded[0]?.banked?.costUsd, 0.5, 'banked survives serialization');
  clearCostSessionHistory(scope);
});

suite('discards legacy rows that predate the per-project key', () => {
  clearCostSessionHistory(scope);
  const legacy = sampleRecord({ id: 'old' }) as Partial<CostSessionRecord>;
  delete legacy.projectKey;
  saveCostSessionHistory(scope, [legacy as CostSessionRecord, sampleRecord({ id: 'new' })]);
  const loaded = loadCostSessionHistory(scope);
  // A keyless row can never be matched to a project, so pooling it with the new rows
  // would permanently double-count whichever project it came from.
  assert.is(loaded.length, 1);
  assert.is(loaded[0]?.id, 'new');
  clearCostSessionHistory(scope);
});

suite('discards rows whose usage is not fully numeric', () => {
  clearCostSessionHistory(scope);
  const broken = sampleRecord({ id: 'broken' });
  // A half-shaped `usage` is worse than a missing row: it sums as NaN and poisons every
  // total on the Usage tab, with no clue where the NaN came from.
  delete (broken.usage as Partial<CostSessionRecord['usage']>).outputTokens;
  saveCostSessionHistory(scope, [broken, sampleRecord({ id: 'ok', projectKey: 'ok' })]);
  const loaded = loadCostSessionHistory(scope);
  assert.is(loaded.length, 1);
  assert.is(loaded[0]?.id, 'ok');
  clearCostSessionHistory(scope);
});

suite('discards rows whose banked figure is half-shaped', () => {
  clearCostSessionHistory(scope);
  const broken = sampleRecord({ id: 'broken', banked: { ...usage(2) } });
  // A malformed `banked` poisons the lifetime total exactly as a malformed `usage` does,
  // since the two are added together to produce it.
  delete (broken.banked as Partial<CostSessionRecord['usage']>).cacheWriteTokens;
  saveCostSessionHistory(scope, [broken, sampleRecord({ id: 'ok', projectKey: 'ok' })]);
  const loaded = loadCostSessionHistory(scope);
  assert.is(loaded.length, 1);
  assert.is(loaded[0]?.id, 'ok');
  clearCostSessionHistory(scope);
});

suite('keeps a row whose banked figure is absent', () => {
  clearCostSessionHistory(scope);
  const legacy = sampleRecord({ id: 'legacy' });
  delete legacy.banked;
  saveCostSessionHistory(scope, [legacy]);
  assert.is(loadCostSessionHistory(scope).length, 1, 'absent is valid, unlike half-shaped');
  clearCostSessionHistory(scope);
});

suite('formats session dates for display', () => {
  assert.is(formatCostSessionDate('2026-06-14T12:00:00.000Z'), 'Jun 14, 2026');
});

suite('sortRecordsByRecency puts the most recently worked-on project first', () => {
  const records = [
    sampleRecord({ id: 'old', projectKey: 'old', updatedAt: '2026-01-01T00:00:00.000Z' }),
    sampleRecord({ id: 'newest', projectKey: 'newest', updatedAt: '2026-08-04T09:00:00.000Z' }),
    sampleRecord({ id: 'mid', projectKey: 'mid', updatedAt: '2026-05-01T00:00:00.000Z' }),
  ];
  const sorted = sortRecordsByRecency(records);
  assert.equal(
    sorted.map((r) => r.id),
    ['newest', 'mid', 'old'],
  );
  // Read-time ordering, so the caller's array must be left alone.
  assert.equal(
    records.map((r) => r.id),
    ['old', 'newest', 'mid'],
    'input not mutated',
  );
});

suite('sortRecordsByRecency overrides insertion order, which upsert leaves alone', () => {
  // The behaviour this fixes: revisiting a project replaces its row IN PLACE, so insertion order
  // strands it wherever it was first seen. Display order must come from updatedAt instead.
  let records = upsertRecord(
    [],
    sampleRecord({ id: 'a', projectKey: 'a', updatedAt: '2026-01-01T00:00:00.000Z' }),
  );
  records = upsertRecord(
    records,
    sampleRecord({ id: 'b', projectKey: 'b', updatedAt: '2026-02-01T00:00:00.000Z' }),
  );
  // Work on 'a' again — newer than 'b', but upsert keeps it in slot 1.
  records = upsertRecord(
    records,
    sampleRecord({ id: 'a', projectKey: 'a', updatedAt: '2026-03-01T00:00:00.000Z' }),
  );
  assert.is(records[1]?.id, 'a', 'stored order is unchanged by the revisit');
  assert.is(sortRecordsByRecency(records)[0]?.id, 'a', 'display order tracks the revisit');
});

suite('banks nothing for a project with no prior row', () => {
  assert.equal(resolveBankedUsage(undefined, true), emptyUsage());
  assert.equal(resolveBankedUsage(undefined, false), emptyUsage());
});

suite('carries the prior banked figure forward when the transcript is authoritative', () => {
  // Restored transcript: it already re-proves everything after the row's own banked figure,
  // so only that figure carries over. Taking `usage` here is the compounding double-count.
  const prior = sampleRecord({ usage: usage(10), banked: { ...usage(4) } });
  assert.is(resolveBankedUsage(prior, true).costUsd, 4);
});

suite('banks the whole prior total when the transcript starts empty', () => {
  // Nothing on screen accounts for the previous total, so all of it must carry over or the
  // next turn rewrites the row down to just that turn.
  const prior = sampleRecord({ usage: usage(10), banked: { ...usage(4) } });
  assert.is(resolveBankedUsage(prior, false).costUsd, 10);
});

suite('treats a row predating the banked field as nothing banked', () => {
  const legacy = sampleRecord({ usage: usage(7) });
  delete legacy.banked;
  assert.equal(resolveBankedUsage(legacy, true), emptyUsage());
  // Without persistence the legacy total still carries, so upgrading loses no spend.
  assert.is(resolveBankedUsage(legacy, false).costUsd, 7);
});

suite('is stable across repeated reloads — the regression this guards', () => {
  // Simulate the reload cycle: usage = banked + whatever the transcript proves. With a
  // restored transcript (worth 6 on top of 4 banked) the row must land on 10 every time,
  // not 10 → 14 → 18 as it did when a reload re-banked the restored history.
  const transcript = 6;
  let row = sampleRecord({ usage: usage(10), banked: { ...usage(4) } });
  for (let reload = 0; reload < 3; reload += 1) {
    const banked = resolveBankedUsage(row, true);
    row = { ...row, banked, usage: { ...usage(banked.costUsd + transcript) } };
    assert.is(row.usage.costUsd, 10, `reload ${reload}`);
  }
});

suite('accumulates across reloads when the transcript does not persist', () => {
  // The mirror case: each page-load starts from an empty transcript, so the row must grow by
  // whatever that load spends rather than being overwritten by it.
  let row = sampleRecord({ usage: usage(10), banked: { ...usage(4) } });
  for (const spend of [2, 3]) {
    const banked = resolveBankedUsage(row, false);
    row = { ...row, banked, usage: { ...usage(banked.costUsd + spend) } };
  }
  // 10 banked, +2 → 12; then 12 banked, +3 → 15. Never decreases.
  assert.is(row.usage.costUsd, 15);
});

suite.run();
