import sessionChatReducer, {
  addMessageFromWebSocket,
  setActiveChannel,
  openChatWindow,
  closeChatWindow,
  markAsRead,
  incrementUnreadCount,
  setWindowPosition,
  setUploadStatus,
  clearUploadError,
  uploadAttachment
} from '../sessionChatSlice';

describe('sessionChatSlice initial state', () => {
  test('has empty messagesByChannel object', () => {
    const state = sessionChatReducer(undefined, { type: 'unknown' });
    expect(state.messagesByChannel).toEqual({});
  });

  test('has null activeChannel', () => {
    const state = sessionChatReducer(undefined, { type: 'unknown' });
    expect(state.activeChannel).toBeNull();
  });

  test('has null channelType', () => {
    const state = sessionChatReducer(undefined, { type: 'unknown' });
    expect(state.channelType).toBeNull();
  });

  test('has empty unreadCounts object', () => {
    const state = sessionChatReducer(undefined, { type: 'unknown' });
    expect(state.unreadCounts).toEqual({});
  });

  test('has empty lastReadAt object', () => {
    const state = sessionChatReducer(undefined, { type: 'unknown' });
    expect(state.lastReadAt).toEqual({});
  });

  test('has empty fetchStatus object', () => {
    const state = sessionChatReducer(undefined, { type: 'unknown' });
    expect(state.fetchStatus).toEqual({});
  });

  test('has empty fetchError object', () => {
    const state = sessionChatReducer(undefined, { type: 'unknown' });
    expect(state.fetchError).toEqual({});
  });

  test('has idle sendStatus', () => {
    const state = sessionChatReducer(undefined, { type: 'unknown' });
    expect(state.sendStatus).toBe('idle');
  });

  test('has null sendError', () => {
    const state = sessionChatReducer(undefined, { type: 'unknown' });
    expect(state.sendError).toBeNull();
  });

  test('has empty nextCursors object', () => {
    const state = sessionChatReducer(undefined, { type: 'unknown' });
    expect(state.nextCursors).toEqual({});
  });

  test('has isWindowOpen set to false', () => {
    const state = sessionChatReducer(undefined, { type: 'unknown' });
    expect(state.isWindowOpen).toBe(false);
  });

  test('has null windowPosition', () => {
    const state = sessionChatReducer(undefined, { type: 'unknown' });
    expect(state.windowPosition).toBeNull();
  });

  test('complete initial state structure matches design', () => {
    const state = sessionChatReducer(undefined, { type: 'unknown' });
    expect(state).toEqual({
      messagesByChannel: {},
      activeChannel: null,
      channelType: null,
      unreadCounts: {},
      lastReadAt: {},
      fetchStatus: {},
      fetchError: {},
      sendStatus: 'idle',
      sendError: null,
      nextCursors: {},
      isWindowOpen: false,
      windowPosition: null,
      uploadState: {
        status: 'idle',
        progress: 0,
        error: null,
        fileName: null
      }
    });
  });

  test('has uploadState with idle status', () => {
    const state = sessionChatReducer(undefined, { type: 'unknown' });
    expect(state.uploadState).toEqual({
      status: 'idle',
      progress: 0,
      error: null,
      fileName: null
    });
  });
});

describe('addMessageFromWebSocket', () => {
  test('adds message to new channel', () => {
    const state = {
      messagesByChannel: {},
      unreadCounts: {},
      isWindowOpen: false,
      activeChannel: null
    };
    const message = {
      id: 'msg-1',
      senderId: 'user-1',
      senderName: 'Alice',
      message: 'Hello',
      channel: 'session',
      sessionId: 'session-abc',
      createdAt: '2026-01-26T12:00:00Z'
    };
    const action = addMessageFromWebSocket(message);
    const newState = sessionChatReducer(state, action);

    expect(newState.messagesByChannel['session-abc']).toBeDefined();
    expect(newState.messagesByChannel['session-abc']).toHaveLength(1);
    expect(newState.messagesByChannel['session-abc'][0]).toEqual(message);
  });

  test('adds message to existing channel', () => {
    const existingMessage = {
      id: 'msg-1',
      message: 'Hello',
      createdAt: '2026-01-26T12:00:00Z'
    };
    const state = {
      messagesByChannel: {
        'session-abc': [existingMessage]
      },
      unreadCounts: {},
      isWindowOpen: false,
      activeChannel: null
    };
    const newMessage = {
      id: 'msg-2',
      senderId: 'user-2',
      senderName: 'Bob',
      message: 'Hi there',
      channel: 'session',
      sessionId: 'session-abc',
      createdAt: '2026-01-26T12:01:00Z'
    };
    const action = addMessageFromWebSocket(newMessage);
    const newState = sessionChatReducer(state, action);

    expect(newState.messagesByChannel['session-abc']).toHaveLength(2);
    expect(newState.messagesByChannel['session-abc'][1]).toEqual(newMessage);
  });

  test('deduplicates messages by msg_id', () => {
    const message = {
      id: 'msg-1',
      senderId: 'user-1',
      message: 'Hello',
      channel: 'session',
      sessionId: 'session-abc',
      createdAt: '2026-01-26T12:00:00Z'
    };
    const state = {
      messagesByChannel: {
        'session-abc': [message]
      },
      unreadCounts: {},
      isWindowOpen: false,
      activeChannel: null
    };
    // Try to add same message again
    const action = addMessageFromWebSocket(message);
    const newState = sessionChatReducer(state, action);

    expect(newState.messagesByChannel['session-abc']).toHaveLength(1);
  });

  test('sorts messages by createdAt ascending', () => {
    const state = {
      messagesByChannel: {
        'session-abc': [
          { id: 'msg-2', message: 'Second', createdAt: '2026-01-26T12:01:00Z' }
        ]
      },
      unreadCounts: {},
      isWindowOpen: false,
      activeChannel: null
    };
    const earlierMessage = {
      id: 'msg-1',
      senderId: 'user-1',
      message: 'First',
      channel: 'session',
      sessionId: 'session-abc',
      createdAt: '2026-01-26T12:00:00Z'
    };
    const action = addMessageFromWebSocket(earlierMessage);
    const newState = sessionChatReducer(state, action);

    expect(newState.messagesByChannel['session-abc']).toHaveLength(2);
    expect(newState.messagesByChannel['session-abc'][0].id).toBe('msg-1');
    expect(newState.messagesByChannel['session-abc'][1].id).toBe('msg-2');
  });

  test('increments unread count if window closed', () => {
    const state = {
      messagesByChannel: {},
      unreadCounts: {},
      isWindowOpen: false,
      activeChannel: 'session-abc'
    };
    const message = {
      id: 'msg-1',
      message: 'Hello',
      channel: 'session',
      sessionId: 'session-abc',
      createdAt: '2026-01-26T12:00:00Z'
    };
    const action = addMessageFromWebSocket(message);
    const newState = sessionChatReducer(state, action);

    expect(newState.unreadCounts['session-abc']).toBe(1);
  });

  test('increments unread count if viewing different channel', () => {
    const state = {
      messagesByChannel: {},
      unreadCounts: {},
      isWindowOpen: true,
      activeChannel: 'session-xyz'
    };
    const message = {
      id: 'msg-1',
      message: 'Hello',
      channel: 'session',
      sessionId: 'session-abc',
      createdAt: '2026-01-26T12:00:00Z'
    };
    const action = addMessageFromWebSocket(message);
    const newState = sessionChatReducer(state, action);

    expect(newState.unreadCounts['session-abc']).toBe(1);
  });

  test('does not increment unread count if window open and viewing same channel', () => {
    const state = {
      messagesByChannel: {},
      unreadCounts: {},
      isWindowOpen: true,
      activeChannel: 'session-abc'
    };
    const message = {
      id: 'msg-1',
      message: 'Hello',
      channel: 'session',
      sessionId: 'session-abc',
      createdAt: '2026-01-26T12:00:00Z'
    };
    const action = addMessageFromWebSocket(message);
    const newState = sessionChatReducer(state, action);

    expect(newState.unreadCounts['session-abc']).toBeUndefined();
  });

  test('handles global channel correctly', () => {
    const state = {
      messagesByChannel: {},
      unreadCounts: {},
      isWindowOpen: false,
      activeChannel: null
    };
    const message = {
      id: 'msg-1',
      message: 'Hello world',
      channel: 'global',
      createdAt: '2026-01-26T12:00:00Z'
    };
    const action = addMessageFromWebSocket(message);
    const newState = sessionChatReducer(state, action);

    expect(newState.messagesByChannel['global']).toBeDefined();
    expect(newState.messagesByChannel['global']).toHaveLength(1);
  });
});

