import { describe, it, expect, beforeEach } from 'vitest';
import { act, cleanup, render, renderHook, screen, fireEvent } from '@testing-library/react';

import { Markdown } from '../Markdown.js';
import { CodeDiff } from '../CodeDiff.js';
import { ExplainThis } from '../ExplainThis.js';
import { usePromptHistory } from '../usePromptHistory.js';
import { useTokenCount } from '../useTokenCount.js';

beforeEach(() => cleanup());

describe('Markdown', () => {
  it('renders headings + paragraphs + bold + italic + inline code', () => {
    render(
      <Markdown
        source={[
          '# Title',
          '',
          'A **bold** word, *italic* word, and `inline` code.',
          '',
          '## Sub',
          '',
          'Closing line.',
        ].join('\n')}
      />,
    );
    const root = screen.getByTestId('bl-markdown');
    expect(root.querySelectorAll('h1')).toHaveLength(1);
    expect(root.querySelectorAll('h2')).toHaveLength(1);
    expect(root.querySelectorAll('strong')).toHaveLength(1);
    expect(root.querySelectorAll('em')).toHaveLength(1);
    expect(root.querySelectorAll('code')).toHaveLength(1);
  });

  it('renders fenced code blocks with lang attribute', () => {
    render(
      <Markdown
        source={['```ts', 'const x = 1;', '```'].join('\n')}
      />,
    );
    const pre = screen.getByTestId('bl-markdown-code');
    expect(pre.getAttribute('data-lang')).toBe('ts');
    expect(pre.textContent).toMatch(/const x = 1;/);
  });

  it('renders unordered and ordered lists', () => {
    render(
      <Markdown
        source={['- one', '- two', '', '1. first', '2. second'].join('\n')}
      />,
    );
    const root = screen.getByTestId('bl-markdown');
    expect(root.querySelectorAll('ul')).toHaveLength(1);
    expect(root.querySelectorAll('ol')).toHaveLength(1);
    expect(root.querySelectorAll('li')).toHaveLength(4);
  });

  it('renders citation chips and marks missing ids', () => {
    render(
      <Markdown
        source="See [cite:doc:42#§3] and [cite:doc:missing]."
        citations={[{ id: 'doc:42#§3', label: 'Q4 review' }]}
      />,
    );
    const chips = screen.getAllByTestId('bl-markdown-citation');
    expect(chips).toHaveLength(2);
    expect(chips[0]!.getAttribute('data-missing')).toBe('false');
    expect(chips[0]!.textContent).toBe('Q4 review');
    expect(chips[1]!.getAttribute('data-missing')).toBe('true');
    expect(chips[1]!.textContent).toBe('[?]');
  });

  it('renders links via [text](url) syntax', () => {
    render(<Markdown source="Read [the docs](https://example.com/docs)." />);
    const a = screen.getByTestId('bl-markdown').querySelector('a');
    expect(a?.getAttribute('href')).toBe('https://example.com/docs');
    expect(a?.textContent).toBe('the docs');
  });
});

describe('CodeDiff', () => {
  const before = ['const x = 1;', 'const y = 2;', 'console.log(x + y);'].join('\n');
  const after = ['const x = 1;', 'const y = 3;', 'console.log(x * y);'].join('\n');

  it('renders split view by default with both panes', () => {
    render(<CodeDiff before={before} after={after} filename="demo.ts" />);
    const el = screen.getByTestId('bl-code-diff');
    expect(el.getAttribute('data-view')).toBe('split');
    expect(screen.getByTestId('bl-code-diff-left')).toBeDefined();
    expect(screen.getByTestId('bl-code-diff-right')).toBeDefined();
  });

  it('detects deletion + addition rows in the diff', () => {
    render(<CodeDiff before={before} after={after} view="unified" />);
    const rows = screen.getAllByTestId('bl-code-diff-row');
    expect(rows.some((r) => r.getAttribute('data-kind') === 'add')).toBe(true);
    expect(rows.some((r) => r.getAttribute('data-kind') === 'del')).toBe(true);
    expect(rows.some((r) => r.getAttribute('data-kind') === 'eq')).toBe(true);
  });

  it('unified view renders all kinds inline', () => {
    render(<CodeDiff before="a\nb" after="a\nc" view="unified" />);
    expect(screen.getByTestId('bl-code-diff-unified')).toBeDefined();
  });

  it('identical inputs produce only `eq` rows', () => {
    render(<CodeDiff before="x\ny" after="x\ny" view="unified" />);
    const kinds = screen
      .getAllByTestId('bl-code-diff-row')
      .map((r) => r.getAttribute('data-kind'));
    expect(kinds.every((k) => k === 'eq')).toBe(true);
  });
});

