import type { AggregateUsage, ChatConfig, ChatMessage } from '@genesislcap/foundation-ai';
import { assert, createLogicSuite } from '@genesislcap/foundation-testing';
import {
  getBankedBaseline,
  resetBankedBaselines,
  setBankedBaseline,
} from '../utils/banked-usage-baselines';
import {
  clearCostSessionHistory,
  loadCostSessionHistory,
  type CostSessionRecord,
} from '../utils/cost-session-history';
import { emptyUsage } from '../utils/sum-usage';
import { FoundationAiAssistant } from './main';

// Hold a reference so the custom-element registration isn't tree-shaken.
FoundationAiAssistant;

// GENC-1475. Pins the banking behaviour behind the Usage tab's lifetime figures: a row must
// report a project's LIFETIME usage whether or not the transcript is restored on load, and it
// must never grow by re-counting spend it already holds.
//
// Two earlier attempts at this got it wrong in opposite directions, which is what these tests
// exist to catch:
//   - projecting the transcript straight onto the row → the row shrank on every refresh in
//     hosts where the transcript does not persist;
//   - re-deriving the banked figure whenever the rows reloaded → `chatConfig` rebinding
//     mid-session (hosts build it in a template expression) folded the live session into its
//     own baseline, compounding on every rebind.
//
// Like `popout-interaction-gate.test.ts` this deliberately does NOT mount the assistant:
// `document.createElement` upgrades the element (constructor only) without connecting, so
// `connectedCallback` never runs, nothing subscribes to `agenticActivityBus`, and its
// cross-tab `BroadcastChannel` — opened lazily on first use — is never created. No teardown
// needed, and the runner's event loop stays closed.

const Suite = createLogicSuite('FoundationAiAssistant cost session banking');

const PROJECT = 'proj-1';

const usage = (costUsd: number, outputTokens = 100): AggregateUsage => ({
  ...emptyUsage(),
  costUsd,
  outputTokens,
});

type Internals = {
  _sessionRef: unknown;
  _activeStateKey?: string;
  _costHistoryLoaded: boolean;
};

/**
 * An unconnected element with just enough session state wired for `finalizeCostSession`:
 * the live usage it projects, the transcript it titles from, and the key it upserts against.
 *
 * Banked baselines are module state keyed by session (so remounts share one), which means they
 * outlive an element. Cleared by default, because every suite but the remount ones is modelling
 * a *fresh page load* — and because leaving them would leak one suite's baseline into the next
 * under the shared `PROJECT` key. Pass `remount: true` to model a new element joining a session
 * that has already banked, which is the case this file exists to pin.
 */
function element(options: {
  scope: string;
  live: AggregateUsage;
  rows?: CostSessionRecord[];
  persistenceActive?: boolean;
  loaded?: boolean;
  providerStatuses?: unknown[];
  remount?: boolean;
}): FoundationAiAssistant {
  if (!options.remount) resetBankedBaselines();
  const el = document.createElement('foundation-ai-assistant') as FoundationAiAssistant;
  const messages: ChatMessage[] = [{ role: 'user', content: 'build me a blotter' } as ChatMessage];
  const internals = el as unknown as Internals;
  internals._sessionRef = {
    store: {
      aiAssistant: {
        state: 'idle',
        messages,
        sessionUsage: options.live,
        // Redux-backed like `messages`, so it goes in the store rather than through the
        // element setter (which would dispatch through `actions` we don't fake). Present at
        // all to keep `collectSessionModels` from warning about an empty registry per write.
        providerStatuses: options.providerStatuses ?? [
          { name: 'anthropic', status: { provider: 'anthropic', model: 'sonnet' } },
        ],
      },
    },
  };
  internals._activeStateKey = PROJECT;
  el.chatConfig = {
    costHistory: { enabled: true, scope: options.scope, title: 'Trade Blotter' },
  } as ChatConfig;
  // `persistenceActive` is `enabled && provider`; a bare object is provider enough here since
  // no cost-history methods are on it, so the localStorage path is used for writes.
  el.persistence = options.persistenceActive
    ? { enabled: true, provider: {} as never }
    : { enabled: false };
  el.costSessionHistory = options.rows ?? [];
  // LAST, deliberately: assigning `chatConfig` above fires `chatConfigChanged` →
  // `reloadCostSessionHistory`, which sets this flag itself. Setting it any earlier would be
  // silently overwritten — which is exactly how this helper was wrong first time round.
  internals._costHistoryLoaded = options.loaded ?? true;
  return el;
}