describe('setActiveChannel', () => {
  test('sets active channel and type', () => {
    const state = {
      activeChannel: null,
      channelType: null
    };
    const action = setActiveChannel({
      channel: 'session-abc',
      channelType: 'session'
    });
    const newState = sessionChatReducer(state, action);

    expect(newState.activeChannel).toBe('session-abc');
    expect(newState.channelType).toBe('session');
  });

  test('updates existing active channel', () => {
    const state = {
      activeChannel: 'session-abc',
      channelType: 'session'
    };
    const action = setActiveChannel({
      channel: 'global',
      channelType: 'global'
    });
    const newState = sessionChatReducer(state, action);

    expect(newState.activeChannel).toBe('global');
    expect(newState.channelType).toBe('global');
  });
});

describe('openChatWindow', () => {
  test('opens window and resets unread count for active channel', () => {
    const state = {
      isWindowOpen: false,
      activeChannel: 'session-abc',
      unreadCounts: { 'session-abc': 5 },
      lastReadAt: {}
    };
    const action = openChatWindow();
    const newState = sessionChatReducer(state, action);

    expect(newState.isWindowOpen).toBe(true);
    expect(newState.unreadCounts['session-abc']).toBe(0);
    expect(newState.lastReadAt['session-abc']).toBeTruthy();
    // Verify it's an ISO timestamp
    expect(newState.lastReadAt['session-abc']).toMatch(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/);
  });

  test('opens window without active channel', () => {
    const state = {
      isWindowOpen: false,
      activeChannel: null,
      unreadCounts: {},
      lastReadAt: {}
    };
    const action = openChatWindow();
    const newState = sessionChatReducer(state, action);

    expect(newState.isWindowOpen).toBe(true);
    expect(Object.keys(newState.unreadCounts)).toHaveLength(0);
  });

  test('preserves other channel unread counts', () => {
    const state = {
      isWindowOpen: false,
      activeChannel: 'session-abc',
      unreadCounts: { 'session-abc': 5, 'global': 3 },
      lastReadAt: {}
    };
    const action = openChatWindow();
    const newState = sessionChatReducer(state, action);

    expect(newState.unreadCounts['session-abc']).toBe(0);
    expect(newState.unreadCounts['global']).toBe(3);
  });
});

describe('closeChatWindow', () => {
  test('closes window without changing unread counts', () => {
    const state = {
      isWindowOpen: true,
      unreadCounts: { 'session-abc': 0, 'global': 2 }
    };
    const action = closeChatWindow();
    const newState = sessionChatReducer(state, action);

    expect(newState.isWindowOpen).toBe(false);
    expect(newState.unreadCounts['session-abc']).toBe(0);
    expect(newState.unreadCounts['global']).toBe(2);
  });

  test('closes already closed window', () => {
    const state = {
      isWindowOpen: false,
      unreadCounts: {}
    };
    const action = closeChatWindow();
    const newState = sessionChatReducer(state, action);

    expect(newState.isWindowOpen).toBe(false);
  });
});

