import React from 'react';
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
import QuickAddButton from '../QuickAddButton';

// Mock the DocumentationButton component
jest.mock('../DocumentationButton', () => {
  return function MockDocumentationButton({ title }: { url: string; title: string }) {
    return <span data-testid="doc-button" title={`Docs: ${title}`}>Doc</span>;
  };
});

// Mock store — need >= 15 successor (non-Event, non-Trigger, non-Condition) components
// so the search field is visible
// (see THRESHOLDS.SEARCH_VISIBILITY_MIN_COMPONENTS in constants/dimensions.ts)
const mockSuccessorFillers = Array.from({ length: 13 }, (_, i) => ({
  plugin: `action:filler_${i}`, label: `Filler Action ${i}`, category: 'Actions', componentType: 4,
}));

const mockComponents = [
  { plugin: 'action:save', label: 'Save Entity', category: 'Actions', componentType: 4, documentationUrl: 'https://docs.example.com/save' },
  { plugin: 'action:delete', label: 'Delete Entity', category: 'Actions', componentType: 4 },
  { plugin: 'gateway:exclusive', label: 'Exclusive Gateway', category: 'Gateways', componentType: 3, documentationUrl: 'https://docs.example.com/gateway' },
  ...mockSuccessorFillers,
  { plugin: 'event:insert', label: 'Entity Insert', category: 'Events', componentType: 1 },
  { plugin: 'condition:is_new', label: 'Entity is New', category: 'Conditions', componentType: 2 },
];

const mockFavoriteComponents = {
  4: ['action:save'], // Action type favorites
};

let mockSelectedContextId: string | null = null;
let mockContexts: any[] = [];

jest.mock('../../store/store', () => ({
  useStore: jest.fn((selector) => {
    const state = {
      components: mockComponents,
      favoriteComponents: mockFavoriteComponents,
      selectedContextId: mockSelectedContextId,
      contexts: mockContexts,
      nodes: [],
      edges: [],
    };
    return selector(state);
  }),
}));

// Mock createPortal to render in place
jest.mock('react-dom', () => ({
  ...jest.requireActual('react-dom'),
  createPortal: (node: React.ReactNode) => node,
}));

