import { beforeEach, describe, expect, it, vi } from 'vitest';

const invokeIpcMock = vi.fn();
const hostApiFetchMock = vi.fn();
const gatewayStoreGetStateMock = vi.fn();
const clearHistoryPoll = vi.fn();
const enrichWithCachedImages = vi.fn((messages) => messages);
const enrichWithToolResultFiles = vi.fn((messages) => messages);
const getMessageErrorMessage = vi.fn((message: { errorMessage?: string; error_message?: string } | undefined) => {
  if (!message) return null;
  return message.errorMessage ?? message.error_message ?? null;
});
const getMessageStopReason = vi.fn((message: { stopReason?: string; stop_reason?: string } | undefined) => {
  if (!message) return null;
  return message.stopReason ?? message.stop_reason ?? null;
});
const getMessageText = vi.fn((content: unknown) => typeof content === 'string' ? content : '');
const hasNonToolAssistantContent = vi.fn((message: { content?: unknown } | undefined) => {
  if (!message) return false;
  return typeof message.content === 'string' ? message.content.trim().length > 0 : true;
});
const isToolResultRole = vi.fn((role: unknown) => role === 'toolresult' || role === 'tool_result');
const isInternalMessage = vi.fn((msg: { role?: unknown; content?: unknown }) => {
  if (msg.role === 'system') return true;
  if (msg.role === 'assistant') {
    const text = typeof msg.content === 'string' ? msg.content : '';
    if (/^(HEARTBEAT_OK|NO_REPLY)\s*$/.test(text)) return true;
  }
  return false;
});
const loadMissingPreviews = vi.fn(async () => false);
const toMs = vi.fn((ts: number) => ts < 1e12 ? ts * 1000 : ts);

vi.mock('@/lib/api-client', () => ({
  invokeIpc: (...args: unknown[]) => invokeIpcMock(...args),
}));

vi.mock('@/lib/host-api', () => ({
  hostApiFetch: (...args: unknown[]) => hostApiFetchMock(...args),
}));

vi.mock('@/stores/gateway', () => ({
  useGatewayStore: {
    getState: () => gatewayStoreGetStateMock(),
  },
}));