const row = (over: Partial<CostSessionRecord> = {}): CostSessionRecord => ({
  id: PROJECT,
  projectKey: PROJECT,
  title: 'Trade Blotter',
  updatedAt: '2026-08-01T00:00:00.000Z',
  usage: usage(10),
  banked: usage(4),
  models: [],
  ...over,
});

Suite('writes the live session as lifetime for a project with no prior row', () => {
  const scope = 'bank-fresh';
  clearCostSessionHistory(scope);
  const el = element({ scope, live: usage(3) });
  el.finalizeCostSession();

  assert.is(el.costSessionHistory.length, 1);
  assert.is(el.costSessionHistory[0].usage.costUsd, 3);
  assert.equal(el.costSessionHistory[0].banked, emptyUsage(), 'nothing came before');
  clearCostSessionHistory(scope);
});

Suite('restored transcript: repeat writes are stable, not compounding', () => {
  // The transcript is authoritative (persistence on), so it re-proves the 6 sitting on top
  // of the 4 already banked. The row must land on 10 no matter how many times we write —
  // this is the `chatConfig`-rebind double-count.
  const scope = 'bank-stable';
  clearCostSessionHistory(scope);
  const el = element({
    scope,
    live: usage(6),
    rows: [row({ usage: usage(10), banked: usage(4) })],
    persistenceActive: true,
  });

  for (let write = 0; write < 3; write += 1) {
    el.finalizeCostSession();
    assert.is(el.costSessionHistory.length, 1, `write ${write}: still one row`);
    assert.is(el.costSessionHistory[0].usage.costUsd, 10, `write ${write}: total held`);
  }
  clearCostSessionHistory(scope);
});

Suite('a reload mid-session does not re-bank what the row already holds', () => {
  // The regression: `chatConfig` rebinds (a new object from a host template expression),
  // reloadCostSessionHistory runs, and the rows it re-reads now contain our own upsert. The
  // banked memo must survive that, or the live session is added to its own baseline.
  const scope = 'bank-rebind';
  clearCostSessionHistory(scope);
  const el = element({
    scope,
    live: usage(6),
    rows: [row({ usage: usage(10), banked: usage(4) })],
    persistenceActive: true,
  });

  el.finalizeCostSession();
  assert.is(el.costSessionHistory[0].usage.costUsd, 10);

  // Rebind chatConfig exactly as a host template would — a fresh object, same values.
  el.chatConfig = {
    costHistory: { enabled: true, scope, title: 'Trade Blotter' },
  } as ChatConfig;
  el.chatConfigChanged();
  el.finalizeCostSession();

  assert.is(el.costSessionHistory[0].usage.costUsd, 10, 'rebind must not inflate the row');
  clearCostSessionHistory(scope);
});

Suite('no restore: the prior total is absorbed so the row never shrinks', () => {
  // The mirror failure. The transcript starts empty every refresh, so this page-load's 2 must
  // be ADDED to the 10 already recorded rather than replacing it.
  const scope = 'bank-absorb';
  clearCostSessionHistory(scope);
  const el = element({
    scope,
    live: usage(2),
    rows: [row({ usage: usage(10), banked: usage(4) })],
    persistenceActive: false,
  });
  el.finalizeCostSession();

  assert.is(el.costSessionHistory[0].usage.costUsd, 12);
  assert.is(el.costSessionHistory[0].banked?.costUsd, 10, 'whole prior total banked');
  clearCostSessionHistory(scope);
});

Suite('skips the write until the ledger has loaded', () => {
  // A provider read is async. Banking from a ledger that simply has not arrived would read as
  // "nothing came before" and rewrite the row down to this turn.
  const scope = 'bank-unloaded';
  clearCostSessionHistory(scope);
  const el = element({ scope, live: usage(3), loaded: false });
  el.finalizeCostSession();
  assert.is(el.costSessionHistory.length, 0, 'no row written while unloaded');

  (el as unknown as Internals)._costHistoryLoaded = true;
  el.finalizeCostSession();
  assert.is(el.costSessionHistory.length, 1, 'writes once loaded');
  clearCostSessionHistory(scope);
});

