import { fireEvent, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';

import { type DataQuery } from '@grafana/schema';

import { type ActionItem } from '../../../actionItem';
import { QueryEditorType } from '../../../constants';
import { ds1SettingsMock, renderWithQueryEditorProvider } from '../../testUtils';
import { type Transformation } from '../../types';

import { QueryCard } from './QueryCard';
import { SidebarCard } from './SidebarCard';
import { TransformationCard } from './TransformationCard';

jest.mock('@grafana/runtime', () => ({
  ...jest.requireActual('@grafana/runtime'),
  getDataSourceSrv: () => ({
    getInstanceSettings: () => ds1SettingsMock,
  }),
}));

interface RenderSidebarCardProps {
  id?: string;
  isSelected?: boolean;
  isMultiSelected?: boolean;
  onSelect?: jest.Mock;
  onToggleMultiSelect?: jest.Mock;
  addQuery?: jest.Mock;
  setSelectedQuery?: jest.Mock;
  setPendingExpression?: jest.Mock;
  isHidden?: boolean;
  multiSelectMode?: boolean;
  actionsOverrides?: {
    onDelete?: jest.Mock;
    onToggleHide?: jest.Mock;
    onDuplicate?: jest.Mock;
  };
}

function renderSidebarCard({
  id = 'A',
  isSelected = false,
  isMultiSelected = false,
  onSelect = jest.fn(),
  onToggleMultiSelect = jest.fn(),
  addQuery = jest.fn().mockReturnValue('B'),
  setSelectedQuery = jest.fn(),
  setPendingExpression = jest.fn(),
  isHidden = false,
  multiSelectMode = false,
  actionsOverrides = {
    onDelete: jest.fn(),
    onToggleHide: jest.fn(),
    onDuplicate: jest.fn(),
  },
}: RenderSidebarCardProps = {}) {
  const queries: DataQuery[] = [{ refId: id, datasource: { type: 'test', uid: 'test' } }];
  const item: ActionItem = {
    id,
    type: QueryEditorType.Query,
    isHidden,
  };

  const result = renderWithQueryEditorProvider(
    <SidebarCard
      isSelected={isSelected}
      isMultiSelected={isMultiSelected}
      id={id}
      onSelect={onSelect}
      onToggleMultiSelect={onToggleMultiSelect}
      onDelete={actionsOverrides.onDelete}
      onToggleHide={actionsOverrides.onToggleHide}
      onDuplicate={actionsOverrides.onDuplicate}
      item={item}
    >
      <span>Card content</span>
    </SidebarCard>,
    {
      queries,
      selectedQuery: queries[0],
      uiStateOverrides: { setSelectedQuery, setPendingExpression, multiSelectMode },
      actionsOverrides: { addQuery },
    }
  );

  return { ...result, addQuery, setSelectedQuery, setPendingExpression, onSelect, onToggleMultiSelect };
}

describe('SidebarCard', () => {
  afterAll(() => {
    jest.clearAllMocks();
  });

  describe('card click → activates the editor', () => {
    it('clicking a QueryCard calls setSelectedQuery with that query', async () => {
      const query: DataQuery = { refId: 'A', datasource: { type: 'test', uid: 'test' } };
      const transformation: Transformation = {
        transformId: 'organize',
        registryItem: undefined,
        transformConfig: { id: 'organize', options: {} },
      };

      const setSelectedQuery = jest.fn();
      const toggleQuerySelection = jest.fn();
      const user = userEvent.setup();

      renderWithQueryEditorProvider(<QueryCard query={query} />, {
        queries: [query],
        transformations: [transformation],
        selectedTransformation: transformation,
        uiStateOverrides: { setSelectedQuery, toggleQuerySelection },
      });

      await user.click(screen.getByRole('button', { name: /select card A/i }));

      // Plain card click activates the editor — it does NOT touch the bulk-selection API.
      expect(setSelectedQuery).toHaveBeenCalledWith(query);
      expect(toggleQuerySelection).not.toHaveBeenCalled();
    });

    it('clicking a TransformationCard calls setSelectedTransformation with that transformation', async () => {
      const query: DataQuery = { refId: 'A', datasource: { type: 'test', uid: 'test' } };
      const transformation: Transformation = {
        transformId: 'organize',
        registryItem: undefined,
        transformConfig: { id: 'organize', options: {} },
      };

      const setSelectedTransformation = jest.fn();
      const toggleTransformationSelection = jest.fn();
      const user = userEvent.setup();

      renderWithQueryEditorProvider(<TransformationCard transformation={transformation} />, {
        queries: [query],
        transformations: [transformation],
        selectedQuery: query,
        uiStateOverrides: { setSelectedTransformation, toggleTransformationSelection },
      });

      await user.click(screen.getByRole('button', { name: /select card organize/i }));

      expect(setSelectedTransformation).toHaveBeenCalledWith(transformation);
      expect(toggleTransformationSelection).not.toHaveBeenCalled();
    });
  });

  describe('add button and menu', () => {
    it('renders the card and add button', () => {
      renderSidebarCard();

      expect(screen.getByRole('button', { name: /select card A/i })).toBeInTheDocument();
      expect(screen.getByRole('button', { name: /add below A/i })).toBeInTheDocument();
      expect(screen.getByText('Card content')).toBeInTheDocument();
    });

    it('clicking "Add query" calls addQuery with the card refId as afterRefId', async () => {
      const { user, addQuery, setSelectedQuery } = renderSidebarCard({ id: 'A' });

      await user.click(screen.getByRole('button', { name: /add below A/i }));
      await user.click(screen.getByRole('menuitem', { name: /add query/i }));

      expect(addQuery).toHaveBeenCalledWith(undefined, 'A');
      expect(setSelectedQuery).toHaveBeenCalledWith({ refId: 'B', hide: false });
    });

    it('auto-selects the newly added query', async () => {
      const addQuery = jest.fn().mockReturnValue('C');
      const { user, setSelectedQuery } = renderSidebarCard({ id: 'B', addQuery });

      await user.click(screen.getByRole('button', { name: /add below B/i }));
      await user.click(screen.getByRole('menuitem', { name: /add query/i }));

      expect(setSelectedQuery).toHaveBeenCalledWith({ refId: 'C', hide: false });
    });

    it('does not call setSelectedQuery when addQuery returns undefined', async () => {
      const addQuery = jest.fn().mockReturnValue(undefined);
      const { user, setSelectedQuery } = renderSidebarCard({ addQuery });

      await user.click(screen.getByRole('button', { name: /add below A/i }));
      await user.click(screen.getByRole('menuitem', { name: /add query/i }));

      expect(addQuery).toHaveBeenCalled();
      expect(setSelectedQuery).not.toHaveBeenCalled();
    });
  });

  describe('add expression', () => {
    it('clicking "Add expression" calls setPendingExpression with insertAfter', async () => {
      const { user, setPendingExpression } = renderSidebarCard({ id: 'A' });

      await user.click(screen.getByRole('button', { name: /add below A/i }));
      await user.click(screen.getByRole('menuitem', { name: /add expression/i }));

      expect(setPendingExpression).toHaveBeenCalledWith({ insertAfter: 'A' });
    });
  });

  describe('card body interactions', () => {
    it('plain click on the card calls onSelect with no arguments', async () => {
      const { user, onSelect } = renderSidebarCard({ id: 'A' });

      await user.click(screen.getByRole('button', { name: /select card A/i }));

      expect(onSelect).toHaveBeenCalledTimes(1);
      expect(onSelect).toHaveBeenCalledWith();
    });

    it('Enter on the card calls onSelect with no arguments', async () => {
      const { user, onSelect } = renderSidebarCard({ id: 'A' });

      const card = screen.getByRole('button', { name: /select card A/i });
      await user.click(card);
      onSelect.mockClear();
      await user.keyboard('[Enter]');

      expect(onSelect).toHaveBeenCalledTimes(1);
      expect(onSelect).toHaveBeenCalledWith();
    });

    it('Ctrl+click on the card body does NOT toggle bulk selection', async () => {
      // Bulk multi-select is driven by the checkbox; the card body only ever activates.
      const { user, onSelect, onToggleMultiSelect } = renderSidebarCard({ id: 'A', multiSelectMode: true });

      const card = screen.getByRole('button', { name: /select card A/i });
      await user.keyboard('[ControlLeft>]');
      await user.click(card);
      await user.keyboard('[/ControlLeft]');

      expect(onSelect).toHaveBeenCalled();
      expect(onToggleMultiSelect).not.toHaveBeenCalled();
    });

    it('Shift+click on the card body does NOT range-select via the bulk handler', async () => {
      const { user, onSelect, onToggleMultiSelect } = renderSidebarCard({ id: 'A', multiSelectMode: true });

      const card = screen.getByRole('button', { name: /select card A/i });
      await user.keyboard('[ShiftLeft>]');
      await user.click(card);
      await user.keyboard('[/ShiftLeft]');

      expect(onSelect).toHaveBeenCalled();
      expect(onToggleMultiSelect).not.toHaveBeenCalled();
    });

    it('sets aria-pressed to true when isSelected is true', () => {
      renderSidebarCard({ id: 'A', isSelected: true });
      expect(screen.getByRole('button', { name: /select card A/i })).toHaveAttribute('aria-pressed', 'true');
    });

    it('sets aria-pressed to false when isSelected is false', () => {
      renderSidebarCard({ id: 'A', isSelected: false });
      expect(screen.getByRole('button', { name: /select card A/i })).toHaveAttribute('aria-pressed', 'false');
    });

    it('fires a DOM blur on the focused editor even when mousedown preventDefault blocks focus transfer', async () => {
      // @hello-pangea/dnd installs a capture-phase mousedown listener that preventDefaults, so the
      // browser's native focus transfer never fires a blur on the previously focused element.
      // Without the force-blur in handleMouseDown, Monaco-like editors never see a DOM blur and
      // their pending value is never flushed through onChange before the editor unmounts.
      const onSelect = jest.fn();
      const onEditorBlur = jest.fn();
      const user = userEvent.setup();
      renderSidebarCard({ id: 'A', onSelect });

      const suppressFocusTransfer = (e: Event) => e.preventDefault();
      window.addEventListener('mousedown', suppressFocusTransfer, true);

      const editorLike = document.createElement('input');
      editorLike.addEventListener('blur', onEditorBlur, true);
      document.body.appendChild(editorLike);
      editorLike.focus();

      try {
        await user.click(screen.getByRole('button', { name: /select card A/i }));

        expect(onEditorBlur).toHaveBeenCalled();
        expect(onSelect).toHaveBeenCalled();
      } finally {
        window.removeEventListener('mousedown', suppressFocusTransfer, true);
        document.body.removeChild(editorLike);
      }
    });
  });

  describe('multi-select checkbox', () => {
    it('does not render the checkbox when multi-select mode is off', () => {
      renderSidebarCard({ id: 'A', multiSelectMode: false });
      expect(screen.queryByRole('checkbox')).not.toBeInTheDocument();
    });

    it('renders the checkbox when multi-select mode is on', () => {
      renderSidebarCard({ id: 'A', multiSelectMode: true });
      expect(screen.getByRole('checkbox')).toBeInTheDocument();
    });

    it('does not render the checkbox when onToggleMultiSelect is not provided', () => {
      const queries: DataQuery[] = [{ refId: 'A', datasource: { type: 'test', uid: 'test' } }];
      const item: ActionItem = { id: 'A', type: QueryEditorType.Query, isHidden: false };

      renderWithQueryEditorProvider(
        <SidebarCard id="A" isSelected={false} item={item} onSelect={jest.fn()}>
          <span>Card content</span>
        </SidebarCard>,
        { queries, uiStateOverrides: { multiSelectMode: true } }
      );

      expect(screen.queryByRole('checkbox')).not.toBeInTheDocument();
    });

    it('reflects isMultiSelected in the checkbox checked state', () => {
      const { rerender } = renderSidebarCard({ id: 'A', multiSelectMode: true, isMultiSelected: false });
      expect(screen.getByRole('checkbox')).not.toBeChecked();

      // Re-render with isMultiSelected=true via a fresh mount.
      rerender(<div />);
      renderSidebarCard({ id: 'A', multiSelectMode: true, isMultiSelected: true });
      expect(screen.getByRole('checkbox')).toBeChecked();
    });

    it('does not check the checkbox just because isSelected is true', () => {
      // Active (single-select) state is independent of the bulk checkbox.
      renderSidebarCard({ id: 'A', multiSelectMode: true, isSelected: true, isMultiSelected: false });
      expect(screen.getByRole('checkbox')).not.toBeChecked();
    });

    it('clicking the checkbox calls onToggleMultiSelect with { multi: true }', async () => {
      const { user, onToggleMultiSelect } = renderSidebarCard({ id: 'A', multiSelectMode: true });

      await user.click(screen.getByRole('checkbox'));

      expect(onToggleMultiSelect).toHaveBeenCalledWith({ multi: true });
    });

    it('Shift+clicking the checkbox calls onToggleMultiSelect with { range: true }', async () => {
      const { user, onSelect, onToggleMultiSelect } = renderSidebarCard({ id: 'A', multiSelectMode: true });

      const checkbox = screen.getByRole('checkbox');
      await user.keyboard('[ShiftLeft>]');
      await user.click(checkbox);
      await user.keyboard('[/ShiftLeft]');

      expect(onToggleMultiSelect).toHaveBeenCalledWith({ range: true });
      // The card body's onSelect must not fire when the user is targeting the checkbox.
      expect(onSelect).not.toHaveBeenCalled();
    });
  });

  describe('hover actions and hidden icon', () => {
    it('renders the hidden icon when the card is hidden', () => {
      renderSidebarCard({ id: 'A', isHidden: true });
      const icons = screen.getAllByTestId('icon-eye-slash');
      expect(icons.length).toBeGreaterThanOrEqual(2);
    });

    it('does not render the hidden icon when the card is visible', () => {
      renderSidebarCard({ id: 'A', isHidden: false });
      expect(screen.queryByTestId('icon-eye-slash')).not.toBeInTheDocument();
    });

    it('renders the hover actions when hasActions is true', () => {
      renderSidebarCard({
        id: 'A',
        actionsOverrides: { onDelete: jest.fn(), onToggleHide: jest.fn(), onDuplicate: jest.fn() },
      });

      expect(screen.getByRole('button', { name: /hide query/i })).toBeInTheDocument();
      expect(screen.getByRole('button', { name: /duplicate query/i })).toBeInTheDocument();
      expect(screen.getByRole('button', { name: /remove query/i })).toBeInTheDocument();
    });

    it('does not render the hover actions when hasActions is false', () => {
      renderSidebarCard({
        id: 'A',
        actionsOverrides: { onDelete: undefined, onToggleHide: undefined, onDuplicate: undefined },
      });

      expect(screen.queryByRole('button', { name: /hide query/i })).not.toBeInTheDocument();
      expect(screen.queryByRole('button', { name: /duplicate query/i })).not.toBeInTheDocument();
      expect(screen.queryByRole('button', { name: /remove query/i })).not.toBeInTheDocument();
    });

    it('does not render the per-card hover actions in multi-select mode even when handlers are present', async () => {
      // Per-card delete/duplicate/hide are intentionally suppressed while multi-select is active:
      // bulk selection drives destructive actions, so exposing the single-card delete path here
      // would reintroduce a stale single-delete against an item that may not be the bulk target.
      const { user } = renderSidebarCard({
        id: 'A',
        multiSelectMode: true,
        actionsOverrides: { onDelete: jest.fn(), onToggleHide: jest.fn(), onDuplicate: jest.fn() },
      });

      // Hover and focus the card to surface any actions that would normally appear on interaction.
      const card = screen.getByRole('button', { name: /select card A/i });
      await user.hover(card);
      fireEvent.focus(card);

      expect(screen.queryByRole('button', { name: /hide query/i })).not.toBeInTheDocument();
      expect(screen.queryByRole('button', { name: /duplicate query/i })).not.toBeInTheDocument();
      expect(screen.queryByRole('button', { name: /remove query/i })).not.toBeInTheDocument();
    });
  });
});
