import { assert, createLogicSuite } from '@genesislcap/foundation-testing';
import { getSessionStore } from '../state/session-store';
import { FoundationAiAssistant } from './main';

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

// GENC-1508 — the host hop of the post-resolve cost channel.
//
// The driver method and the orchestrator delegation both have tests; this pins the hop
// between them, which is the one most likely to break silently: a rename on either side of
// `handleInteractionCost` is a no-op that every other test still passes, and the "no spinner"
// guarantee is a design promise stated only in a comment.
//
// Unconnected element (createElement upgrades but does not connect), same as
// blocked-state.test.ts — this handler touches no session state.

const Suite = createLogicSuite('FoundationAiAssistant interaction cost');

type CostHost = {
  handleInteractionCost(e: Event): void;
  driver: unknown;
  startLoadingTimer(): void;
};

function host(): { el: CostHost; calls: Array<[string, number]>; spinners: number } {
  const el = document.createElement('foundation-ai-assistant') as unknown as CostHost;
  const calls: Array<[string, number]> = [];
  const state = { spinners: 0 };
  el.driver = {
    recordExternalCost: (id: string, cost: number) => {
      calls.push([id, cost]);
      return true;
    },
  };
  el.startLoadingTimer = () => {
    state.spinners += 1;
  };
  return {
    el,
    calls,
    get spinners() {
      return state.spinners;
    },
  };
}

const costEvent = (detail: unknown) => new CustomEvent('interaction-cost', { detail });

Suite('a cost report reaches the driver', () => {
  const h = host();
  h.el.handleInteractionCost(costEvent({ interactionId: 'abc', costUsd: 0.42 }));
  assert.equal(h.calls, [['abc', 0.42]]);
});

Suite('reporting cost NEVER starts the loading spinner', () => {
  // The whole point of the channel: this is bookkeeping about a turn that already finished, so
  // it must be invisible. A spinner would tell the user the assistant is working when nothing
  // is pending — the one thing `handleInteractionCompleted` does that this must not copy.
  const h = host();
  h.el.handleInteractionCost(costEvent({ interactionId: 'abc', costUsd: 0.42 }));
  assert.is(h.spinners, 0);
});

Suite('an event with no interactionId is ignored', () => {
  const h = host();
  h.el.handleInteractionCost(costEvent({ costUsd: 0.42 }));
  h.el.handleInteractionCost(costEvent(undefined));
  assert.equal(h.calls, []);
});

Suite('a driver that does not implement the optional method is not called into', () => {
  // `recordExternalCost` is optional on `AiDriver`, so a driver predating this — or one that
  // simply does not track spend — must be ignored rather than throw.
  const h = host();
  h.el.driver = {};
  assert.not.throws(() =>
    h.el.handleInteractionCost(costEvent({ interactionId: 'a', costUsd: 1 })),
  );
  h.el.driver = undefined;
  assert.not.throws(() =>
    h.el.handleInteractionCost(costEvent({ interactionId: 'a', costUsd: 1 })),
  );
});

Suite('a driver without the method leaves a TRACE, not a silent no-op', () => {
  // Reviewer catch on #2474: every other hop traces its drops. A driver predating
  // `recordExternalCost` made this one a silent no-op — money gone, nothing recorded — which is
  // the invisible gap the whole channel exists to close, reintroduced at the last hop.
  const h = host();
  const meta: Array<[string, Record<string, unknown>]> = [];
  (h.el as unknown as { logMeta(t: string, d: Record<string, unknown>): void }).logMeta = (
    t,
    d,
  ) => {
    meta.push([t, d]);
  };
  h.el.driver = {};
  h.el.handleInteractionCost(costEvent({ interactionId: 'abc', costUsd: 0.42 }));
  assert.is(meta.length, 1);
  assert.is(meta[0][0], 'interaction.external-cost-dropped');
  assert.is(meta[0][1].costUsd, 0.42);
  assert.is(meta[0][1].interactionId, 'abc');
});

// The OUTBOUND half: the driver announced, and the host has to turn that into a moved number.
// Tested because it is the same shape of hop as the one that already went wrong — the wrapper
// forward that was missing — and because it deliberately reads the DRIVER's history rather than
// `this.messages`, which is the part a reader would most likely "simplify" back.

let storeSeq = 0;

/** An element wired to its own real session store, so `sessionUsage` round-trips. */
function costHost(): FoundationAiAssistant {
  const el = document.createElement('foundation-ai-assistant') as FoundationAiAssistant;
  storeSeq += 1;
  (el as unknown as { _sessionRef: unknown })._sessionRef = getSessionStore(
    `cost-test-${(storeSeq += 1)}`,
    false,
  );
  return el;
}

Suite('the recorded spend reaches the session total from the DRIVER history', () => {
  const el = costHost();
  const sync = (el as unknown as { syncUsageTotals(m: unknown[]): void }).syncUsageTotals.bind(el);

  sync([{ role: 'assistant', content: 'a', cost: 0.1, model: 'claude-sonnet-5' }]);
  assert.is(el.sessionCostUsd, 0.1);

  // The post-resolve delta lands on the interaction message as `externalCostUsd`; the total has
  // to fold it in, or every hop before this one was wasted.
  sync([
    { role: 'assistant', content: 'a', cost: 0.1, model: 'claude-sonnet-5' },
    { role: 'assistant', content: 'b', externalCostUsd: 0.25 },
  ]);
  assert.is(el.sessionCostUsd, 0.35);
});

Suite.run();