Suite('clearing history drops the banked figure so it cannot be resurrected', () => {
  const scope = 'bank-cleared';
  clearCostSessionHistory(scope);
  const el = element({
    scope,
    live: usage(2),
    rows: [row({ usage: usage(10), banked: usage(4) })],
    persistenceActive: false,
  });

  el.clearCostSessionHistory();
  assert.is(el.costSessionHistory.length, 0);

  // The next turn must start from zero, not resurrect the 10 the user just discarded.
  el.finalizeCostSession();
  assert.is(el.costSessionHistory[0].usage.costUsd, 2);
  clearCostSessionHistory(scope);
});

Suite('deleting this project row drops its banked figure too', () => {
  const scope = 'bank-removed';
  clearCostSessionHistory(scope);
  const el = element({
    scope,
    live: usage(2),
    rows: [row({ usage: usage(10), banked: usage(4) })],
    persistenceActive: false,
  });

  // Bank first, so the memo is populated before the removal — the ordering that matters.
  el.finalizeCostSession();
  assert.is(el.costSessionHistory[0].usage.costUsd, 12);

  el.removeCostSessionFromHistory(PROJECT);
  assert.is(el.costSessionHistory.length, 0);
  el.finalizeCostSession();
  assert.is(el.costSessionHistory[0].usage.costUsd, 2, 'removed spend must not return');
  clearCostSessionHistory(scope);
});

Suite('lifetime usage sums every row, per bucket', () => {
  const scope = 'bank-lifetime';
  clearCostSessionHistory(scope);
  const el = element({
    scope,
    live: usage(1),
    rows: [
      row({ id: 'a', projectKey: 'a', usage: usage(2, 30) }),
      row({ id: 'b', projectKey: 'b', usage: usage(3, 40) }),
    ],
  });

  assert.is(el.cumulativeUsage.costUsd, 5);
  assert.is(el.cumulativeUsage.outputTokens, 70);
  assert.is(el.cumulativeCostUsd, 5);
  // Rows only — adding the live session would count the open project twice, since its own
  // row is kept current on every transcript change.
  assert.is(el.cumulativeCostUsd, 5, 'live session is not added on top');
  clearCostSessionHistory(scope);
});

Suite('exposes build history most-recently-worked-on first', () => {
  const el = element({
    scope: 'bank-order',
    live: usage(1),
    rows: [
      row({ id: 'stale', projectKey: 'stale', updatedAt: '2026-01-01T00:00:00.000Z' }),
      row({ id: 'fresh', projectKey: 'fresh', updatedAt: '2026-08-04T09:00:00.000Z' }),
    ],
  });

  // The raw list keeps insertion order; only the display getter reorders.
  assert.is(el.costSessionHistory[0].id, 'stale');
  assert.equal(
    el.sortedCostSessionHistory.map((r) => r.id),
    ['fresh', 'stale'],
  );
});

Suite('lifetime usage falls back to the live session when history is off', () => {
  const el = element({ scope: 'bank-off', live: usage(4) });
  el.chatConfig = { costHistory: { enabled: false } } as ChatConfig;
  assert.is(el.cumulativeUsage.costUsd, 4, 'no rows exist to sum');
  assert.is(el.settingsCostLifetimeVisible, false, 'and the section is hidden');
});

Suite('lists each model once even when several registry tiers resolve to it', () => {
  // providerStatuses has one entry per registry SLOT. A vendor configured across tiers that
  // resolve to the same model reported it once per tier, so the chip rendered twice.
  const el = element({
    scope: 'models-dedupe',
    live: usage(1),
    providerStatuses: [
      { name: 'low', status: { provider: 'gemini', model: 'gemini-3.5-flash' } },
      { name: 'high', status: { provider: 'gemini', model: 'gemini-3.5-flash' } },
      { name: 'lite', status: { provider: 'gemini', model: 'gemini-3.1-flash-lite' } },
    ],
  });

  assert.equal(
    el.uniqueProviderModels.map((m) => m.model),
    ['gemini-3.5-flash', 'gemini-3.1-flash-lite'],
  );
  assert.is(el.uniqueProviderModels[0].provider, 'gemini', 'vendor retained for the colour');
});