describe('markAsRead', () => {
  test('resets unread count for specified channel', () => {
    const state = {
      unreadCounts: { 'session-abc': 5, 'global': 2 },
      lastReadAt: {}
    };
    const action = markAsRead({ channel: 'session-abc' });
    const newState = sessionChatReducer(state, action);

    expect(newState.unreadCounts['session-abc']).toBe(0);
    expect(newState.unreadCounts['global']).toBe(2); // Unchanged
    expect(newState.lastReadAt['session-abc']).toBeTruthy();
    expect(newState.lastReadAt['session-abc']).toMatch(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/);
  });

  test('handles channel with no existing unread count', () => {
    const state = {
      unreadCounts: {},
      lastReadAt: {}
    };
    const action = markAsRead({ channel: 'session-abc' });
    const newState = sessionChatReducer(state, action);

    expect(newState.unreadCounts['session-abc']).toBe(0);
    expect(newState.lastReadAt['session-abc']).toBeTruthy();
  });

  test('updates lastReadAt timestamp', () => {
    const oldTimestamp = '2026-01-26T10:00:00.000Z';
    const state = {
      unreadCounts: { 'session-abc': 3 },
      lastReadAt: { 'session-abc': oldTimestamp }
    };
    const action = markAsRead({ channel: 'session-abc' });
    const newState = sessionChatReducer(state, action);

    expect(newState.lastReadAt['session-abc']).not.toBe(oldTimestamp);
    expect(new Date(newState.lastReadAt['session-abc']).getTime()).toBeGreaterThan(
      new Date(oldTimestamp).getTime()
    );
  });
});

describe('incrementUnreadCount', () => {
  test('increments unread count for channel', () => {
    const state = {
      unreadCounts: { 'session-abc': 3 }
    };
    const action = incrementUnreadCount({ channel: 'session-abc' });
    const newState = sessionChatReducer(state, action);

    expect(newState.unreadCounts['session-abc']).toBe(4);
  });

  test('initializes unread count if not exists', () => {
    const state = {
      unreadCounts: {}
    };
    const action = incrementUnreadCount({ channel: 'session-abc' });
    const newState = sessionChatReducer(state, action);

    expect(newState.unreadCounts['session-abc']).toBe(1);
  });

  test('handles multiple channels independently', () => {
    const state = {
      unreadCounts: { 'session-abc': 2, 'global': 5 }
    };
    const action = incrementUnreadCount({ channel: 'session-abc' });
    const newState = sessionChatReducer(state, action);

    expect(newState.unreadCounts['session-abc']).toBe(3);
    expect(newState.unreadCounts['global']).toBe(5); // Unchanged
  });
});

describe('setWindowPosition', () => {
  test('updates window position', () => {
    const state = {
      windowPosition: null
    };
    const action = setWindowPosition({ x: 300, y: 400 });
    const newState = sessionChatReducer(state, action);

    expect(newState.windowPosition).toEqual({ x: 300, y: 400 });
  });

  test('overwrites existing position', () => {
    const state = {
      windowPosition: { x: 100, y: 200 }
    };
    const action = setWindowPosition({ x: 500, y: 600 });
    const newState = sessionChatReducer(state, action);

    expect(newState.windowPosition).toEqual({ x: 500, y: 600 });
  });

  test('can set position to null', () => {
    const state = {
      windowPosition: { x: 300, y: 400 }
    };
    const action = setWindowPosition(null);
    const newState = sessionChatReducer(state, action);

    expect(newState.windowPosition).toBeNull();
  });
});

