import { within } from '@testing-library/react';
import type { ReactNode } from 'react';
import { beforeEach, describe, expect, it, vi } from 'vitest';

import { checkAccessibility } from '@/test/utils/a11y';
import { render, screen } from '@/test/utils/render';

import { AgentSelector } from './agent-selector';

vi.mock('@/lib/i18n/client', () => ({
  useT: () => ({
    t: (key: string) => {
      const translations: Record<string, string> = {
        'agentSelector.label': 'Select agent',
        'agentSelector.defaultAgent': 'Default agent',
        'agentSelector.viewDetails': 'View agent details',
        'agentSelector.searchPlaceholder': 'Search agents...',
        'agentSelector.noResults': 'No agents found',
        'agentSelector.addAgent': 'Add agent',
      };
      return translations[key] ?? key;
    },
  }),
}));

const mockSetSelectedAgent = vi.fn();
let mockSelectedAgent: { name: string; displayName: string } | null = null;
vi.mock('../context/chat-layout-context', () => ({
  useChatLayout: () => ({
    selectedAgent: mockSelectedAgent,
    setSelectedAgent: mockSetSelectedAgent,
  }),
}));

// The selector reads a project to honor `allowedAgentSlugs`. These tests render
// without a project (org-level chat), so stub it to "no project" — also avoids
// pulling in the real Convex/react-query hook (no QueryClient in this harness).
vi.mock('@/app/features/projects/hooks/queries', () => ({
  useProject: () => ({ project: null }),
}));

interface MockAgent {
  name: string;
  displayName: string;
  description: string;
}

const defaultAgents: MockAgent[] = [
  {
    name: 'chat-agent',
    displayName: 'Default Chat',
    description: 'Default assistant',
  },
  {
    name: 'custom-agent',
    displayName: 'Custom Agent',
    description: 'A custom agent',
  },
];

let mockAgents: MockAgent[] = defaultAgents;

vi.mock('../hooks/queries', () => ({
  useChatAgents: () => ({
    agents: mockAgents,
  }),
}));

let mockEffectiveAgent: { name: string; displayName: string } | null = {
  name: 'chat-agent',
  displayName: 'Default Chat',
};
let mockEffectiveAgentLoading = false;

vi.mock('../hooks/use-effective-agent', () => ({
  useEffectiveAgent: () => ({
    agent: mockEffectiveAgent,
    isLoading: mockEffectiveAgentLoading,
  }),
}));

let mockCanWrite = true;
vi.mock('@/app/hooks/use-ability', () => ({
  useAbility: () => ({
    can: (_action: string, _subject: string) => mockCanWrite,
    cannot: (_action: string, _subject: string) => !mockCanWrite,
  }),
}));

const mockDialogOpen = vi.fn();
const mockDialogClose = vi.fn();
const mockDialogOnOpenChange = vi.fn();
let mockDialogIsOpen = false;

vi.mock('@/app/hooks/use-dialog-search-param', () => ({
  useDialogSearchParam: () => ({
    isOpen: mockDialogIsOpen,
    open: mockDialogOpen,
    close: mockDialogClose,
    onOpenChange: mockDialogOnOpenChange,
  }),
}));

vi.mock('@/app/features/agents/components/agent-create-dialog', () => ({
  CreateAgentDialog: ({
    open,
  }: {
    open: boolean;
    onOpenChange: (open: boolean) => void;
    organizationId: string;
  }) =>
    open ? (
      <div data-testid="create-agent-dialog">Create Agent Dialog</div>
    ) : null,
}));

const mockNavigate = vi.fn();
vi.mock('@tanstack/react-router', () => ({
  useSearch: () => ({}),
  useLocation: () => ({ pathname: '/dashboard/org-1/chat' }),
  useNavigate: () => mockNavigate,
  // The "view agent details" row link renders a TanStack `Link`. Stub it as a
  // plain anchor so tests don't need a RouterProvider. Router-only props (`to`,
  // `params`) are dropped so React doesn't get object-valued DOM attributes.
  Link: ({
    children,
    'aria-label': ariaLabel,
    className,
    onClick,
  }: {
    children: ReactNode;
    'aria-label'?: string;
    className?: string;
    onClick?: (e: unknown) => void;
  }) => (
    // `href` gives the stub the implicit ARIA `link` role (getByRole('link')).
    // This is a router-Link test double, not real navigation — the a11y rule
    // about href+onClick anchors doesn't apply.
    // oxlint-disable-next-line jsx-a11y/anchor-is-valid
    <a href="#" aria-label={ariaLabel} className={className} onClick={onClick}>
      {children}
    </a>
  ),
}));

