/**
 * Unit tests for Web Editor V2 Event Controller.
 *
 * These tests focus on the selecting mode behavior:
 * - Clicking within selection subtree prepares drag candidate
 * - Clicking outside selection triggers reselection (Bug 1 fix)
 */

import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';

import {
  createEventController,
  type EventController,
  type EventControllerOptions,
} from '@/entrypoints/web-editor/core/event-controller';

import type { RestoreFn } from './test-utils/dom';
import { mockBoundingClientRect } from './test-utils/dom';

// =============================================================================
// Test Utilities
// =============================================================================

/**
 * Check if an element is part of the editor overlay.
 */
function isOverlayElement(node: unknown): boolean {
  return node instanceof Element && node.getAttribute('data-overlay') === 'true';
}

/**
 * Create a minimal mock PointerEvent for testing.
 * jsdom doesn't support PointerEvent, so we create a MouseEvent and extend it.
 */
function createPointerEvent(
  type: string,
  options: {
    clientX?: number;
    clientY?: number;
    button?: number;
    pointerId?: number;
    target?: EventTarget | null;
  } = {},
): MouseEvent & { pointerId: number } {
  const event = new MouseEvent(type, {
    bubbles: true,
    cancelable: true,
    clientX: options.clientX ?? 0,
    clientY: options.clientY ?? 0,
    button: options.button ?? 0,
  });

  // Add pointerId property (jsdom doesn't have PointerEvent)
  Object.defineProperty(event, 'pointerId', {
    value: options.pointerId ?? 1,
    writable: false,
  });

  // Mock composedPath to return target path
  if (options.target) {
    vi.spyOn(event, 'composedPath').mockReturnValue([options.target as EventTarget]);
  }

  return event as MouseEvent & { pointerId: number };
}

// =============================================================================
// Test Setup
// =============================================================================

let restores: RestoreFn[] = [];
let controller: EventController | null = null;

beforeEach(() => {
  restores = [];
  document.body.innerHTML = '';
});

afterEach(() => {
  controller?.dispose();
  controller = null;
  for (let i = restores.length - 1; i >= 0; i--) {
    restores[i]!();
  }
  restores = [];
  vi.restoreAllMocks();
});

// =============================================================================
// Selecting Mode Tests (Bug 1 Fix)
// =============================================================================

describe('event-controller: selecting mode click behavior', () => {
  it('clicking within selection subtree prepares drag candidate (does not trigger onSelect)', () => {
    // Setup DOM
    const selected = document.createElement('div');
    selected.id = 'selected';
    const child = document.createElement('span');
    child.id = 'child';
    selected.appendChild(child);
    document.body.appendChild(selected);

    // Mock rect for selected element
    restores.push(mockBoundingClientRect(selected, { left: 0, top: 0, width: 100, height: 100 }));
    restores.push(mockBoundingClientRect(child, { left: 10, top: 10, width: 50, height: 50 }));

    // Setup callbacks
    const onSelect = vi.fn();
    const onStartDrag = vi.fn().mockReturnValue(true);

    const options: EventControllerOptions = {
      isOverlayElement,
      getSelectedElement: () => selected,
      findTargetForSelect: () => child,
      onHover: vi.fn(),
      onSelect,
      onDeselect: vi.fn(),
      onStartDrag,
    };

    controller = createEventController(options);
    controller.setMode('selecting');

    // Simulate pointerdown within selected element
    const event = createPointerEvent('pointerdown', {
      clientX: 20,
      clientY: 20,
      target: child,
    });

    document.dispatchEvent(event);

    // onSelect should NOT be called (we're preparing drag instead)
    expect(onSelect).not.toHaveBeenCalled();
  });

  it('clicking outside selection triggers reselection (Bug 1 fix)', () => {
    // Setup DOM
    const selected = document.createElement('div');
    selected.id = 'selected';
    document.body.appendChild(selected);

    const other = document.createElement('div');
    other.id = 'other';
    document.body.appendChild(other);

    // Mock rects
    restores.push(mockBoundingClientRect(selected, { left: 0, top: 0, width: 100, height: 100 }));
    restores.push(mockBoundingClientRect(other, { left: 200, top: 0, width: 100, height: 100 }));

    // Setup callbacks
    const onSelect = vi.fn();
    const onStartDrag = vi.fn().mockReturnValue(true);

    const options: EventControllerOptions = {
      isOverlayElement,
      getSelectedElement: () => selected,
      findTargetForSelect: () => other, // Returns the "other" element as target
      onHover: vi.fn(),
      onSelect,
      onDeselect: vi.fn(),
      onStartDrag,
    };

    controller = createEventController(options);
    controller.setMode('selecting');

    // Simulate mousedown outside selected element (on "other")
    // Use mousedown since jsdom doesn't support PointerEvent
    const event = new MouseEvent('mousedown', {
      bubbles: true,
      cancelable: true,
      clientX: 250, // Outside selected (0-100), inside other (200-300)
      clientY: 50,
      button: 0,
    });

    // Mock composedPath to return a path that does NOT include "selected"
    // This simulates clicking outside the selection
    vi.spyOn(event, 'composedPath').mockReturnValue([other, document.body, document]);

    document.dispatchEvent(event);

    // onSelect SHOULD be called with the new element
    expect(onSelect).toHaveBeenCalledWith(other, expect.any(Object));
  });

  it('clicking outside with no valid target does not trigger onSelect', () => {
    // Setup DOM
    const selected = document.createElement('div');
    selected.id = 'selected';
    document.body.appendChild(selected);

    // Mock rect
    restores.push(mockBoundingClientRect(selected, { left: 0, top: 0, width: 100, height: 100 }));

    // Setup callbacks
    const onSelect = vi.fn();

    const options: EventControllerOptions = {
      isOverlayElement,
      getSelectedElement: () => selected,
      findTargetForSelect: () => null, // No valid target found
      onHover: vi.fn(),
      onSelect,
      onDeselect: vi.fn(),
      onStartDrag: vi.fn(),
    };

    controller = createEventController(options);
    controller.setMode('selecting');

    // Simulate pointerdown outside selected element
    const event = createPointerEvent('pointerdown', {
      clientX: 500,
      clientY: 500,
      target: document.body,
    });

    document.dispatchEvent(event);

    // onSelect should NOT be called (no valid target)
    expect(onSelect).not.toHaveBeenCalled();
  });
});
