import { describe, test, expect, vi } from 'vitest';
import { render, screen, waitFor } from '@testing-library/react';
import { type UserEvent, userEvent } from '@testing-library/user-event';
import ContentGeneratorTextArea from './ContentGenerator.js';

vi.mock('../../assets/lightbulb.svg?react', () => ({
  default: () => <div data-testid="lightbulb-icon">Lightbulb</div>,
}));

const clickLightbulb = async (user: UserEvent) => {
  const lightbulb = await screen.findByLabelText('Generate content with AI');
  await user.click(lightbulb);
};

describe('ContentGeneratorTextArea', () => {
  test('renders lightbulb button after editor toolbar mounts', async () => {
    render(<ContentGeneratorTextArea draftId="12345" />);

    await waitFor(() => {
      expect(screen.getByLabelText('Generate content with AI')).toBeInTheDocument();
    });
  });

  test('opens menu when lightbulb is clicked', async () => {
    const user = userEvent.setup();
    render(<ContentGeneratorTextArea draftId="12345" />);
    await clickLightbulb(user);

    expect(screen.getByTestId('content-generator__context-menu')).toBeInTheDocument();
    expect(screen.getByText('Replace with generated outline')).toBeInTheDocument();
    expect(screen.getByText('Replace with generated draft')).toBeInTheDocument();
  });

  test('closes menu when lightbulb is clicked again', async () => {
    const user = userEvent.setup();
    render(<ContentGeneratorTextArea draftId="12345" />);
    await clickLightbulb(user);

    expect(screen.getByText('Replace with generated outline')).toBeInTheDocument();
    const lightbulb = await screen.findByLabelText('Generate content with AI');
    await user.click(lightbulb);
    expect(screen.queryByText('Replace with generated outline')).not.toBeInTheDocument();
  });

  test('displays streaming overlay when loading', async () => {
    const user = userEvent.setup();
    render(<ContentGeneratorTextArea draftId="12345" />);
    await clickLightbulb(user);

    const generateOutline = screen.getByText('Replace with generated outline');
    await user.click(generateOutline);

    const messageElement = screen.getByText(/Generating content/i);
    expect(messageElement).toBeInTheDocument();
    await waitFor(
      () => {
        expect(screen.queryByText(/Generating content/i)).not.toBeInTheDocument();
      },
      { timeout: 3000 },
    );
  });

  test('generates text when "Generate outline" is clicked', async () => {
    const user = userEvent.setup();
    render(<ContentGeneratorTextArea draftId="12345" />);
    await clickLightbulb(user);

    const generateOutline = screen.getByText('Replace with generated outline');
    await user.click(generateOutline);

    const messageElement = screen.getByText(/Generating content/i);
    expect(messageElement).toBeInTheDocument();

    await waitFor(() => {
      const editableArea = screen.getByRole('textbox');
      expect(editableArea.textContent).toContain(
        'Strategic overview: This content focuses specifically on the unique cultural significance',
      );
    });
  });

  test('generates text when "Generate draft" is clicked', async () => {
    const user = userEvent.setup();
    render(<ContentGeneratorTextArea draftId="12345" />);
    await clickLightbulb(user);

    const generateDraft = screen.getByText('Replace with generated draft');
    await user.click(generateDraft);
    const generateContentButton = screen.getByRole('button', { name: 'Generate content' });
    await user.click(generateContentButton);

    const messageElement = screen.getByText(/Generating content/i);
    expect(messageElement).toBeInTheDocument();

    await waitFor(() => {
      const editableArea = screen.getByRole('textbox');
      expect(editableArea.textContent).toContain(
        'Philadelphia sports culture is legendary for its passionate fans and compelling traditions',
      );
    });
  });

  test('generates text with user options when "Generate draft" is clicked', async () => {
    const user = userEvent.setup();
    const fetchSpy = vi.spyOn(global, 'fetch');
    render(<ContentGeneratorTextArea draftId="0ef76729-8512-432b-8b72-8a1bc47e4d60" />);
    await clickLightbulb(user);

    const generateDraft = screen.getByText('Replace with generated draft');
    await user.click(generateDraft);

    // Fill in tone and style
    const toneAndStyleInput = screen.getByPlaceholderText('Describe the voice of your content');
    await user.type(toneAndStyleInput, 'over excited');

    // Fill in terms to exclude (tag input)
    const tagInput = screen.getByRole('textbox', { name: '' });
    await user.type(tagInput, 'orange{Enter}');

    // Fill in additional instructions
    const additionalInstructionsInput = screen.getByPlaceholderText(
      'What else should we know about your content?',
    );
    await user.type(additionalInstructionsInput, 'the audience is non-US');

    const generateContentButton = screen.getByRole('button', { name: 'Generate content' });
    await user.click(generateContentButton);

    // Verify the POST request was made with correct URL and payload
    await waitFor(() => {
      expect(fetchSpy).toHaveBeenCalledWith(
        '/conductor/proxy/v3/draft-generation/content-generation?draft_id=0ef76729-8512-432b-8b72-8a1bc47e4d60&cache=false',
        expect.objectContaining({
          method: 'POST',
          body: JSON.stringify({
            voice_and_tone: 'over excited',
            terms_to_exclude: ['orange'],
            additional_instructions: 'the audience is non-US',
          }),
          headers: expect.objectContaining({
            'Content-Type': 'application/json',
          }),
        }),
      );
    });
  });

  test('closes menu after "Generate outline" is clicked', async () => {
    const user = userEvent.setup();
    render(<ContentGeneratorTextArea draftId="12345" />);
    await clickLightbulb(user);

    await user.click(screen.getByText('Replace with generated outline'));

    expect(screen.queryByText('Replace with generated outline')).not.toBeInTheDocument();
  });

  test('closes menu after "Generate draft" is clicked', async () => {
    const user = userEvent.setup();
    render(<ContentGeneratorTextArea draftId="12345" />);
    await clickLightbulb(user);

    await user.click(screen.getByText('Replace with generated draft'));

    expect(screen.queryByText('Replace with generated draft')).not.toBeInTheDocument();
  });

  test('calls cancel when Cancel button is clicked in streaming overlay', async () => {
    const user = userEvent.setup();
    render(<ContentGeneratorTextArea draftId="12345" />);
    await clickLightbulb(user);

    await user.click(screen.getByText('Replace with generated outline'));

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

    const editableArea = screen.getByRole('textbox');
    expect(editableArea.textContent).not.toContain(
      'enduring role in Philadelphia sports and fandom',
    );
  });

  test('displays error overlay when there is an error', async () => {
    const user = userEvent.setup();
    render(<ContentGeneratorTextArea draftId="12345-bad" />);
    await clickLightbulb(user);

    const generateOutline = screen.getByText('Replace with generated outline');
    await user.click(generateOutline);

    expect(screen.getByText('Error')).toBeInTheDocument();
    expect(screen.getByText(/There was an error generating content/)).toBeInTheDocument();
    expect(screen.getByRole('button', { name: 'Close' })).toBeInTheDocument();
  });

  test('calls cancel when Close button is clicked in error overlay', async () => {
    const user = userEvent.setup();
    render(<ContentGeneratorTextArea draftId="12345-bad" />);
    await clickLightbulb(user);

    const generateOutline = screen.getByText('Replace with generated outline');
    await user.click(generateOutline);

    expect(screen.getByText('Error')).toBeInTheDocument();
    expect(screen.getByText(/There was an error generating content/)).toBeInTheDocument();
    expect(screen.getByRole('button', { name: 'Close' })).toBeInTheDocument();

    const cancelButton = screen.getByRole('button', { name: 'Close' });
    await user.click(cancelButton);
    expect(screen.queryByText('Error')).not.toBeInTheDocument();
  });

  test('applies streaming class when loading', async () => {
    const user = userEvent.setup();
    const { container } = render(<ContentGeneratorTextArea draftId="12345" />);
    await clickLightbulb(user);

    const generateOutline = screen.getByText('Replace with generated outline');
    await user.click(generateOutline);

    // eslint-disable-next-line testing-library/no-container, testing-library/no-node-access -- checking CSS class state
    const mainDiv = container.querySelector('.content-generator');
    expect(mainDiv).toHaveClass('content-generator--is-streaming');
  });

  test('does not apply streaming class when error occurs', async () => {
    const user = userEvent.setup();
    const { container } = render(<ContentGeneratorTextArea draftId="12345-bad" />);
    await clickLightbulb(user);

    const generateOutline = screen.getByText('Replace with generated outline');
    await user.click(generateOutline);

    // eslint-disable-next-line testing-library/no-container, testing-library/no-node-access -- checking CSS class state
    const mainDiv = container.querySelector('.content-generator');
    expect(mainDiv).not.toHaveClass('conductor-content-generator--is-streaming');
  });

  test('converts Markdown to HTML when streaming', async () => {
    const user = userEvent.setup();
    render(<ContentGeneratorTextArea draftId="12345" />);
    await clickLightbulb(user);
    await user.click(screen.getByText('Replace with generated draft'));
    const generateContentButton = screen.getByRole('button', { name: 'Generate content' });
    await user.click(generateContentButton);

    const editableArea = screen.getByRole('textbox');

    /* eslint-disable testing-library/no-node-access -- verifying Lexical rich text HTML structure from streaming */
    await waitFor(
      () => {
        expect(editableArea.querySelector('h1')).toBeInTheDocument();
        expect(editableArea.querySelector('h2')).toBeInTheDocument();
        expect(editableArea.querySelector('h3')).toBeInTheDocument();
        expect(editableArea.querySelector('p')).toBeInTheDocument();
      },
      { timeout: 5000 },
    );
    /* eslint-enable testing-library/no-node-access */
  });

  describe('Expand and Shorten Selection', () => {
    const clickInEditor = async (user: UserEvent) => {
      const editableArea = await screen.findByRole('textbox');
      // Click on the first paragraph to place cursor there
      // eslint-disable-next-line testing-library/no-node-access -- Lexical editor DOM
      const firstParagraph = editableArea.querySelector('p');
      if (firstParagraph) {
        await user.click(firstParagraph);
      } else {
        await user.click(editableArea);
      }
    };

    test('shows expand and shorten options when editor has content and cursor is in non-empty block', async () => {
      const user = userEvent.setup();
      render(
        <ContentGeneratorTextArea draftId="12345" initialText="<p>Some existing content</p>" />,
      );

      // Wait for content to be rendered and click in the editor
      await waitFor(() => {
        expect(screen.getByRole('textbox').textContent).toContain('Some existing content');
      });
      await clickInEditor(user);
      await clickLightbulb(user);

      expect(screen.getByText('Expand selection')).toBeInTheDocument();
      expect(screen.getByText('Shorten selection')).toBeInTheDocument();
      expect(screen.getByText('Expand selection')).toHaveAttribute('aria-disabled', 'false');
      expect(screen.getByText('Shorten selection')).toHaveAttribute('aria-disabled', 'false');
    });

    test('shows disabled expand and shorten options when editor is empty', async () => {
      const user = userEvent.setup();
      render(<ContentGeneratorTextArea draftId="12345" />);

      await clickLightbulb(user);

      expect(screen.getByText('Expand selection')).toHaveAttribute('aria-disabled', 'true');
      expect(screen.getByText('Shorten selection')).toHaveAttribute('aria-disabled', 'true');
    });

    test('clicking Expand selection opens replace text panel and streams content', async () => {
      const user = userEvent.setup();
      render(
        <ContentGeneratorTextArea draftId="12345" initialText="<p>Some existing content</p>" />,
      );

      await waitFor(() => {
        expect(screen.getByRole('textbox').textContent).toContain('Some existing content');
      });
      await clickInEditor(user);
      await clickLightbulb(user);
      await user.click(screen.getByText('Expand selection'));

      // Should show the replace text panel with buttons
      expect(screen.getByRole('button', { name: 'Cancel' })).toBeInTheDocument();
      expect(screen.getByRole('button', { name: 'Insert text' })).toBeInTheDocument();

      // Wait for streamed content to appear
      await waitFor(
        () => {
          expect(screen.getByText(/role on game day is distinct/i)).toBeInTheDocument();
        },
        { timeout: 10000 },
      );
    }, 15000);

    test('clicking Shorten selection opens replace text panel and streams content', async () => {
      const user = userEvent.setup();
      render(
        <ContentGeneratorTextArea draftId="12345" initialText="<p>Some existing content</p>" />,
      );

      await waitFor(() => {
        expect(screen.getByRole('textbox').textContent).toContain('Some existing content');
      });
      await clickInEditor(user);
      await clickLightbulb(user);
      await user.click(screen.getByText('Shorten selection'));

      // Should show the replace text panel with buttons
      expect(screen.getByRole('button', { name: 'Cancel' })).toBeInTheDocument();
      expect(screen.getByRole('button', { name: 'Insert text' })).toBeInTheDocument();

      // Wait for streamed content to appear
      await waitFor(
        () => {
          expect(screen.getByText(/Gritty, Swoop, and the Phillie Phanatic/i)).toBeInTheDocument();
        },
        { timeout: 5000 },
      );
    });

    test('Cancel button closes the replace text panel', async () => {
      const user = userEvent.setup();
      render(
        <ContentGeneratorTextArea draftId="12345" initialText="<p>Some existing content</p>" />,
      );

      await waitFor(() => {
        expect(screen.getByRole('textbox').textContent).toContain('Some existing content');
      });
      await clickInEditor(user);
      await clickLightbulb(user);
      await user.click(screen.getByText('Expand selection'));

      // Wait for panel to be visible
      await waitFor(() => {
        expect(screen.getByRole('button', { name: 'Cancel' })).toBeInTheDocument();
      });

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

      // Panel should be closed
      expect(screen.queryByRole('button', { name: 'Insert text' })).not.toBeInTheDocument();
    });

    test('Reload button is replaced with a loader while loading', { timeout: 15000 }, async () => {
      const user = userEvent.setup();
      const { container } = render(
        <ContentGeneratorTextArea draftId="12345" initialText="<p>Some existing content</p>" />,
      );

      await waitFor(() => {
        expect(screen.getByRole('textbox').textContent).toContain('Some existing content');
      });
      await clickInEditor(user);
      await clickLightbulb(user);
      await user.click(screen.getByText('Expand selection'));

      /* eslint-disable testing-library/no-container, testing-library/no-node-access -- checking icon toggle state by CSS class */
      // Reload button should be disabled while loading
      const reloadButton = container.querySelector('.reload-icon');
      expect(reloadButton).toBeNull();

      const loadingIcon = container.querySelector('.content-generator__expand-shorten-loading');
      expect(loadingIcon).toBeInTheDocument();

      // Wait for loading to complete
      await waitFor(
        () => {
          const reloadButton = container.querySelector('.reload-icon');
          const loadingIcon = container.querySelector('.content-generator__expand-shorten-loading');
          expect(reloadButton).toBeInTheDocument();
          expect(loadingIcon).toBeNull();
        },
        { timeout: 10000 },
      );
      /* eslint-enable testing-library/no-container, testing-library/no-node-access */
    });

    test('Insert text button is disabled while loading', async () => {
      const user = userEvent.setup();
      render(
        <ContentGeneratorTextArea draftId="12345" initialText="<p>Some existing content</p>" />,
      );

      await waitFor(() => {
        expect(screen.getByRole('textbox').textContent).toContain('Some existing content');
      });
      await clickInEditor(user);
      await clickLightbulb(user);
      await user.click(screen.getByText('Expand selection'));

      // Insert text button should be disabled initially while loading
      const insertButton = screen.getByRole('button', { name: 'Insert text' });
      expect(insertButton).toBeDisabled();

      // Wait for loading to complete
      await waitFor(
        () => {
          expect(insertButton).not.toBeDisabled();
        },
        { timeout: 10000 },
      );
    }, 15000);

    test('clicking Insert text closes the panel', async () => {
      const user = userEvent.setup();
      render(
        <ContentGeneratorTextArea draftId="12345" initialText="<p>Some existing content</p>" />,
      );

      await waitFor(() => {
        expect(screen.getByRole('textbox').textContent).toContain('Some existing content');
      });
      await clickInEditor(user);
      await clickLightbulb(user);
      await user.click(screen.getByText('Expand selection'));

      // Wait for content to load
      await waitFor(
        () => {
          expect(screen.getByRole('button', { name: 'Insert text' })).not.toBeDisabled();
        },
        { timeout: 10000 },
      );

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

      // Panel should be closed
      expect(screen.queryByRole('button', { name: 'Insert text' })).not.toBeInTheDocument();
    }, 15000);

    test('clicking Insert text replaces the selected text in the editor', async () => {
      const user = userEvent.setup();
      render(
        <ContentGeneratorTextArea draftId="12345" initialText="<p>Some existing content</p>" />,
      );

      const editableArea = await screen.findByRole('textbox');
      await waitFor(() => {
        expect(editableArea.textContent).toContain('Some existing content');
      });
      await clickInEditor(user);
      await clickLightbulb(user);
      await user.click(screen.getByText('Expand selection'));

      // Wait for content to load
      await waitFor(
        () => {
          expect(screen.getByRole('button', { name: 'Insert text' })).not.toBeDisabled();
        },
        { timeout: 10000 },
      );

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

      // Editor should now contain the expanded text instead of the original
      await waitFor(() => {
        expect(editableArea.textContent).toContain('role on game day is distinct');
        expect(editableArea.textContent).not.toContain('Some existing content');
      });
    }, 15000);

    test('sends correct payload when expanding selection', async () => {
      const user = userEvent.setup();
      const fetchSpy = vi.spyOn(global, 'fetch');
      render(
        <ContentGeneratorTextArea
          draftId="test-expand-draft"
          initialText="<p>My outline content here</p>"
        />,
      );

      await waitFor(() => {
        expect(screen.getByRole('textbox').textContent).toContain('My outline content here');
      });
      await clickInEditor(user);
      await clickLightbulb(user);
      await user.click(screen.getByText('Expand selection'));

      await waitFor(() => {
        expect(fetchSpy).toHaveBeenCalledWith(
          '/conductor/proxy/v3/outline-expansion/content-generation?draft_id=test-expand-draft&cache=false',
          expect.objectContaining({
            method: 'POST',
            body: expect.stringContaining('outline'),
          }),
        );
      });
    });

    test('sends correct payload when shortening selection', async () => {
      const user = userEvent.setup();
      const fetchSpy = vi.spyOn(global, 'fetch');
      render(
        <ContentGeneratorTextArea
          draftId="test-shorten-draft"
          initialText="<p>My content to shorten</p>"
        />,
      );

      await waitFor(() => {
        expect(screen.getByRole('textbox').textContent).toContain('My content to shorten');
      });
      await clickInEditor(user);
      await clickLightbulb(user);
      await user.click(screen.getByText('Shorten selection'));

      await waitFor(() => {
        expect(fetchSpy).toHaveBeenCalledWith(
          '/conductor/proxy/v3/text-shortener/content-generation?draft_id=test-shorten-draft&cache=false',
          expect.objectContaining({
            method: 'POST',
            body: expect.stringContaining('text'),
          }),
        );
      });
    });

    test('expanding a list item replaces it with valid HTML structure', async () => {
      const user = userEvent.setup();
      render(
        <ContentGeneratorTextArea
          draftId="12345"
          initialText="<ul><li>List item to expand</li></ul>"
        />,
      );

      const editableArea = await screen.findByRole('textbox');
      await waitFor(() => {
        expect(editableArea.textContent).toContain('List item to expand');
      });
      await clickInEditor(user);
      await clickLightbulb(user);
      await user.click(screen.getByText('Expand selection'));

      // Wait for content to load
      await waitFor(
        () => {
          expect(screen.getByRole('button', { name: 'Insert text' })).not.toBeDisabled();
        },
        { timeout: 10000 },
      );

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

      // Verify that the editor content is valid HTML (no span directly inside ul)
      await waitFor(() => {
        // The expanded content should replace the list
        expect(editableArea.textContent).toContain('role on game day is distinct');
        expect(editableArea.textContent).not.toContain('List item to expand');

        // Verify no invalid HTML structure: span should not be direct child of ul
        /* eslint-disable testing-library/no-node-access -- verifying valid HTML structure in Lexical editor */
        const lists = editableArea.querySelectorAll('ul');
        lists.forEach((list) => {
          const directChildren = Array.from(list.children);
          directChildren.forEach((child) => {
            // All direct children of ul should be li elements, not span or other inline elements
            expect(child.tagName.toLowerCase()).toBe('li');
          });
        });
        /* eslint-enable testing-library/no-node-access */
      });
    }, 15000);
  });

  describe('Copy Text Button', () => {
    test('copies text to clipboard when clicked', async () => {
      const user = userEvent.setup();
      render(<ContentGeneratorTextArea draftId="12345" initialText="<p>Hello, world!</p>" />);

      await screen.findByRole('textbox');

      const copyButton = screen.getByRole('button', { name: /Copy Text/i });
      await user.click(copyButton);
      const copiedText = await window.navigator.clipboard.readText();
      expect(copiedText).toEqual('Hello, world!');
    });

    test('copies formatted HTML content correctly', async () => {
      const user = userEvent.setup();
      render(<ContentGeneratorTextArea draftId="12345" initialText="<p>Hello, world!</p>" />);

      await screen.findByRole('textbox');

      const copyButton = screen.getByRole('button', { name: /Copy Text/i });
      await user.click(copyButton);
      const clipboardItems = await navigator.clipboard.read();
      const clipboardBlobs = [];

      for (const clipboardItem of clipboardItems) {
        for (const type of clipboardItem.types) {
          const blob = await clipboardItem.getType(type);
          clipboardBlobs.push(blob);
        }
      }

      expect(clipboardBlobs).toHaveLength(2);
      expect(clipboardBlobs[0]!.type).toEqual('text/html');

      const htmlText = await clipboardBlobs[0]!.text();
      expect(htmlText).toEqual('<p>Hello, world!</p>');
    });

    test('strips HTML for plain text copy', async () => {
      const user = userEvent.setup();
      render(<ContentGeneratorTextArea draftId="12345" initialText="<p>Hello, world!</p>" />);

      await screen.findByRole('textbox');

      const copyButton = screen.getByRole('button', { name: /Copy Text/i });
      await user.click(copyButton);
      const clipboardItems = await navigator.clipboard.read();
      const clipboardBlobs = [];

      for (const clipboardItem of clipboardItems) {
        for (const type of clipboardItem.types) {
          const blob = await clipboardItem.getType(type);
          clipboardBlobs.push(blob);
        }
      }

      expect(clipboardBlobs).toHaveLength(2);
      expect(clipboardBlobs[1]!.type).toEqual('text/plain');
      expect(clipboardBlobs[1]!.type).toEqual('text/plain');

      const htmlText = await clipboardBlobs[1]!.text();
      expect(htmlText).toEqual('Hello, world!');
    });
  });
});