describe('fetchChatHistory async thunk', () => {
  // Import will be added in implementation phase
  // This is the RED phase - tests should fail
  let fetchChatHistory;

  test('sets loading state on pending', () => {
    // Mock the pending action type
    const action = {
      type: 'sessionChat/fetchChatHistory/pending',
      meta: { arg: { channel: 'session-abc' } }
    };
    const state = {
      fetchStatus: {},
      fetchError: {},
      messagesByChannel: {},
      unreadCounts: {},
      isWindowOpen: false,
      activeChannel: null
    };
    const newState = sessionChatReducer(state, action);
    expect(newState.fetchStatus['session-abc']).toBe('loading');
    expect(newState.fetchError['session-abc']).toBeNull();
  });

  test('adds messages on fulfilled', () => {
    const action = {
      type: 'sessionChat/fetchChatHistory/fulfilled',
      meta: { arg: { channel: 'session-abc' } },
      payload: {
        channel: 'session-abc',
        chats: [
          { id: 'msg-1', message: 'Hello', sender_id: 'user-1', created_at: '2026-01-26T12:00:00Z' }
        ],
        next: 20
      }
    };
    const state = {
      messagesByChannel: {},
      fetchStatus: { 'session-abc': 'loading' },
      fetchError: {},
      nextCursors: {},
      unreadCounts: {},
      isWindowOpen: false,
      activeChannel: null
    };
    const newState = sessionChatReducer(state, action);
    expect(newState.messagesByChannel['session-abc']).toHaveLength(1);
    expect(newState.fetchStatus['session-abc']).toBe('succeeded');
    expect(newState.nextCursors['session-abc']).toBe(20);
  });

  test('deduplicates messages on fulfilled', () => {
    // Test that fetched messages don't duplicate existing ones
    const state = {
      messagesByChannel: {
        'session-abc': [{ id: 'msg-1', message: 'Hello', createdAt: '2026-01-26T12:00:00Z' }]
      },
      fetchStatus: { 'session-abc': 'loading' },
      fetchError: {},
      nextCursors: {},
      unreadCounts: {},
      isWindowOpen: false,
      activeChannel: null
    };
    const action = {
      type: 'sessionChat/fetchChatHistory/fulfilled',
      meta: { arg: { channel: 'session-abc' } },
      payload: {
        channel: 'session-abc',
        chats: [
          { id: 'msg-1', user_id: 'user-1', user: { name: 'User' }, message: 'Hello', created_at: '2026-01-26T12:00:00Z', channel: 'session' }, // Duplicate
          { id: 'msg-2', user_id: 'user-2', user: { name: 'User2' }, message: 'World', created_at: '2026-01-26T12:01:00Z', channel: 'session' }  // New
        ],
        next: null
      }
    };
    const newState = sessionChatReducer(state, action);
    expect(newState.messagesByChannel['session-abc']).toHaveLength(2);
    expect(newState.messagesByChannel['session-abc'][0].id).toBe('msg-1');
    expect(newState.messagesByChannel['session-abc'][1].id).toBe('msg-2');
  });

  test('prepends older messages for pagination', () => {
    // When fetching older messages, they should be prepended (oldest first)
    const state = {
      messagesByChannel: {
        'session-abc': [
          { id: 'msg-3', message: 'Newest', createdAt: '2026-01-26T12:02:00Z' }
        ]
      },
      fetchStatus: { 'session-abc': 'loading' },
      fetchError: {},
      nextCursors: {},
      unreadCounts: {},
      isWindowOpen: false,
      activeChannel: null
    };
    const action = {
      type: 'sessionChat/fetchChatHistory/fulfilled',
      meta: { arg: { channel: 'session-abc', before: 3 } },
      payload: {
        channel: 'session-abc',
        chats: [
          { id: 'msg-1', message: 'Oldest', createdAt: '2026-01-26T12:00:00Z' },
          { id: 'msg-2', message: 'Middle', createdAt: '2026-01-26T12:01:00Z' }
        ],
        next: null
      }
    };
    const newState = sessionChatReducer(state, action);
    expect(newState.messagesByChannel['session-abc']).toHaveLength(3);
    expect(newState.messagesByChannel['session-abc'][0].id).toBe('msg-1');
    expect(newState.messagesByChannel['session-abc'][1].id).toBe('msg-2');
    expect(newState.messagesByChannel['session-abc'][2].id).toBe('msg-3');
  });

  test('sets error state on rejected', () => {
    const action = {
      type: 'sessionChat/fetchChatHistory/rejected',
      meta: { arg: { channel: 'session-abc' } },
      error: { message: 'Not Found' }
    };
    const state = {
      fetchStatus: { 'session-abc': 'loading' },
      fetchError: {},
      messagesByChannel: {},
      unreadCounts: {},
      isWindowOpen: false,
      activeChannel: null
    };
    const newState = sessionChatReducer(state, action);
    expect(newState.fetchStatus['session-abc']).toBe('failed');
    expect(newState.fetchError['session-abc']).toBe('Not Found');
  });

  test('handles null next cursor', () => {
    const action = {
      type: 'sessionChat/fetchChatHistory/fulfilled',
      meta: { arg: { channel: 'session-abc' } },
      payload: {
        channel: 'session-abc',
        chats: [
          { id: 'msg-1', message: 'Hello', createdAt: '2026-01-26T12:00:00Z' }
        ],
        next: null
      }
    };
    const state = {
      messagesByChannel: {},
      fetchStatus: { 'session-abc': 'loading' },
      fetchError: {},
      nextCursors: {},
      unreadCounts: {},
      isWindowOpen: false,
      activeChannel: null
    };
    const newState = sessionChatReducer(state, action);
    expect(newState.nextCursors['session-abc']).toBeNull();
  });

  test('transforms music_notation nested object to attachment fields', () => {
    // Mock REST API response with attachment message
    const action = {
      type: 'sessionChat/fetchChatHistory/fulfilled',
      meta: { arg: { channel: 'session-abc' } },
      payload: {
        channel: 'session-abc',
        chats: [
          {
            id: 'msg-1',
            user_id: 'user-1',
            user: { name: 'Test User' },
            message: null,  // Attachment messages may have no text
            created_at: '2024-01-15T12:00:00Z',
            channel: 'session',
            purpose: 'Notation File',
            music_notation: {
              id: 'notation-uuid-123',
              file_name: 'sheet-music.pdf',
              attachment_type: 'notation'
            }
          }
        ],
        next: null
      }
    };
    const state = {
      messagesByChannel: {},
      fetchStatus: { 'session-abc': 'loading' },
      fetchError: {},
      nextCursors: {},
      unreadCounts: {},
      isWindowOpen: false,
      activeChannel: null
    };
    const newState = sessionChatReducer(state, action);

    expect(newState.messagesByChannel['session-abc']).toHaveLength(1);
    const message = newState.messagesByChannel['session-abc'][0];
    expect(message.attachmentId).toBe('notation-uuid-123');
    expect(message.attachmentName).toBe('sheet-music.pdf');
    expect(message.attachmentType).toBe('notation');
    expect(message.purpose).toBe('Notation File');
    expect(message.attachmentSize).toBeNull(); // Not available from REST API
  });
});

