/**
 * What a conversation is called, and what renaming it does.
 *
 * The title has two possible sources — the name the user gave it, and the first
 * message it derives from otherwise — and two surfaces that must never disagree
 * about which is showing (the pane header and the rail row). Both read the
 * derivation here; both write through `commitSessionRename`.
 */
import { describe, expect, it, vi } from 'vitest';

import {
  commitSessionRename,
  isDerivedSessionTitle,
  normalizeSessionTitleInput,
  normalizeSessionTitleOverride,
  SESSION_TITLE_MAX_LENGTH,
  workbenchSessionTitle,
} from '@/lib/workbench/session-title';
import { foldEvents, type WorkbenchEvent, type WorkbenchFold } from '@/lib/workbench/session-store';

describe('what a conversation is called', () => {
  it('prefers the name the user gave it', () => {
    expect(workbenchSessionTitle({ title: '期末复习课', prompt: '帮我做一节课' })).toBe(
      '期末复习课',
    );
  });

  it('derives it from the first message when there is no name', () => {
    expect(workbenchSessionTitle({ title: null, prompt: '帮我做一节课' })).toBe('帮我做一节课');
    expect(workbenchSessionTitle({ title: '   ', prompt: '帮我做一节课' })).toBe('帮我做一节课');
  });

  it('has nothing to show for an empty conversation', () => {
    // The caller supplies its own placeholder — the rail says "untitled chat", the
    // pane header says "new chat", because they answer different questions.
    expect(workbenchSessionTitle({ title: null, prompt: '' })).toBeNull();
    expect(workbenchSessionTitle({})).toBeNull();
  });
});

describe('what a rename sends', () => {
  const session = { title: '期末复习课', prompt: '帮我做一节课' };

  it('trims and caps', () => {
    expect(normalizeSessionTitleInput(session, '  新名字  ')).toBe('新名字');
    expect(normalizeSessionTitleInput(session, 'x'.repeat(400))).toHaveLength(
      SESSION_TITLE_MAX_LENGTH,
    );
  });

  it('uses the input maxLength budget without splitting a surrogate pair', () => {
    expect(normalizeSessionTitleOverride(`${'x'.repeat(118)}😀`)).toBe(`${'x'.repeat(118)}😀`);
    expect(normalizeSessionTitleOverride(`${'x'.repeat(119)}😀tail`)).toBe('x'.repeat(119));
  });

  it('makes PostgreSQL-invalid API text safe for storage and projection', () => {
    expect(normalizeSessionTitleOverride(`Safe\ud83d title`)).toBe('Safe� title');
    expect(normalizeSessionTitleOverride(`Safe\u0000 title`)).toBe('Safe� title');
  });

  it('reads an empty box as "clear the name", not as a blank title', () => {
    expect(normalizeSessionTitleInput(session, '')).toBeNull();
    expect(normalizeSessionTitleInput(session, '   ')).toBeNull();
  });

  it('reads the derived title typed back in as a clear too', () => {
    // Storing it would freeze a name the user never chose.
    expect(isDerivedSessionTitle(session, '帮我做一节课')).toBe(true);
    expect(normalizeSessionTitleInput(session, ' 帮我做一节课 ')).toBeNull();
  });
});

