// SESSION_SWITCH + the HISTORY_LOAD_DONE stale-page guard
// (chat-remount-flicker F2) — the pure state-machine half of the in-place
// session switch. The hook-level integration lives in
// hooks/__tests__/useChat.sessionSwitch.test.tsx.
import { describe, expect, it } from 'vitest';

import { initialState, reducer, type ChatState } from '../reducer';
import type { ChatMessage } from '../../types';

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

/** A lived-in room-A state: transcript, stream flag, error banner, cursor. */
function roomA(): ChatState {
  return {
    ...initialState,
    sessionId: 'room-a',
    messages: [msg('a1', 'hello'), msg('a2', 'world', 'assistant')],
    historyLoaded: true,
    isStreaming: true,
    hasMore: false,
    oldestCursor: 'cur-a',
    error: 'boom',
    errorTurnId: 'a2',
  };
}

describe('reducer — SESSION_SWITCH', () => {
  it('seeded switch swaps id + transcript atomically (no loading state)', () => {
    const next = reducer(roomA(), {
      type: 'SESSION_SWITCH',
      sessionId: 'room-b',
      messages: [msg('b1', 'cached b')],
    });
    expect(next.sessionId).toBe('room-b');
    expect(next.messages.map((m) => m.id)).toEqual(['b1']);
    expect(next.historyLoaded).toBe(true);
    expect(next.isLoading).toBe(false);
  });

  it('unseeded switch enters the loading state with an empty feed', () => {
    const next = reducer(roomA(), { type: 'SESSION_SWITCH', sessionId: 'room-b' });
    expect(next.sessionId).toBe('room-b');
    expect(next.messages).toEqual([]);
    expect(next.historyLoaded).toBe(false);
    expect(next.isLoading).toBe(true);
  });

  it('resets the OLD room\'s stream / error / paging state wholesale', () => {
    const next = reducer(roomA(), {
      type: 'SESSION_SWITCH',
      sessionId: 'room-b',
      messages: [msg('b1', 'cached b')],
    });
    expect(next.isStreaming).toBe(false);
    expect(next.error).toBeNull();
    expect(next.errorTurnId).toBeNull();
    expect(next.hasMore).toBe(true);
    expect(next.oldestCursor).toBeNull();
  });
});

describe('reducer — HISTORY_LOAD_DONE stale-page guard', () => {
  it('drops a page tagged for a session the state already left', () => {
    const state = reducer(roomA(), {
      type: 'SESSION_SWITCH',
      sessionId: 'room-b',
      messages: [msg('b1', 'cached b')],
    });
    const after = reducer(state, {
      type: 'HISTORY_LOAD_DONE',
      sessionId: 'room-a', // slow read for the abandoned room
      messages: [msg('a9', 'stale a page')],
      hasMore: false,
      cursor: null,
    });
    expect(after).toBe(state); // identical reference — nothing committed
  });

  it('commits a page tagged for the CURRENT session', () => {
    const state = reducer(roomA(), { type: 'SESSION_SWITCH', sessionId: 'room-b' });
    const after = reducer(state, {
      type: 'HISTORY_LOAD_DONE',
      sessionId: 'room-b',
      messages: [msg('b1', 'fresh b page')],
      hasMore: false,
      cursor: null,
    });
    expect(after.messages.map((m) => m.id)).toEqual(['b1']);
    expect(after.historyLoaded).toBe(true);
    expect(after.isLoading).toBe(false);
  });

  it('commits an UNTAGGED page unconditionally (legacy dispatch sites)', () => {
    const after = reducer(roomA(), {
      type: 'HISTORY_LOAD_DONE',
      messages: [msg('a3', 'server page')],
      hasMore: true,
      cursor: 'cur-2',
    });
    expect(after.messages.map((m) => m.id)).toEqual(['a3']);
    expect(after.hasMore).toBe(true);
    expect(after.oldestCursor).toBe('cur-2');
  });
});

describe('reducer — MESSAGE_INJECT { afterId } (transcript-order splice)', () => {
  it('inserts right after the anchor message', () => {
    const state: ChatState = {
      ...initialState,
      sessionId: 's',
      messages: [msg('srv-1', 'old'), msg('u-1', 'my new question')],
    };
    const next = reducer(state, {
      type: 'MESSAGE_INJECT',
      message: msg('srv-2', 'scheduled reminder', 'assistant'),
      position: { afterId: 'srv-1' },
    });
    expect(next.messages.map((m) => m.id)).toEqual(['srv-1', 'srv-2', 'u-1']);
  });

  it('falls back to append when the anchor is not rendered', () => {
    const state: ChatState = {
      ...initialState,
      sessionId: 's',
      messages: [msg('u-1', 'hello')],
    };
    const next = reducer(state, {
      type: 'MESSAGE_INJECT',
      message: msg('srv-9', 'late row', 'assistant'),
      position: { afterId: 'gone' },
    });
    expect(next.messages.map((m) => m.id)).toEqual(['u-1', 'srv-9']);
  });

  it('still dedupes by id regardless of position', () => {
    const state: ChatState = {
      ...initialState,
      sessionId: 's',
      messages: [msg('srv-1', 'old')],
    };
    const next = reducer(state, {
      type: 'MESSAGE_INJECT',
      message: msg('srv-1', 'old (edited)'),
      position: { afterId: 'srv-1' },
    });
    expect(next.messages).toHaveLength(1);
    expect(next.messages[0].content).toBe('old (edited)');
  });
});