describe('sendMessage async thunk', () => {
  // Import will be added in implementation phase
  // This is the RED phase - tests should fail
  let sendMessage;

  test('sets loading state on pending', () => {
    const action = {
      type: 'sessionChat/sendMessage/pending',
      meta: {
        arg: {
          channel: 'session-abc',
          message: 'Hello world',
          optimisticId: 'temp-1',
          userId: 'user-1',
          userName: 'John Doe'
        }
      }
    };
    const state = {
      sendStatus: 'idle',
      sendError: null,
      messagesByChannel: { 'session-abc': [] },
      unreadCounts: {},
      isWindowOpen: false,
      activeChannel: null
    };
    const newState = sessionChatReducer(state, action);
    expect(newState.sendStatus).toBe('loading');
    expect(newState.sendError).toBeNull();
  });

  test('adds message optimistically on pending', () => {
    const action = {
      type: 'sessionChat/sendMessage/pending',
      meta: {
        arg: {
          channel: 'session-abc',
          message: 'Hello world',
          optimisticId: 'temp-1',
          userId: 'user-1',
          userName: 'John Doe'
        }
      }
    };
    const state = {
      messagesByChannel: { 'session-abc': [] },
      sendStatus: 'idle',
      sendError: null,
      unreadCounts: {},
      isWindowOpen: false,
      activeChannel: null
    };
    const newState = sessionChatReducer(state, action);
    expect(newState.messagesByChannel['session-abc']).toHaveLength(1);
    expect(newState.messagesByChannel['session-abc'][0].id).toBe('temp-1');
    expect(newState.messagesByChannel['session-abc'][0].message).toBe('Hello world');
    expect(newState.messagesByChannel['session-abc'][0].senderId).toBe('user-1');
    expect(newState.messagesByChannel['session-abc'][0].senderName).toBe('John Doe');
    expect(newState.messagesByChannel['session-abc'][0].isOptimistic).toBe(true);
  });

  test('initializes channel if not exists on pending', () => {
    const action = {
      type: 'sessionChat/sendMessage/pending',
      meta: {
        arg: {
          channel: 'new-channel',
          message: 'First message',
          optimisticId: 'temp-1',
          userId: 'user-1',
          userName: 'John'
        }
      }
    };
    const state = {
      messagesByChannel: {},
      sendStatus: 'idle',
      sendError: null,
      unreadCounts: {},
      isWindowOpen: false,
      activeChannel: null
    };
    const newState = sessionChatReducer(state, action);
    expect(newState.messagesByChannel['new-channel']).toBeDefined();
    expect(newState.messagesByChannel['new-channel']).toHaveLength(1);
  });

  test('replaces optimistic message with real one on fulfilled', () => {
    const state = {
      messagesByChannel: {
        'session-abc': [
          { id: 'temp-1', message: 'Hello world', senderId: 'user-1', isOptimistic: true }
        ]
      },
      sendStatus: 'loading',
      sendError: null,
      unreadCounts: {},
      isWindowOpen: false,
      activeChannel: null
    };
    const action = {
      type: 'sessionChat/sendMessage/fulfilled',
      meta: {
        arg: { optimisticId: 'temp-1', channel: 'session-abc' }
      },
      payload: {
        id: 'msg-real',
        message: 'Hello world',
        user_id: 'user-1',
        user: { name: 'John Doe' },
        created_at: '2026-01-26T12:00:00Z',
        channel: 'session'
      }
    };
    const newState = sessionChatReducer(state, action);
    expect(newState.messagesByChannel['session-abc']).toHaveLength(1);
    expect(newState.messagesByChannel['session-abc'][0].id).toBe('msg-real');
    expect(newState.messagesByChannel['session-abc'][0].isOptimistic).toBeUndefined();
    expect(newState.sendStatus).toBe('succeeded');
  });

  test('removes optimistic message on rejected', () => {
    const state = {
      messagesByChannel: {
        'session-abc': [
          { id: 'temp-1', message: 'Hello world', senderId: 'user-1', isOptimistic: true }
        ]
      },
      sendStatus: 'loading',
      sendError: null,
      unreadCounts: {},
      isWindowOpen: false,
      activeChannel: null
    };
    const action = {
      type: 'sessionChat/sendMessage/rejected',
      meta: {
        arg: { optimisticId: 'temp-1', channel: 'session-abc' }
      },
      error: { message: 'Network error' }
    };
    const newState = sessionChatReducer(state, action);
    expect(newState.messagesByChannel['session-abc']).toHaveLength(0);
    expect(newState.sendStatus).toBe('failed');
    expect(newState.sendError).toBe('Network error');
  });

  test('keeps other messages when replacing optimistic message', () => {
    const state = {
      messagesByChannel: {
        'session-abc': [
          { id: 'msg-1', message: 'Existing', createdAt: '2026-01-26T11:00:00Z' },
          { id: 'temp-1', message: 'Optimistic', senderId: 'user-1', isOptimistic: true }
        ]
      },
      sendStatus: 'loading',
      sendError: null,
      unreadCounts: {},
      isWindowOpen: false,
      activeChannel: null
    };
    const action = {
      type: 'sessionChat/sendMessage/fulfilled',
      meta: {
        arg: { optimisticId: 'temp-1', channel: 'session-abc' }
      },
      payload: {
        id: 'msg-real',
        message: 'Optimistic',
        user_id: 'user-1',
        user: { name: 'User 1' },
        created_at: '2026-01-26T12:00:00Z',
        channel: 'session'
      }
    };
    const newState = sessionChatReducer(state, action);
    expect(newState.messagesByChannel['session-abc']).toHaveLength(2);
    expect(newState.messagesByChannel['session-abc'][0].id).toBe('msg-1');
    expect(newState.messagesByChannel['session-abc'][1].id).toBe('msg-real');
  });

  test('keeps other messages when removing optimistic message', () => {
    const state = {
      messagesByChannel: {
        'session-abc': [
          { id: 'msg-1', message: 'Existing', createdAt: '2026-01-26T11:00:00Z' },
          { id: 'temp-1', message: 'Failed', senderId: 'user-1', isOptimistic: true }
        ]
      },
      sendStatus: 'loading',
      sendError: null,
      unreadCounts: {},
      isWindowOpen: false,
      activeChannel: null
    };
    const action = {
      type: 'sessionChat/sendMessage/rejected',
      meta: {
        arg: { optimisticId: 'temp-1', channel: 'session-abc' }
      },
      error: { message: 'Failed to send' }
    };
    const newState = sessionChatReducer(state, action);
    expect(newState.messagesByChannel['session-abc']).toHaveLength(1);
    expect(newState.messagesByChannel['session-abc'][0].id).toBe('msg-1');
  });
});

