/**
 * @vitest-environment jsdom
 */
import { describe, test, expect } 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 EditorHandle } from './TextEditor.js';

// Mock Range.prototype.getClientRects
if (typeof Range !== 'undefined' && !Range.prototype.getClientRects) {
  Range.prototype.getClientRects = function () {
    return {
      length: 1,
      item: (index: number) => {
        if (index === 0) {
          return new DOMRect(0, 0, 0, 0);
        }
        return null;
      },
      [Symbol.iterator]: function* () {
        yield new DOMRect(0, 0, 0, 0);
      },
    } as DOMRectList;
  };
}

// Mock InputEvent.prototype.getTargetRanges
if (typeof InputEvent !== 'undefined' && !InputEvent.prototype.getTargetRanges) {
  InputEvent.prototype.getTargetRanges = function () {
    const range = document.createRange();
    return [range];
  };
}

// Mock scrollTo
global.scrollTo = vi.fn();

// Mock ResizeObserver
global.ResizeObserver = vi.fn().mockImplementation(() => ({
  observe: vi.fn(),
  unobserve: vi.fn(),
  disconnect: vi.fn(),
}));

describe('TextEditor', () => {
  afterEach(() => {
    // Clean up any CKEditor instances after each test
    document.body.innerHTML = '';
  });

  test('renders without crashing', async () => {
    render(<TextEditor />);
    const editableArea = await screen.findByRole('textbox', {
      name: /Rich Text Editor.*Editing area: main/i,
    });

    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', {
      name: /Rich Text Editor.*Editing area: main/i,
    });
    expect(editableArea).toHaveAttribute('aria-labelledby', 'Knock knock zoom zoom');
  });

  test('hides powered by UI', async () => {
    const { container } = render(<TextEditor />);
    await screen.findByRole('textbox', {
      name: /Rich Text Editor.*Editing area: main/i,
    });
    const poweredBy = container.querySelector('.ck-powered-by');
    expect(poweredBy).not.toBeInTheDocument();
  });

  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<EditorHandle>(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<EditorHandle>(null);
      const [content, setContent] = useState('');

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

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

    const editableArea = await screen.findByRole('textbox');
    await user.click(editableArea);
    await user.keyboard('My content');
    await waitFor(() => {
      expect(editableArea.textContent).toContain('My content');
    });

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

    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<EditorHandle>(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<EditorHandle>(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<EditorHandle>(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();
    });
  });

  test('converts Markdown to HTML when setText is called with Markdown', async () => {
    const TestWrapper = () => {
      const editorRef = useRef<EditorHandle>(null);

      const setMarkdown = () => {
        const markdown = `# Heading 1

## Heading 2

This is **bold** and this is *italic*.

- Item 1
- Item 2
- Item 3

> This is a blockquote

[Link text](https://example.com)`;

        editorRef.current?.setText(markdown);
      };

      return (
        <>
          <TextEditor ref={editorRef} />
          <button onClick={setMarkdown}>Set Markdown</button>
        </>
      );
    };

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

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

    await waitFor(() => {
      const editableArea = screen.getByRole('textbox');

      // Check for heading elements
      expect(editableArea.querySelector('h1')).toBeInTheDocument();
      expect(editableArea.querySelector('h1')?.textContent).toBe('Heading 1');
      expect(editableArea.querySelector('h2')).toBeInTheDocument();
      expect(editableArea.querySelector('h2')?.textContent).toBe('Heading 2');

      // Check for formatted text
      expect(editableArea.querySelector('strong')).toBeInTheDocument();
      expect(editableArea.querySelector('strong')?.textContent).toBe('bold');
      expect(editableArea.querySelector('em')).toBeInTheDocument();
      expect(editableArea.querySelector('em')?.textContent).toBe('italic');

      // Check for list
      const list = editableArea.querySelector('ul');
      expect(list).toBeInTheDocument();
      const listItems = list?.querySelectorAll('li');
      expect(listItems).toHaveLength(3);
      expect(listItems?.[0]?.textContent).toContain('Item 1');

      // Check for blockquote
      expect(editableArea.querySelector('blockquote')).toBeInTheDocument();
      expect(editableArea.querySelector('blockquote')?.textContent).toContain(
        'This is a blockquote',
      );

      // Check for link
      const link = editableArea.querySelector('a');
      expect(link).toBeInTheDocument();
      expect(link?.textContent).toBe('Link text');
      expect(link?.getAttribute('href')).toBe('https://example.com');
    });
  });
});
