import { beforeEach, describe, expect, test } from 'vitest';
import {
  capturedEditorWidth,
  clampSideWidth,
  HANDLE_WIDTH,
  HANDLES_TOTAL,
  maxSideWidth,
  PANE_DEFAULT,
  PANE_MIN,
  shownSideWidths,
  sideWidthValue,
  splitLibraryWidth,
  splitValue,
  usePanesStore,
} from './PanesStore';

const store = () => usePanesStore.getState();

describe('clampSideWidth', () => {
  // A roomy container where nothing is constrained.
  const W = 1400;

  test('passes a comfortable width through unchanged', () => {
    expect(clampSideWidth('library', 300, W, PANE_DEFAULT.preview)).toBe(300);
    expect(clampSideWidth('preview', 400, W, PANE_DEFAULT.library)).toBe(400);
  });

  test('never goes below the pane minimum', () => {
    expect(clampSideWidth('library', 50, W, PANE_DEFAULT.preview)).toBe(PANE_MIN.library);
    expect(clampSideWidth('preview', 10, W, PANE_DEFAULT.library)).toBe(PANE_MIN.preview);
  });

  test('caps the width so the editor keeps at least its minimum', () => {
    const other = PANE_DEFAULT.preview;
    const max = W - other - HANDLES_TOTAL - PANE_MIN.editor;
    // Asking for far more than the editor can spare clamps to that maximum.
    expect(clampSideWidth('library', W, W, other)).toBe(max);
    // The editor sits exactly at its minimum at that point.
    expect(W - max - other - HANDLES_TOTAL).toBe(PANE_MIN.editor);
  });

  test('a container too narrow for all minimums still never drops below the min', () => {
    // 500px total can't fit library(216)+preview(300)+editor(384)+handles.
    expect(clampSideWidth('library', 300, 500, PANE_MIN.preview)).toBe(PANE_MIN.library);
  });
});

describe('sideWidthValue (aria-valuenow, 0–100 per WAI-ARIA APG window splitter)', () => {
  const W = 1400;

  test('reports 0 at the pane minimum and 100 at the pane maximum', () => {
    const other = PANE_DEFAULT.preview;
    const max = maxSideWidth('library', W, other);
    expect(sideWidthValue('library', PANE_MIN.library, W, other)).toBe(0);
    expect(sideWidthValue('library', max, W, other)).toBe(100);
  });

  test('reports the midpoint as ~50', () => {
    const other = PANE_DEFAULT.preview;
    const mid = (PANE_MIN.library + maxSideWidth('library', W, other)) / 2;
    expect(sideWidthValue('library', mid, W, other)).toBe(50);
  });

  test('clamps out-of-range widths into 0–100', () => {
    const other = PANE_DEFAULT.preview;
    expect(sideWidthValue('library', 0, W, other)).toBe(0);
    expect(sideWidthValue('library', 99999, W, other)).toBe(100);
  });

  test('returns null before layout is known (container width 0)', () => {
    expect(sideWidthValue('library', 280, 0, PANE_DEFAULT.preview)).toBeNull();
  });

  test('returns null when the range has collapsed (container too narrow)', () => {
    // 500px can't fit all the minimums, so min == max and there is no range.
    expect(sideWidthValue('library', 200, 500, PANE_MIN.preview)).toBeNull();
  });
});

describe('usePanesStore', () => {
  beforeEach(() =>
    store().hydrate({ libraryWidth: PANE_DEFAULT.library, previewWidth: PANE_DEFAULT.preview }),
  );

  test('setWidth updates the targeted side only', () => {
    store().setWidth('library', 320);
    expect(store().libraryWidth).toBe(320);
    expect(store().previewWidth).toBe(PANE_DEFAULT.preview);

    store().setWidth('preview', 420);
    expect(store().previewWidth).toBe(420);
    expect(store().libraryWidth).toBe(320);
  });

  test('hydrate fills missing values from defaults', () => {
    store().hydrate({ libraryWidth: 250 });
    expect(store().libraryWidth).toBe(250);
    expect(store().previewWidth).toBe(PANE_DEFAULT.preview);
  });
});

