import { describe, test, expect, vi } from 'vitest';
import { render, screen, waitFor } from '@testing-library/react';
import { userEvent } from '@testing-library/user-event';
import { useRef, useState } from 'react';
import { $getRoot, $isElementNode, $isTextNode } from 'lexical';
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
import TextEditor, { type TextEditorRef, type BlockSelection } from './TextEditor.js';

vi.mock('../../assets/icons/blockquote.svg?react', () => ({
  default: () => <div data-testid="blockquote-icon">Quote</div>,
}));
vi.mock('../../assets/icons/bold.svg?react', () => ({
  default: () => <div data-testid="bold-icon">B</div>,
}));
vi.mock('../../assets/icons/bulletedlist.svg?react', () => ({
  default: () => <div data-testid="bulletedlist-icon">List</div>,
}));
vi.mock('../../assets/icons/italic.svg?react', () => ({
  default: () => <div data-testid="italic-icon">I</div>,
}));
vi.mock('../../assets/icons/link.svg?react', () => ({
  default: () => <div data-testid="link-icon">Link</div>,
}));
vi.mock('../../assets/icons/numberedlist.svg?react', () => ({
  default: () => <div data-testid="numberedlist-icon">Numbered list</div>,
}));
vi.mock('../../assets/icons/redo.svg?react', () => ({
  default: () => <div data-testid="redo-icon">Redo</div>,
}));
vi.mock('../../assets/icons/remove-format.svg?react', () => ({
  default: () => <div data-testid="removeformat-icon">Remove format</div>,
}));
vi.mock('../../assets/icons/undo.svg?react', () => ({
  default: () => <div data-testid="undo-icon">Unfo</div>,
}));