describe('sessionChat integration', () => {
  test('complete message flow: receive → set active → open window → mark read', () => {
    const initialState = {
      messagesByChannel: {},
      activeChannel: null,
      channelType: null,
      unreadCounts: {},
      lastReadAt: {},
      isWindowOpen: false,
      windowPosition: null
    };

    // Step 1: Receive message (window closed, no active channel)
    let state = sessionChatReducer(
      initialState,
      addMessageFromWebSocket({
        id: 'msg-1',
        senderId: 'user-1',
        message: 'Hello',
        channel: 'session',
        sessionId: 'session-abc',
        createdAt: '2026-01-26T12:00:00Z'
      })
    );
    expect(state.messagesByChannel['session-abc']).toHaveLength(1);
    expect(state.unreadCounts['session-abc']).toBe(1);

    // Step 2: Set active channel
    state = sessionChatReducer(
      state,
      setActiveChannel({ channel: 'session-abc', channelType: 'session' })
    );
    expect(state.activeChannel).toBe('session-abc');
    expect(state.channelType).toBe('session');

    // Step 3: Open window (should mark as read)
    state = sessionChatReducer(state, openChatWindow());
    expect(state.isWindowOpen).toBe(true);
    expect(state.unreadCounts['session-abc']).toBe(0);
    expect(state.lastReadAt['session-abc']).toBeTruthy();

    // Step 4: Receive another message while window open and viewing channel
    state = sessionChatReducer(
      state,
      addMessageFromWebSocket({
        id: 'msg-2',
        senderId: 'user-2',
        message: 'Hi back',
        channel: 'session',
        sessionId: 'session-abc',
        createdAt: '2026-01-26T12:01:00Z'
      })
    );
    expect(state.messagesByChannel['session-abc']).toHaveLength(2);
    expect(state.unreadCounts['session-abc']).toBe(0); // Should NOT increment

    // Step 5: Close window
    state = sessionChatReducer(state, closeChatWindow());
    expect(state.isWindowOpen).toBe(false);

    // Step 6: Receive message while window closed
    state = sessionChatReducer(
      state,
      addMessageFromWebSocket({
        id: 'msg-3',
        senderId: 'user-1',
        message: 'Are you there?',
        channel: 'session',
        sessionId: 'session-abc',
        createdAt: '2026-01-26T12:02:00Z'
      })
    );
    expect(state.messagesByChannel['session-abc']).toHaveLength(3);
    expect(state.unreadCounts['session-abc']).toBe(1); // Should increment
  });

  test('multi-channel flow: messages in different channels', () => {
    const initialState = {
      messagesByChannel: {},
      activeChannel: null,
      channelType: null,
      unreadCounts: {},
      lastReadAt: {},
      isWindowOpen: true,
      windowPosition: null
    };

    // Receive message in session-abc (window open, but no active channel)
    let state = sessionChatReducer(
      initialState,
      addMessageFromWebSocket({
        id: 'msg-1',
        message: 'Session message',
        channel: 'session',
        sessionId: 'session-abc',
        createdAt: '2026-01-26T12:00:00Z'
      })
    );
    expect(state.unreadCounts['session-abc']).toBe(1); // Increments (no active channel)

    // Set active to session-abc
    state = sessionChatReducer(
      state,
      setActiveChannel({ channel: 'session-abc', channelType: 'session' })
    );

    // Receive message in global (window open, but viewing session-abc)
    state = sessionChatReducer(
      state,
      addMessageFromWebSocket({
        id: 'msg-2',
        message: 'Global message',
        channel: 'global',
        createdAt: '2026-01-26T12:01:00Z'
      })
    );
    expect(state.unreadCounts['global']).toBe(1); // Increments (different channel)

    // Mark global as read
    state = sessionChatReducer(state, markAsRead({ channel: 'global' }));
    expect(state.unreadCounts['global']).toBe(0);
    expect(state.unreadCounts['session-abc']).toBe(1); // Unchanged
  });

  test('message deduplication prevents double unread increment', () => {
    const initialState = {
      messagesByChannel: {},
      activeChannel: null,
      channelType: null,
      unreadCounts: {},
      lastReadAt: {},
      isWindowOpen: false,
      windowPosition: null
    };

    const message = {
      id: 'msg-1',
      message: 'Hello',
      channel: 'session',
      sessionId: 'session-abc',
      createdAt: '2026-01-26T12:00:00Z'
    };

    // Add message first time
    let state = sessionChatReducer(initialState, addMessageFromWebSocket(message));
    expect(state.unreadCounts['session-abc']).toBe(1);

    // Try to add same message again (WebSocket + REST scenario)
    state = sessionChatReducer(state, addMessageFromWebSocket(message));
    expect(state.unreadCounts['session-abc']).toBe(1); // Should NOT increment again
    expect(state.messagesByChannel['session-abc']).toHaveLength(1);
  });
});