describe('pane visibility (§01A)', () => {
  test('all panes are visible by default', () => {
    store().hydrate({}, undefined);
    expect(store().libraryVisible).toBe(true);
    expect(store().editorVisible).toBe(true);
    expect(store().previewVisible).toBe(true);
  });

  test('togglePane flips only the targeted pane', () => {
    store().hydrate({}, {});
    store().togglePane('editor');
    expect(store().editorVisible).toBe(false);
    expect(store().libraryVisible).toBe(true);
    expect(store().previewVisible).toBe(true);

    store().togglePane('editor');
    expect(store().editorVisible).toBe(true);
  });

  test('hiding all panes is permitted', () => {
    store().hydrate({}, {});
    store().togglePane('library');
    store().togglePane('editor');
    store().togglePane('preview');
    expect(store().libraryVisible).toBe(false);
    expect(store().editorVisible).toBe(false);
    expect(store().previewVisible).toBe(false);
  });

  test('hydrate restores explicit visibility; missing flags default to shown', () => {
    store().hydrate({}, { library: false });
    expect(store().libraryVisible).toBe(false);
    expect(store().editorVisible).toBe(true);
    expect(store().previewVisible).toBe(true);
  });

  test('a hidden pane keeps its remembered width (re-shows at it)', () => {
    store().hydrate({ libraryWidth: 250 }, {});
    store().togglePane('library'); // hide
    expect(store().libraryWidth).toBe(250);
    store().togglePane('library'); // re-show
    expect(store().libraryWidth).toBe(250);
  });
});

describe('splitLibraryWidth (editor-hidden library↔preview split, §01A)', () => {
  // The two side panes share this span when the editor is hidden.
  const AVAIL = 1000;

  test('passes a comfortable width through unchanged', () => {
    expect(splitLibraryWidth(400, AVAIL)).toBe(400);
  });

  test('never goes below the library minimum', () => {
    expect(splitLibraryWidth(50, AVAIL)).toBe(PANE_MIN.library);
  });

  test('caps so the preview keeps at least its minimum', () => {
    expect(splitLibraryWidth(AVAIL, AVAIL)).toBe(AVAIL - PANE_MIN.preview);
  });

  test('a span too small for both minimums still keeps the library minimum', () => {
    // 400px can't fit library(216)+preview(300); the library holds its own min.
    expect(splitLibraryWidth(250, 400)).toBe(PANE_MIN.library);
  });
});

describe('splitValue (aria-valuenow for the editor-hidden split)', () => {
  test('reports 0 at the library minimum and 100 at its maximum', () => {
    const span = 1000;
    expect(splitValue(PANE_MIN.library, span - PANE_MIN.library)).toBe(0);
    expect(splitValue(span - PANE_MIN.preview, PANE_MIN.preview)).toBe(100);
  });

  test('reports the midpoint as ~50', () => {
    // Library halfway between its min and its max (preview at min) over a 1000 span.
    const max = 1000 - PANE_MIN.preview;
    const mid = (PANE_MIN.library + max) / 2;
    expect(splitValue(mid, 1000 - mid)).toBe(50);
  });

  test('returns null when the span has no range (too narrow for both minimums)', () => {
    expect(splitValue(190, 190)).toBeNull();
  });
});

describe('capturedEditorWidth (remembered on hide, §01A)', () => {
  const PANES_INNER = 1200;

  test('is the span left after both side panes and their two handles', () => {
    expect(capturedEditorWidth(PANES_INNER, 280, 360, true, true)).toBe(
      PANES_INNER - 280 - 360 - HANDLES_TOTAL,
    );
  });

  test('accounts for only the visible side panes (one handle when one is hidden)', () => {
    expect(capturedEditorWidth(PANES_INNER, 280, 360, true, false)).toBe(
      PANES_INNER - 280 - HANDLE_WIDTH,
    );
  });

  test('floors at the editor minimum on a cramped row', () => {
    expect(capturedEditorWidth(700, 300, 300, true, true)).toBe(PANE_MIN.editor);
  });
});

