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

import { LineChart } from '../LineChart.js';
import { BarChart } from '../BarChart.js';
import { AreaChart } from '../AreaChart.js';
import { Donut } from '../Donut.js';
import { Gauge } from '../Gauge.js';
import { RadarChart } from '../RadarChart.js';
import { compactNumber, extent, filterFinite, linearScale, smoothPath } from '../utils.js';

beforeEach(() => cleanup());

describe('utils', () => {
  it('extent ignores non-finite values + pads single-value domain', () => {
    expect(extent([1, 2, 3])).toEqual([1, 3]);
    expect(extent([NaN, 5, Infinity])).toEqual([5 - 1, 5 + 1]);
    expect(extent([])).toEqual([0, 1]);
  });

  it('linearScale maps endpoints + handles zero-width domain', () => {
    const s = linearScale(0, 10, 0, 100);
    expect(s(0)).toBe(0);
    expect(s(10)).toBe(100);
    expect(s(5)).toBe(50);
    const zero = linearScale(5, 5, 0, 100);
    expect(zero(5)).toBe(50);
  });

  it('smoothPath returns empty for empty input, M-only for single', () => {
    expect(smoothPath([])).toBe('');
    expect(smoothPath([[1, 2]])).toBe('M 1 2');
    expect(
      smoothPath([
        [0, 0],
        [10, 10],
      ])
    ).toMatch(/^M 0 0 C/);
  });

  it('compactNumber renders k-suffix, fractions, and empty for non-finite', () => {
    expect(compactNumber(2400)).toBe('2.4k');
    expect(compactNumber(0.5)).toBe('0.50');
    expect(compactNumber(7)).toBe('7');
    expect(compactNumber(NaN)).toBe('');
    expect(compactNumber(Infinity)).toBe('');
  });

  it('filterFinite drops NaN/Infinity but preserves original indices', () => {
    expect(filterFinite([1, NaN, 3, Infinity, 5])).toEqual([
      [0, 1],
      [2, 3],
      [4, 5],
    ]);
  });
});

describe('LineChart', () => {
  it('renders one <g data-testid=bl-line-chart-series> per series', () => {
    render(
      <LineChart
        series={[
          { id: 'a', values: [1, 2, 3] },
          { id: 'b', values: [3, 2, 1] },
        ]}
      />
    );
    expect(screen.getAllByTestId('bl-line-chart-series')).toHaveLength(2);
  });

  it('uses series.color when provided', () => {
    render(<LineChart series={[{ id: 'a', values: [1, 2, 3], color: 'red' }]} />);
    const g = screen.getByTestId('bl-line-chart-series');
    const path = g.querySelector('path');
    expect(path?.getAttribute('stroke')).toBe('red');
  });

  it('records SSR-safe useId on the title', () => {
    render(<LineChart series={[{ id: 'a', values: [1] }]} ariaLabel="My label" />);
    expect(screen.getByTestId('bl-line-chart').querySelector('title')?.textContent).toBe(
      'My label'
    );
  });

  it('drops NaN / Infinity from a series rather than emitting NaN in the SVG path', () => {
    render(<LineChart series={[{ id: 'gappy', values: [1, NaN, 3, Infinity, 5] }]} />);
    const path = screen.getByTestId('bl-line-chart-series').querySelector('path');
    const d = path?.getAttribute('d') ?? '';
    expect(d).not.toMatch(/NaN/);
    expect(d).not.toMatch(/Infinity/);
    expect(d.length).toBeGreaterThan(0);
  });

  it('renders nothing for a series of entirely non-finite values', () => {
    render(<LineChart series={[{ id: 'broken', values: [NaN, NaN] }]} />);
    expect(screen.queryByTestId('bl-line-chart-series')).toBeNull();
  });
});

describe('BarChart', () => {
  it('renders one bar per datum', () => {
    render(
      <BarChart
        data={[
          { id: 'a', value: 10 },
          { id: 'b', value: 20 },
          { id: 'c', value: 15 },
        ]}
      />
    );
    expect(screen.getAllByTestId('bl-bar-chart-bar')).toHaveLength(3);
  });

  it('supports negative values via diverging baseline', () => {
    render(
      <BarChart
        data={[
          { id: 'a', value: -5 },
          { id: 'b', value: 5 },
        ]}
      />
    );
    // Just confirm no throw + both rendered.
    expect(screen.getAllByTestId('bl-bar-chart-bar')).toHaveLength(2);
  });

  it('uses datum.color override', () => {
    render(<BarChart data={[{ id: 'a', value: 10, color: 'blue' }]} />);
    const bar = screen.getByTestId('bl-bar-chart-bar').querySelector('rect');
    expect(bar?.getAttribute('fill')).toBe('blue');
  });
});

