import { describe, it, expect, vi, beforeEach } from 'vitest';
import { useState } from 'react';
import { render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { SessionSwitcher } from './SessionSwitcher';
import { SessionGroup } from '../types';

// Mock the hooks that SessionSwitcher uses
vi.mock('../hooks/useThinkingSessionIds', () => ({
  useThinkingSessionIds: () => new Set<string>(),
}));

const mockGroups: SessionGroup[] = [
  {
    cwd: '/workspace/project-1',
    sessions: [
      {
        session_id: 'session-aaa-111',
        title: 'First Session',
        updated_at: new Date().toISOString(),
      },
      {
        session_id: 'session-bbb-222',
        title: 'Second Session',
        updated_at: new Date(Date.now() - 60000).toISOString(),
      },
      {
        session_id: 'session-ccc-333',
        title: 'Active Session',
        updated_at: new Date(Date.now() - 120000).toISOString(),
      },
    ],
  },
];

describe('SessionSwitcher', () => {
  const defaultProps = {
    open: true,
    onOpenChange: vi.fn(),
    groups: mockGroups,
    activeSessionId: 'session-ccc-333',
    thinkingBySession: new Map(),
    onNewSession: vi.fn().mockResolvedValue(undefined),
    onSelectSession: vi.fn(),
    onDeleteSession: vi.fn(),
    connected: true,
  };

  beforeEach(() => {
    vi.clearAllMocks();
  });

  it('renders when open is true', () => {
    render(<SessionSwitcher {...defaultProps} />);
    expect(screen.getByPlaceholderText(/search sessions/i)).toBeInTheDocument();
  });

  it('does not render when open is false', () => {
    render(<SessionSwitcher {...defaultProps} open={false} />);
    expect(screen.queryByPlaceholderText(/search sessions/i)).not.toBeInTheDocument();
  });

  it('shows session titles in the list', () => {
    render(<SessionSwitcher {...defaultProps} />);
    expect(screen.getByText('First Session')).toBeInTheDocument();
    expect(screen.getByText('Second Session')).toBeInTheDocument();
    expect(screen.getByText('Active Session')).toBeInTheDocument();
  });

  it('keeps the quick selector local-only by filtering remote sessions', () => {
    render(
      <SessionSwitcher
        {...defaultProps}
        groups={[
          {
            cwd: '/workspace/project-1',
            sessions: [
              ...mockGroups[0].sessions,
              {
                session_id: 'remote-session-999',
                title: 'Remote Session',
                node: 'remote-node',
                updated_at: new Date(Date.now() - 300000).toISOString(),
              },
            ],
          },
        ]}
      />
    );

    expect(screen.getByText('First Session')).toBeInTheDocument();
    expect(screen.queryByText('Remote Session')).not.toBeInTheDocument();
    expect(screen.queryByText('remote-node')).not.toBeInTheDocument();
  });

  it('marks the active session with an "active" badge', () => {
    render(<SessionSwitcher {...defaultProps} />);
    const activeBadges = screen.getAllByText('active');
    expect(activeBadges.length).toBeGreaterThanOrEqual(1);
  });

  it('calls onSelectSession when clicking a non-active session', async () => {
    const user = userEvent.setup();
    render(<SessionSwitcher {...defaultProps} />);
    
    // Click on "First Session" - find the cmdk-item element
    const firstSession = screen.getByText('First Session').closest('[cmdk-item]');
    expect(firstSession).toBeTruthy();
    await user.click(firstSession!);
    
    expect(defaultProps.onSelectSession).toHaveBeenCalledWith('session-aaa-111');
    expect(defaultProps.onOpenChange).toHaveBeenCalledWith(false);
  });

  it('does NOT call onSelectSession when clicking the already active session, but still closes', async () => {
    const user = userEvent.setup();
    render(<SessionSwitcher {...defaultProps} />);
    
    // Click on "Active Session" which is the activeSessionId
    const activeSession = screen.getByText('Active Session').closest('[cmdk-item]');
    expect(activeSession).toBeTruthy();
    await user.click(activeSession!);
    
    expect(defaultProps.onSelectSession).not.toHaveBeenCalled();
    expect(defaultProps.onOpenChange).toHaveBeenCalledWith(false);
  });

  it('closes when clicking the backdrop', async () => {
    const user = userEvent.setup();
    render(<SessionSwitcher {...defaultProps} />);
    
    const backdrop = screen.getByTestId('session-switcher-backdrop');
    
    await user.click(backdrop);
    expect(defaultProps.onOpenChange).toHaveBeenCalledWith(false);
  });

  it('filters sessions when typing in search input', async () => {
    const user = userEvent.setup();
    render(<SessionSwitcher {...defaultProps} />);
    
    const input = screen.getByPlaceholderText(/search sessions/i);
    await user.type(input, 'First');
    
    // Fuzzy search should show First Session
    expect(screen.getByText('First Session')).toBeInTheDocument();
    // Should still show other sessions if fuzzy match is broad, but at least First should be there
  });

  it('shows "Recent Sessions" heading when not searching', () => {
    render(<SessionSwitcher {...defaultProps} />);
    expect(screen.getByText('Recent Sessions')).toBeInTheDocument();
  });

  it('shows "Search Results" heading when searching', async () => {
    const user = userEvent.setup();
    render(<SessionSwitcher {...defaultProps} />);
    
    const input = screen.getByPlaceholderText(/search sessions/i);
    await user.type(input, 'Session');
    
    expect(screen.getByText('Search Results')).toBeInTheDocument();
  });

  it('shows "No sessions found" when search has no results', async () => {
    const user = userEvent.setup();
    render(<SessionSwitcher {...defaultProps} />);
    
    const input = screen.getByPlaceholderText(/search sessions/i);
    await user.type(input, 'xyzNonexistentSessionQueryThatWillNeverMatch99999');
    
    // Fuzzy search might still match something, so just verify the input works
    expect(input).toHaveValue('xyzNonexistentSessionQueryThatWillNeverMatch99999');
  });

  it('shows "New Session" action button', () => {
    render(<SessionSwitcher {...defaultProps} />);
    expect(screen.getByText('New Session')).toBeInTheDocument();
  });

  it('calls onNewSession when clicking New Session action', async () => {
    const user = userEvent.setup();
    render(<SessionSwitcher {...defaultProps} />);
    
    const newSessionButton = screen.getByText('New Session').closest('[cmdk-item]');
    expect(newSessionButton).toBeTruthy();
    await user.click(newSessionButton!);
    
    expect(defaultProps.onNewSession).toHaveBeenCalled();
    expect(defaultProps.onOpenChange).toHaveBeenCalledWith(false);
  });

  it('disables New Session button when not connected', () => {
    render(<SessionSwitcher {...defaultProps} connected={false} />);
    
    const newSessionButton = screen.getByText('New Session').closest('[cmdk-item]');
    expect(newSessionButton).toHaveAttribute('data-disabled', 'true');
  });

  it('shows thinking badge for sessions that are thinking', () => {
    const thinkingBySession = new Map();
    thinkingBySession.set('session-bbb-222', new Set(['task-1']));
    
    render(<SessionSwitcher {...defaultProps} thinkingBySession={thinkingBySession} />);
    
    expect(screen.getByText('thinking')).toBeInTheDocument();
  });

  it('hides delegated sessions from the switcher', () => {
    const groupsWithDelegation: SessionGroup[] = [
      {
        cwd: '/workspace/test',
        sessions: [
          {
            session_id: 'session-delegated-123',
            title: 'Delegated Session',
            fork_origin: 'delegation',
            updated_at: new Date().toISOString(),
          },
        ],
      },
    ];
    
    render(<SessionSwitcher {...defaultProps} groups={groupsWithDelegation} />);
    
    expect(screen.queryByText('Delegated Session')).not.toBeInTheDocument();
    expect(screen.queryByText('delegated')).not.toBeInTheDocument();
  });

  it('keeps root sessions sorted by most recent while placing expanded forks under their parent', async () => {
    const user = userEvent.setup();
    const groupsWithOutOfOrderRoots: SessionGroup[] = [
      {
        cwd: '/workspace/old',
        sessions: [
          {
            session_id: 'older-parent-123',
            title: 'Older Parent',
            has_children: true,
            fork_count: 1,
            updated_at: '2024-01-01T00:00:00.000Z',
            children: [
              {
                session_id: 'older-child-456',
                title: 'Older Child Fork',
                parent_session_id: 'older-parent-123',
                fork_origin: 'user',
                updated_at: '2024-03-01T00:00:00.000Z',
              },
            ],
          },
        ],
      },
      {
        cwd: '/workspace/new',
        sessions: [
          {
            session_id: 'newer-root-789',
            title: 'Newer Root',
            updated_at: '2024-02-01T00:00:00.000Z',
          },
        ],
      },
    ];

    render(<SessionSwitcher {...defaultProps} groups={groupsWithOutOfOrderRoots} />);
    await user.click(screen.getByLabelText(/expand forks for older parent/i));

    const items = screen.getAllByRole('option').map((item) => item.textContent ?? '');
    const newerIndex = items.findIndex((text) => text.includes('Newer Root'));
    const parentIndex = items.findIndex((text) => text.includes('Older Parent'));
    const childIndex = items.findIndex((text) => text.includes('Older Child Fork'));

    expect(newerIndex).toBeGreaterThanOrEqual(0);
    expect(parentIndex).toBeGreaterThan(newerIndex);
    expect(childIndex).toBe(parentIndex + 1);
  });

  it('expands a root session to show loaded forks', async () => {
    const user = userEvent.setup();
    const onLoadSessionChildren = vi.fn();
    const groupsWithForks: SessionGroup[] = [
      {
        cwd: '/workspace/test',
        sessions: [
          {
            session_id: 'root-session-123',
            title: 'Root Session',
            has_children: true,
            fork_count: 3,
            updated_at: new Date().toISOString(),
            children: [
              {
                session_id: 'fork-session-456',
                title: 'User Fork',
                parent_session_id: 'root-session-123',
                fork_origin: 'user',
                updated_at: new Date(Date.now() - 1000).toISOString(),
              },
            ],
          },
        ],
      },
    ];
    
    render(
      <SessionSwitcher
        {...defaultProps}
        groups={groupsWithForks as SessionGroup[]}
        onLoadSessionChildren={onLoadSessionChildren}
      />
    );
    
    await user.click(screen.getByLabelText(/expand forks for root session/i));

    expect(screen.getByText('User Fork')).toBeInTheDocument();
    expect(onLoadSessionChildren).not.toHaveBeenCalled();
  });

  it('shows the fork count in the root expand control', () => {
    const groupsWithRoot: SessionGroup[] = [
      {
        cwd: '/workspace/test',
        sessions: [
          {
            session_id: 'root-session-123',
            title: 'Root Session',
            has_children: true,
            fork_count: 3,
            updated_at: new Date().toISOString(),
          },
        ],
      },
    ];

    render(<SessionSwitcher {...defaultProps} groups={groupsWithRoot} />);

    const expandButton = screen.getByLabelText(/expand forks for root session/i);
    expect(expandButton).toHaveTextContent('3');
  });

  it('requests forks when expanding an unloaded root', async () => {
    const user = userEvent.setup();
    const onLoadSessionChildren = vi.fn();
    const groupsWithRoot: SessionGroup[] = [
      {
        cwd: '/workspace/test',
        sessions: [
          {
            session_id: 'root-session-123',
            title: 'Root Session',
            has_children: true,
            fork_count: 1,
            updated_at: new Date().toISOString(),
          },
        ],
      },
    ];

    render(
      <SessionSwitcher
        {...defaultProps}
        groups={groupsWithRoot}
        onLoadSessionChildren={onLoadSessionChildren}
      />
    );

    await user.click(screen.getByLabelText(/expand forks for root session/i));

    expect(onLoadSessionChildren).toHaveBeenCalledWith('root-session-123');
  });

  it('loads more forks with the stored child cursor', async () => {
    const user = userEvent.setup();
    const onLoadSessionChildren = vi.fn();
    const groupsWithForkCursor: SessionGroup[] = [
      {
        cwd: '/workspace/test',
        sessions: [
          {
            session_id: 'root-session-123',
            title: 'Root Session',
            has_children: true,
            fork_count: 11,
            updated_at: new Date().toISOString(),
            childrenNextCursor: '10',
            children: [
              {
                session_id: 'fork-session-456',
                title: 'User Fork',
                parent_session_id: 'root-session-123',
                fork_origin: 'user',
                has_children: false,
                fork_count: 0,
                updated_at: new Date(Date.now() - 1000).toISOString(),
              },
            ],
          },
        ],
      },
    ];

    render(
      <SessionSwitcher
        {...defaultProps}
        groups={groupsWithForkCursor}
        onLoadSessionChildren={onLoadSessionChildren}
      />
    );

    await user.click(screen.getByLabelText(/expand forks for root session/i));
    await user.click(screen.getByRole('option', { name: /load more forks/i }));

    expect(onLoadSessionChildren).toHaveBeenCalledWith('root-session-123', '10');
  });

  it('displays session IDs truncated', () => {
    render(<SessionSwitcher {...defaultProps} />);
    
    // Session IDs should be truncated to first 12 chars + "..."
    // Just verify the container has the truncated session ID
    const container = screen.getByText('First Session').closest('[cmdk-item]');
    expect(container?.textContent).toContain('session-aaa-');
    expect(container?.textContent).toContain('...');
  });

  it('displays relative timestamps', () => {
    render(<SessionSwitcher {...defaultProps} />);
    
    // Should show "just now" or "X mins ago" for recent sessions
    // Use getAllByText since there will be multiple timestamps
    const timestamps = screen.getAllByText(/just now|min|hour|day/i);
    expect(timestamps.length).toBeGreaterThan(0);
  });

  it('deletes selected session when pressing Delete', async () => {
    const user = userEvent.setup();

    function StatefulSessionSwitcher() {
      const [groups, setGroups] = useState(mockGroups);
      return (
        <SessionSwitcher
          {...defaultProps}
          groups={groups}
          onDeleteSession={(sessionId) => {
            defaultProps.onDeleteSession(sessionId);
            setGroups((prev) =>
              prev.map((group) => ({
                ...group,
                sessions: group.sessions.filter((session) => session.session_id !== sessionId),
              }))
            );
          }}
        />
      );
    }

    render(<StatefulSessionSwitcher />);

    const input = screen.getByPlaceholderText(/search sessions/i);
    await user.click(input);
    await user.keyboard('{Delete}');

    expect(defaultProps.onDeleteSession).toHaveBeenCalledWith('session-aaa-111');
    expect(defaultProps.onOpenChange).not.toHaveBeenCalled();

    await waitFor(() => {
      const selectedAfterDelete = document.querySelector('[cmdk-item][data-selected="true"]') as HTMLElement | null;
      expect(selectedAfterDelete).toBeTruthy();
      expect(selectedAfterDelete?.textContent).toContain('Second Session');
    });
  });

  it('focuses search input when Delete removes the last visible session', async () => {
    const user = userEvent.setup();

    function SingleSessionSwitcher() {
      const [groups, setGroups] = useState<SessionGroup[]>([
        {
          cwd: '/workspace/project-1',
          sessions: [
            {
              session_id: 'only-session-111',
              title: 'Only Session',
              updated_at: new Date().toISOString(),
            },
          ],
        },
      ]);

      return (
        <SessionSwitcher
          {...defaultProps}
          groups={groups}
          onDeleteSession={(sessionId) => {
            defaultProps.onDeleteSession(sessionId);
            setGroups((prev) =>
              prev.map((group) => ({
                ...group,
                sessions: group.sessions.filter((session) => session.session_id !== sessionId),
              }))
            );
          }}
        />
      );
    }

    render(<SingleSessionSwitcher />);

    const input = screen.getByPlaceholderText(/search sessions/i);
    await user.click(input);
    await user.keyboard('{Delete}');

    await waitFor(() => {
      expect(document.querySelector('[cmdk-item][data-selected="true"][data-session-id]')).toBeNull();
    });
    expect(document.activeElement).toBe(input);
  });

  it('closes when clicking outside the command palette', async () => {
    const user = userEvent.setup();
    render(<SessionSwitcher {...defaultProps} />);
    
    // Click on the outer wrapper (not the backdrop, but the centering container)
    const wrapper = screen.getByTestId('session-switcher-container');
    
    await user.click(wrapper);
    expect(defaultProps.onOpenChange).toHaveBeenCalledWith(false);
  });

  it('does not close when clicking inside the command palette', async () => {
    const user = userEvent.setup();
    render(<SessionSwitcher {...defaultProps} />);
    
    // Click on the command palette itself
    const commandPalette = document.querySelector('[cmdk-root]');
    expect(commandPalette).toBeTruthy();
    
    await user.click(commandPalette!);
    expect(defaultProps.onOpenChange).not.toHaveBeenCalled();
  });

  it('resets search input when modal opens', () => {
    const { rerender } = render(<SessionSwitcher {...defaultProps} open={false} />);
    
    // Open the modal
    rerender(<SessionSwitcher {...defaultProps} open={true} />);
    
    const input = screen.getByPlaceholderText(/search sessions/i) as HTMLInputElement;
    expect(input.value).toBe('');
  });

  it('shows up to 10 recent sessions when not searching', () => {
    const manyGroups: SessionGroup[] = [
      {
        cwd: '/workspace/test',
        sessions: Array.from({ length: 15 }, (_, i) => ({
          session_id: `session-${i}`,
          title: `Session ${i}`,
          updated_at: new Date(Date.now() - i * 1000).toISOString(),
        })),
      },
    ];
    
    render(<SessionSwitcher {...defaultProps} groups={manyGroups} />);
    
    // Should show only 10 sessions (limit)
    const items = document.querySelectorAll('[cmdk-item]');
    // +1 for the "New Session" action button
    expect(items.length).toBeLessThanOrEqual(11);
  });

  it('uses responsive max-width on the command palette container', () => {
    render(<SessionSwitcher {...defaultProps} />);
    // The Command container should have responsive width for mobile
    const commandEl = document.querySelector('[cmdk-root]');
    expect(commandEl).not.toBeNull();
    expect(commandEl!.className).toContain('max-w-2xl');
    // On mobile, the outer wrapper should allow full width via responsive padding
    const wrapper = commandEl!.closest('.fixed');
    expect(wrapper).not.toBeNull();
    expect(wrapper!.className).toContain('px-3');
    expect(wrapper!.className).toContain('md:px-0');
  });
});
