import { describe, test, expect, vi } from 'vitest';
import React from 'react';
import { createRoot, Root } from 'react-dom/client';
import { act } from 'react-dom/test-utils';
import {
  NumericUnitInput,
  parseCssLength,
  SizeControl,
  AspectRatioControl,
  parseAspectRatioInput,
  FocalPointGrid,
  FOCAL_POINTS,
  SpacingControl,
  BorderControl,
  buildBorderShorthand,
  AnimationControl,
  ANIMATIONS,
  VisibilityControl,
} from './shared';

/* Same DOM-harness pattern as AssetPicker.test.tsx / ImageBlock.render.test.tsx
   (no @testing-library/react in this repo). */
let container: HTMLDivElement;
let root: Root;

function render(ui: React.ReactElement) {
  container = document.createElement('div');
  document.body.appendChild(container);
  act(() => {
    root = createRoot(container);
    root.render(ui);
  });
}

function unmount() {
  act(() => { root.unmount(); });
  container.remove();
}

function click(el: Element | null) {
  if (!el) throw new Error('element not found');
  act(() => { (el as HTMLElement).dispatchEvent(new MouseEvent('click', { bubbles: true })); });
}

function setValue(input: HTMLInputElement | HTMLSelectElement, value: string) {
  const proto = input instanceof HTMLSelectElement ? window.HTMLSelectElement.prototype : window.HTMLInputElement.prototype;
  const setter = Object.getOwnPropertyDescriptor(proto, 'value')!.set!;
  setter.call(input, value);
  input.dispatchEvent(new Event('input', { bubbles: true }));
  input.dispatchEvent(new Event('change', { bubbles: true }));
}

function blur(el: Element) {
  act(() => { el.dispatchEvent(new FocusEvent('blur', { bubbles: true })); el.dispatchEvent(new FocusEvent('focusout', { bubbles: true })); });
}

function q<T extends Element = Element>(testId: string): T | null {
  return container.querySelector(`[data-testid="${testId}"]`);
}

/* ---------- NumericUnitInput / parseCssLength ---------- */
describe('parseCssLength', () => {
  test('parses "16px" into number 16 + unit px', () => {
    expect(parseCssLength('16px')).toEqual({ num: '16', unit: 'px' });
  });
  test('parses "50%" into number 50 + unit %', () => {
    expect(parseCssLength('50%', ['px', '%'])).toEqual({ num: '50', unit: '%' });
  });
  test('treats "auto" and "" as empty', () => {
    expect(parseCssLength('auto')).toEqual({ num: '', unit: 'px' });
    expect(parseCssLength('')).toEqual({ num: '', unit: 'px' });
    expect(parseCssLength(undefined)).toEqual({ num: '', unit: 'px' });
  });
  test('falls back to the first configured unit for an unrecognized unit', () => {
    expect(parseCssLength('16vh', ['px', '%'])).toEqual({ num: '16', unit: 'px' });
  });
});

describe('NumericUnitInput', () => {
  test('renders the parsed number and unit from a "16px" value', () => {
    render(<NumericUnitInput value="16px" onChange={vi.fn()} />);
    expect(q<HTMLInputElement>('numeric-unit-number')!.value).toBe('16');
    expect(q<HTMLSelectElement>('numeric-unit-unit')!.value).toBe('px');
    unmount();
  });

  test('changing the number emits the recombined string', () => {
    const onChange = vi.fn();
    render(<NumericUnitInput value="16px" onChange={onChange} />);
    setValue(q<HTMLInputElement>('numeric-unit-number')!, '32');
    expect(onChange).toHaveBeenCalledWith('32px');
    unmount();
  });

  test('changing the unit re-emits with the current number', () => {
    const onChange = vi.fn();
    render(<NumericUnitInput value="16px" onChange={onChange} />);
    setValue(q<HTMLSelectElement>('numeric-unit-unit')!, '%');
    expect(onChange).toHaveBeenCalledWith('16%');
    unmount();
  });

  test('clearing the number emits empty string, not "0px"', () => {
    const onChange = vi.fn();
    render(<NumericUnitInput value="16px" onChange={onChange} />);
    setValue(q<HTMLInputElement>('numeric-unit-number')!, '');
    expect(onChange).toHaveBeenCalledWith('');
    unmount();
  });

  test('omits the unit <select> when only one unit is configured', () => {
    render(<NumericUnitInput value="1px" onChange={vi.fn()} units={['px']} />);
    expect(q('numeric-unit-unit')).toBeNull();
    unmount();
  });
});