vi.mock('../hooks/use-composer-capabilities', () => ({
  useIntegrationReadiness: () => ({
    readyBySlug: new Map<string, boolean>(),
    titleBySlug: new Map<string, string>(),
  }),
  getAgentMissingIntegrations: () => [],
}));

beforeEach(() => {
  vi.clearAllMocks();
  mockCanWrite = true;
  mockDialogIsOpen = false;
  mockAgents = defaultAgents;
  mockEffectiveAgent = { name: 'chat-agent', displayName: 'Default Chat' };
  mockEffectiveAgentLoading = false;
  mockSelectedAgent = null;
});

describe('AgentSelector', () => {
  it('renders the agent selector trigger', () => {
    render(<AgentSelector organizationId="org-1" />);
    expect(screen.getByLabelText('Select agent')).toBeInTheDocument();
  });

  it('displays the effective agent name when an agent is pinned', () => {
    // With a pinned selection the trigger reflects that agent (not "Auto",
    // which is only the label when the selection is null/Auto).
    mockSelectedAgent = { name: 'chat-agent', displayName: 'Default Chat' };
    render(<AgentSelector organizationId="org-1" />);
    expect(screen.getByText('Default Chat')).toBeInTheDocument();
  });

  it('falls back to translation when no effective agent', () => {
    // Single agent → "Auto" is not offered, so a null selection resolves to the
    // effective agent (here null) and falls back to the default-agent label.
    mockAgents = [defaultAgents[0]];
    mockEffectiveAgent = null;
    render(<AgentSelector organizationId="org-1" />);
    expect(screen.getByText('Default agent')).toBeInTheDocument();
  });

  it('renders a skeleton inside the trigger while the effective agent is loading', () => {
    mockAgents = [defaultAgents[0]];
    mockEffectiveAgent = null;
    mockEffectiveAgentLoading = true;
    render(<AgentSelector organizationId="org-1" />);

    // Granular masking: the real label stays mounted (so the footprint is
    // stable) but is wrapped in a `<SkeletonBox>` that covers it with a pulse
    // overlay while loading — there's no visual "Default agent" flash even
    // though the text node remains in the DOM (aria-hidden).
    const label = screen.getByText('Default agent');
    expect(label.closest('[aria-hidden="true"]')).not.toBeNull();
    const trigger = screen.getByRole('button', { name: 'Select agent' });
    expect(trigger).toBeDisabled();
    // Trigger keeps its leading + trailing icons so the width footprint is
    // stable across loading and loaded states.
    expect(trigger.querySelectorAll('svg')).toHaveLength(2);
    // The loading region is announced once via role="status"; the masked label
    // sits under a pulse overlay. Granular masking auto-sizes the overlay to the
    // label (no fixed skeleton dimensions), so assert the overlay is present
    // rather than brittle width/height classes.
    const skeleton = within(trigger).getByRole('status');
    expect(skeleton).toBeInTheDocument();
    expect(trigger.querySelector('.animate-pulse')).not.toBeNull();
    // Trigger has a min-width pin (from `sm` up) so loading→loaded never
    // reflows for the common label range. Mobile drops the pin so the
    // composer toolbar fits — see the source comment in agent-selector.tsx.
    expect(trigger).toHaveClass('sm:min-w-32');
  });

  it('shows "Add agent" button when user has write permission', async () => {
    const { user } = render(<AgentSelector organizationId="org-1" />);

    const trigger = screen.getByLabelText('Select agent');
    await user.click(trigger);

    expect(screen.getByText('Add agent')).toBeInTheDocument();
  });

  it('hides "Add agent" button when user lacks write permission', async () => {
    mockCanWrite = false;

    const { user } = render(<AgentSelector organizationId="org-1" />);

    const trigger = screen.getByLabelText('Select agent');
    await user.click(trigger);

    expect(screen.queryByText('Add agent')).not.toBeInTheDocument();
  });

  it('shows a "view agent details" link on each agent row (not Auto) for managers', async () => {
    const { user } = render(<AgentSelector organizationId="org-1" />);

    await user.click(screen.getByLabelText('Select agent'));

    // One link per real agent (chat-agent + custom-agent), none on the Auto row.
    const links = screen.getAllByRole('link', { name: 'View agent details' });
    expect(links).toHaveLength(2);
    // The Auto option (a pseudo-agent) has no details link.
    const autoOption = screen
      .getAllByRole('option')
      .find((el) => el.textContent?.includes('agentSelector.auto'));
    expect(autoOption).toBeDefined();
    expect(
      within(autoOption as HTMLElement).queryByRole('link'),
    ).not.toBeInTheDocument();
  });

  it('hides the "view agent details" link when the user cannot manage agents', async () => {
    mockCanWrite = false;

    const { user } = render(<AgentSelector organizationId="org-1" />);

    await user.click(screen.getByLabelText('Select agent'));

    expect(
      screen.queryByRole('link', { name: 'View agent details' }),
    ).not.toBeInTheDocument();
  });

  it('does not select the agent when its details link is clicked', async () => {
    const { user } = render(<AgentSelector organizationId="org-1" />);

    await user.click(screen.getByLabelText('Select agent'));

    const links = screen.getAllByRole('link', { name: 'View agent details' });
    await user.click(links[0]);

    // The link's onClick stops propagation, so the row's select handler must
    // not fire.
    expect(mockSetSelectedAgent).not.toHaveBeenCalled();
  });

  it('opens create dialog when "Add agent" is clicked', async () => {
    const { user } = render(<AgentSelector organizationId="org-1" />);

    const trigger = screen.getByLabelText('Select agent');
    await user.click(trigger);

    const addButton = screen.getByText('Add agent');
    await user.click(addButton);

    expect(mockDialogOpen).toHaveBeenCalled();
  });

  it('renders CreateAgentDialog when dialog is open', () => {
    mockDialogIsOpen = true;

    render(<AgentSelector organizationId="org-1" />);

    expect(screen.getByTestId('create-agent-dialog')).toBeInTheDocument();
  });

  it('does not render CreateAgentDialog when dialog is closed', () => {
    mockDialogIsOpen = false;

    render(<AgentSelector organizationId="org-1" />);

    expect(screen.queryByTestId('create-agent-dialog')).not.toBeInTheDocument();
  });

  it('calls setSelectedAgent with agent name when option is clicked', async () => {
    const { user } = render(<AgentSelector organizationId="org-1" />);

    const trigger = screen.getByLabelText('Select agent');
    await user.click(trigger);

    const customOption = screen.getByText('Custom Agent');
    await user.click(customOption);

    expect(mockSetSelectedAgent).toHaveBeenCalledWith({
      name: 'custom-agent',
      displayName: 'Custom Agent',
    });
  });

  it('calls setSelectedAgent with name for default agent', async () => {
    mockEffectiveAgent = { name: 'custom-agent', displayName: 'Custom Agent' };

    const { user } = render(<AgentSelector organizationId="org-1" />);

    const trigger = screen.getByLabelText('Select agent');
    await user.click(trigger);

    const defaultOption = screen.getByText('Default Chat');
    await user.click(defaultOption);

    expect(mockSetSelectedAgent).toHaveBeenCalledWith({
      name: 'chat-agent',
      displayName: 'Default Chat',
    });
  });

  it('only highlights one agent when selected', async () => {
    mockAgents = [
      {
        name: 'chat-agent',
        displayName: 'Assistant',
        description: 'Default assistant',
      },
      {
        name: 'another-chat-agent',
        displayName: 'Another Chat',
        description: 'Also a chat agent',
      },
    ];
    mockEffectiveAgent = { name: 'chat-agent', displayName: 'Assistant' };
    // Pin the agent so the highlighted option is that agent rather than "Auto"
    // (the selected option when the selection is null/Auto).
    mockSelectedAgent = { name: 'chat-agent', displayName: 'Assistant' };

    const { user } = render(<AgentSelector organizationId="org-1" />);

    const trigger = screen.getByLabelText('Select agent');
    await user.click(trigger);

    const selectedOptions = screen.getAllByRole('option');
    const selected = selectedOptions.filter(
      (el) => el.getAttribute('aria-selected') === 'true',
    );
    expect(selected).toHaveLength(1);
    expect(selected[0]).toHaveTextContent('Assistant');
  });

  describe('accessibility', () => {
    it('passes axe audit', async () => {
      const { container } = render(<AgentSelector organizationId="org-1" />);
      await checkAccessibility(container);
    });
  });
});