describe('TextEditor', () => {
  test('renders without crashing', async () => {
    render(<TextEditor />);
    const editableArea = await screen.findByRole('textbox');

    expect(editableArea).toBeInTheDocument();
    expect(editableArea).toHaveAttribute('contenteditable', 'true');
  });

  test('sets aria-labelledby when labelId is provided', async () => {
    render(<TextEditor labelId="Knock knock zoom zoom" />);
    const editableArea = await screen.findByRole('textbox');
    expect(editableArea).toHaveAttribute('aria-labelledby', 'Knock knock zoom zoom');
  });

  test('calls onChange callback when content changes', async () => {
    const user = userEvent.setup();
    const handleChange = vi.fn();

    render(<TextEditor onChange={handleChange} />);
    const editableArea = await screen.findByRole('textbox');

    await user.click(editableArea);
    await user.keyboard('Hello');

    await waitFor(() => {
      expect(handleChange).toHaveBeenCalled();
    });
  });

  test('setText updates editor content via ref', async () => {
    const TestWrapper = () => {
      const editorRef = useRef<TextEditorRef>(null);

      return (
        <>
          <TextEditor ref={editorRef} />
          <button onClick={() => editorRef.current?.setText('<p>Test content</p>')}>
            Set Text
          </button>
        </>
      );
    };

    const user = userEvent.setup();
    render(<TestWrapper />);

    await screen.findByRole('textbox');
    const button = screen.getByRole('button', { name: 'Set Text' });
    await user.click(button);

    await waitFor(() => {
      const editableArea = screen.getByRole('textbox');
      expect(editableArea.textContent).toContain('Test content');
    });
  });

  test('getText returns current editor content via ref', async () => {
    const TestWrapper = () => {
      const editorRef = useRef<TextEditorRef>(null);
      const [content, setContent] = useState('');

      return (
        <>
          <TextEditor ref={editorRef} />
          <button onClick={() => editorRef.current?.setText('My content')}>Set Text</button>
          <button onClick={() => setContent(editorRef.current?.getText() || '')}>Get Text</button>
          <div data-testid="content-display">{content}</div>
        </>
      );
    };

    const user = userEvent.setup();
    render(<TestWrapper />);

    const setButton = screen.getByRole('button', { name: 'Set Text' });
    await user.click(setButton);

    const getButton = screen.getByRole('button', { name: 'Get Text' });
    await user.click(getButton);

    await waitFor(() => {
      const display = screen.getByTestId('content-display');
      expect(display.textContent).toContain('My content');
    });
  });

  test('renders overlay when provided', async () => {
    const overlay = <div data-testid="custom-overlay">Loading...</div>;

    render(<TextEditor overlay={overlay} />);
    await screen.findByRole('textbox');

    const overlayElement = screen.getByTestId('custom-overlay');
    expect(overlayElement).toBeInTheDocument();
    expect(overlayElement).toHaveTextContent('Loading...');
  });

  test('does not render overlay when null', async () => {
    const { container } = render(<TextEditor overlay={null} />);
    await screen.findByRole('textbox');

    // eslint-disable-next-line testing-library/no-container, testing-library/no-node-access -- checking overlay absence by CSS class
    const overlayContainer = container.querySelector('.text-editor__overlay');
    expect(overlayContainer).not.toBeInTheDocument();
  });

  test('handles rapid setText calls', async () => {
    const TestWrapper = () => {
      const editorRef = useRef<TextEditorRef>(null);

      const handleClick = () => {
        editorRef.current?.setText('<p>First</p>');
        editorRef.current?.setText('<p>Second</p>');
        editorRef.current?.setText('<p>Third</p>');
      };

      return (
        <>
          <TextEditor ref={editorRef} />
          <button onClick={handleClick}>Update Multiple Times</button>
        </>
      );
    };

    const user = userEvent.setup();
    render(<TestWrapper />);

    await screen.findByRole('textbox');
    const button = screen.getByRole('button', { name: 'Update Multiple Times' });
    await user.click(button);

    await waitFor(() => {
      const editableArea = screen.getByRole('textbox');
      expect(editableArea.textContent).toContain('Third');
    });
  });

  test('getText returns empty string when editor is empty', async () => {
    const TestWrapper = () => {
      const editorRef = useRef<TextEditorRef>(null);
      const [content, setContent] = useState<string | null>(null);

      return (
        <>
          <TextEditor ref={editorRef} />
          <button onClick={() => setContent(editorRef.current?.getText() || 'EMPTY')}>
            Get Text
          </button>
          {content !== null && <div data-testid="result">{content || 'EMPTY'}</div>}
        </>
      );
    };

    const user = userEvent.setup();
    render(<TestWrapper />);

    await screen.findByRole('textbox');
    const button = screen.getByRole('button', { name: 'Get Text' });
    await user.click(button);

    await waitFor(() => {
      const result = screen.getByTestId('result');
      expect(result.textContent).toBe('EMPTY');
    });
  });

  test('handles multiple editor instances', async () => {
    render(
      <>
        <TextEditor labelId="editor-1" />
        <TextEditor labelId="editor-2" />
      </>,
    );

    const editors = await screen.findAllByRole('textbox');
    expect(editors).toHaveLength(2);
    expect(editors[0]).toHaveAttribute('aria-labelledby', 'editor-1');
    expect(editors[1]).toHaveAttribute('aria-labelledby', 'editor-2');
  });

  test('preserves HTML formatting in setText', async () => {
    const TestWrapper = () => {
      const editorRef = useRef<TextEditorRef>(null);

      return (
        <>
          <TextEditor ref={editorRef} />
          <button
            onClick={() =>
              editorRef.current?.setText('<p><strong>Bold</strong> and <em>italic</em></p>')
            }
          >
            Set Formatted Text
          </button>
        </>
      );
    };

    const user = userEvent.setup();
    render(<TestWrapper />);

    await screen.findByRole('textbox');
    const button = screen.getByRole('button', { name: 'Set Formatted Text' });
    await user.click(button);

    await waitFor(() => {
      const editableArea = screen.getByRole('textbox');
      // eslint-disable-next-line testing-library/no-node-access -- Lexical rich text DOM
      expect(editableArea.querySelector('strong')).toBeInTheDocument();
      // eslint-disable-next-line testing-library/no-node-access -- Lexical rich text DOM
      expect(editableArea.querySelector('em')).toBeInTheDocument();
    });
  });

  test('isEmpty returns true when editor is empty', async () => {
    const TestWrapper = () => {
      const editorRef = useRef<TextEditorRef>(null);
      const [result, setResult] = useState<boolean | null>(null);

      return (
        <>
          <TextEditor ref={editorRef} />
          <button onClick={() => setResult(editorRef.current?.isEmpty() ?? null)}>
            Check Empty
          </button>
          {result !== null && <div data-testid="result">{result ? 'empty' : 'not-empty'}</div>}
        </>
      );
    };

    const user = userEvent.setup();
    render(<TestWrapper />);

    await screen.findByRole('textbox');
    const button = screen.getByRole('button', { name: 'Check Empty' });
    await user.click(button);

    await waitFor(() => {
      expect(screen.getByTestId('result')).toHaveTextContent('empty');
    });
  });

  test('isEmpty returns false when editor has content', async () => {
    const TestWrapper = () => {
      const editorRef = useRef<TextEditorRef>(null);
      const [result, setResult] = useState<boolean | null>(null);

      return (
        <>
          <TextEditor ref={editorRef} />
          <button onClick={() => editorRef.current?.setText('<p>Some content</p>')}>
            Set Text
          </button>
          <button onClick={() => setResult(editorRef.current?.isEmpty() ?? null)}>
            Check Empty
          </button>
          {result !== null && <div data-testid="result">{result ? 'empty' : 'not-empty'}</div>}
        </>
      );
    };

    const user = userEvent.setup();
    render(<TestWrapper />);

    await screen.findByRole('textbox');
    await user.click(screen.getByRole('button', { name: 'Set Text' }));

    await waitFor(() => {
      const editableArea = screen.getByRole('textbox');
      expect(editableArea.textContent).toContain('Some content');
    });

    await user.click(screen.getByRole('button', { name: 'Check Empty' }));

    await waitFor(() => {
      expect(screen.getByTestId('result')).toHaveTextContent('not-empty');
    });
  });

  test('selectCurrentBlock returns BlockSelection with correct properties', async () => {
    const TestWrapper = () => {
      const editorRef = useRef<TextEditorRef>(null);
      const [result, setResult] = useState<string>('');

      return (
        <>
          <TextEditor ref={editorRef} />
          <button onClick={() => editorRef.current?.setText('<p>Test paragraph content</p>')}>
            Set Text
          </button>
          <button
            onClick={() => {
              const selection = editorRef.current?.selectCurrentBlock();
              if (selection) {
                setResult(
                  JSON.stringify({
                    hasAnchorKey: typeof selection.anchorKey === 'string',
                    hasAnchorOffset: typeof selection.anchorOffset === 'number',
                    hasAnchorType: ['text', 'element'].includes(selection.anchorType),
                    hasFocusKey: typeof selection.focusKey === 'string',
                    hasFocusOffset: typeof selection.focusOffset === 'number',
                    hasFocusType: ['text', 'element'].includes(selection.focusType),
                    hasText: typeof selection.text === 'string',
                    text: selection.text,
                  }),
                );
              }
            }}
          >
            Select Block
          </button>
          <div data-testid="result">{result}</div>
        </>
      );
    };

    const user = userEvent.setup();
    render(<TestWrapper />);

    const editableArea = await screen.findByRole('textbox');
    await user.click(screen.getByRole('button', { name: 'Set Text' }));

    await waitFor(() => {
      expect(editableArea.textContent).toContain('Test paragraph content');
    });

    // Click in the editor to place cursor
    await user.click(editableArea);
    await user.click(screen.getByRole('button', { name: 'Select Block' }));

    await waitFor(() => {
      const resultEl = screen.getByTestId('result');
      const result = JSON.parse(resultEl.textContent || '{}');
      expect(result.hasAnchorKey).toBe(true);
      expect(result.hasAnchorOffset).toBe(true);
      expect(result.hasAnchorType).toBe(true);
      expect(result.hasFocusKey).toBe(true);
      expect(result.hasFocusOffset).toBe(true);
      expect(result.hasFocusType).toBe(true);
      expect(result.hasText).toBe(true);
      expect(result.text).toBe('Test paragraph content');
    });
  });

  test('replaceSelection replaces text at stored selection with plain text', async () => {
    const TestWrapper = () => {
      const editorRef = useRef<TextEditorRef>(null);
      const [storedSelection, setStoredSelection] = useState<ReturnType<
        NonNullable<typeof editorRef.current>['selectCurrentBlock']
      > | null>(null);

      return (
        <>
          <TextEditor ref={editorRef} />
          <button onClick={() => editorRef.current?.setText('<p>Original text</p>')}>
            Set Text
          </button>
          <button
            onClick={() => {
              const selection = editorRef.current?.selectCurrentBlock();
              setStoredSelection(selection ?? null);
            }}
          >
            Store Selection
          </button>
          <button
            onClick={() => {
              if (storedSelection) {
                editorRef.current?.replaceSelection(storedSelection, 'Replacement text');
              }
            }}
          >
            Replace Selection
          </button>
        </>
      );
    };

    const user = userEvent.setup();
    render(<TestWrapper />);

    const editableArea = await screen.findByRole('textbox');
    await user.click(screen.getByRole('button', { name: 'Set Text' }));

    await waitFor(() => {
      expect(editableArea.textContent).toContain('Original text');
    });

    await user.click(editableArea);
    await user.click(screen.getByRole('button', { name: 'Store Selection' }));
    await user.click(screen.getByRole('button', { name: 'Replace Selection' }));

    await waitFor(() => {
      expect(editableArea.textContent).toContain('Replacement text');
      expect(editableArea.textContent).not.toContain('Original text');
    });
  });

  test('replaceSelection replaces text at stored selection with HTML', async () => {
    const TestWrapper = () => {
      const editorRef = useRef<TextEditorRef>(null);
      const [storedSelection, setStoredSelection] = useState<ReturnType<
        NonNullable<typeof editorRef.current>['selectCurrentBlock']
      > | null>(null);

      return (
        <>
          <TextEditor ref={editorRef} />
          <button onClick={() => editorRef.current?.setText('<p>Original text</p>')}>
            Set Text
          </button>
          <button
            onClick={() => {
              const selection = editorRef.current?.selectCurrentBlock();
              setStoredSelection(selection ?? null);
            }}
          >
            Store Selection
          </button>
          <button
            onClick={() => {
              if (storedSelection) {
                editorRef.current?.replaceSelection(
                  storedSelection,
                  '<p><strong>Bold replacement</strong></p>',
                );
              }
            }}
          >
            Replace Selection
          </button>
        </>
      );
    };

    const user = userEvent.setup();
    render(<TestWrapper />);

    const editableArea = await screen.findByRole('textbox');
    await user.click(screen.getByRole('button', { name: 'Set Text' }));

    await waitFor(() => {
      expect(editableArea.textContent).toContain('Original text');
    });

    await user.click(editableArea);
    await user.click(screen.getByRole('button', { name: 'Store Selection' }));
    await user.click(screen.getByRole('button', { name: 'Replace Selection' }));

    await waitFor(() => {
      expect(editableArea.textContent).toContain('Bold replacement');
      // eslint-disable-next-line testing-library/no-node-access -- Lexical rich text DOM
      expect(editableArea.querySelector('strong')).toBeInTheDocument();
    });
  });

  test('replaceSelection with partial selection only replaces selected portion', async () => {
    // Helper rendered inside LexicalComposer (via floatingMenu) to access
    // the actual text node key needed for a valid text-level BlockSelection.
    const PartialSelector = ({ onSelect }: { onSelect: (selection: BlockSelection) => void }) => {
      const [editor] = useLexicalComposerContext();
      return (
        <button
          onClick={() => {
            editor.getEditorState().read(() => {
              const root = $getRoot();
              const paragraph = root.getFirstChild();
              if (!paragraph || !$isElementNode(paragraph)) return;
              const textNode = paragraph.getFirstChild();
              if (!textNode || !$isTextNode(textNode)) return;
              onSelect({
                anchorKey: textNode.getKey(),
                anchorOffset: 10,
                anchorType: 'text',
                focusKey: textNode.getKey(),
                focusOffset: 16,
                focusType: 'text',
                text: 'MIDDLE',
              });
            });
          }}
        >
          Store Partial Selection
        </button>
      );
    };

    const TestWrapper = () => {
      const editorRef = useRef<TextEditorRef>(null);
      const [storedSelection, setStoredSelection] = useState<BlockSelection | null>(null);

      return (
        <>
          <TextEditor
            ref={editorRef}
            floatingMenu={<PartialSelector onSelect={setStoredSelection} />}
          />
          <button onClick={() => editorRef.current?.setText('<p>Beginning MIDDLE ending</p>')}>
            Set Text
          </button>
          <button
            onClick={() => {
              if (storedSelection) {
                editorRef.current?.replaceSelection(storedSelection, 'REPLACED');
              }
            }}
          >
            Replace Selection
          </button>
        </>
      );
    };

    const user = userEvent.setup();
    render(<TestWrapper />);

    const editableArea = await screen.findByRole('textbox');
    await user.click(screen.getByRole('button', { name: 'Set Text' }));

    await waitFor(() => {
      expect(editableArea.textContent).toContain('Beginning MIDDLE ending');
    });

    await user.click(screen.getByRole('button', { name: 'Store Partial Selection' }));
    await user.click(screen.getByRole('button', { name: 'Replace Selection' }));

    await waitFor(() => {
      expect(editableArea.textContent).toContain('REPLACED');
    });
    expect(editableArea.textContent).toContain('Beginning');
    expect(editableArea.textContent).toContain('ending');
    expect(editableArea.textContent).not.toContain('MIDDLE');
  });
});