vi.mock('@/stores/chat/helpers', () => ({
  clearHistoryPoll: (...args: unknown[]) => clearHistoryPoll(...args),
  enrichWithCachedImages: (...args: unknown[]) => enrichWithCachedImages(...args),
  enrichWithToolResultFiles: (...args: unknown[]) => enrichWithToolResultFiles(...args),
  getLatestOptimisticUserMessage: (messages: Array<{ role: string; timestamp?: number }>, userTimestampMs: number) =>
    [...messages].reverse().find(
      (message) => message.role === 'user'
        && (!message.timestamp || Math.abs(toMs(message.timestamp) - userTimestampMs) < 120_000),
    ),
  getMessageText: (...args: unknown[]) => getMessageText(...args),
  hasNonToolAssistantContent: (...args: unknown[]) => hasNonToolAssistantContent(...args),
  isInternalMessage: (...args: unknown[]) => isInternalMessage(...args),
  isToolResultRole: (...args: unknown[]) => isToolResultRole(...args),
  loadMissingPreviews: (...args: unknown[]) => loadMissingPreviews(...args),
  mergePendingOptimisticUserMessages: (_sessionKey: string, messages: unknown[]) => messages,
  hasOptimisticServerEcho: (
    loadedMessages: Array<{ role: string; timestamp?: number; content?: unknown; _attachedFiles?: Array<{ filePath?: string; fileName?: string; mimeType?: string; fileSize?: number }> }>,
    optimistic: { role: string; timestamp?: number; content?: unknown; _attachedFiles?: Array<{ filePath?: string; fileName?: string; mimeType?: string; fileSize?: number }> },
    optimisticTimestampMs: number,
  ) => loadedMessages.some((candidate) => {
    if (candidate.role !== 'user') return false;
    const normalizeText = (content: unknown) => (typeof content === 'string' ? content : '')
      .replace(/^\[(?:Mon|Tue|Wed|Thu|Fri|Sat|Sun)\s+\d{4}-\d{2}-\d{2}\s+\d{2}:\d{2}\s+[^\]]+\]\s*/i, '')
      .replace(/\s+/g, ' ')
      .trim();
    const candidateText = normalizeText(candidate.content);
    const optimisticText = normalizeText(optimistic.content);
    const candidateAttachments = (candidate._attachedFiles || []).map((file) => file.filePath || `${file.fileName}|${file.mimeType}|${file.fileSize}`).sort().join('::');
    const optimisticAttachments = (optimistic._attachedFiles || []).map((file) => file.filePath || `${file.fileName}|${file.mimeType}|${file.fileSize}`).sort().join('::');
    const hasCandidateTimestamp = candidate.timestamp != null;
    const timestampMatches = hasCandidateTimestamp
      ? Math.abs(toMs(candidate.timestamp as number) - optimisticTimestampMs) < 120_000
      : false;

    if (candidateText && optimisticText && candidateText === optimisticText && candidateAttachments === optimisticAttachments) return true;
    if (candidateText && optimisticText && candidateText === optimisticText && (!hasCandidateTimestamp || timestampMatches)) return true;
    if (candidateAttachments && optimisticAttachments && candidateAttachments === optimisticAttachments && (!hasCandidateTimestamp || timestampMatches)) return true;
    return false;
  }),
  dropRedundantOptimisticUserMessages: (_sessionKey: string, messages: unknown[]) => messages,
  isRecoverableRuntimeError: (message: string) => /\bterminated\b/i.test(message),
  matchesOptimisticUserMessage: (
    candidate: { role: string; timestamp?: number; content?: unknown; _attachedFiles?: Array<{ filePath?: string; fileName?: string; mimeType?: string; fileSize?: number }> },
    optimistic: { role: string; timestamp?: number; content?: unknown; _attachedFiles?: Array<{ filePath?: string; fileName?: string; mimeType?: string; fileSize?: number }> },
    optimisticTimestampMs: number,
  ) => {
    if (candidate.role !== 'user') return false;
    const normalizeText = (content: unknown) => (typeof content === 'string' ? content : '')
      .replace(/^\[(?:Mon|Tue|Wed|Thu|Fri|Sat|Sun)\s+\d{4}-\d{2}-\d{2}\s+\d{2}:\d{2}\s+[^\]]+\]\s*/i, '')
      .replace(/\s+/g, ' ')
      .trim();
    const candidateText = normalizeText(candidate.content);
    const optimisticText = normalizeText(optimistic.content);
    const candidateAttachments = (candidate._attachedFiles || []).map((file) => file.filePath || `${file.fileName}|${file.mimeType}|${file.fileSize}`).sort().join('::');
    const optimisticAttachments = (optimistic._attachedFiles || []).map((file) => file.filePath || `${file.fileName}|${file.mimeType}|${file.fileSize}`).sort().join('::');
    const hasCandidateTimestamp = candidate.timestamp != null;
    const timestampMatches = hasCandidateTimestamp
      ? Math.abs(toMs(candidate.timestamp as number) - optimisticTimestampMs) < 120_000
      : false;

    if (candidateText && optimisticText && candidateText === optimisticText && candidateAttachments === optimisticAttachments) return true;
    if (candidateText && optimisticText && candidateText === optimisticText && (!hasCandidateTimestamp || timestampMatches)) return true;
    if (candidateAttachments && optimisticAttachments && candidateAttachments === optimisticAttachments && (!hasCandidateTimestamp || timestampMatches)) return true;
    return false;
  },
  getMessageErrorMessage: (...args: unknown[]) => getMessageErrorMessage(...args),
  getMessageStopReason: (...args: unknown[]) => getMessageStopReason(...args),
  toMs: (...args: unknown[]) => toMs(...args as Parameters<typeof toMs>),
}));