/* ---------- SizeControl ---------- */
describe('SizeControl', () => {
  test('clicking a preset button emits its value', () => {
    const onChange = vi.fn();
    render(<SizeControl label="Width" value="" onChange={onChange} />);
    const buttons = container.querySelectorAll('[data-testid="size-control"] .preset-btn');
    const hundred = Array.from(buttons).find((b) => b.textContent === '100%')!;
    click(hundred);
    expect(onChange).toHaveBeenCalledWith('100%');
    unmount();
  });

  test('custom numeric input emits a non-preset value', () => {
    const onChange = vi.fn();
    render(<SizeControl label="Width" value="" onChange={onChange} />);
    setValue(q<HTMLInputElement>('size-control-custom-number')!, '400');
    expect(onChange).toHaveBeenCalledWith('400px');
    unmount();
  });
});

/* ---------- AspectRatioControl / parseAspectRatioInput ---------- */
describe('parseAspectRatioInput', () => {
  test('parses "16:9" into "16 / 9"', () => {
    expect(parseAspectRatioInput('16:9')).toBe('16 / 9');
  });
  test('accepts "16/9" and "16x9" separators too', () => {
    expect(parseAspectRatioInput('16/9')).toBe('16 / 9');
    expect(parseAspectRatioInput('16x9')).toBe('16 / 9');
  });
  test('returns empty string for unparseable input', () => {
    expect(parseAspectRatioInput('not-a-ratio')).toBe('');
  });
});

describe('AspectRatioControl', () => {
  test('clicking the 16:9 preset emits "16 / 9"', () => {
    const onChange = vi.fn();
    render(<AspectRatioControl value="" onChange={onChange} />);
    const buttons = container.querySelectorAll('[data-testid="aspect-ratio-control"] .preset-btn');
    const btn = Array.from(buttons).find((b) => b.textContent === '16:9')!;
    click(btn);
    expect(onChange).toHaveBeenCalledWith('16 / 9');
    unmount();
  });

  test('clicking Original emits empty string', () => {
    const onChange = vi.fn();
    render(<AspectRatioControl value="1 / 1" onChange={onChange} />);
    const buttons = container.querySelectorAll('[data-testid="aspect-ratio-control"] .preset-btn');
    const btn = Array.from(buttons).find((b) => b.textContent === 'Original')!;
    click(btn);
    expect(onChange).toHaveBeenCalledWith('');
    unmount();
  });

  test('typing a custom "16:9" and blurring emits "16 / 9"', () => {
    const onChange = vi.fn();
    render(<AspectRatioControl value="" onChange={onChange} />);
    const input = q<HTMLInputElement>('aspect-ratio-custom-input')!;
    setValue(input, '16:9');
    blur(input);
    expect(onChange).toHaveBeenCalledWith('16 / 9');
    unmount();
  });
});

/* ---------- FocalPointGrid ---------- */
describe('FocalPointGrid', () => {
  test('has 9 cells matching FOCAL_POINTS', () => {
    render(<FocalPointGrid value="center" onChange={vi.fn()} />);
    const cells = container.querySelectorAll('[data-testid="focal-point-grid"] button');
    expect(cells.length).toBe(9);
    expect(FOCAL_POINTS.length).toBe(9);
    unmount();
  });

  test('clicking a cell emits its object-position value', () => {
    const onChange = vi.fn();
    render(<FocalPointGrid value="center" onChange={onChange} />);
    click(q('focal-point-left-top'));
    expect(onChange).toHaveBeenCalledWith('left top');
    unmount();
  });

  test('the active cell is visually highlighted (distinct background)', () => {
    render(<FocalPointGrid value="right bottom" onChange={vi.fn()} />);
    const active = q<HTMLButtonElement>('focal-point-right-bottom')!;
    const inactive = q<HTMLButtonElement>('focal-point-center')!;
    expect(active.style.background).not.toBe(inactive.style.background);
    unmount();
  });

  test('defaults the highlight to center when value is empty', () => {
    render(<FocalPointGrid value="" onChange={vi.fn()} />);
    const center = q<HTMLButtonElement>('focal-point-center')!;
    const other = q<HTMLButtonElement>('focal-point-left-top')!;
    expect(center.style.background).not.toBe(other.style.background);
    unmount();
  });
});

/* ---------- SpacingControl ---------- */
describe('SpacingControl', () => {
  test('linked mode: editing the shared field emits onChange for all 4 sides with the same value', () => {
    const onChange = vi.fn();
    render(<SpacingControl label="Margin" value={{}} onChange={onChange} />);
    setValue(q<HTMLInputElement>('spacing-linked-number')!, '16');
    expect(onChange).toHaveBeenCalledWith('top', '16px');
    expect(onChange).toHaveBeenCalledWith('right', '16px');
    expect(onChange).toHaveBeenCalledWith('bottom', '16px');
    expect(onChange).toHaveBeenCalledWith('left', '16px');
    expect(onChange).toHaveBeenCalledTimes(4);
    unmount();
  });

  test('a preset fills all sides with the preset value', () => {
    const onChange = vi.fn();
    render(<SpacingControl label="Padding" value={{}} onChange={onChange} />);
    const buttons = container.querySelectorAll('[data-testid="spacing-control"] .preset-btn');
    const medium = Array.from(buttons).find((b) => b.textContent === 'M')!;
    click(medium);
    expect(onChange).toHaveBeenCalledWith('top', '16px');
    expect(onChange).toHaveBeenCalledWith('left', '16px');
    expect(onChange).toHaveBeenCalledTimes(4);
    unmount();
  });

  test('unlinking lets each side emit independently', () => {
    const onChange = vi.fn();
    render(<SpacingControl label="Margin" value={{ top: '4px', right: '4px', bottom: '4px', left: '4px' }} onChange={onChange} />);
    click(q('spacing-link-toggle'));
    setValue(q<HTMLInputElement>('spacing-top-number')!, '20');
    expect(onChange).toHaveBeenCalledWith('top', '20px');
    expect(onChange).toHaveBeenCalledTimes(1);
    unmount();
  });
});

