/**
 * Data inspector — input vs. resolved views (spec §04).
 *
 * `DataInspectorPanel` is prop-driven (rows come via `getData`, not the live
 * view), so these cover the behaviour without the render pipeline: the lazy read,
 * the Input | Resolved switch, the per-view empty/guidance states, truncation, the
 * re-read on `renderEpoch`, and the toggle. `DataInspector` is the thin
 * AppStore-bound wrapper.
 */

import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest';
import { act } from 'react';
import { createRoot, type Root } from 'react-dom/client';
import type { InspectedData } from '../services/chart-renderer';
import { useAppStore } from '../stores/AppStore';
import { DataInspector, DataInspectorPanel } from './DataInspector';

(globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true;

let container: HTMLDivElement;
let root: Root;

beforeEach(() => {
  container = document.createElement('div');
  document.body.appendChild(container);
  root = createRoot(container);
});

afterEach(() => {
  act(() => root.unmount());
  container.remove();
  vi.clearAllMocks();
});

const toggle = () =>
  Array.from(container.querySelectorAll<HTMLButtonElement>('button[aria-expanded]')).find((b) =>
    b.textContent?.includes('Data'),
  )!;
const text = () => container.textContent ?? '';
const viewButton = (label: string) =>
  Array.from(container.querySelectorAll<HTMLButtonElement>('[role="radio"]')).find(
    (b) => b.textContent === label,
  )!;
/** The view picker's trigger, or null when it is not shown (single-table case). */
const picker = () => container.querySelector<HTMLButtonElement>('[aria-label^="Inspected view"]');

/** Build the inspector payload — one table per (label, input, resolved) entry. */
const tablesOf = (
  ...entries: Array<{
    label: string;
    input?: Record<string, unknown>[];
    resolved: Record<string, unknown>[];
  }>
): InspectedData => ({
  tables: entries.map((e, i) => ({
    id: `t${i}`,
    label: e.label,
    input: e.input ?? [],
    resolved: e.resolved,
  })),
});
/** The common single-table payload. */
const oneTable = (
  input: Record<string, unknown>[],
  resolved: Record<string, unknown>[],
): InspectedData => tablesOf({ label: 'View 1', input, resolved });

const render = (props: Partial<Parameters<typeof DataInspectorPanel>[0]> = {}) =>
  act(() => {
    root.render(
      <DataInspectorPanel
        open
        onToggle={() => {}}
        getData={() => null}
        renderEpoch={0}
        {...props}
      />,
    );
  });

describe('DataInspectorPanel', () => {
  test('collapsed by default: no table, no view switch, and getData is not read (lazy)', () => {
    const getData = vi.fn(() => null);
    render({ open: false, getData });
    expect(toggle().getAttribute('aria-expanded')).toBe('false');
    expect(container.querySelector('table')).toBeNull();
    expect(container.querySelector('[role="radiogroup"]')).toBeNull();
    expect(getData).not.toHaveBeenCalled();
  });

  test('open with no live chart: guidance to render one', () => {
    render({ getData: () => null });
    expect(text()).toContain('Render a chart');
    expect(container.querySelector('table')).toBeNull();
  });

  test('open with a chart that draws nothing inspectable: says so', () => {
    render({ getData: () => ({ tables: [] }) });
    expect(text()).toContain('no inspectable data');
    expect(container.querySelector('table')).toBeNull();
  });

  test('defaults to the Resolved view and renders its rows', () => {
    render({
      getData: () =>
        oneTable([{ region: 'West', sales: '1204' }], [{ region: 'West', total: 1204 }]),
    });
    // Resolved is the default — its column ("total"), not the input's ("sales").
    const headers = Array.from(container.querySelectorAll('th')).map((th) => th.textContent);
    expect(headers).toEqual(['region', 'total']);
  });

  test('switching to Input shows the source rows', () => {
    render({
      getData: () =>
        oneTable(
          [
            { region: 'West', sales: '1204' },
            { region: 'East', sales: '980' },
          ],
          [{ region: 'West', total: 1204 }],
        ),
    });
    act(() => viewButton('Input').click());
    const headers = Array.from(container.querySelectorAll('th')).map((th) => th.textContent);
    expect(headers).toEqual(['region', 'sales']);
    expect(container.querySelectorAll('tbody tr')).toHaveLength(2);
    expect(text()).toContain('2 rows');
  });

  test('resolved empty: names the empty-transform signal', () => {
    render({ getData: () => oneTable([{ a: 1 }], []) });
    expect(text()).toContain('left nothing to draw');
  });

  test('input empty: names the empty source', () => {
    render({ getData: () => oneTable([], []) });
    act(() => viewButton('Input').click());
    expect(text()).toContain('source data has no rows');
  });

  test('caps the table and reports the total', () => {
    const resolved = Array.from({ length: 120 }, (_, i) => ({ i }));
    render({ getData: () => oneTable([], resolved) });
    expect(container.querySelectorAll('tbody tr')).toHaveLength(50);
    expect(text()).toContain('first 50 of 120');
  });

  test('re-reads getData when renderEpoch changes', () => {
    const getData = vi.fn(() => oneTable([], [{ a: 1 }]));
    render({ getData, renderEpoch: 0 });
    const before = getData.mock.calls.length;
    render({ getData, renderEpoch: 1 });
    expect(getData.mock.calls.length).toBeGreaterThan(before);
  });

  test('applies an explicit height when given (resizable mode)', () => {
    render({ getData: () => oneTable([], [{ a: 1 }]), heightPx: 240 });
    const panel = container.firstElementChild as HTMLElement;
    expect(panel.style.height).toBe('240px');
  });

  test('toggle reports the next open state', () => {
    const onToggle = vi.fn();
    render({ open: false, onToggle });
    act(() => toggle().click());
    expect(onToggle).toHaveBeenCalledWith(true);
  });

  // ── Multi-view selector ──────────────────────────────────────────────────────

  test('a single drawn table shows no view picker', () => {
    render({ getData: () => oneTable([], [{ a: 1 }]) });
    expect(picker()).toBeNull();
  });

  test('multiple drawn tables show a picker, defaulting to the first table', () => {
    render({
      getData: () =>
        tablesOf(
          { label: 'sales', resolved: [{ region: 'W', revenue: 1 }] },
          { label: 'regions', resolved: [{ region: 'W', population: 2 }] },
        ),
    });
    // Picker present and on the first table; the grid shows that table's columns.
    expect(picker()?.getAttribute('aria-label')).toBe('Inspected view: sales');
    const headers = Array.from(container.querySelectorAll('th')).map((th) => th.textContent);
    expect(headers).toEqual(['region', 'revenue']);
  });

  test('choosing another table switches the inspected data', () => {
    render({
      getData: () =>
        tablesOf(
          { label: 'sales', resolved: [{ region: 'W', revenue: 1 }] },
          { label: 'regions', resolved: [{ region: 'W', population: 2 }] },
        ),
    });
    act(() => picker()!.click()); // open the portaled popover
    const option = Array.from(document.querySelectorAll<HTMLButtonElement>('button')).find((b) =>
      b.textContent?.includes('regions'),
    )!;
    act(() => option.click());
    const headers = Array.from(container.querySelectorAll('th')).map((th) => th.textContent);
    expect(headers).toEqual(['region', 'population']);
  });
});

describe('DataInspector', () => {
  test('binds the panel to the persisted AppStore open state', () => {
    act(() => useAppStore.getState().setDataInspectorOpen(false));
    act(() => {
      root.render(<DataInspector getData={() => null} renderEpoch={0} />);
    });
    expect(toggle().getAttribute('aria-expanded')).toBe('false');

    act(() => useAppStore.getState().setDataInspectorOpen(true));
    expect(toggle().getAttribute('aria-expanded')).toBe('true');

    act(() => toggle().click());
    expect(useAppStore.getState().dataInspectorOpen).toBe(false);
  });
});