Suite('keeps slots that report no model distinct, and recovers a missing vendor', () => {
  const el = element({
    scope: 'models-fallback',
    live: usage(1),
    providerStatuses: [
      // No model reported: the chip falls back to the slot name, and two such slots are
      // genuinely different things — deduping on model alone would have merged them.
      { name: 'custom-a', status: undefined },
      { name: 'custom-b', status: undefined },
      // Same model twice, vendor known on only one of them.
      { name: 'tier-1', status: { model: 'gemini-3.5-flash' } },
      { name: 'tier-2', status: { provider: 'gemini', model: 'gemini-3.5-flash' } },
    ],
  });

  assert.equal(
    el.uniqueProviderModels.map((m) => m.model),
    ['custom-a', 'custom-b', 'gemini-3.5-flash'],
  );
  assert.is(el.uniqueProviderModels[2].provider, 'gemini', 'vendor picked up from the other tier');
});

Suite(
  'an unreadable ledger blocks writes instead of replacing it with what we could not read',
  () => {
    // The review case. `saveCostHistory` REPLACES the stored list, so a failed read followed by a
    // write would delete every other project's row and reset this project's banked total.
    const scope = 'ledger-unreadable';
    clearCostSessionHistory(scope);
    const el = element({ scope, live: usage(4) });
    const internals = el as unknown as Internals & {
      _ledgerUnreadable: boolean;
      flushCostHistory(): void;
    };
    internals._ledgerUnreadable = true;

    el.finalizeCostSession();
    // Writes are debounced, so drive the flush directly rather than waiting on a timer.
    internals.flushCostHistory();
    assert.is(
      loadCostSessionHistory(scope).length,
      0,
      'nothing written back over a ledger we failed to read',
    );
    clearCostSessionHistory(scope);
  },
);

Suite('deleting a provider row whose id differs from its projectKey still drops the memo', () => {
  // Baselines are keyed by projectKey; the delete action passes a row id. A host provider can
  // assign any id, so deleting by the wrong namespace left the baseline in place and the next
  // turn resurrected the deleted total. Reachable when a provider load lands AFTER a finalize has
  // banked: our own upsert would otherwise have rewritten the row's id to the projectKey.
  const scope = 'ledger-remove-id';
  clearCostSessionHistory(scope);
  const el = element({
    scope,
    live: usage(2),
    rows: [row({ id: 'host-assigned-uuid', projectKey: PROJECT, usage: usage(10) })],
    persistenceActive: false,
  });
  // Baseline already resolved for this project, as it would be after a turn — set directly
  // rather than by finalizing, because our own upsert would rewrite the row's id to the
  // projectKey and the id mismatch under test would vanish with it.
  setBankedBaseline(PROJECT, usage(10));

  el.removeCostSessionFromHistory('host-assigned-uuid');
  assert.is(el.costSessionHistory.length, 0);

  el.finalizeCostSession();
  assert.is(el.costSessionHistory[0].usage.costUsd, 2, 'deleted total must not come back');
  clearCostSessionHistory(scope);
});

Suite('a remount mid-session does not re-bank what the row already holds', () => {
  // The reported regression (GENC-1475). Reproduces the field report exactly: with persistence
  // off, the host remounted the assistant part-way through a session (a layout re-render — the
  // debug log showed seven connects in one session) and the LIFETIME figure jumped by the whole
  // session-to-date while the live figure kept climbing normally.
  //
  // The new element joins a live session: same key, same transcript, and rows that already
  // contain the previous instance's upsert. An element-scoped baseline re-derived here and, with
  // no restore to defer to, banked `row.usage` — which already included this very transcript.
  const scope = 'bank-remount';
  clearCostSessionHistory(scope);
  const first = element({
    scope,
    live: usage(6),
    rows: [row({ usage: usage(1), banked: usage(1) })],
    persistenceActive: false,
  });
  first.finalizeCostSession();
  assert.is(first.costSessionHistory[0].usage.costUsd, 7, '1 banked + 6 live');

  // Remount: a brand new element wired to the same session, handed the rows as they now stand.
  const second = element({
    scope,
    live: usage(6),
    rows: first.costSessionHistory,
    persistenceActive: false,
    remount: true,
  });
  second.finalizeCostSession();

  assert.is(second.costSessionHistory[0].usage.costUsd, 7, 'remount must not inflate the row');
  assert.is(second.costSessionHistory[0].banked?.costUsd, 1, 'baseline unchanged by the remount');

  // And the session keeps accruing normally afterwards, on top of the SAME baseline.
  const third = element({
    scope,
    live: usage(9),
    rows: second.costSessionHistory,
    persistenceActive: false,
    remount: true,
  });
  third.finalizeCostSession();
  assert.is(third.costSessionHistory[0].usage.costUsd, 10, '1 banked + 9 live');
  clearCostSessionHistory(scope);
});

