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

import { ToolCallCard } from '../ToolCallCard.js';
import { CitationChip } from '../CitationChip.js';
import { useToolCalls } from '../useToolCalls.js';
import { AgentTimeline } from '../AgentTimeline.js';
import { ModelPicker, type ModelOption } from '../ModelPicker.js';
import { ToolPalette, type ToolDescriptor } from '../ToolPalette.js';
import type { ToolInvocation } from '../types.js';

const INV: ToolInvocation = {
  id: 'inv-1',
  name: 'web_search',
  status: 'success',
  input: { query: 'react streaming' },
  output: { hits: 3 },
  durationMs: 124,
};

// ─── ToolCallCard ─────────────────────────────────────────────────────────

describe('ToolCallCard', () => {
  beforeEach(() => cleanup());

  it('renders collapsed by default with status pill + name + duration', () => {
    render(<ToolCallCard invocation={INV} />);
    expect(screen.getByTestId('bl-tool-call-inv-1')).toBeDefined();
    expect(screen.getByTestId('bl-tool-call-status').textContent).toBe('✓');
    expect(screen.getByText('web_search')).toBeDefined();
    expect(screen.getByText('124ms')).toBeDefined();
    expect(screen.queryByTestId('bl-tool-call-body')).toBeNull();
  });

  it('expands on toggle click and shows input + output', () => {
    render(<ToolCallCard invocation={INV} />);
    fireEvent.click(screen.getByTestId('bl-tool-call-toggle'));
    const body = screen.getByTestId('bl-tool-call-body');
    expect(body.textContent).toContain('react streaming');
    expect(body.textContent).toContain('"hits": 3');
  });

  it('renders error body when status is error', () => {
    render(
      <ToolCallCard
        invocation={{ ...INV, status: 'error', output: undefined, error: 'rate limited' }}
        defaultExpanded
      />,
    );
    expect(screen.getByTestId('bl-tool-call-status').textContent).toBe('✕');
    expect(screen.getByText('rate limited')).toBeDefined();
  });

  it('honors renderOutput override', () => {
    render(
      <ToolCallCard
        invocation={INV}
        defaultExpanded
        renderOutput={() => <div data-testid="custom-out">custom</div>}
      />,
    );
    expect(screen.getByTestId('custom-out').textContent).toBe('custom');
  });
});

// ─── CitationChip ─────────────────────────────────────────────────────────

describe('CitationChip', () => {
  beforeEach(() => cleanup());

  it('renders the citation index by default', () => {
    render(
      <CitationChip
        citation={{ id: 'c1', index: 3, title: 'Streaming SSR Patterns', url: 'https://example.com/a' }}
      />,
    );
    const el = screen.getByTestId('bl-citation-c1');
    expect(el.textContent).toBe('3');
    expect(el.getAttribute('data-citation-index')).toBe('3');
  });

  it('shows preview popover on hover', () => {
    render(
      <CitationChip
        citation={{
          id: 'c2',
          index: 1,
          title: 'My Paper',
          snippet: 'A concise summary…',
          source: 'arxiv',
          url: 'https://arxiv.org/x/1',
        }}
      />,
    );
    // Hover the outer wrapper to fire onMouseEnter.
    fireEvent.mouseEnter(screen.getByTestId('bl-citation-c2').parentElement!);
    expect(screen.getByTestId('bl-citation-preview-c2').textContent).toContain('My Paper');
    expect(screen.getByText('arxiv')).toBeDefined();
  });

  it('renders a non-anchor span when url is omitted', () => {
    render(<CitationChip citation={{ id: 'c3', index: 2, title: 'Local' }} />);
    const el = screen.getByTestId('bl-citation-c3');
    expect(el.tagName.toLowerCase()).toBe('span');
  });

  it('respects noPreview', () => {
    render(
      <CitationChip
        noPreview
        citation={{ id: 'c4', index: 1, title: 'Hidden' }}
      />,
    );
    fireEvent.mouseEnter(screen.getByTestId('bl-citation-c4').parentElement!);
    expect(screen.queryByTestId('bl-citation-preview-c4')).toBeNull();
  });
});

// ─── useToolCalls ─────────────────────────────────────────────────────────