/* ---------- BorderControl ---------- */
describe('BorderControl', () => {
  test('changing width/style/color merges into the full BorderValue object', () => {
    const onChange = vi.fn();
    const value = { width: '1px', style: 'solid', color: '#000000' };
    render(<BorderControl value={value} onChange={onChange} />);
    setValue(q<HTMLSelectElement>('border-style-select')!, 'dashed');
    expect(onChange).toHaveBeenCalledWith({ width: '1px', style: 'dashed', color: '#000000' });
    unmount();
  });

  test('buildBorderShorthand builds a CSS border shorthand', () => {
    expect(buildBorderShorthand({ width: '2px', style: 'solid', color: '#3b82f6' })).toBe('2px solid #3b82f6');
  });

  test('buildBorderShorthand returns "none" when style is none or width is empty', () => {
    expect(buildBorderShorthand({ width: '2px', style: 'none', color: '#000' })).toBe('none');
    expect(buildBorderShorthand({ width: '', style: 'solid', color: '#000' })).toBe('none');
  });
});

/* ---------- AnimationControl ---------- */
describe('AnimationControl', () => {
  test('ANIMATIONS matches the exact data-animation values html-export.ts supports', () => {
    expect(ANIMATIONS.map((a) => a.value)).toEqual([
      'none', 'fade-in', 'slide-up', 'slide-left', 'slide-right', 'zoom-in', 'bounce',
    ]);
  });

  test('selecting an animation emits { animation, animationDelay }', () => {
    const onChange = vi.fn();
    render(<AnimationControl value={{ animation: 'none' }} onChange={onChange} />);
    const buttons = container.querySelectorAll('[data-testid="animation-control"] .preset-btn');
    const fadeIn = Array.from(buttons).find((b) => b.textContent === 'Fade In')!;
    click(fadeIn);
    expect(onChange).toHaveBeenCalledWith({ animation: 'fade-in', animationDelay: '0' });
    unmount();
  });

  test('the delay input is hidden when animation is "none" and shown otherwise', () => {
    render(<AnimationControl value={{ animation: 'none' }} onChange={vi.fn()} />);
    expect(q('animation-delay-input')).toBeNull();
    unmount();

    render(<AnimationControl value={{ animation: 'bounce', animationDelay: '0.5' }} onChange={vi.fn()} />);
    expect(q<HTMLInputElement>('animation-delay-input')!.value).toBe('0.5');
    unmount();
  });

  test('changing the delay emits the current animation with the new delay', () => {
    const onChange = vi.fn();
    render(<AnimationControl value={{ animation: 'zoom-in', animationDelay: '0' }} onChange={onChange} />);
    setValue(q<HTMLInputElement>('animation-delay-input')!, '0.3');
    expect(onChange).toHaveBeenCalledWith({ animation: 'zoom-in', animationDelay: '0.3' });
    unmount();
  });
});

/* ---------- VisibilityControl ---------- */
describe('VisibilityControl', () => {
  test('emits the exact hideOnDesktop/hideOnTablet/hideOnMobile prop names', () => {
    const onChange = vi.fn();
    render(<VisibilityControl value={{}} onChange={onChange} />);
    click(q('visibility-hideOnDesktop'));
    expect(onChange).toHaveBeenCalledWith({ hideOnDesktop: true });
    unmount();
  });

  test('toggling reflects existing true values back to false', () => {
    const onChange = vi.fn();
    render(<VisibilityControl value={{ hideOnMobile: true }} onChange={onChange} />);
    click(q('visibility-hideOnMobile'));
    expect(onChange).toHaveBeenCalledWith({ hideOnMobile: false });
    unmount();
  });

  test('checkboxes reflect the initial value', () => {
    render(<VisibilityControl value={{ hideOnTablet: true }} onChange={vi.fn()} />);
    expect(q<HTMLInputElement>('visibility-hideOnDesktop')!.checked).toBe(false);
    expect(q<HTMLInputElement>('visibility-hideOnTablet')!.checked).toBe(true);
    expect(q<HTMLInputElement>('visibility-hideOnMobile')!.checked).toBe(false);
    unmount();
  });
});
