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 TextEditor, { type TextEditorRef } 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');

    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');
      expect(editableArea.querySelector('strong')).toBeInTheDocument();
      expect(editableArea.querySelector('em')).toBeInTheDocument();
    });
  });
});
