// @vitest-environment jsdom
//
// newSession() atomicity (chat-remount-flicker F2).
//
// The legacy sequence was MESSAGES_CLEAR → await createSession → SESSION_SET:
// the feed rendered EMPTY for the whole round-trip, and stayed empty forever
// when createSession failed. The contract now:
//   1. The previous conversation stays readable until the new session lands.
//   2. SESSION_SET swaps id + transcript in one commit.
//   3. On failure the old transcript survives alongside the error banner.
import { act, createElement } from 'react';
import { createRoot, type Root } from 'react-dom/client';
import { afterEach, beforeEach, describe, expect, it } from 'vitest';

(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 };
}

function makeTransport(opts: {
  createGate?: Promise<void>;
  failCreate?: boolean;
}): ChatTransport {
  return {
    async createSession(): Promise<SessionInfo> {
      if (opts.createGate) await opts.createGate;
      if (opts.failCreate) throw new Error('relay unreachable');
      return { sessionId: 'fresh-room', messages: [] };
    },
    async loadHistory(): Promise<HistoryPage> {
      return {
        messages: [msg('h1', 'old history')],
        hasMore: false,
        nextCursor: null,
      };
    },
    async *stream() {
      /* unused */
    },
    async send(): Promise<ChatMessage> {
      return msg('x', 'x', 'assistant');
    },
    async closeSession() {
      /* unused */
    },
  };
}

function mountChat(roots: Root[], transport: ChatTransport): () => UseChatReturn {
  let latest!: UseChatReturn;
  const root = createRoot(document.createElement('div'));
  roots.push(root);
  act(() => {
    root.render(
      createElement(function W() {
        latest = useChat({ transport, initialSessionId: 'room-old' });
        return null;
      }),
    );
  });
  return () => latest;
}

describe('useChat — newSession atomicity', () => {
  let roots: Root[] = [];
  beforeEach(() => {
    roots = [];
  });
  afterEach(() => {
    act(() => roots.forEach((r) => r.unmount()));
  });

  it('keeps the old transcript readable while createSession is in flight', async () => {
    let release!: () => void;
    const gate = new Promise<void>((r) => {
      release = r;
    });
    const chat = mountChat(roots, makeTransport({ createGate: gate }));
    await act(async () => {
      await Promise.resolve();
    });
    expect(chat().messages.map((m) => m.content)).toEqual(['old history']);

    let done!: Promise<void>;
    act(() => {
      done = chat().newSession();
    });
    await act(async () => {
      await Promise.resolve();
    });
    // Round-trip in flight — the old conversation is STILL on screen.
    expect(chat().messages.map((m) => m.content)).toEqual(['old history']);

    await act(async () => {
      release();
      await done;
    });
    // Atomic swap: new id, new (empty) transcript, load settled.
    expect(chat().sessionId).toBe('fresh-room');
    expect(chat().messages).toEqual([]);
    expect(chat().historyLoaded).toBe(true);
  });

  it('a failed createSession keeps the old transcript and raises the banner', async () => {
    const chat = mountChat(roots, makeTransport({ failCreate: true }));
    await act(async () => {
      await Promise.resolve();
    });

    await act(async () => {
      await chat().newSession();
    });
    expect(chat().messages.map((m) => m.content)).toEqual(['old history']);
    expect(chat().sessionId).toBe('room-old');
    expect(chat().error).toBe('relay unreachable');
  });
});
