/**
 * 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 = () => container.querySelector<HTMLButtonElement>('button[aria-expanded]')!;
const text = () => container.textContent ?? '';
const viewButton = (label: string) =>
  Array.from(container.querySelectorAll<HTMLButtonElement>('[role="radio"]')).find(
    (b) => b.textContent === label,
  )!;

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('defaults to the Resolved view and renders its rows', () => {
    const data: InspectedData = {
      input: [{ region: 'West', sales: '1204' }],
      resolved: [{ region: 'West', total: 1204 }],
    };
    render({ getData: () => data });
    // 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', () => {
    const data: InspectedData = {
      input: [
        { region: 'West', sales: '1204' },
        { region: 'East', sales: '980' },
      ],
      resolved: [{ region: 'West', total: 1204 }],
    };
    render({ getData: () => data });
    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: () => ({ input: [{ a: 1 }], resolved: [] }) });
    expect(text()).toContain('left nothing to draw');
  });

  test('input empty: names the empty source', () => {
    render({ getData: () => ({ input: [], resolved: [] }) });
    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: () => ({ input: [], 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((): InspectedData => ({ input: [], resolved: [{ 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: () => ({ input: [], resolved: [{ 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);
  });
});

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);
  });
});