describe('QuickAddButton', () => {
  const mockOnAddNode = jest.fn();

  beforeEach(() => {
    jest.clearAllMocks();
    mockSelectedContextId = null;
    mockContexts = [];
  });

  describe('rendering', () => {
    it('should render the button', () => {
      render(<QuickAddButton onAddNode={mockOnAddNode} />);
      
      const button = screen.getByTitle('Add successor node');
      expect(button).toBeInTheDocument();
    });

    it('should not render when disabled', () => {
      render(<QuickAddButton onAddNode={mockOnAddNode} disabled={true} />);
      
      expect(screen.queryByTitle('Add successor node')).not.toBeInTheDocument();
    });
  });

  describe('popup interaction', () => {
    it('should open popup when button is clicked', () => {
      render(<QuickAddButton onAddNode={mockOnAddNode} />);
      
      const button = screen.getByTitle('Add successor node');
      fireEvent.click(button);
      
      expect(screen.getByText('Add Successor')).toBeInTheDocument();
    });

    it('should show search input in popup', () => {
      render(<QuickAddButton onAddNode={mockOnAddNode} />);
      
      fireEvent.click(screen.getByTitle('Add successor node'));
      
      expect(screen.getByPlaceholderText('Search components...')).toBeInTheDocument();
    });

    it('should close popup when close button is clicked', () => {
      render(<QuickAddButton onAddNode={mockOnAddNode} />);
      
      fireEvent.click(screen.getByTitle('Add successor node'));
      expect(screen.getByText('Add Successor')).toBeInTheDocument();
      
      fireEvent.click(screen.getByTitle('Close'));
      
      expect(screen.queryByText('Add Successor')).not.toBeInTheDocument();
    });

    it('should close popup on Escape key', () => {
      render(<QuickAddButton onAddNode={mockOnAddNode} />);
      
      fireEvent.click(screen.getByTitle('Add successor node'));
      expect(screen.getByText('Add Successor')).toBeInTheDocument();
      
      fireEvent.keyDown(document, { key: 'Escape' });
      
      expect(screen.queryByText('Add Successor')).not.toBeInTheDocument();
    });

    it('should close popup when the quick-add button is clicked again', () => {
      render(<QuickAddButton onAddNode={mockOnAddNode} />);

      const button = screen.getByTitle('Add successor node');
      fireEvent.click(button);
      expect(screen.getByText('Add Successor')).toBeInTheDocument();

      // Clicking the same button again should close the popup
      fireEvent.click(button);
      expect(screen.queryByText('Add Successor')).not.toBeInTheDocument();
    });

    it('should close popup when clicking outside', async () => {
      render(
        <div>
          <QuickAddButton onAddNode={mockOnAddNode} />
          <div data-testid="outside-area">Outside</div>
        </div>
      );

      fireEvent.click(screen.getByTitle('Add successor node'));

      // Wait for popup to render AND for the setTimeout(0) inside the
      // click-outside effect to register the document listener.
      await waitFor(() => {
        expect(screen.getByText('Add Successor')).toBeInTheDocument();
      });

      await waitFor(() => {
        fireEvent.pointerDown(screen.getByTestId('outside-area'));
      });

      await waitFor(() => {
        expect(screen.queryByText('Add Successor')).not.toBeInTheDocument();
      });
    });
  });

  describe('component filtering', () => {
    it('should exclude Events and Triggers from the list', () => {
      render(<QuickAddButton onAddNode={mockOnAddNode} />);
      
      fireEvent.click(screen.getByTitle('Add successor node'));
      
      // Actions should be visible
      expect(screen.getByText('Save Entity')).toBeInTheDocument();
      expect(screen.getByText('Delete Entity')).toBeInTheDocument();
      
      // Gateways should be visible
      expect(screen.getByText('Exclusive Gateway')).toBeInTheDocument();
      
      // Events should NOT be visible
      expect(screen.queryByText('Entity Insert')).not.toBeInTheDocument();
    });

    it('should exclude Conditions from the list', () => {
      render(<QuickAddButton onAddNode={mockOnAddNode} />);
      
      fireEvent.click(screen.getByTitle('Add successor node'));
      
      // Conditions should NOT be visible (they attach to edges)
      expect(screen.queryByText('Entity is New')).not.toBeInTheDocument();
    });

    it('should filter components by search term', async () => {
      render(<QuickAddButton onAddNode={mockOnAddNode} />);
      
      fireEvent.click(screen.getByTitle('Add successor node'));
      
      const searchInput = screen.getByPlaceholderText('Search components...');
      fireEvent.change(searchInput, { target: { value: 'Delete' } });
      
      await waitFor(() => {
        expect(screen.getByText('Delete Entity')).toBeInTheDocument();
        expect(screen.queryByText('Save Entity')).not.toBeInTheDocument();
      });
    });

    it('should show category filter buttons', () => {
      render(<QuickAddButton onAddNode={mockOnAddNode} />);
      
      fireEvent.click(screen.getByTitle('Add successor node'));
      
      expect(screen.getByText('All')).toBeInTheDocument();
      expect(screen.getByText('Actions')).toBeInTheDocument();
      expect(screen.getByText('Gateways')).toBeInTheDocument();
    });

    it('should filter by category when category button is clicked', () => {
      render(<QuickAddButton onAddNode={mockOnAddNode} />);
      
      fireEvent.click(screen.getByTitle('Add successor node'));
      fireEvent.click(screen.getByText('Gateways'));
      
      expect(screen.getByText('Exclusive Gateway')).toBeInTheDocument();
      expect(screen.queryByText('Save Entity')).not.toBeInTheDocument();
    });
  });

  describe('favorites sorting', () => {
    it('should show favorites at the top of the list', () => {
      render(<QuickAddButton onAddNode={mockOnAddNode} />);
      
      fireEvent.click(screen.getByTitle('Add successor node'));
      
      const items = screen.getAllByRole('button', { name: /Save Entity|Delete Entity/i });
      // Save Entity is a favorite, should come first
      expect(items[0]).toHaveTextContent('Save Entity');
    });

    it('should show favorite indicator for favorite components', () => {
      render(<QuickAddButton onAddNode={mockOnAddNode} />);
      
      fireEvent.click(screen.getByTitle('Add successor node'));
      
      // Find the favorite item
      const saveEntityButton = screen.getByText('Save Entity').closest('button');
      expect(saveEntityButton).toHaveClass('favorite');
    });
  });

  describe('component selection', () => {
    it('should call onAddNode when a component is selected', () => {
      render(<QuickAddButton onAddNode={mockOnAddNode} />);
      
      fireEvent.click(screen.getByTitle('Add successor node'));
      fireEvent.click(screen.getByText('Delete Entity'));
      
      expect(mockOnAddNode).toHaveBeenCalledWith(
        expect.objectContaining({
          plugin: 'action:delete',
          label: 'Delete Entity',
        })
      );
    });

    it('should close popup after selection', () => {
      render(<QuickAddButton onAddNode={mockOnAddNode} />);
      
      fireEvent.click(screen.getByTitle('Add successor node'));
      fireEvent.click(screen.getByText('Delete Entity'));
      
      expect(screen.queryByText('Add Successor')).not.toBeInTheDocument();
    });

    it('should stop event propagation on selection', () => {
      const parentClickHandler = jest.fn();
      
      render(
        <div onClick={parentClickHandler}>
          <QuickAddButton onAddNode={mockOnAddNode} />
        </div>
      );
      
      fireEvent.click(screen.getByTitle('Add successor node'));
      fireEvent.click(screen.getByText('Delete Entity'));
      
      // Parent should not receive the click
      expect(parentClickHandler).not.toHaveBeenCalled();
    });
  });

  describe('focus trapping', () => {
    it('should trap Tab focus within the popup', async () => {
      render(<QuickAddButton onAddNode={mockOnAddNode} />);
      
      fireEvent.click(screen.getByTitle('Add successor node'));
      
      // Wait for popup to appear and focus to be set
      await waitFor(() => {
        expect(screen.getByText('Add Successor')).toBeInTheDocument();
      });

      // Get focusable elements inside the popup
      const searchInput = screen.getByPlaceholderText('Search components...');
      
      // Tab from last focusable element should wrap to first
      // The focus trap hook handles this via keydown on document (capture phase)
      searchInput.focus();
      
      // Verify the popup has role="dialog" and aria-modal="true" (focus trap prerequisites)
      const dialog = screen.getByRole('dialog');
      expect(dialog).toHaveAttribute('aria-modal', 'true');
      expect(dialog).toHaveAttribute('aria-label', 'Add Successor');
    });

    it('should close popup via focus trap Escape handling', () => {
      render(<QuickAddButton onAddNode={mockOnAddNode} />);
      
      fireEvent.click(screen.getByTitle('Add successor node'));
      expect(screen.getByText('Add Successor')).toBeInTheDocument();
      
      // useFocusTrap handles Escape via capture-phase keydown
      fireEvent.keyDown(document, { key: 'Escape' });
      
      expect(screen.queryByText('Add Successor')).not.toBeInTheDocument();
    });
  });

  describe('search visibility threshold', () => {
    afterEach(() => {
      // Restore default mock so subsequent tests are not affected
      const { useStore } = require('../../store/store');
      useStore.mockImplementation((selector: any) => {
        const state = {
          components: mockComponents,
          favoriteComponents: mockFavoriteComponents,
          selectedContextId: mockSelectedContextId,
          contexts: mockContexts,
          nodes: [],
          edges: [],
        };
        return selector(state);
      });
    });

    it('should hide search when fewer than 15 successor components are available', () => {
      const { useStore } = require('../../store/store');
      useStore.mockImplementation((selector: any) => {
        const state = {
          components: [
            { plugin: 'action:save', label: 'Save Entity', category: 'Actions', componentType: 4 },
            { plugin: 'action:delete', label: 'Delete Entity', category: 'Actions', componentType: 4 },
          ],
          favoriteComponents: {},
          selectedContextId: null,
          contexts: [],
          nodes: [],
          edges: [],
        };
        return selector(state);
      });

      render(<QuickAddButton onAddNode={mockOnAddNode} />);

      fireEvent.click(screen.getByTitle('Add successor node'));

      // Popup should open but search should be hidden
      expect(screen.getByText('Add Successor')).toBeInTheDocument();
      expect(screen.queryByPlaceholderText('Search components...')).not.toBeInTheDocument();
    });
  });

  describe('empty state', () => {
    it('should show empty message when no components match search', async () => {
      render(<QuickAddButton onAddNode={mockOnAddNode} />);
      
      fireEvent.click(screen.getByTitle('Add successor node'));
      
      const searchInput = screen.getByPlaceholderText('Search components...');
      fireEvent.change(searchInput, { target: { value: 'nonexistent' } });
      
      await waitFor(() => {
        expect(screen.getByText('No components found')).toBeInTheDocument();
      });
    });
  });

  describe('documentation button', () => {
    it('should show documentation button for components with documentationUrl', async () => {
      render(<QuickAddButton onAddNode={mockOnAddNode} />);
      
      fireEvent.click(screen.getByTitle('Add successor node'));
      
      await waitFor(() => {
        // Save Entity and Exclusive Gateway have documentation URLs
        const docButtons = screen.getAllByTestId('doc-button');
        expect(docButtons.length).toBe(2);
      });
    });

    it('should not show documentation button for components without documentationUrl', async () => {
      render(<QuickAddButton onAddNode={mockOnAddNode} />);
      
      fireEvent.click(screen.getByTitle('Add successor node'));
      
      await waitFor(() => {
        // Only 2 components have documentationUrl
        const docButtons = screen.getAllByTestId('doc-button');
        expect(docButtons.length).toBe(2);
      });
    });
  });

  describe('context filtering', () => {
    afterEach(() => {
      mockSelectedContextId = null;
      mockContexts = [];
    });

    it('should show all successor components when no context is selected', () => {
      mockSelectedContextId = null;
      mockContexts = [{
        id: 'ctx_1',
        topic: 'Test',
        model_owner: 'test_owner',
        components: { element: { plugins: ['action:save'] } },
      }];

      render(<QuickAddButton onAddNode={mockOnAddNode} />);
      fireEvent.click(screen.getByTitle('Add successor node'));

      expect(screen.getByText('Save Entity')).toBeInTheDocument();
      expect(screen.getByText('Delete Entity')).toBeInTheDocument();
      expect(screen.getByText('Exclusive Gateway')).toBeInTheDocument();
    });

    it('should only show plugins from the selected context', () => {
      mockSelectedContextId = 'ctx_1';
      mockContexts = [{
        id: 'ctx_1',
        topic: 'Content Editing',
        model_owner: 'test_owner',
        components: {
          element: { plugins: ['action:save'] },
        },
      }];

      render(<QuickAddButton onAddNode={mockOnAddNode} />);
      fireEvent.click(screen.getByTitle('Add successor node'));

      // action:save is in context
      expect(screen.getByText('Save Entity')).toBeInTheDocument();
      // action:delete is NOT in context
      expect(screen.queryByText('Delete Entity')).not.toBeInTheDocument();
      // gateway:exclusive is NOT in context
      expect(screen.queryByText('Exclusive Gateway')).not.toBeInTheDocument();
    });

    it('should ignore favorite status when a context is selected', () => {
      mockSelectedContextId = 'ctx_1';
      mockContexts = [{
        id: 'ctx_1',
        topic: 'Content Editing',
        model_owner: 'test_owner',
        components: {
          element: { plugins: ['action:save', 'action:delete', 'gateway:exclusive'] },
        },
      }];

      render(<QuickAddButton onAddNode={mockOnAddNode} />);
      fireEvent.click(screen.getByTitle('Add successor node'));

      // Save Entity is normally a favorite but should NOT have the favorite class
      const saveEntityButton = screen.getByText('Save Entity').closest('button');
      expect(saveEntityButton).not.toHaveClass('favorite');

      // Components should be in plain alphabetical order (no favorites-first sorting)
      const items = screen.getAllByRole('button', { name: /Delete Entity|Exclusive Gateway|Save Entity/i });
      expect(items[0]).toHaveTextContent('Delete Entity');
      expect(items[1]).toHaveTextContent('Exclusive Gateway');
      expect(items[2]).toHaveTextContent('Save Entity');

      // No favorite divider should be present
      const popup = screen.getByRole('dialog');
      expect(popup.querySelector('.quick-add-divider')).not.toBeInTheDocument();
    });

    it('should still exclude Events even when they are in context', () => {
      mockSelectedContextId = 'ctx_1';
      mockContexts = [{
        id: 'ctx_1',
        topic: 'All Plugins',
        model_owner: 'test_owner',
        components: {
          start: { plugins: ['event:insert'] },
          element: { plugins: ['action:save'] },
        },
      }];

      render(<QuickAddButton onAddNode={mockOnAddNode} />);
      fireEvent.click(screen.getByTitle('Add successor node'));

      // Events are still excluded by QuickAddButton's category filter
      expect(screen.queryByText('Entity Insert')).not.toBeInTheDocument();
      // But actions pass through
      expect(screen.getByText('Save Entity')).toBeInTheDocument();
    });
  });
});
