import { describe, test, expect, vi, beforeEach, afterEach } from 'vitest';
import React from 'react';
import { createRoot, Root } from 'react-dom/client';
import { act } from 'react-dom/test-utils';

/* Same DOM-harness pattern as MediaStylePanel.slides.test.tsx -- mock
   @craftjs/core's useEditor so setProp calls can be observed without
   mounting a real <Editor> tree, and mock utils/assets so AssetPicker
   doesn't hit the network. */
const setPropSpy = vi.fn((_id: string, updater: (p: any) => void) => {
  updater(lastProps);
});
let lastProps: any;

vi.mock('@craftjs/core', () => ({
  useEditor: () => ({ actions: { setProp: setPropSpy } }),
}));

vi.mock('../../../utils/assets', () => ({
  uploadAsset: vi.fn(),
  listAssets: vi.fn(),
}));

import { MediaStylePanel } from './MediaStylePanel';

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

function qAll<T extends Element = Element>(testId: string): T[] {
  return Array.from(container.querySelectorAll(`[data-testid="${testId}"]`));
}

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

beforeEach(() => {
  setPropSpy.mockClear();
});

afterEach(() => {
  if (container) unmount();
});

describe('MediaStylePanel Video source uses AssetPicker (upload/browse/paste-URL), not a plain text input', () => {
  test('renders a full-variant AssetPicker (not the old plain input) for videoUrl', () => {
    lastProps = { videoUrl: '', poster: '', style: {} };
    render(<MediaStylePanel selectedId="vid-1" nodeProps={lastProps} />);
    expect(q('asset-picker-full')).not.toBeNull();
  });

  test('applying a pasted URL via the AssetPicker writes videoUrl (not some other key)', () => {
    lastProps = { videoUrl: '', poster: '', style: {} };
    render(<MediaStylePanel selectedId="vid-1" nodeProps={lastProps} />);

    // The Video Source AssetPicker is the first url-input on the page (the
    // Poster picker is a second, separate AssetPicker instance below it).
    const urlInput = qAll<HTMLInputElement>('asset-picker-url-input')[0];
    expect(urlInput).toBeTruthy();
    setValue(urlInput, 'https://example.com/clip.mp4');

    const applyBtn = qAll<HTMLButtonElement>('asset-picker-apply')[0];
    act(() => {
      applyBtn.dispatchEvent(new MouseEvent('click', { bubbles: true }));
    });

    expect(setPropSpy).toHaveBeenCalled();
    expect(lastProps.videoUrl).toBe('https://example.com/clip.mp4');
  });

  test('a video-shaped selection also renders a Poster AssetPicker (image) that writes `poster`', () => {
    lastProps = { videoUrl: 'https://example.com/clip.mp4', poster: '', style: {} };
    render(<MediaStylePanel selectedId="vid-1" nodeProps={lastProps} />);

    const pickers = qAll('asset-picker-full');
    expect(pickers.length).toBe(2); // Video Source + Poster Image

    const urlInputs = qAll<HTMLInputElement>('asset-picker-url-input');
    const posterInput = urlInputs[1];
    setValue(posterInput, 'https://example.com/poster.jpg');
    const applyBtns = qAll<HTMLButtonElement>('asset-picker-apply');
    act(() => {
      applyBtns[1].dispatchEvent(new MouseEvent('click', { bubbles: true }));
    });

    expect(lastProps.poster).toBe('https://example.com/poster.jpg');
  });
});