describe('sessionChat selectors', () => {
  let mockState;

  beforeEach(() => {
    mockState = {
      sessionChat: {
        messagesByChannel: {
          'session-abc': [
            { id: 'msg-1', message: 'Hello', createdAt: '2026-01-26T12:00:00Z' },
            { id: 'msg-2', message: 'World', createdAt: '2026-01-26T12:01:00Z' }
          ],
          'global': [
            { id: 'msg-3', message: 'Global msg', createdAt: '2026-01-26T12:02:00Z' }
          ]
        },
        activeChannel: 'session-abc',
        unreadCounts: {
          'session-abc': 0,
          'global': 5,
          'session-xyz': 3
        },
        isWindowOpen: true,
        fetchStatus: {
          'session-abc': 'succeeded',
          'global': 'loading'
        },
        sendStatus: 'idle',
        sendError: null
      }
    };
  });

  describe('selectChatMessages', () => {
    let selectChatMessages;

    beforeAll(() => {
      // Import will be added after implementation
      try {
        const slice = require('../sessionChatSlice');
        selectChatMessages = slice.selectChatMessages;
      } catch (e) {
        // Selector not yet exported
      }
    });

    test('returns messages for specified channel', () => {
      if (!selectChatMessages) {
        expect(true).toBe(false); // RED phase
        return;
      }
      const messages = selectChatMessages(mockState, 'session-abc');
      expect(messages).toHaveLength(2);
      expect(messages[0].id).toBe('msg-1');
      expect(messages[1].id).toBe('msg-2');
    });

    test('returns empty array for non-existent channel', () => {
      if (!selectChatMessages) {
        expect(true).toBe(false); // RED phase
        return;
      }
      const messages = selectChatMessages(mockState, 'session-nonexistent');
      expect(messages).toEqual([]);
    });

    test('memoizes result for same channel', () => {
      if (!selectChatMessages) {
        expect(true).toBe(false); // RED phase
        return;
      }
      const result1 = selectChatMessages(mockState, 'session-abc');
      const result2 = selectChatMessages(mockState, 'session-abc');
      expect(result1).toBe(result2); // Same reference
    });
  });

  describe('selectUnreadCount', () => {
    let selectUnreadCount;

    beforeAll(() => {
      try {
        const slice = require('../sessionChatSlice');
        selectUnreadCount = slice.selectUnreadCount;
      } catch (e) {
        // Selector not yet exported
      }
    });

    test('returns unread count for channel', () => {
      if (!selectUnreadCount) {
        expect(true).toBe(false); // RED phase
        return;
      }
      const count = selectUnreadCount(mockState, 'global');
      expect(count).toBe(5);
    });

    test('returns 0 for channel with no unread', () => {
      if (!selectUnreadCount) {
        expect(true).toBe(false); // RED phase
        return;
      }
      const count = selectUnreadCount(mockState, 'session-abc');
      expect(count).toBe(0);
    });

    test('returns 0 for non-existent channel', () => {
      if (!selectUnreadCount) {
        expect(true).toBe(false); // RED phase
        return;
      }
      const count = selectUnreadCount(mockState, 'nonexistent');
      expect(count).toBe(0);
    });
  });

  describe('selectTotalUnreadCount', () => {
    let selectTotalUnreadCount;

    beforeAll(() => {
      try {
        const slice = require('../sessionChatSlice');
        selectTotalUnreadCount = slice.selectTotalUnreadCount;
      } catch (e) {
        // Selector not yet exported
      }
    });

    test('sums all unread counts across channels', () => {
      if (!selectTotalUnreadCount) {
        expect(true).toBe(false); // RED phase
        return;
      }
      const total = selectTotalUnreadCount(mockState);
      expect(total).toBe(8); // 0 + 5 + 3
    });

    test('returns 0 when no unread messages', () => {
      if (!selectTotalUnreadCount) {
        expect(true).toBe(false); // RED phase
        return;
      }
      const emptyState = {
        sessionChat: { unreadCounts: {} }
      };
      const total = selectTotalUnreadCount(emptyState);
      expect(total).toBe(0);
    });

    test('memoizes result', () => {
      if (!selectTotalUnreadCount) {
        expect(true).toBe(false); // RED phase
        return;
      }
      const result1 = selectTotalUnreadCount(mockState);
      const result2 = selectTotalUnreadCount(mockState);
      expect(result1).toBe(result2);
    });
  });

  describe('selectIsChatWindowOpen', () => {
    let selectIsChatWindowOpen;

    beforeAll(() => {
      try {
        const slice = require('../sessionChatSlice');
        selectIsChatWindowOpen = slice.selectIsChatWindowOpen;
      } catch (e) {
        // Selector not yet exported
      }
    });

    test('returns window open state', () => {
      if (!selectIsChatWindowOpen) {
        expect(true).toBe(false); // RED phase
        return;
      }
      const isOpen = selectIsChatWindowOpen(mockState);
      expect(isOpen).toBe(true);
    });
  });

  describe('selectActiveChannel', () => {
    let selectActiveChannel;

    beforeAll(() => {
      try {
        const slice = require('../sessionChatSlice');
        selectActiveChannel = slice.selectActiveChannel;
      } catch (e) {
        // Selector not yet exported
      }
    });

    test('returns active channel', () => {
      if (!selectActiveChannel) {
        expect(true).toBe(false); // RED phase
        return;
      }
      const channel = selectActiveChannel(mockState);
      expect(channel).toBe('session-abc');
    });
  });

  describe('selectFetchStatus', () => {
    let selectFetchStatus;

    beforeAll(() => {
      try {
        const slice = require('../sessionChatSlice');
        selectFetchStatus = slice.selectFetchStatus;
      } catch (e) {
        // Selector not yet exported
      }
    });

    test('returns fetch status for channel', () => {
      if (!selectFetchStatus) {
        expect(true).toBe(false); // RED phase
        return;
      }
      const status = selectFetchStatus(mockState, 'session-abc');
      expect(status).toBe('succeeded');
    });

    test('returns idle for non-existent channel', () => {
      if (!selectFetchStatus) {
        expect(true).toBe(false); // RED phase
        return;
      }
      const status = selectFetchStatus(mockState, 'nonexistent');
      expect(status).toBe('idle');
    });
  });

  describe('selectSendStatus', () => {
    let selectSendStatus;

    beforeAll(() => {
      try {
        const slice = require('../sessionChatSlice');
        selectSendStatus = slice.selectSendStatus;
      } catch (e) {
        // Selector not yet exported
      }
    });

    test('returns send status', () => {
      if (!selectSendStatus) {
        expect(true).toBe(false); // RED phase
        return;
      }
      const status = selectSendStatus(mockState);
      expect(status).toBe('idle');
    });
  });

  describe('selectSendError', () => {
    let selectSendError;

    beforeAll(() => {
      try {
        const slice = require('../sessionChatSlice');
        selectSendError = slice.selectSendError;
      } catch (e) {
        // Selector not yet exported
      }
    });

    test('returns send error', () => {
      if (!selectSendError) {
        expect(true).toBe(false); // RED phase
        return;
      }
      const error = selectSendError(mockState);
      expect(error).toBeNull();
    });
  });
});

// Phase 13 Plan 02: Upload state management tests
describe('setUploadStatus reducer', () => {
  test('updates upload status', () => {
    const state = {
      uploadState: {
        status: 'idle',
        progress: 0,
        error: null,
        fileName: null
      }
    };
    const action = setUploadStatus({ status: 'uploading', fileName: 'test.pdf' });
    const newState = sessionChatReducer(state, action);

    expect(newState.uploadState.status).toBe('uploading');
    expect(newState.uploadState.fileName).toBe('test.pdf');
  });

  test('updates progress when provided', () => {
    const state = {
      uploadState: {
        status: 'uploading',
        progress: 0,
        error: null,
        fileName: 'test.pdf'
      }
    };
    const action = setUploadStatus({ progress: 50 });
    const newState = sessionChatReducer(state, action);

    expect(newState.uploadState.progress).toBe(50);
    expect(newState.uploadState.status).toBe('uploading');
  });

  test('updates error when provided', () => {
    const state = {
      uploadState: {
        status: 'uploading',
        progress: 50,
        error: null,
        fileName: 'test.pdf'
      }
    };
    const action = setUploadStatus({ status: 'error', error: 'Upload failed' });
    const newState = sessionChatReducer(state, action);

    expect(newState.uploadState.status).toBe('error');
    expect(newState.uploadState.error).toBe('Upload failed');
  });

  test('preserves other fields when updating one field', () => {
    const state = {
      uploadState: {
        status: 'uploading',
        progress: 25,
        error: null,
        fileName: 'document.pdf'
      }
    };
    const action = setUploadStatus({ progress: 75 });
    const newState = sessionChatReducer(state, action);

    expect(newState.uploadState.progress).toBe(75);
    expect(newState.uploadState.status).toBe('uploading');
    expect(newState.uploadState.fileName).toBe('document.pdf');
  });
});

describe('clearUploadError reducer', () => {
  test('clears error and resets status to idle', () => {
    const state = {
      uploadState: {
        status: 'error',
        progress: 0,
        error: 'Upload failed',
        fileName: 'test.pdf'
      }
    };
    const action = clearUploadError();
    const newState = sessionChatReducer(state, action);

    expect(newState.uploadState.error).toBeNull();
    expect(newState.uploadState.status).toBe('idle');
  });

  test('preserves fileName when clearing error', () => {
    const state = {
      uploadState: {
        status: 'error',
        progress: 0,
        error: 'File too large',
        fileName: 'large-file.pdf'
      }
    };
    const action = clearUploadError();
    const newState = sessionChatReducer(state, action);

    expect(newState.uploadState.fileName).toBe('large-file.pdf');
    expect(newState.uploadState.error).toBeNull();
  });
});