Suite('a page reload still absorbs the prior total after a remount banked nothing new', () => {
  // The other half of the pairing: dropping the baseline on remount is wrong, but keeping it
  // across a *page load* would be too — the transcript starts empty there (persistence off), so
  // the row's whole total must be banked. `resetBankedBaselines` stands in for the realm dying.
  const scope = 'bank-reload';
  clearCostSessionHistory(scope);
  const before = element({
    scope,
    live: usage(6),
    rows: [row({ usage: usage(1), banked: usage(1) })],
    persistenceActive: false,
  });
  before.finalizeCostSession();
  assert.is(before.costSessionHistory[0].usage.costUsd, 7);

  const afterReload = element({
    scope,
    live: usage(2),
    rows: before.costSessionHistory,
    persistenceActive: false,
  });
  afterReload.finalizeCostSession();
  assert.is(afterReload.costSessionHistory[0].usage.costUsd, 9, 'prior 7 banked, this load adds 2');
  clearCostSessionHistory(scope);
});

Suite('clearing the transcript re-baselines instead of discarding spend', () => {
  // Clear empties the transcript, so the row's total now predates what is on screen and has to be
  // banked wholesale. Merely dropping the baseline would re-derive through `persistenceActive`,
  // which with persistence ON carries the row's OLD banked figure forward and silently loses
  // every turn between that baseline and the clear.
  const scope = 'bank-clear-session';
  clearCostSessionHistory(scope);
  const el = element({
    scope,
    live: usage(6),
    rows: [row({ usage: usage(10), banked: usage(4) })],
    persistenceActive: true,
  });
  el.finalizeCostSession();
  assert.is(el.costSessionHistory[0].usage.costUsd, 10);

  // `clearSession` itself tears down and rebuilds the driver, which an unconnected element has
  // none of — so drive the one step under test. `clearSession` reads the live figure before
  // `resetSession` zeroes it and passes it in; the 6 here is that capture.
  const priv = el as unknown as {
    rebaselineOnClearedTranscript(key: string, discarded: AggregateUsage): void;
    _sessionRef: { store: { aiAssistant: { sessionUsage: AggregateUsage } } };
    _costHistoryLoaded: boolean;
  };
  priv.rebaselineOnClearedTranscript(PROJECT, usage(6));

  // A fresh transcript worth 3, on top of the whole 10 the project had accrued.
  priv._sessionRef.store.aiAssistant.sessionUsage = usage(3);
  el.finalizeCostSession();
  assert.is(el.costSessionHistory[0].usage.costUsd, 13, 'pre-clear spend kept, new turn added');
  assert.is(el.costSessionHistory[0].banked?.costUsd, 10, 'whole pre-clear total banked');
  clearCostSessionHistory(scope);
});

Suite('clearing while the ledger is still loading must not pin a baseline', () => {
  // Review case. A stamped baseline is sticky — `bankedUsageFor` short-circuits on any stored
  // entry and a zero usage is a *resolved* value there. On the provider path the row list is empty
  // and `_costHistoryLoaded` false while the read is in flight, so a clear landing in that window
  // used to pin a baseline derived from no rows at all. The load arriving afterwards with the
  // project's real total was then never consulted, and the next write replaced its row with
  // `banked: 0` — the project's whole history gone.
  const scope = 'bank-clear-unloaded';
  clearCostSessionHistory(scope);
  const el = element({ scope, live: usage(5), loaded: false });
  const priv = el as unknown as {
    rebaselineOnClearedTranscript(key: string, discarded: AggregateUsage): void;
    _sessionRef: { store: { aiAssistant: { sessionUsage: AggregateUsage } } };
    _costHistoryLoaded: boolean;
  };

  // Clear arrives before the provider read lands: rows are empty and nothing is known yet.
  priv.rebaselineOnClearedTranscript(PROJECT, usage(5));
  assert.is(getBankedBaseline(PROJECT), undefined, 'nothing pinned from a ledger we cannot see');

  // The read lands with this project's real history, and a turn follows.
  el.costSessionHistory = [row({ usage: usage(42), banked: usage(40) })];
  priv._costHistoryLoaded = true;
  priv._sessionRef.store.aiAssistant.sessionUsage = usage(1);
  el.finalizeCostSession();

  assert.is(el.costSessionHistory[0].usage.costUsd, 43, "the project's 42 survives the race");
  assert.is(el.costSessionHistory[0].banked?.costUsd, 42, 'baseline re-derived from real rows');
  clearCostSessionHistory(scope);
});

