// @vitest-environment jsdom
//
// F5 Part 2a — the `initialMessages` warm-hydrate seam.
//
// Contract under test (see useChat bootstrap):
//   1. With `initialSessionId` + non-empty `initialMessages`, the engine renders
//      the seed synchronously on first commit — `historyLoaded` is already true,
//      so the host never paints a Spinner (which gates on `!historyLoaded`).
//   2. The resume-path `loadHistory` STILL runs and REPLACES the seed with the
//      server's authoritative page (server = source of truth; no stale-read).
//   3. Empty `initialMessages` (or the create path) is a no-op — normal
//      loading behaviour, seed ignored.
import { act, createElement } from 'react';
import { createRoot, type Root } from 'react-dom/client';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';

// React needs this global to accept act(...) outside a test-runner integration.
(globalThis as Record<string, unknown>).IS_REACT_ACT_ENVIRONMENT = true;

import { useChat, type UseChatReturn } from '../useChat';
import type { ChatMessage } from '../../types';
import type { ChatTransport } from '../../types/transport';
import type { HistoryPage, SessionInfo } from '../../types/session';

function msg(id: string, content: string, role: ChatMessage['role'] = 'user'): ChatMessage {
  return { id, role, content, createdAt: 1 };
}

/** Transport whose loadHistory resolves only when the test releases it — lets us
 *  observe the synchronous-seed render BEFORE the server page lands. */
function deferredTransport(page: HistoryPage): {
  transport: ChatTransport;
  resolveHistory: () => void;
  historyCalls: () => number;
} {
  let release!: () => void;
  const gate = new Promise<void>((r) => {
    release = r;
  });
  let calls = 0;
  const transport: ChatTransport = {
    async createSession(): Promise<SessionInfo> {
      return { sessionId: 'new-session' };
    },
    async loadHistory(): Promise<HistoryPage> {
      calls += 1;
      await gate;
      return page;
    },
    async *stream() {
      /* unused */
    },
    async send(): Promise<ChatMessage> {
      return msg('x', 'x', 'assistant');
    },
    async closeSession() {
      /* unused */
    },
  };
  return { transport, resolveHistory: release, historyCalls: () => calls };
}

describe('useChat — initialMessages warm-hydrate seam', () => {
  let roots: Root[] = [];
  beforeEach(() => {
    roots = [];
  });
  afterEach(() => {
    act(() => roots.forEach((r) => r.unmount()));
  });

  it('renders the seed synchronously (historyLoaded true, seed messages) on the resume path', async () => {
    const seed = [msg('s1', 'cached hello'), msg('s2', 'cached reply', 'assistant')];
    const { transport, historyCalls } = deferredTransport({
      messages: [msg('h1', 'server hello')],
      hasMore: false,
      nextCursor: null,
    });

    const snaps: UseChatReturn[] = [];
    const root = createRoot(document.createElement('div'));
    roots.push(root);
    await act(async () => {
      root.render(createElement(function W() {
        const chat = useChat({ transport, initialSessionId: 'room-1', initialMessages: seed });
        snaps.push(chat);
        return null;
      }));
    });

    // The seed was committed and the load settled flag is true → no Spinner.
    const seededSnap = snaps.find((s) => s.messages.length === 2 && s.historyLoaded);
    expect(seededSnap, 'a snapshot with the 2 seed messages + historyLoaded=true').toBeDefined();
    expect(seededSnap!.messages.map((m) => m.content)).toEqual(['cached hello', 'cached reply']);
    expect(seededSnap!.sessionId).toBe('room-1');
    // loadHistory was invoked (background revalidation is in flight, still gated).
    expect(historyCalls()).toBe(1);
  });

  it('replaces the seed with the server page once loadHistory resolves', async () => {
    const seed = [msg('s1', 'STALE cached')];
    const serverPage: HistoryPage = {
      messages: [msg('h1', 'fresh server a'), msg('h2', 'fresh server b', 'assistant')],
      hasMore: false,
      nextCursor: null,
    };
    const { transport, resolveHistory } = deferredTransport(serverPage);

    const snaps: UseChatReturn[] = [];
    const root = createRoot(document.createElement('div'));
    roots.push(root);
    await act(async () => {
      root.render(createElement(function W() {
        const chat = useChat({ transport, initialSessionId: 'room-1', initialMessages: seed });
        snaps.push(chat);
        return null;
      }));
    });
    // Release the background load and let the reducer commit HISTORY_LOAD_DONE.
    await act(async () => {
      resolveHistory();
      await Promise.resolve();
    });

    const last = snaps[snaps.length - 1];
    expect(last.messages.map((m) => m.content)).toEqual(['fresh server a', 'fresh server b']);
    expect(last.messages.some((m) => m.content === 'STALE cached')).toBe(false);
    expect(last.historyLoaded).toBe(true);
  });

  it('ignores an empty initialMessages array (normal load path)', async () => {
    const loadHistory = vi.fn(async (): Promise<HistoryPage> => ({
      messages: [msg('h1', 'server only')],
      hasMore: false,
      nextCursor: null,
    }));
    const transport = {
      createSession: async () => ({ sessionId: 'x' }),
      loadHistory,
      async *stream() {},
      send: async () => msg('a', 'a', 'assistant'),
      closeSession: async () => {},
    } as unknown as ChatTransport;

    const snaps: UseChatReturn[] = [];
    const root = createRoot(document.createElement('div'));
    roots.push(root);
    await act(async () => {
      root.render(createElement(function W() {
        const chat = useChat({ transport, initialSessionId: 'room-1', initialMessages: [] });
        snaps.push(chat);
        return null;
      }));
    });

    // Empty seed: the load still drives the transcript, no seed shortcut.
    const last = snaps[snaps.length - 1];
    expect(last.messages.map((m) => m.content)).toEqual(['server only']);
    expect(loadHistory).toHaveBeenCalledTimes(1);
  });
});
