import { render, waitFor } from '@testing-library/vue';
import userEvent from '@testing-library/user-event';
import { afterEach, describe, expect, test, vi, beforeEach } from 'vitest';
import UiCodeBlock from '../UiCodeBlock.vue';

vi.mock('shiki', () => ({
  codeToHtml: vi
    .fn()
    .mockResolvedValue(
      '<pre class="shiki shiki-themes github-light github-dark"><code><span class="line">const x = 1;</span></code></pre>',
    ),
}));

describe('UiCodeBlock', () => {
  let originalClipboardDescriptor: PropertyDescriptor | undefined;
  let originalExecCommand: typeof document.execCommand;

  beforeEach(() => {
    vi.clearAllMocks();

    originalClipboardDescriptor = Object.getOwnPropertyDescriptor(
      navigator,
      'clipboard',
    );
    originalExecCommand = document.execCommand;

    Object.defineProperty(navigator, 'clipboard', {
      value: {
        writeText: vi.fn().mockResolvedValue(undefined),
        readText: vi.fn().mockResolvedValue(''),
      },
      writable: true,
      configurable: true,
    });
    document.execCommand = vi.fn().mockReturnValue(true);
  });

  afterEach(() => {
    if (originalClipboardDescriptor) {
      Object.defineProperty(
        navigator,
        'clipboard',
        originalClipboardDescriptor,
      );
    } else {
      delete (navigator as never)['clipboard'];
    }
    document.execCommand = originalExecCommand;
  });

  test('renders Shiki highlighted output after async load', async () => {
    const { container } = render(UiCodeBlock, {
      props: { code: 'const x = 1;', language: 'typescript' },
    });

    await waitFor(() => {
      expect(container.querySelector('.shiki')).toBeInTheDocument();
    });
  });

  test('normalizes low-contrast Shiki token colors to theme tokens', async () => {
    const { codeToHtml } = await import('shiki');
    vi.mocked(codeToHtml).mockResolvedValueOnce(
      '<pre class="shiki shiki-themes github-light github-dark"><code><span class="line"><span style="color:#E36209;--shiki-dark:#FFAB70">columns</span></span></code></pre>',
    );

    const { container } = render(UiCodeBlock, {
      props: { code: 'columns', language: 'typescript' },
    });

    await waitFor(() => {
      expect(container.querySelector('.shiki')).toBeInTheDocument();
    });

    expect(container.innerHTML).toContain(
      'oklch(var(--content-on-surface-primary))',
    );
    expect(container.innerHTML).not.toContain('#E36209');
    expect(container.innerHTML).not.toContain('#FFAB70');
  });

  test('shows language label in header', async () => {
    const { getByText } = render(UiCodeBlock, {
      props: { code: 'print("hi")', language: 'python' },
    });

    await waitFor(() => {
      expect(getByText('python')).toBeInTheDocument();
    });
  });

  test('shows filename in header and takes precedence over language label', async () => {
    const { getByText, queryByText } = render(UiCodeBlock, {
      props: { code: '{}', language: 'json', filename: 'config.json' },
    });

    await waitFor(() => {
      expect(getByText('config.json')).toBeInTheDocument();
      expect(queryByText('json')).not.toBeInTheDocument();
    });
  });

  test('shows copy button by default', async () => {
    const { getByRole } = render(UiCodeBlock, {
      props: { code: 'code', language: 'typescript' },
    });

    await waitFor(() => {
      expect(getByRole('button', { name: 'Copy code' })).toBeInTheDocument();
    });
  });

  test('hides copy button when showCopyButton is false', async () => {
    const { queryByRole } = render(UiCodeBlock, {
      props: { code: 'code', language: 'typescript', showCopyButton: false },
    });

    await waitFor(() => {
      expect(
        queryByRole('button', { name: 'Copy code' }),
      ).not.toBeInTheDocument();
    });
  });

  test('changes button label to "Copied" after clicking copy', async () => {
    const user = userEvent.setup();

    const { getByRole } = render(UiCodeBlock, {
      props: { code: 'const x = 1;', language: 'typescript' },
    });

    await waitFor(() => {
      expect(getByRole('button', { name: 'Copy code' })).toBeInTheDocument();
    });

    await user.click(getByRole('button', { name: 'Copy code' }));

    await waitFor(() => {
      expect(getByRole('button', { name: 'Copied' })).toBeInTheDocument();
    });
  });

  test('applies line-numbers class when showLineNumbers is true', async () => {
    const { container } = render(UiCodeBlock, {
      props: { code: 'code', language: 'typescript', showLineNumbers: true },
    });

    await waitFor(() => {
      expect(
        container.querySelector('.ui-code-block__line-numbers'),
      ).toBeInTheDocument();
    });
  });

  test('applies maxHeight style when provided', async () => {
    const { container } = render(UiCodeBlock, {
      props: { code: 'code', language: 'typescript', maxHeight: '200px' },
    });

    await waitFor(() => {
      const scrollContainer = container.querySelector('.overflow-auto');
      expect(scrollContainer).toHaveStyle({ maxHeight: '200px' });
    });
  });

  test('uses custom copy labels when provided', async () => {
    const user = userEvent.setup();

    const { getByRole } = render(UiCodeBlock, {
      props: {
        code: 'x = 1',
        language: 'python',
        copyLabel: 'Code kopieren',
        copiedLabel: 'Kopiert',
      },
    });

    await waitFor(() => {
      expect(
        getByRole('button', { name: 'Code kopieren' }),
      ).toBeInTheDocument();
    });

    await user.click(getByRole('button', { name: 'Code kopieren' }));

    await waitFor(() => {
      expect(getByRole('button', { name: 'Kopiert' })).toBeInTheDocument();
    });
  });

  test('falls back to text highlighting when language is unsupported', async () => {
    const { codeToHtml } = await import('shiki');
    const mock = vi.mocked(codeToHtml);
    mock
      .mockRejectedValueOnce(new Error('Unknown language'))
      .mockResolvedValueOnce(
        '<pre class="shiki shiki-themes github-light github-dark"><code><span class="line">code</span></code></pre>',
      );

    const { container } = render(UiCodeBlock, {
      props: { code: 'code', language: 'unknown-lang' },
    });

    await waitFor(() => {
      expect(container.querySelector('.shiki')).toBeInTheDocument();
    });

    expect(mock).toHaveBeenCalledTimes(2);
    expect(mock).toHaveBeenLastCalledWith(
      'code',
      expect.objectContaining({ lang: 'text' }),
    );
  });

  test('shows plain pre/code fallback when all highlighting fails', async () => {
    const { codeToHtml } = await import('shiki');
    vi.mocked(codeToHtml).mockRejectedValue(new Error('Shiki broken'));

    const { container } = render(UiCodeBlock, {
      props: { code: 'fallback content', language: 'typescript' },
    });

    await waitFor(() => {
      const codeEl = container.querySelector('pre code');
      expect(codeEl).toBeInTheDocument();
      expect(codeEl?.textContent).toBe('fallback content');
    });
  });

  test('calls codeToHtml with correct language and dual themes', async () => {
    const { codeToHtml } = await import('shiki');

    render(UiCodeBlock, {
      props: { code: 'const x = 1;', language: 'typescript' },
    });

    await waitFor(() => {
      expect(codeToHtml).toHaveBeenCalledWith('const x = 1;', {
        lang: 'typescript',
        themes: {
          light: 'github-light',
          dark: 'github-dark',
        },
      });
    });
  });
});