describe('MediaStylePanel Video size controls (Width + Aspect Ratio) are gated on videoUrl', () => {
  test('a video-shaped selection renders SizeControl + AspectRatioControl', () => {
    lastProps = { videoUrl: '', poster: '', style: {} };
    render(<MediaStylePanel selectedId="vid-1" nodeProps={lastProps} />);
    expect(q('size-control')).not.toBeNull();
    expect(q('aspect-ratio-control')).not.toBeNull();
  });

  test('a Map-shaped selection (no videoUrl) does NOT render the video size controls', () => {
    lastProps = { address: 'New York, NY', zoom: 14, height: '400px', style: {} };
    render(<MediaStylePanel selectedId="map-1" nodeProps={lastProps} />);
    expect(q('size-control')).toBeNull();
    expect(q('aspect-ratio-control')).toBeNull();
  });

  test('a video-shaped selection only renders one SizeControl (Width) -- no separate Height control', () => {
    // See MediaStylePanel.tsx's note: VideoBlock sizes its <video>/iframe from
    // width + aspectRatio directly, not from an outer-wrapper height, so a
    // Height control would reintroduce the empty-space bug this fix targets.
    lastProps = { videoUrl: 'https://example.com/clip.mp4', poster: '', style: {} };
    render(<MediaStylePanel selectedId="vid-1" nodeProps={lastProps} />);
    expect(qAll('size-control').length).toBe(1);
  });
});

/* FIX (fix-anim-image contract, B+C): applying a crop aspect-ratio to a video
   should fill by default (objectFit defaults to 'cover' when unset) and clear
   any stale height, matching ImageStylePanel's treatment -- for prop-schema
   consistency even though VideoBlock's file-type <video> already hardcodes
   object-fit: cover today. */
describe('MediaStylePanel Video AspectRatioControl applies crop-fills-by-default treatment (fix-anim-image B+C)', () => {
  function clickPresetByLabel(root: Element | null, label: string) {
    const btn = Array.from(root?.querySelectorAll('button') ?? []).find((b) => b.textContent === label);
    expect(btn).toBeTruthy();
    act(() => { btn!.dispatchEvent(new MouseEvent('click', { bubbles: true })); });
  }

  test('applying a non-empty ratio with objectFit unset also sets objectFit to cover', () => {
    lastProps = { videoUrl: 'https://example.com/clip.mp4', poster: '', style: {} };
    render(<MediaStylePanel selectedId="vid-1" nodeProps={lastProps} />);

    clickPresetByLabel(q('aspect-ratio-control'), '1:1');

    expect(lastProps.style.aspectRatio).toBe('1 / 1');
    expect(lastProps.style.objectFit).toBe('cover');
  });

  test('applying a ratio clears a stale height', () => {
    lastProps = { videoUrl: 'https://example.com/clip.mp4', poster: '', style: { height: '50%' } };
    render(<MediaStylePanel selectedId="vid-1" nodeProps={lastProps} />);

    clickPresetByLabel(q('aspect-ratio-control'), '16:9');

    expect(lastProps.style.aspectRatio).toBe('16 / 9');
    expect(lastProps.style.height).toBe('');
  });

  test('clearing the ratio (Original) does not force-clear objectFit', () => {
    lastProps = { videoUrl: 'https://example.com/clip.mp4', poster: '', style: { aspectRatio: '1 / 1', objectFit: 'cover' } };
    render(<MediaStylePanel selectedId="vid-1" nodeProps={lastProps} />);

    clickPresetByLabel(q('aspect-ratio-control'), 'Original');

    expect(lastProps.style.aspectRatio).toBe('');
    expect(lastProps.style.objectFit).toBe('cover');
  });
});

function openCollapsibleByTitle(title: string) {
  const btn = Array.from(container.querySelectorAll('button')).find((b) => b.textContent?.includes(title));
  expect(btn).toBeTruthy();
  act(() => {
    btn!.dispatchEvent(new MouseEvent('click', { bubbles: true }));
  });
}

describe('MediaStylePanel box-model + animation/visibility rollout renders for every media type', () => {
  test('a Map-shaped selection still gets Spacing / Border & Effects / Animation & Visibility sections (collapsed by default, open on click)', () => {
    lastProps = { address: 'New York, NY', zoom: 14, height: '400px', style: {}, animation: '', hideOnDesktop: false };
    render(<MediaStylePanel selectedId="map-1" nodeProps={lastProps} />);

    openCollapsibleByTitle('Spacing');
    expect(q('spacing-control')).not.toBeNull();

    openCollapsibleByTitle('Border & Effects');
    expect(q('border-control')).not.toBeNull();

    openCollapsibleByTitle('Animation & Visibility');
    expect(q('animation-control')).not.toBeNull();
    expect(q('visibility-control')).not.toBeNull();
  });
});
