import type {
  AIProvider,
  AIProviderRegistry,
  ChatMessage,
  ChatRequestOptions,
} from '@genesislcap/foundation-ai';
import { assert, createLogicSuite } from '@genesislcap/foundation-testing';
// Side-effect import — MUST precede the driver import so it subclasses jsdom's
// EventTarget (CustomEvent dispatch then works in node).
import '../../components/chat-driver/align-event-globals';
import { OrchestratingDriver } from '../../components/orchestrating-driver/orchestrating-driver';
import type { AgentConfig } from '../../config/config';
import { clearDiagnosticsCursorRegistry } from './diagnostics-cursors';
import { clearPersisterRegistry } from './persister-registry';
import { SessionPersister, type SessionPersisterDeps } from './session-persister';
import type { PersistedSession } from './session-snapshot';

// GENC-1351 — end-to-end save→restore→route→re-activate proof. The persister unit
// tests exercise `persistSession`/`restore` against a FAKE driver; the driver pin/
// flow-owner tests exercise routing against a real driver. NOTHING wired the two
// together — the exact integration that broke once (bug #2: the live agent's
// `serialize()` was read via `driver.getActiveAgent()` — a host minting fresh
// configs handed back a never-activated closure whose `serialize()` returned
// undefined, so the machine reset on reload). This test closes that gap with a REAL
// `OrchestratingDriver` + a REAL `SessionPersister` + an in-memory provider:
//   persist (capture the LIVE agent's state) → restore (prime + re-lock routing) →
//   send (route back to the flow-owner without re-classifying) → re-activate aligned.
// `Planner` is `excludeFromClassifier` AND never pinned, so a turn landing on it after
// restore proves the RESTORED flow-owner lock (not the pin, not the classifier) routed
// it — the `setFlowOwner`-on-restore half of the fix.

const KEY = 'e2e-session';

const makeRegistry = (provider: AIProvider): AIProviderRegistry => ({
  get: () => provider,
  default: () => provider,
  defaultName: () => 'test',
  names: () => ['test'],
  getStatus: async () => null,
  listStatuses: async () => [],
});

/** A provider that ends every turn immediately with a plain-text reply (no tools). */
const noToolProvider = (): AIProvider =>
  ({
    chat: async (
      _history: ChatMessage[],
      _userMessage: string,
      _options?: ChatRequestOptions,
    ): Promise<ChatMessage> => ({ role: 'assistant', content: 'ok' }),
  }) as AIProvider;

const agentCfg = (overrides: Partial<AgentConfig> & { name: string }): AgentConfig =>
  ({ description: 'test agent', ...overrides }) as AgentConfig;

/** In-memory persistence provider (single session), enough for one save + one load. */
function makeMemoryProvider() {
  let stored: PersistedSession | null = null;
  return {
    saveDebounceMs: 0,
    save: async (_k: string, snap: PersistedSession) => {
      stored = snap;
    },
    load: async () => stored,
    clear: async () => {
      stored = null;
    },
    get current(): PersistedSession | null {
      return stored;
    },
  };
}

/** The functional store shape the persister reads scalars from / hydrates into. */
function makeStoreFake(over: Record<string, unknown> = {}) {
  return {
    store: {
      aiAssistant: {
        messages: [] as ChatMessage[],
        flowOwnerAgentName: null as string | null,
        flowActivationPrompt: null as string | null,
        activeAgent: undefined as { name: string } | undefined,
        pinnedAgentName: null as string | null,
        // The persister reads `sessionUsage.costUsd` when building a snapshot, so this
        // has to be the object rather than a bare cost scalar.
        sessionUsage: {
          costUsd: 0,
          uncachedInputTokens: 0,
          cacheReadTokens: 0,
          cacheWriteTokens: 0,
          outputTokens: 0,
        },
        contextTokens: 0,
        contextLimit: 0,
        activeModel: undefined,
        activeProviderName: undefined,
        restoring: false,
        ...over,
      },
    },
    actions: {
      aiAssistant: {
        loadSession: () => {},
        setInputValue: () => {},
      },
    },
  };
}