type ChatLikeState = {
  currentSessionKey: string;
  messages: Array<{ role: string; timestamp?: number; content?: unknown; _attachedFiles?: unknown[] }>;
  loading: boolean;
  error: string | null;
  runError: string | null;
  sending: boolean;
  lastUserMessageAt: number | null;
  pendingFinal: boolean;
  sessionLabels: Record<string, string>;
  sessionLastActivity: Record<string, number>;
  thinkingLevel: string | null;
  activeRunId: string | null;
};

function makeHarness(initial?: Partial<ChatLikeState>) {
  let state: ChatLikeState = {
    currentSessionKey: 'agent:main:main',
    messages: [],
    loading: false,
    error: null,
    runError: null,
    sending: false,
    lastUserMessageAt: null,
    pendingFinal: false,
    sessionLabels: {},
    sessionLastActivity: {},
    thinkingLevel: null,
    activeRunId: null,
    ...initial,
  };

  const set = (partial: Partial<ChatLikeState> | ((s: ChatLikeState) => Partial<ChatLikeState>)) => {
    const patch = typeof partial === 'function' ? partial(state) : partial;
    state = { ...state, ...patch };
  };
  const get = () => state;
  return { set, get, read: () => state };
}

describe('chat history actions', () => {
  beforeEach(() => {
    vi.resetAllMocks();
    vi.resetModules();
    vi.useRealTimers();
    invokeIpcMock.mockResolvedValue({ success: true, result: { messages: [] } });
    hostApiFetchMock.mockResolvedValue({ messages: [] });
    gatewayStoreGetStateMock.mockReturnValue({
      status: { state: 'running', port: 18789, connectedAt: Date.now() },
    });
  });

  it('uses cron session fallback when gateway history is empty', async () => {
    const { createHistoryActions } = await import('@/stores/chat/history-actions');
    const h = makeHarness({
      currentSessionKey: 'agent:main:cron:job-1',
    });
    const actions = createHistoryActions(h.set as never, h.get as never);

    hostApiFetchMock.mockResolvedValueOnce({
      messages: [
        {
          id: 'cron-meta-job-1',
          role: 'system',
          content: 'Scheduled task: Drink water',
          timestamp: 1773281731495,
        },
        {
          id: 'cron-run-1',
          role: 'assistant',
          content: 'Drink water 💧',
          timestamp: 1773281732751,
        },
      ],
    });

    await actions.loadHistory();

    expect(hostApiFetchMock).toHaveBeenCalledWith(
      '/api/cron/session-history?sessionKey=agent%3Amain%3Acron%3Ajob-1&limit=200',
    );
    expect(h.read().messages.map((message) => message.content)).toEqual([
      'Drink water 💧',
    ]);
    expect(h.read().sessionLastActivity['agent:main:cron:job-1']).toBe(1773281732751);
    expect(h.read().loading).toBe(false);
  });

  it('does not use cron fallback for normal sessions', async () => {
    const { createHistoryActions } = await import('@/stores/chat/history-actions');
    const h = makeHarness({
      currentSessionKey: 'agent:main:main',
    });
    const actions = createHistoryActions(h.set as never, h.get as never);

    await actions.loadHistory();

    expect(hostApiFetchMock).not.toHaveBeenCalled();
    expect(h.read().messages).toEqual([]);
    expect(h.read().loading).toBe(false);
  });

  it('preserves existing messages when history refresh fails for the current session', async () => {
    const { createHistoryActions } = await import('@/stores/chat/history-actions');
    const h = makeHarness({
      currentSessionKey: 'agent:main:main',
      messages: [
        {
          role: 'assistant',
          content: 'still here',
          timestamp: 1773281732,
        },
      ],
    });
    const actions = createHistoryActions(h.set as never, h.get as never);

    invokeIpcMock.mockRejectedValueOnce(new Error('Gateway unavailable'));

    await actions.loadHistory();

    expect(h.read().messages.map((message) => message.content)).toEqual(['still here']);
    expect(h.read().error).toBe('Gateway unavailable');
    expect(h.read().loading).toBe(false);
  });

  it('finalizes sending and surfaces the latest terminal assistant error from history', async () => {
    const { createHistoryActions } = await import('@/stores/chat/history-actions');
    const h = makeHarness({
      currentSessionKey: 'agent:main:main',
      sending: true,
      activeRunId: 'run-error',
      pendingFinal: true,
      lastUserMessageAt: 1773281731000,
    });
    const actions = createHistoryActions(h.set as never, h.get as never);

    invokeIpcMock.mockResolvedValueOnce({
      success: true,
      result: {
        messages: [
          { role: 'user', content: 'What model are you?', timestamp: 1773281731 },
          {
            role: 'assistant',
            content: [],
            timestamp: 1773281732,
            stopReason: 'error',
            errorMessage: '404 Resource not found',
          },
        ],
      },
    });

    await actions.loadHistory(true);

    expect(clearHistoryPoll).toHaveBeenCalledTimes(1);
    expect(h.read().runError).toBe('404 Resource not found');
    expect(h.read().sending).toBe(false);
    expect(h.read().pendingFinal).toBe(false);
    expect(h.read().activeRunId).toBeNull();
    expect(h.read().lastUserMessageAt).toBeNull();
  });

  it('does not surface an earlier assistant error when a later assistant reply succeeded', async () => {
    const { createHistoryActions } = await import('@/stores/chat/history-actions');
    const h = makeHarness({
      currentSessionKey: 'agent:main:main',
      runError: 'Connection error.',
    });
    const actions = createHistoryActions(h.set as never, h.get as never);

    invokeIpcMock.mockResolvedValueOnce({
      success: true,
      result: {
        messages: [
          { role: 'user', content: 'First question', timestamp: 1000 },
          { role: 'assistant', content: [], timestamp: 1001, stopReason: 'error', errorMessage: 'Connection error.' },
          { role: 'user', content: 'Second question', timestamp: 1002 },
          { role: 'assistant', content: 'Recovered answer', timestamp: 1003 },
        ],
      },
    });

    await actions.loadHistory(true);

    expect(h.read().runError).toBeNull();
    expect(h.read().messages.map((message) => message.content)).toEqual([
      'First question',
      [],
      'Second question',
      'Recovered answer',
    ]);
  });

  it('clears stale runError when refreshed history no longer contains a terminal assistant error', async () => {
    const { createHistoryActions } = await import('@/stores/chat/history-actions');
    const h = makeHarness({
      currentSessionKey: 'agent:main:main',
      runError: 'old model error',
    });
    const actions = createHistoryActions(h.set as never, h.get as never);

    invokeIpcMock.mockResolvedValueOnce({
      success: true,
      result: {
        messages: [
          { role: 'user', content: 'What model are you?', timestamp: 1773281731 },
          { role: 'assistant', content: 'I am MiniMax-M2.7', timestamp: 1773281732 },
        ],
      },
    });

    await actions.loadHistory(true);

    expect(h.read().runError).toBeNull();
    expect(h.read().messages.map((message) => message.content)).toEqual([
      'What model are you?',
      'I am MiniMax-M2.7',
    ]);
  });

  it('does not set runError from an older assistant failure when a later turn succeeded', async () => {
    const { createHistoryActions } = await import('@/stores/chat/history-actions');
    const h = makeHarness({
      currentSessionKey: 'agent:main:main',
      runError: 'stale',
    });
    const actions = createHistoryActions(h.set as never, h.get as never);

    invokeIpcMock.mockResolvedValueOnce({
      success: true,
      result: {
        messages: [
          { role: 'user', content: 'first', timestamp: 1773281730 },
          {
            role: 'assistant',
            content: 'fail',
            timestamp: 1773281731,
            stopReason: 'error',
            errorMessage: '400 bad model',
          },
          { role: 'user', content: 'retry', timestamp: 1773281732 },
          { role: 'assistant', content: 'ok now', timestamp: 1773281733 },
        ],
      },
    });

    await actions.loadHistory(true);

    expect(h.read().runError).toBeNull();
  });

  it('retries the first foreground startup history load after a timeout and then succeeds', async () => {
    vi.useFakeTimers();
    const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
    const { createHistoryActions } = await import('@/stores/chat/history-actions');
    const h = makeHarness({
      currentSessionKey: 'agent:main:main',
    });
    const actions = createHistoryActions(h.set as never, h.get as never);
    gatewayStoreGetStateMock.mockReturnValue({
      status: { state: 'running', port: 18789, connectedAt: Date.now() - 40_000 },
    });

    invokeIpcMock
      .mockResolvedValueOnce({ success: false, error: 'RPC timeout: chat.history' })
      .mockResolvedValueOnce({
        success: true,
        result: {
          messages: [
            { role: 'assistant', content: 'restored after retry', timestamp: 1000 },
          ],
        },
      });

    const loadPromise = actions.loadHistory();
    await vi.runAllTimersAsync();
    await loadPromise;

    expect(invokeIpcMock).toHaveBeenNthCalledWith(
      1,
      'gateway:rpc',
      'chat.history',
      { sessionKey: 'agent:main:main', limit: 200 },
      35_000,
    );
    expect(invokeIpcMock).toHaveBeenNthCalledWith(
      2,
      'gateway:rpc',
      'chat.history',
      { sessionKey: 'agent:main:main', limit: 200 },
      35_000,
    );
    expect(h.read().messages.map((message) => message.content)).toEqual(['restored after retry']);
    expect(h.read().error).toBeNull();
    expect(warnSpy).toHaveBeenCalledWith(
      '[chat.history] startup retry scheduled',
      expect.objectContaining({
        sessionKey: 'agent:main:main',
        attempt: 1,
        errorKind: 'timeout',
      }),
    );
    warnSpy.mockRestore();
  });

  it('stops retrying once the load no longer belongs to the active session', async () => {
    vi.useFakeTimers();
    const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
    const { createHistoryActions } = await import('@/stores/chat/history-actions');
    const h = makeHarness({
      currentSessionKey: 'agent:main:main',
    });
    const actions = createHistoryActions(h.set as never, h.get as never);

    invokeIpcMock.mockImplementationOnce(async () => {
      h.set({
        currentSessionKey: 'agent:main:other',
        loading: false,
        messages: [{ role: 'assistant', content: 'other session', timestamp: 1001 }],
      });
      return { success: false, error: 'RPC timeout: chat.history' };
    });

    await actions.loadHistory();

    expect(invokeIpcMock).toHaveBeenCalledTimes(1);
    expect(h.read().currentSessionKey).toBe('agent:main:other');
    expect(h.read().messages.map((message) => message.content)).toEqual(['other session']);
    expect(h.read().error).toBeNull();
    expect(warnSpy).not.toHaveBeenCalled();
    warnSpy.mockRestore();
  });

  it('surfaces a final error only after startup retry budget is exhausted', async () => {
    vi.useFakeTimers();
    const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
    const { createHistoryActions } = await import('@/stores/chat/history-actions');
    const h = makeHarness({
      currentSessionKey: 'agent:main:main',
    });
    const actions = createHistoryActions(h.set as never, h.get as never);

    invokeIpcMock.mockResolvedValue({
      success: false,
      error: 'RPC timeout: chat.history',
    });

    const loadPromise = actions.loadHistory();
    await vi.runAllTimersAsync();
    await loadPromise;

    expect(invokeIpcMock).toHaveBeenCalledTimes(5);
    expect(h.read().messages).toEqual([]);
    expect(h.read().error).toBe('RPC timeout: chat.history');
    expect(warnSpy).toHaveBeenCalledWith(
      '[chat.history] startup retry exhausted',
      expect.objectContaining({
        sessionKey: 'agent:main:main',
      }),
    );
    warnSpy.mockRestore();
  });

  it('does not retry quiet history refreshes', async () => {
    const { createHistoryActions } = await import('@/stores/chat/history-actions');
    const h = makeHarness({
      currentSessionKey: 'agent:main:main',
    });
    const actions = createHistoryActions(h.set as never, h.get as never);

    invokeIpcMock.mockResolvedValue({
      success: false,
      error: 'RPC timeout: chat.history',
    });

    await actions.loadHistory(true);

    expect(invokeIpcMock).toHaveBeenCalledTimes(1);
    expect(h.read().error).toBeNull();
  });

  it('does not retry non-retryable startup failures', async () => {
    const { createHistoryActions } = await import('@/stores/chat/history-actions');
    const h = makeHarness({
      currentSessionKey: 'agent:main:main',
    });
    const actions = createHistoryActions(h.set as never, h.get as never);

    invokeIpcMock.mockResolvedValue({
      success: false,
      error: 'Validation failed: bad session key',
    });

    await actions.loadHistory();

    expect(invokeIpcMock).toHaveBeenCalledTimes(1);
    expect(h.read().error).toBe('Validation failed: bad session key');
  });

  it('filters out system messages from loaded history', async () => {
    const { createHistoryActions } = await import('@/stores/chat/history-actions');
    const h = makeHarness();
    const actions = createHistoryActions(h.set as never, h.get as never);

    invokeIpcMock.mockResolvedValueOnce({
      success: true,
      result: {
        messages: [
          { role: 'user', content: 'Hello', timestamp: 1000 },
          { role: 'system', content: 'Gateway restarted', timestamp: 1001 },
          { role: 'assistant', content: 'Hi there!', timestamp: 1002 },
        ],
      },
    });

    await actions.loadHistory();

    expect(h.read().messages.map((m) => m.content)).toEqual([
      'Hello',
      'Hi there!',
    ]);
  });

  it('filters out HEARTBEAT_OK assistant messages', async () => {
    const { createHistoryActions } = await import('@/stores/chat/history-actions');
    const h = makeHarness();
    const actions = createHistoryActions(h.set as never, h.get as never);

    invokeIpcMock.mockResolvedValueOnce({
      success: true,
      result: {
        messages: [
          { role: 'user', content: 'Hello', timestamp: 1000 },
          { role: 'assistant', content: 'HEARTBEAT_OK', timestamp: 1001 },
          { role: 'assistant', content: 'Real response', timestamp: 1002 },
        ],
      },
    });

    await actions.loadHistory();

    expect(h.read().messages.map((m) => m.content)).toEqual([
      'Hello',
      'Real response',
    ]);
  });

  it('filters out NO_REPLY assistant messages', async () => {
    const { createHistoryActions } = await import('@/stores/chat/history-actions');
    const h = makeHarness();
    const actions = createHistoryActions(h.set as never, h.get as never);

    invokeIpcMock.mockResolvedValueOnce({
      success: true,
      result: {
        messages: [
          { role: 'user', content: 'Hello', timestamp: 1000 },
          { role: 'assistant', content: 'NO_REPLY', timestamp: 1001 },
          { role: 'assistant', content: 'Actual answer', timestamp: 1002 },
        ],
      },
    });

    await actions.loadHistory();

    expect(h.read().messages.map((m) => m.content)).toEqual([
      'Hello',
      'Actual answer',
    ]);
  });

  it('keeps normal assistant messages that contain HEARTBEAT_OK as substring', async () => {
    const { createHistoryActions } = await import('@/stores/chat/history-actions');
    const h = makeHarness();
    const actions = createHistoryActions(h.set as never, h.get as never);

    invokeIpcMock.mockResolvedValueOnce({
      success: true,
      result: {
        messages: [
          { role: 'user', content: 'What is HEARTBEAT_OK?', timestamp: 1000 },
          { role: 'assistant', content: 'HEARTBEAT_OK is a status code', timestamp: 1001 },
        ],
      },
    });

    await actions.loadHistory();

    expect(h.read().messages.map((m) => m.content)).toEqual([
      'What is HEARTBEAT_OK?',
      'HEARTBEAT_OK is a status code',
    ]);
  });

  it('drops stale history results after the user switches sessions', async () => {
    const { createHistoryActions } = await import('@/stores/chat/history-actions');
    let resolveHistory: ((value: unknown) => void) | null = null;
    invokeIpcMock.mockImplementationOnce(() => new Promise((resolve) => {
      resolveHistory = resolve;
    }));

    const h = makeHarness({
      currentSessionKey: 'agent:main:session-a',
      messages: [
        {
          role: 'assistant',
          content: 'session b content',
          timestamp: 1773281732,
        },
      ],
    });
    const actions = createHistoryActions(h.set as never, h.get as never);

    const loadPromise = actions.loadHistory();
    h.set({
      currentSessionKey: 'agent:main:session-b',
      messages: [
        {
          role: 'assistant',
          content: 'session b content',
          timestamp: 1773281733,
        },
      ],
    });
    resolveHistory?.({
      success: true,
      result: {
        messages: [
          {
            role: 'assistant',
            content: 'stale session a content',
            timestamp: 1773281734,
          },
        ],
      },
    });

    await loadPromise;

    expect(h.read().currentSessionKey).toBe('agent:main:session-b');
    expect(h.read().messages.map((message) => message.content)).toEqual(['session b content']);
  });

  it('preserves newer same-session messages when preview hydration finishes later', async () => {
    const { createHistoryActions } = await import('@/stores/chat/history-actions');
    let releasePreviewHydration: (() => void) | null = null;
    loadMissingPreviews.mockImplementationOnce(async (messages) => {
      await new Promise<void>((resolve) => {
        releasePreviewHydration = () => {
          messages[0]!._attachedFiles = [
            {
              fileName: 'image.png',
              mimeType: 'image/png',
              fileSize: 42,
              preview: 'data:image/png;base64,abc',
              filePath: '/tmp/image.png',
            },
          ];
          resolve();
        };
      });
      return true;
    });

    invokeIpcMock.mockResolvedValueOnce({
      success: true,
      result: {
        messages: [
          {
            id: 'history-1',
            role: 'assistant',
            content: 'older message',
            timestamp: 1000,
          },
        ],
      },
    });

    const h = makeHarness({
      currentSessionKey: 'agent:main:main',
    });
    const actions = createHistoryActions(h.set as never, h.get as never);

    await actions.loadHistory();

    h.set((state) => ({
      messages: [
        ...state.messages,
        {
          id: 'newer-1',
          role: 'assistant',
          content: 'newer message',
          timestamp: 1001,
        },
      ],
    }));

    releasePreviewHydration?.();
    await Promise.resolve();

    expect(h.read().messages.map((message) => message.content)).toEqual([
      'older message',
      'newer message',
    ]);
    expect(h.read().messages[0]?._attachedFiles?.[0]?.preview).toBe('data:image/png;base64,abc');
  });

  it('does not append an optimistic duplicate when history already includes the user message without timestamp', async () => {
    const { createHistoryActions } = await import('@/stores/chat/history-actions');
    const h = makeHarness({
      currentSessionKey: 'agent:main:main',
      sending: true,
      lastUserMessageAt: 1_773_281_732_000,
      messages: [
        {
          role: 'user',
          content: '[Fri 2026-03-13 10:00 GMT+8] Open browser, search for tech news, and take a screenshot',
          timestamp: 1_773_281_732,
        },
      ],
    });
    const actions = createHistoryActions(h.set as never, h.get as never);

    invokeIpcMock.mockResolvedValueOnce({
      success: true,
      result: {
        messages: [
          {
            role: 'user',
            content: 'Open browser, search for tech news, and take a screenshot',
          },
          {
            role: 'assistant',
            content: 'Processing',
            timestamp: 1_773_281_733,
          },
        ],
      },
    });

    await actions.loadHistory(true);

    expect(h.read().messages.map((message) => message.content)).toEqual([
      'Open browser, search for tech news, and take a screenshot',
      'Processing',
    ]);
  });
});