describe('useToolCalls', () => {
  it('begins, updates, and settles a tool call with durationMs', () => {
    const { result } = renderHook(() => useToolCalls());

    act(() => result.current.begin({ id: 't1', name: 'search', input: { q: 'x' } }));
    expect(result.current.ordered).toHaveLength(1);
    expect(result.current.invocations.t1?.status).toBe('pending');

    act(() => result.current.update('t1', { status: 'streaming' }));
    expect(result.current.invocations.t1?.status).toBe('streaming');

    act(() => result.current.settle('t1', { status: 'success', output: { hits: 2 } }));
    expect(result.current.invocations.t1?.status).toBe('success');
    expect(result.current.invocations.t1?.output).toEqual({ hits: 2 });
    expect(typeof result.current.invocations.t1?.durationMs).toBe('number');
  });

  it('preserves insertion order across updates', () => {
    const { result } = renderHook(() => useToolCalls());
    act(() => {
      result.current.begin({ id: 'a', name: 'first' });
      result.current.begin({ id: 'b', name: 'second' });
      result.current.begin({ id: 'c', name: 'third' });
      result.current.update('a', { status: 'success' });
    });
    expect(result.current.ordered.map(x => x.id)).toEqual(['a', 'b', 'c']);
  });

  it('settle with error sets status + error + clears prior output', () => {
    const { result } = renderHook(() => useToolCalls());
    act(() => result.current.begin({ id: 't', name: 'x' }));
    act(() => result.current.settle('t', { status: 'error', error: 'boom' }));
    expect(result.current.invocations.t?.status).toBe('error');
    expect(result.current.invocations.t?.error).toBe('boom');
  });

  it('clear and clearAll', () => {
    const { result } = renderHook(() => useToolCalls());
    act(() => {
      result.current.begin({ id: 'a', name: 'x' });
      result.current.begin({ id: 'b', name: 'y' });
    });
    act(() => result.current.clear('a'));
    expect(result.current.ordered.map(x => x.id)).toEqual(['b']);
    act(() => result.current.clearAll());
    expect(result.current.ordered).toEqual([]);
  });

  it('hydrates from initial invocations', () => {
    const initial: ToolInvocation[] = [
      { id: 'i1', name: 'seed', status: 'success' },
    ];
    const { result } = renderHook(() => useToolCalls(initial));
    expect(result.current.ordered).toHaveLength(1);
    expect(result.current.invocations.i1?.status).toBe('success');
  });
});

// ─── AgentTimeline ────────────────────────────────────────────────────────

describe('AgentTimeline', () => {
  beforeEach(() => cleanup());

  it('renders all steps in order with data-kind attributes', () => {
    render(
      <AgentTimeline
        steps={[
          { id: 's1', kind: 'thought', content: 'I should search.' },
          {
            id: 's2',
            kind: 'tool_call',
            invocation: { ...INV, id: 't1' },
          },
          { id: 's3', kind: 'observation', content: '3 hits.' },
          { id: 's4', kind: 'response', content: 'Here is the summary.' },
        ]}
      />,
    );
    expect(screen.getByTestId('bl-agent-timeline')).toBeDefined();
    expect(screen.getByTestId('bl-agent-step-s1').getAttribute('data-kind')).toBe('thought');
    expect(screen.getByTestId('bl-agent-step-s2').getAttribute('data-kind')).toBe('tool_call');
    // tool_call step embeds a ToolCallCard
    expect(screen.getByTestId('bl-tool-call-t1')).toBeDefined();
  });

  it('renders custom label when provided', () => {
    render(
      <AgentTimeline
        steps={[{ id: 's', kind: 'custom', label: 'Plan', content: 'Outline.' }]}
      />,
    );
    expect(screen.getByText('Plan')).toBeDefined();
  });
});

// ─── ModelPicker ──────────────────────────────────────────────────────────

const MODELS: ModelOption[] = [
  {
    id: 'gpt-4o',
    name: 'GPT-4o',
    provider: 'OpenAI',
    providerGlyph: '🟢',
    inputCostPer1k: 0.005,
    outputCostPer1k: 0.015,
    latencyMs: 400,
    contextTokens: 128_000,
    capabilities: [
      { id: 'tools', label: 'tools' },
      { id: 'vision', label: 'vision' },
    ],
    description: 'Flagship multimodal.',
  },
  {
    id: 'claude-3-5-sonnet',
    name: 'Claude 3.5 Sonnet',
    provider: 'Anthropic',
    inputCostPer1k: 0.003,
    outputCostPer1k: 0.015,
    latencyMs: 380,
    contextTokens: 200_000,
  },
  { id: 'gpt-4o-mini', name: 'GPT-4o mini', provider: 'OpenAI', disabled: true },
];

