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

import { Workspace } from '../Workspace.js';
import { useWorkspaceLayout, reconcile } from '../useWorkspaceLayout.js';
import type { LayoutPersistence, LayoutSpec, TileSpec } from '../types.js';

const TILES: TileSpec[] = [
  { id: 'a', title: 'Tile A', content: <div data-testid="body-a">A</div>, defaultSpan: 2 },
  { id: 'b', title: 'Tile B', content: <div data-testid="body-b">B</div>, defaultSpan: 2 },
  { id: 'c', title: 'Tile C', content: <div data-testid="body-c">C</div>, defaultSpan: 1 },
];

function memoryPersistence(): LayoutPersistence & { current: LayoutSpec | null } {
  let value: LayoutSpec | null = null;
  return {
    get current() {
      return value;
    },
    save: (_k, l) => {
      value = l;
    },
    load: () => value,
  } as LayoutPersistence & { current: LayoutSpec | null };
}

beforeEach(() => cleanup());

describe('reconcile', () => {
  it('drops entries for unknown tiles', () => {
    const out = reconcile(
      { entries: [{ id: 'a', span: 2 }, { id: 'gone', span: 1 }] },
      [{ id: 'a', title: 'A', content: null }],
    );
    expect(out.entries.map((e) => e.id)).toEqual(['a']);
  });

  it('sanitises corrupt spans (NaN / 0 / negative / >4) to the legal [1, 4] range', () => {
    const out = reconcile(
      { entries: [
        { id: 'a', span: Number.NaN as unknown as 2 },
        { id: 'b', span: 0 as unknown as 2 },
        { id: 'c', span: -3 as unknown as 2 },
        { id: 'd', span: 99 as unknown as 2 },
      ] },
      [
        { id: 'a', title: 'A', content: null },
        { id: 'b', title: 'B', content: null },
        { id: 'c', title: 'C', content: null },
        { id: 'd', title: 'D', content: null },
      ],
    );
    expect(out.entries.map((e) => e.span)).toEqual([2, 1, 1, 4]);
  });

  it('appends new tiles in registry order with defaultSpan fallback', () => {
    const out = reconcile(
      { entries: [{ id: 'a', span: 3 }] },
      [
        { id: 'a', title: 'A', content: null },
        { id: 'b', title: 'B', content: null, defaultSpan: 4 },
      ],
    );
    expect(out.entries).toEqual([
      { id: 'a', span: 3 },
      { id: 'b', span: 4 },
    ]);
  });

  it('preserves order of surviving entries', () => {
    const out = reconcile(
      { entries: [{ id: 'b', span: 2 }, { id: 'a', span: 2 }] },
      [
        { id: 'a', title: 'A', content: null },
        { id: 'b', title: 'B', content: null },
      ],
    );
    expect(out.entries.map((e) => e.id)).toEqual(['b', 'a']);
  });
});

describe('useWorkspaceLayout', () => {
  it('hydrates with defaults when persistence returns null', async () => {
    const p = memoryPersistence();
    const { result } = renderHook(() =>
      useWorkspaceLayout(TILES, { storageKey: 'k1', persistence: p }),
    );
    await waitFor(() => expect(result.current.hydrated).toBe(true));
    expect(result.current.layout.entries.map((e) => e.id)).toEqual(['a', 'b', 'c']);
  });

  it('move() reorders entries and persists', async () => {
    const p = memoryPersistence();
    const { result } = renderHook(() =>
      useWorkspaceLayout(TILES, { storageKey: 'k2', persistence: p }),
    );
    await waitFor(() => expect(result.current.hydrated).toBe(true));
    act(() => result.current.move(0, 2));
    expect(result.current.layout.entries.map((e) => e.id)).toEqual(['b', 'c', 'a']);
    await waitFor(() =>
      expect(p.current?.entries.map((e) => e.id)).toEqual(['b', 'c', 'a']),
    );
  });

  it('resize() updates a tile span', async () => {
    const p = memoryPersistence();
    const { result } = renderHook(() =>
      useWorkspaceLayout(TILES, { storageKey: 'k3', persistence: p }),
    );
    await waitFor(() => expect(result.current.hydrated).toBe(true));
    act(() => result.current.resize('a', 4));
    expect(result.current.layout.entries.find((e) => e.id === 'a')!.span).toBe(4);
  });

  it('reset() restores defaults', async () => {
    const p = memoryPersistence();
    const { result } = renderHook(() =>
      useWorkspaceLayout(TILES, { storageKey: 'k4', persistence: p }),
    );
    await waitFor(() => expect(result.current.hydrated).toBe(true));
    act(() => result.current.move(0, 2));
    act(() => result.current.reset());
    expect(result.current.layout.entries.map((e) => e.id)).toEqual(['a', 'b', 'c']);
  });
});

describe('<Workspace>', () => {
  it('renders one cell per tile', async () => {
    const p = memoryPersistence();
    render(<Workspace tiles={TILES} storageKey="r1" persistence={p} />);
    await waitFor(() =>
      expect(screen.getAllByTestId('bl-workspace-tile')).toHaveLength(3),
    );
  });

  it('arrow-right keypress on focused tile increases its span', async () => {
    const p = memoryPersistence();
    render(<Workspace tiles={TILES} storageKey="r2" persistence={p} />);
    await waitFor(() => screen.getAllByTestId('bl-workspace-tile'));
    const first = screen.getAllByTestId('bl-workspace-tile')[0]!;
    first.focus();
    fireEvent.keyDown(first, { key: 'ArrowRight' });
    await waitFor(() => {
      expect(p.current?.entries[0]!.span).toBeGreaterThanOrEqual(3);
    });
  });

  it('readOnly hides resize controls + reset CTA', async () => {
    const p = memoryPersistence();
    render(<Workspace tiles={TILES} storageKey="r3" persistence={p} readOnly />);
    await waitFor(() => screen.getAllByTestId('bl-workspace-tile'));
    expect(screen.queryByTestId('bl-workspace-reset')).toBeNull();
    expect(screen.queryByTestId('bl-workspace-tile-size-1')).toBeNull();
  });
});