Suite('clearing banks a transcript the row has not caught up with', () => {
  // Review case: without this, the suite passed identically against the OLD derivation
  // (`existing?.usage`), because everywhere else the row already agreed with baseline + live.
  //
  // The formulas only diverge when the row is STALE relative to the transcript — which is exactly
  // what the new one exists for, since `finalizeCostSession` skips its write while the ledger is
  // unloaded and while the session has no usage. No finalize here, so the row lags by 3.
  const scope = 'bank-clear-stale-row';
  clearCostSessionHistory(scope);
  const el = element({
    scope,
    live: usage(3),
    rows: [row({ usage: usage(42), banked: usage(40) })],
    persistenceActive: false,
  });
  const priv = el as unknown as {
    rebaselineOnClearedTranscript(key: string, discarded: AggregateUsage): void;
    _sessionRef: { store: { aiAssistant: { sessionUsage: AggregateUsage } } };
  };

  priv.rebaselineOnClearedTranscript(PROJECT, usage(3));
  // 42 from the row plus the 3 the discarded transcript proved. The old row-read banked 42 flat.
  assert.is(getBankedBaseline(PROJECT)?.costUsd, 45, 'the stale 3 is banked, not dropped');

  priv._sessionRef.store.aiAssistant.sessionUsage = usage(1);
  el.finalizeCostSession();
  assert.is(el.costSessionHistory[0].usage.costUsd, 46, '45 banked + the new turn');
  clearCostSessionHistory(scope);
});

Suite('a memoised baseline survives a clear during a reload instead of being dropped', () => {
  // Review case. The guard is broader than the derivation needs: once a baseline is memoised the
  // new figure is determined without any rows, so deleting it there throws away something correct.
  // Concretely — row at usage 120 / banked 90, baseline memoised at 90, 30 on screen — dropping it
  // made the next turn re-derive `row.banked` and rewrite the row BACKWARDS from 120 to 95.
  const scope = 'bank-clear-memoised';
  clearCostSessionHistory(scope);
  const el = element({
    scope,
    live: usage(30),
    rows: [row({ usage: usage(120), banked: usage(90) })],
    persistenceActive: true,
  });
  el.finalizeCostSession();
  assert.is(getBankedBaseline(PROJECT)?.costUsd, 90, 'baseline memoised off the restored row');

  // A chatConfig rebind puts the ledger back in flight, and the clear lands inside that window.
  const priv = el as unknown as {
    rebaselineOnClearedTranscript(key: string, discarded: AggregateUsage): void;
    _sessionRef: { store: { aiAssistant: { sessionUsage: AggregateUsage } } };
    _costHistoryLoaded: boolean;
  };
  priv._costHistoryLoaded = false;
  priv.rebaselineOnClearedTranscript(PROJECT, usage(30));
  assert.is(getBankedBaseline(PROJECT)?.costUsd, 120, '90 kept and the discarded 30 added');

  priv._costHistoryLoaded = true;
  priv._sessionRef.store.aiAssistant.sessionUsage = usage(5);
  el.finalizeCostSession();
  assert.is(el.costSessionHistory[0].usage.costUsd, 125, 'row moves forward, never backwards');
  clearCostSessionHistory(scope);
});