describe('ExplainThis', () => {
  it('renders children and stays hidden when selection is empty', () => {
    render(
      <ExplainThis onExplain={() => {}}>
        <p data-testid="text">Hello there.</p>
      </ExplainThis>,
    );
    expect(screen.queryByTestId('bl-explain-this-popover')).toBeNull();
  });

  it('disabled flag suppresses the popover even on selection', () => {
    render(
      <ExplainThis disabled onExplain={() => {}}>
        <p data-testid="text">Hello there.</p>
      </ExplainThis>,
    );
    expect(
      screen.getByTestId('bl-explain-this').getAttribute('data-disabled'),
    ).toBe('true');
  });
});

describe('usePromptHistory', () => {
  it('pushes + dedupes consecutive duplicates', () => {
    const { result } = renderHook(() => usePromptHistory({ storageKey: null }));
    act(() => result.current.push('hello'));
    act(() => result.current.push('hello'));
    act(() => result.current.push('world'));
    expect(result.current.history).toEqual(['hello', 'world']);
  });

  it('trims to capacity', () => {
    const { result } = renderHook(() =>
      usePromptHistory({ storageKey: null, capacity: 3 }),
    );
    act(() => result.current.push('a'));
    act(() => result.current.push('b'));
    act(() => result.current.push('c'));
    act(() => result.current.push('d'));
    expect(result.current.history).toEqual(['b', 'c', 'd']);
  });

  it('prev/next walks the history in bash-style order', () => {
    const { result } = renderHook(() => usePromptHistory({ storageKey: null }));
    act(() => result.current.push('a'));
    act(() => result.current.push('b'));
    act(() => result.current.push('c'));
    let v: string | null = null;
    act(() => {
      v = result.current.prev();
    });
    expect(v).toBe('c');
    act(() => {
      v = result.current.prev();
    });
    expect(v).toBe('b');
    act(() => {
      v = result.current.next();
    });
    expect(v).toBe('c');
    act(() => {
      v = result.current.next();
    });
    expect(v).toBeNull();
  });

  it('clear empties the history', () => {
    const { result } = renderHook(() => usePromptHistory({ storageKey: null }));
    act(() => result.current.push('a'));
    act(() => result.current.clear());
    expect(result.current.history).toEqual([]);
  });
});

describe('useTokenCount', () => {
  it('approximates tokens at 4 chars/token by default', () => {
    const { result } = renderHook(() => useTokenCount('hello there'));
    expect(result.current.chars).toBe(11);
    expect(result.current.tokens).toBe(3); // ceil(11/4)
    expect(result.current.costUsd).toBe(0);
  });

  it('computes cost when costPer1kUsd is provided', () => {
    const { result } = renderHook(() =>
      useTokenCount('aaaa bbbb cccc dddd', { costPer1kUsd: 1 }),
    );
    expect(result.current.tokens).toBe(5); // ceil(19/4)
    expect(result.current.costUsd).toBeCloseTo(0.005, 5);
  });

  it('respects custom charsPerToken (3.5 for code-heavy)', () => {
    const { result } = renderHook(() =>
      useTokenCount('const x = 1;', { charsPerToken: 3.5 }),
    );
    expect(result.current.tokens).toBe(Math.ceil(12 / 3.5));
  });

  it('returns zero for empty input', () => {
    const { result } = renderHook(() => useTokenCount(''));
    expect(result.current.tokens).toBe(0);
    expect(result.current.costUsd).toBe(0);
  });
});