describe('shownSideWidths (editor re-shown, §01A)', () => {
  const PANES_INNER = 1200;
  // The width captured when hiding the editor from a 280/360 layout.
  const E = capturedEditorWidth(PANES_INNER, 280, 360, true, true); // 548

  test('an untouched split returns to pixel-perfect on re-show', () => {
    expect(shownSideWidths(PANES_INNER, E, 280, 360, true, true)).toEqual({
      libraryWidth: 280,
      previewWidth: 360,
    });
  });

  test('pins the editor width and keeps the ratio set while hidden', () => {
    // The user re-split to 340:300 while the editor was hidden (both ≥ their
    // minimums, so the rescale preserves the ratio rather than clamping).
    const { libraryWidth, previewWidth } = shownSideWidths(PANES_INNER, E, 340, 300, true, true);
    // Editor reclaims its slot, so the side panes' combined width is unchanged…
    expect(libraryWidth + previewWidth).toBe(PANES_INNER - E - HANDLES_TOTAL);
    // …and the 340:300 ratio is preserved.
    expect(libraryWidth / previewWidth).toBeCloseTo(340 / 300, 5);
  });

  test('no remembered editor width leaves the side widths untouched (legacy/never hidden)', () => {
    expect(shownSideWidths(PANES_INNER, 0, 280, 360, true, true)).toEqual({
      libraryWidth: 280,
      previewWidth: 360,
    });
  });

  test('a lone visible side pane takes the whole leftover span', () => {
    const { libraryWidth, previewWidth } = shownSideWidths(PANES_INNER, E, 280, 360, true, false);
    expect(libraryWidth).toBe(PANES_INNER - E - HANDLE_WIDTH);
    expect(previewWidth).toBe(360); // the hidden preview keeps its remembered width
  });
});

describe('editor toggle remembers and restores its width (§01A)', () => {
  const PANES_INNER = 1200;

  test('hiding captures the editor width; showing restores the whole layout', () => {
    store().hydrate({ libraryWidth: 280, previewWidth: 360 });
    store().togglePane('editor', PANES_INNER);
    expect(store().editorVisible).toBe(false);
    expect(store().editorWidth).toBe(PANES_INNER - 280 - 360 - HANDLES_TOTAL);
    // The side widths are untouched while hidden (they fill via proportional flex).
    expect(store().libraryWidth).toBe(280);
    expect(store().previewWidth).toBe(360);

    store().togglePane('editor', PANES_INNER);
    expect(store().editorVisible).toBe(true);
    expect(store().libraryWidth).toBe(280);
    expect(store().previewWidth).toBe(360);
  });

  test('a split made while hidden carries its ratio back, editor width pinned', () => {
    store().hydrate({ libraryWidth: 280, previewWidth: 360 });
    store().togglePane('editor', PANES_INNER);
    const captured = store().editorWidth;
    // Drag the library↔preview handle while hidden (setSplit writes both widths).
    store().setSplit(340, 300);

    store().togglePane('editor', PANES_INNER);
    expect(store().editorWidth).toBe(captured); // editor reclaims its slot
    expect(store().libraryWidth + store().previewWidth).toBe(
      PANES_INNER - captured - HANDLES_TOTAL,
    );
    expect(store().libraryWidth / store().previewWidth).toBeCloseTo(340 / 300, 5);
  });
});

describe('applyOnboardingSplit (spec §02 → First-Run & Empty Workspace)', () => {
  test('lays out 25·25·50 on a roomy container', () => {
    const W = 1600;
    store().applyOnboardingSplit(W);
    expect(store().libraryWidth).toBe(W * 0.25); // 400
    expect(store().previewWidth).toBe(W * 0.5); // 800 → editor flexes to the remaining ~25%
  });

  test('shows all three panes, even ones that were hidden', () => {
    store().hydrate({}, { library: false, editor: false, preview: false });
    store().applyOnboardingSplit(1600);
    expect(store().libraryVisible).toBe(true);
    expect(store().editorVisible).toBe(true);
    expect(store().previewVisible).toBe(true);
  });

  test('never drops a pane below its minimum on a narrow container', () => {
    store().applyOnboardingSplit(600);
    expect(store().libraryWidth).toBeGreaterThanOrEqual(PANE_MIN.library);
    expect(store().previewWidth).toBeGreaterThanOrEqual(PANE_MIN.preview);
  });
});