describe('AreaChart', () => {
  it('renders a gradient + filled area + line', () => {
    render(<AreaChart values={[1, 2, 3, 2, 4]} />);
    const root = screen.getByTestId('bl-area-chart');
    expect(root.querySelectorAll('path')).toHaveLength(2); // area + line
    expect(root.querySelector('linearGradient')).not.toBeNull();
  });

  it('renders safely with a single point', () => {
    render(<AreaChart values={[42]} />);
    expect(screen.getByTestId('bl-area-chart')).toBeDefined();
  });
});

describe('Donut', () => {
  it('renders one slice per data entry when total > 0', () => {
    render(
      <Donut
        slices={[
          { id: 's1', value: 30 },
          { id: 's2', value: 20 },
          { id: 's3', value: 50 },
        ]}
      />
    );
    expect(screen.getAllByTestId('bl-donut-slice')).toHaveLength(3);
  });

  it('renders an empty muted ring when total is 0', () => {
    render(
      <Donut
        slices={[
          { id: 'a', value: 0 },
          { id: 'b', value: 0 },
        ]}
      />
    );
    expect(screen.queryAllByTestId('bl-donut-slice')).toHaveLength(0);
    expect(screen.getByTestId('bl-donut').querySelector('[data-kind="empty"]')).not.toBeNull();
  });

  it('collapses a single near-100% slice to a closed ring', () => {
    render(
      <Donut
        slices={[
          { id: 'all', value: 100 },
          { id: 'tiny', value: 0 },
        ]}
      />
    );
    expect(screen.getByTestId('bl-donut').querySelector('[data-kind="full"]')).not.toBeNull();
  });

  it('renders centerContent in the hole', () => {
    render(
      <Donut
        slices={[
          { id: 'a', value: 50 },
          { id: 'b', value: 50 },
        ]}
        centerContent={<span data-testid="donut-center">75%</span>}
      />
    );
    expect(screen.getByTestId('donut-center')).toBeDefined();
  });
});

describe('Gauge', () => {
  it('clamps in-domain values', () => {
    render(<Gauge value={60} min={0} max={100} />);
    const el = screen.getByTestId('bl-gauge');
    expect(el.getAttribute('data-value')).toBe('60');
  });

  it('clamps out-of-domain values', () => {
    render(<Gauge value={150} min={0} max={100} />);
    expect(screen.getByTestId('bl-gauge').getAttribute('data-value')).toBe('100');
  });

  it('clamps NaN to min', () => {
    render(<Gauge value={NaN} min={0} max={100} />);
    expect(screen.getByTestId('bl-gauge').getAttribute('data-value')).toBe('0');
  });

  it('renders caption slot', () => {
    render(<Gauge value={42} caption={<span data-testid="cap">nps</span>} />);
    expect(screen.getByTestId('cap')).toBeDefined();
  });
});

describe('RadarChart (Wave 9.A.5)', () => {
  const axes = ['Speed', 'Power', 'Range', 'Cost', 'A11y'];

  it('renders one polygon per series + a labelled, role=img svg', () => {
    render(
      <RadarChart
        ariaLabel="Capability profile"
        axes={axes}
        series={[
          { id: 'now', values: [8, 6, 7, 4, 9] },
          { id: 'goal', values: [9, 9, 8, 6, 10] },
        ]}
      />
    );
    const svg = screen.getByTestId('bl-radar');
    expect(svg.getAttribute('role')).toBe('img');
    expect(screen.getByText('Capability profile')).toBeDefined();
    expect(screen.getAllByTestId('bl-radar-series')).toHaveLength(2);
    for (const a of axes) expect(screen.getByText(a)).toBeDefined();
  });

  it('draws `levels` concentric grid rings', () => {
    render(<RadarChart axes={axes} levels={3} series={[{ id: 's', values: [1, 2, 3, 4, 5] }]} />);
    expect(screen.getAllByTestId('bl-radar-ring')).toHaveLength(3);
  });

  it('is NaN-safe — produces no NaN in the path', () => {
    render(<RadarChart axes={axes} series={[{ id: 's', values: [NaN, 2, Infinity, 4, 5] }]} />);
    const d = screen.getAllByTestId('bl-radar-series')[0]!.getAttribute('d') ?? '';
    expect(d).not.toMatch(/NaN/);
  });
});