describe('uploadAttachment async thunk', () => {
  test('sets uploading state on pending', () => {
    const mockFile = new File(['content'], 'test.pdf', { type: 'application/pdf' });
    const action = {
      type: 'sessionChat/uploadAttachment/pending',
      meta: {
        arg: {
          file: mockFile,
          sessionId: 'session-123',
          clientId: 'client-456'
        }
      }
    };
    const state = {
      uploadState: {
        status: 'idle',
        progress: 0,
        error: null,
        fileName: null
      }
    };
    const newState = sessionChatReducer(state, action);

    expect(newState.uploadState.status).toBe('uploading');
    expect(newState.uploadState.error).toBeNull();
    expect(newState.uploadState.fileName).toBe('test.pdf');
    expect(newState.uploadState.progress).toBe(0);
  });

  test('resets to idle on fulfilled', () => {
    const state = {
      uploadState: {
        status: 'uploading',
        progress: 0,
        error: null,
        fileName: 'test.pdf'
      }
    };
    const action = {
      type: 'sessionChat/uploadAttachment/fulfilled',
      payload: {
        id: 'notation-789',
        file_name: 'test.pdf'
      }
    };
    const newState = sessionChatReducer(state, action);

    expect(newState.uploadState.status).toBe('idle');
    expect(newState.uploadState.fileName).toBeNull();
    expect(newState.uploadState.progress).toBe(0);
  });

  test('sets error state on rejected', () => {
    const state = {
      uploadState: {
        status: 'uploading',
        progress: 0,
        error: null,
        fileName: 'test.pdf'
      }
    };
    const action = {
      type: 'sessionChat/uploadAttachment/rejected',
      error: { message: 'Upload failed' }
    };
    const newState = sessionChatReducer(state, action);

    expect(newState.uploadState.status).toBe('error');
    expect(newState.uploadState.error).toBe('Upload failed');
    expect(newState.uploadState.progress).toBe(0);
  });

  test('handles 413 error with custom message', () => {
    const state = {
      uploadState: {
        status: 'uploading',
        progress: 0,
        error: null,
        fileName: 'large.pdf'
      }
    };
    const action = {
      type: 'sessionChat/uploadAttachment/rejected',
      error: { message: 'File too large - maximum 10 MB' }
    };
    const newState = sessionChatReducer(state, action);

    expect(newState.uploadState.error).toBe('File too large - maximum 10 MB');
  });

  test('handles 422 error with custom message', () => {
    const state = {
      uploadState: {
        status: 'uploading',
        progress: 0,
        error: null,
        fileName: 'invalid.exe'
      }
    };
    const action = {
      type: 'sessionChat/uploadAttachment/rejected',
      error: { message: 'Invalid file type or format' }
    };
    const newState = sessionChatReducer(state, action);

    expect(newState.uploadState.error).toBe('Invalid file type or format');
  });
});

// Phase 13 Plan 02: Upload selectors tests
describe('upload selectors', () => {
  let mockState;
  let selectUploadStatus;
  let selectUploadError;
  let selectUploadProgress;
  let selectUploadFileName;
  let selectIsUploading;

  beforeEach(() => {
    mockState = {
      sessionChat: {
        uploadState: {
          status: 'uploading',
          progress: 50,
          error: null,
          fileName: 'test.pdf'
        }
      }
    };

    try {
      const slice = require('../sessionChatSlice');
      selectUploadStatus = slice.selectUploadStatus;
      selectUploadError = slice.selectUploadError;
      selectUploadProgress = slice.selectUploadProgress;
      selectUploadFileName = slice.selectUploadFileName;
      selectIsUploading = slice.selectIsUploading;
    } catch (e) {
      // Selectors not yet exported
    }
  });

  describe('selectUploadStatus', () => {
    test('returns upload status', () => {
      if (!selectUploadStatus) {
        expect(true).toBe(false); // RED phase
        return;
      }
      const status = selectUploadStatus(mockState);
      expect(status).toBe('uploading');
    });
  });

  describe('selectUploadError', () => {
    test('returns upload error', () => {
      if (!selectUploadError) {
        expect(true).toBe(false); // RED phase
        return;
      }
      const error = selectUploadError(mockState);
      expect(error).toBeNull();
    });

    test('returns error message when present', () => {
      if (!selectUploadError) {
        expect(true).toBe(false); // RED phase
        return;
      }
      mockState.sessionChat.uploadState.error = 'Upload failed';
      const error = selectUploadError(mockState);
      expect(error).toBe('Upload failed');
    });
  });

  describe('selectUploadProgress', () => {
    test('returns upload progress', () => {
      if (!selectUploadProgress) {
        expect(true).toBe(false); // RED phase
        return;
      }
      const progress = selectUploadProgress(mockState);
      expect(progress).toBe(50);
    });
  });

  describe('selectUploadFileName', () => {
    test('returns upload file name', () => {
      if (!selectUploadFileName) {
        expect(true).toBe(false); // RED phase
        return;
      }
      const fileName = selectUploadFileName(mockState);
      expect(fileName).toBe('test.pdf');
    });
  });

  describe('selectIsUploading', () => {
    test('returns true when status is uploading', () => {
      if (!selectIsUploading) {
        expect(true).toBe(false); // RED phase
        return;
      }
      const isUploading = selectIsUploading(mockState);
      expect(isUploading).toBe(true);
    });

    test('returns false when status is idle', () => {
      if (!selectIsUploading) {
        expect(true).toBe(false); // RED phase
        return;
      }
      mockState.sessionChat.uploadState.status = 'idle';
      const isUploading = selectIsUploading(mockState);
      expect(isUploading).toBe(false);
    });

    test('returns false when status is error', () => {
      if (!selectIsUploading) {
        expect(true).toBe(false); // RED phase
        return;
      }
      mockState.sessionChat.uploadState.status = 'error';
      const isUploading = selectIsUploading(mockState);
      expect(isUploading).toBe(false);
    });
  });
});