describe('committing a rename', () => {
  const session = { title: null, prompt: '帮我做一节课' };

  it('writes it locally first, then settles on what the server stored', async () => {
    const applied: { title: string | null; settled: boolean }[] = [];
    const save = vi.fn(async () => '期末复习');
    const outcome = await commitSessionRename({
      current: session,
      raw: '期末复习课',
      apply: (title, settled) => applied.push({ title, settled }),
      save,
    });
    expect(outcome).toBe('renamed');
    expect(save).toHaveBeenCalledWith('期末复习课');
    // Optimistic value, then the server's — which can differ (it caps).
    expect(applied).toEqual([
      { title: '期末复习课', settled: false },
      { title: '期末复习', settled: true },
    ]);
  });

  it('puts the old name back when the write is refused', async () => {
    const applied: { title: string | null; settled: boolean }[] = [];
    const outcome = await commitSessionRename({
      current: { title: '旧名字', prompt: '帮我做一节课' },
      raw: '新名字',
      apply: (title, settled) => applied.push({ title, settled }),
      save: async () => {
        throw new Error('500');
      },
    });
    expect(outcome).toBe('failed');
    expect(applied).toEqual([
      { title: '新名字', settled: false },
      { title: '旧名字', settled: true },
    ]);
  });

  it.each([
    { settlement: 'success', save: async () => '服务端名字', outcome: 'renamed' },
    {
      settlement: 'failure',
      save: async () => {
        throw new Error('500');
      },
      outcome: 'failed',
    },
  ])('does not apply a stale $settlement settlement', async ({ save, outcome: expected }) => {
    const applied: (string | null)[] = [];
    let current = true;
    const outcome = await commitSessionRename({
      current: { title: '旧名字', prompt: '帮我做一节课' },
      raw: '本地名字',
      apply: (title) => {
        applied.push(title);
        current = false;
      },
      save,
      isCurrent: () => current,
    });

    expect(outcome).toBe(expected);
    expect(applied).toEqual(['本地名字']);
  });

  it('clears the override on an empty box, so the derived title comes back', async () => {
    const applied: (string | null)[] = [];
    const save = vi.fn(async () => null);
    const outcome = await commitSessionRename({
      current: { title: '旧名字', prompt: '帮我做一节课' },
      raw: '  ',
      apply: (title) => applied.push(title),
      save,
    });
    expect(outcome).toBe('renamed');
    expect(save).toHaveBeenCalledWith(null);
    expect(applied).toEqual([null, null]);
  });

  it.each([
    ['an empty input', '  '],
    ['the unchanged prompt fallback', ' 帮我做一节课 '],
  ])('does not turn %s into a manual clear when no override exists', async (_case, raw) => {
    const save = vi.fn(async () => null);
    const outcome = await commitSessionRename({
      current: session,
      raw,
      apply: () => expect.unreachable('nothing should be written'),
      save,
    });

    expect(outcome).toBe('unchanged');
    expect(save).not.toHaveBeenCalled();
  });

  it('spends no round trip when nothing changed', async () => {
    const save = vi.fn(async () => null);
    const outcome = await commitSessionRename({
      current: { title: '旧名字', prompt: '帮我做一节课' },
      raw: ' 旧名字 ',
      apply: () => expect.unreachable('nothing should be written'),
      save,
    });
    expect(outcome).toBe('unchanged');
    expect(save).not.toHaveBeenCalled();
  });

  it('preserves an explicit same-value decision queued behind an ambiguous write', async () => {
    const applied: (string | null)[] = [];
    const save = vi.fn(async () => '旧名字');
    const outcome = await commitSessionRename({
      current: { title: '旧名字', prompt: '帮我做一节课' },
      raw: '旧名字',
      apply: (title) => applied.push(title),
      save,
      forceSave: true,
    });

    expect(outcome).toBe('renamed');
    expect(save).toHaveBeenCalledWith('旧名字');
    expect(applied).toEqual(['旧名字', '旧名字']);
  });
});

describe('the fold leaves the name alone', () => {
  /**
   * A rename is not something the run did, so it is NOT in the event log: it
   * arrives with the session meta, like the prompt. Replaying the whole log —
   * which is what a reconnect and a fresh attach both do — must therefore leave
   * it standing, or a renamed chat would revert to its first message every time
   * the stream caught up.
   */
  it('survives a replay of the whole event log', () => {
    const named = { ...BLANK, sessionPrompt: '帮我做一节课', sessionTitle: '期末复习课' };
    const replayed = foldEvents(named, [
      event('session_start', { prompt: '帮我做一节课' }),
      event('message_update', { text: '好的' }),
      event('session_end', { status: 'succeeded' }),
    ]);
    expect(replayed.sessionTitle).toBe('期末复习课');
    expect(
      workbenchSessionTitle({ title: replayed.sessionTitle, prompt: replayed.sessionPrompt }),
    ).toBe('期末复习课');
  });
});

let seq = 0;
function event(type: string, data: unknown): WorkbenchEvent {
  seq += 1;
  return { id: seq, ts: 1000 + seq, attempt: 1, type, data };
}

const BLANK: WorkbenchFold = {
  status: 'connecting',
  lastEventId: 0,
  error: null,
  courseTitle: null,
  sessionPrompt: null,
  sessionTitle: null,
  skillId: null,
  skillViolations: [],
  plan: [],
  pages: {},
  chat: [],
  libraryRevision: 0,
  stageLinkStageIds: [],
  touchedStageIds: [],
  runCourseStageIds: [],
  generatingOrder: null,
  panelOpen: false,
  panelPinned: false,
  thinkingKey: null,
  assistantKey: null,
  generationOpen: false,
  epoch: 0,
  waitingKey: null,
  waitingArmed: false,
  stageId: null,
};