Suite('clearing with the ledger disabled pins nothing, even though it reads as loaded', () => {
  // Review case. `reloadCostSessionHistory` sets `_costHistoryLoaded` TRUE over a deliberately
  // empty list when `enabled` is false — "nothing will be loaded", not "rows are known" — so the
  // flag alone let a clear pin a baseline from a ledger that was never read. A host enabling
  // history afterwards (an async setting, a feature flag) then had its real row buried by it.
  const scope = 'bank-clear-disabled';
  clearCostSessionHistory(scope);
  const el = element({ scope, live: usage(3) });
  el.chatConfig = { costHistory: { enabled: false, scope } } as ChatConfig;
  const priv = el as unknown as {
    rebaselineOnClearedTranscript(key: string, discarded: AggregateUsage): void;
    _sessionRef: { store: { aiAssistant: { sessionUsage: AggregateUsage } } };
    _costHistoryLoaded: boolean;
  };
  assert.is(priv._costHistoryLoaded, true, 'reads as loaded despite nothing being known');

  priv.rebaselineOnClearedTranscript(PROJECT, usage(3));
  assert.is(getBankedBaseline(PROJECT), undefined, 'nothing pinned while the ledger is off');

  // History switched on afterwards, and this project turns out to have real spend.
  el.chatConfig = { costHistory: { enabled: true, scope, title: 'Trade Blotter' } } as ChatConfig;
  el.costSessionHistory = [row({ usage: usage(50), banked: usage(50) })];
  priv._costHistoryLoaded = true;
  priv._sessionRef.store.aiAssistant.sessionUsage = usage(1);
  el.finalizeCostSession();

  assert.is(el.costSessionHistory[0].usage.costUsd, 51, "the project's 50 is not buried");
  clearCostSessionHistory(scope);
});

Suite('a host slotting its own build history does not have to enable our ledger', () => {
  // Review case. `enabled` governs the BUILT-IN ledger and defaults false, while the Usage tab
  // shows for showCost/showTokens which default true. Once the section's ?hidden binding became
  // effective, short-circuiting on `enabled` above the slot check hid a host's own markup — and
  // the slot is the only thing in that container.
  const el = element({ scope: 'slotted-history', live: usage(1) });
  el.chatConfig = { costHistory: { enabled: false } } as ChatConfig;
  assert.is(el.settingsCostHistorySectionVisible, false, 'nothing slotted, ledger off → hidden');

  el.settingsCostHistorySlotted = [document.createElement('div')];
  assert.is(el.settingsCostHistorySectionVisible, true, 'slotted content shows without `enabled`');
  assert.is(el.settingsCostHistoryHeadingVisible, true, 'and the heading comes with it');
});

Suite('hides the lifetime section when no state key at all can produce a row', () => {
  const el = element({ scope: 'ledger-nokey', live: usage(3) });
  assert.is(el.settingsCostLifetimeVisible, true, 'visible with a key');

  // getStateKey falls back to `id::headerTitle`, and headerTitle has a non-empty default — so
  // clearing the wired key alone is NOT enough to lose the key. All three have to be absent.
  (el as unknown as Internals)._activeStateKey = undefined;
  assert.is(el.settingsCostLifetimeVisible, true, 'still keyed by the default header title');

  el.headerTitle = '';
  assert.is(
    el.settingsCostLifetimeVisible,
    false,
    'no key at all: the total could only ever read $0.00 beside real live spend',
  );
});

Suite('labels the live figures by whether the transcript is authoritative', () => {
  const withPersistence = element({ scope: 'bank-label', live: usage(1), persistenceActive: true });
  assert.is(withPersistence.liveUsageIsProjectLifetime, true, 'restored → project lifetime');

  const without = element({ scope: 'bank-label', live: usage(1), persistenceActive: false });
  assert.is(without.liveUsageIsProjectLifetime, false, 'not restored → this session only');
});

Suite('a stored baseline is isolated from the row and the caller it passed through', () => {
  // A baseline arrives as a reference into a ledger row (`resolveBankedUsage` returns `row.banked`
  // or `row.usage`) and is read back onto the NEXT row's `banked` field, so an uncloned map entry
  // is reachable from page-global state, two rows, and any host handed the row list. Mutating
  // either end must not move the stored figure, or a project's baseline is silently rewritten for
  // the rest of the page.
  resetBankedBaselines();
  const source = usage(10);
  setBankedBaseline(PROJECT, source);

  source.costUsd = 999;
  assert.is(getBankedBaseline(PROJECT)?.costUsd, 10, 'mutating the source cannot reach the map');

  const readBack = getBankedBaseline(PROJECT)!;
  readBack.costUsd = 777;
  assert.is(getBankedBaseline(PROJECT)?.costUsd, 10, 'mutating a read copy cannot reach the map');
});

Suite.run();