describe('ModelPicker', () => {
  beforeEach(() => cleanup());

  it('renders trigger with active model name', () => {
    render(<ModelPicker models={MODELS} value="gpt-4o" onChange={() => {}} />);
    expect(screen.getByTestId('bl-model-picker-trigger').textContent).toContain('GPT-4o');
  });

  it('opens listbox on trigger click and lists all models', () => {
    render(<ModelPicker models={MODELS} value="gpt-4o" onChange={() => {}} />);
    fireEvent.click(screen.getByTestId('bl-model-picker-trigger'));
    expect(screen.getByTestId('bl-model-picker-listbox')).toBeDefined();
    expect(screen.getByTestId('bl-model-picker-option-claude-3-5-sonnet')).toBeDefined();
  });

  it('selects model on option click and closes', () => {
    const onChange = vi.fn();
    render(<ModelPicker models={MODELS} value="gpt-4o" onChange={onChange} />);
    fireEvent.click(screen.getByTestId('bl-model-picker-trigger'));
    fireEvent.click(screen.getByTestId('bl-model-picker-option-claude-3-5-sonnet'));
    expect(onChange).toHaveBeenCalledWith('claude-3-5-sonnet');
    expect(screen.queryByTestId('bl-model-picker-listbox')).toBeNull();
  });

  it('does not invoke onChange for disabled models', () => {
    const onChange = vi.fn();
    render(<ModelPicker models={MODELS} value="gpt-4o" onChange={onChange} />);
    fireEvent.click(screen.getByTestId('bl-model-picker-trigger'));
    fireEvent.click(screen.getByTestId('bl-model-picker-option-gpt-4o-mini'));
    expect(onChange).not.toHaveBeenCalled();
  });
});

// ─── ToolPalette ──────────────────────────────────────────────────────────

const TOOLS: ToolDescriptor[] = [
  { name: 'web_search', description: 'Search the web', source: 'platform' },
  { name: 'calculator', description: 'Evaluate math expressions' },
  { name: 'image_gen', description: 'Generate images', disabled: true },
];

describe('ToolPalette', () => {
  beforeEach(() => cleanup());

  it('renders an array source synchronously', () => {
    render(<ToolPalette source={TOOLS} onSelect={() => {}} />);
    expect(screen.getByTestId('bl-tool-palette-option-web_search')).toBeDefined();
    expect(screen.getByTestId('bl-tool-palette-option-calculator')).toBeDefined();
  });

  it('filters by substring across name + description + source', () => {
    render(<ToolPalette source={TOOLS} onSelect={() => {}} filter="math" />);
    expect(screen.getByTestId('bl-tool-palette-option-calculator')).toBeDefined();
    expect(screen.queryByTestId('bl-tool-palette-option-web_search')).toBeNull();
  });

  it('reports an empty filter result via status', () => {
    render(<ToolPalette source={TOOLS} onSelect={() => {}} filter="nonexistent" />);
    expect(screen.getByTestId('bl-tool-palette-status').textContent).toMatch(/no tools/i);
  });

  it('uses the supplied discover adapter for string sources (MCP-style)', async () => {
    const discover = vi.fn(async (src: string) => {
      expect(src).toBe('mcp://platform-service/tools');
      return TOOLS;
    });
    render(
      <ToolPalette
        source="mcp://platform-service/tools"
        discover={discover}
        onSelect={() => {}}
      />,
    );
    expect(screen.getByTestId('bl-tool-palette-status').textContent).toMatch(/discovering/i);
    await waitFor(() =>
      expect(screen.getByTestId('bl-tool-palette-option-web_search')).toBeDefined(),
    );
    expect(discover).toHaveBeenCalledOnce();
  });

  it('surfaces discovery errors via status (danger tone)', async () => {
    const discover = vi.fn(async () => {
      throw new Error('unreachable');
    });
    render(
      <ToolPalette
        source="mcp://nowhere/tools"
        discover={discover}
        onSelect={() => {}}
      />,
    );
    await waitFor(() =>
      expect(screen.getByTestId('bl-tool-palette-status').textContent).toMatch(
        /unreachable/i,
      ),
    );
  });

  it('selects on click', () => {
    const onSelect = vi.fn();
    render(<ToolPalette source={TOOLS} onSelect={onSelect} />);
    fireEvent.click(screen.getByTestId('bl-tool-palette-option-calculator'));
    expect(onSelect).toHaveBeenCalledWith(TOOLS[1]);
  });

  it('does not select disabled tools', () => {
    const onSelect = vi.fn();
    render(<ToolPalette source={TOOLS} onSelect={onSelect} />);
    fireEvent.click(screen.getByTestId('bl-tool-palette-option-image_gen'));
    expect(onSelect).not.toHaveBeenCalled();
  });
});