function makePersister(driver: unknown, storeFake: unknown, provider: unknown): SessionPersister {
  const deps: SessionPersisterDeps = {
    stateKey: KEY,
    getPersistence: () => ({ enabled: true, provider: provider as never }),
    getAgentNames: () => ['Genesis Assistant', 'Planner'],
    getDriver: () => driver as never,
    getStore: () => storeFake as never,
    getDiagnosticEntries: () => [],
  };
  return new SessionPersister(deps);
}

const tick = (): Promise<void> =>
  new Promise((r) => {
    setTimeout(r, 0);
  });

const Suite = createLogicSuite('stateful-restore-e2e');

Suite.before.each(() => {
  clearPersisterRegistry();
  clearDiagnosticsCursorRegistry();
});

Suite(
  'a flow-owned stateful agent survives persist → restore → resume, routing back without re-classifying',
  async () => {
    const provider = makeMemoryProvider();

    // ── Persist side ────────────────────────────────────────────────────────────
    // `stateA` is the Planner's live state bag; `serialize` closes over it, exactly
    // like a real stateful agent's machine snapshot.
    let stateA: { cursor: number } = { cursor: 0 };
    const planner1 = agentCfg({
      name: 'Planner',
      excludeFromClassifier: true,
      onActivate: async ({ restored }) => {
        if (restored !== undefined) stateA = restored as { cursor: number };
      },
      serialize: () => stateA,
      systemPrompt: () => 'planner',
    });
    const driver1 = new OrchestratingDriver(
      makeRegistry(noToolProvider()),
      [agentCfg({ name: 'Genesis Assistant' }), planner1],
      { sessionKey: KEY },
    );

    // Flow-owned but NOT pinned, then run a turn: the flow-owner lock routes the
    // Planner (it's excludeFromClassifier, so the classifier could never pick it).
    driver1.setFlowOwner('Planner');
    await driver1.sendMessage('go');
    assert.is(
      driver1.getActiveAgent()?.name,
      'Planner',
      'flow-owner routed the Planner on persist side',
    );

    // The flow advances — mutate the SAME object `serialize()` returns.
    stateA.cursor = 3;

    // The store mirrors what main.ts keeps: the flow-owner lock lives in the store
    // (that's the scalar `persistSession` reads).
    const storeFake1 = makeStoreFake({ flowOwnerAgentName: 'Planner' });
    const persister1 = makePersister(driver1, storeFake1, provider);
    persister1.scheduleSave();
    await tick();
    await tick();

    const saved = provider.current;
    assert.ok(saved, 'a snapshot was persisted');
    assert.is(saved!.flowOwnerAgentName, 'Planner', 'flow-owner lock persisted');
    // Bug-#2 assertion: the LIVE (activated) agent's serialize was captured via
    // driver.getActiveAgent(), so the advanced state is in the snapshot.
    assert.equal(
      saved!.agentSnapshots.Planner,
      { cursor: 3 },
      "live agent's advanced state captured (not a never-activated closure)",
    );

    // ── Restore side (fresh instances share the same in-memory provider) ──────────
    let stateB: { cursor: number } = { cursor: -1 }; // sentinel; restore must overwrite
    const planner2 = agentCfg({
      name: 'Planner',
      excludeFromClassifier: true,
      onActivate: async ({ restored }) => {
        if (restored !== undefined) stateB = restored as { cursor: number };
      },
      serialize: () => stateB,
      systemPrompt: () => 'planner',
    });
    const driver2 = new OrchestratingDriver(
      makeRegistry(noToolProvider()),
      [agentCfg({ name: 'Genesis Assistant' }), planner2],
      { sessionKey: KEY },
    );
    const storeFake2 = makeStoreFake();
    const persister2 = makePersister(driver2, storeFake2, provider);

    await persister2.restore();
    // The restored flow-owner lock (setFlowOwner-on-restore) routes the next turn
    // back to the Planner without re-classifying.
    await driver2.sendMessage('again');

    assert.is(
      driver2.getActiveAgent()?.name,
      'Planner',
      'restored flow-owner routed the Planner (excludeFromClassifier + unpinned ⇒ only the restored lock could have)',
    );
    // primeRestoredAgentState → onActivate({ restored }) fed the snapshot back, so
    // the fresh Planner resumed exactly where the persisted one left off.
    assert.equal(
      stateB,
      { cursor: 3 },
      'restored agent re-activated aligned to the persisted state',
    );
  },
);

Suite.run();
