import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import {
  findTourTarget,
  getScrollableParent,
  isElementFullyVisible,
  scrollTargetIntoView,
  findNextValidStepIndex,
  prefersReducedMotion,
} from '@/features/onboarding/utils/tourScroll';
import { getTooltipPosition } from '@/features/onboarding/utils/tourTooltip';
import type { TourStep } from '@/features/onboarding/types/onboarding.types';

describe('tourScroll utilities', () => {
  beforeEach(() => {
    document.body.innerHTML = '';
    vi.restoreAllMocks();
  });

  afterEach(() => {
    document.body.innerHTML = '';
  });

  it('findTourTarget returns null for missing selectors without throwing', () => {
    expect(findTourTarget('[data-tour="missing"]')).toBeNull();
  });

  it('findTourTarget returns HTMLElement when present', () => {
    document.body.innerHTML = '<div data-tour="stats">Stats</div>';
    const target = findTourTarget('[data-tour="stats"]');
    expect(target).not.toBeNull();
    expect(target?.textContent).toBe('Stats');
  });

  it('getScrollableParent finds nearest overflow container', () => {
    document.body.innerHTML = `
      <div id="scroll-root" style="overflow-y: auto; height: 100px;">
        <div id="target">Target</div>
      </div>
    `;
    const scrollRoot = document.getElementById('scroll-root') as HTMLElement;
    const target = document.getElementById('target') as HTMLElement;
    Object.defineProperty(scrollRoot, 'scrollHeight', { value: 400, configurable: true });
    Object.defineProperty(scrollRoot, 'clientHeight', { value: 100, configurable: true });

    expect(getScrollableParent(target)).toBe(scrollRoot);
  });

  it('isElementFullyVisible returns false when target is below viewport', () => {
    document.body.innerHTML = '<div id="target" style="height: 200px;">Target</div>';
    const target = document.getElementById('target') as HTMLElement;
    vi.spyOn(target, 'getBoundingClientRect').mockReturnValue({
      top: 900,
      left: 0,
      bottom: 1100,
      right: 200,
      width: 200,
      height: 200,
      x: 0,
      y: 900,
      toJSON: () => ({}),
    } as DOMRect);

    expect(isElementFullyVisible(target, window)).toBe(false);
  });

  it('scrollTargetIntoView calls scrollIntoView when target is not fully visible', async () => {
    document.body.innerHTML = '<div id="target">Target</div>';
    const target = document.getElementById('target') as HTMLElement;
    const scrollIntoView = vi.fn();
    target.scrollIntoView = scrollIntoView;

    vi.spyOn(target, 'getBoundingClientRect').mockReturnValue({
      top: 900,
      left: 0,
      bottom: 1100,
      right: 200,
      width: 200,
      height: 200,
      x: 0,
      y: 900,
      toJSON: () => ({}),
    } as DOMRect);

    vi.spyOn(window, 'matchMedia').mockReturnValue({
      matches: false,
      media: '(prefers-reduced-motion: reduce)',
      onchange: null,
      addListener: vi.fn(),
      removeListener: vi.fn(),
      addEventListener: vi.fn(),
      removeEventListener: vi.fn(),
      dispatchEvent: vi.fn(),
    } as MediaQueryList);

    await scrollTargetIntoView(target);

    expect(scrollIntoView).toHaveBeenCalledWith({
      behavior: 'smooth',
      block: 'center',
      inline: 'nearest',
    });
  });

  it('scrollTargetIntoView uses auto behavior when reduced motion is preferred', async () => {
    document.body.innerHTML = '<div id="target">Target</div>';
    const target = document.getElementById('target') as HTMLElement;
    const scrollIntoView = vi.fn();
    target.scrollIntoView = scrollIntoView;

    vi.spyOn(target, 'getBoundingClientRect').mockReturnValue({
      top: 900,
      left: 0,
      bottom: 1100,
      right: 200,
      width: 200,
      height: 200,
      x: 0,
      y: 900,
      toJSON: () => ({}),
    } as DOMRect);

    vi.spyOn(window, 'matchMedia').mockImplementation((query: string) => ({
      matches: query === '(prefers-reduced-motion: reduce)',
      media: query,
      onchange: null,
      addListener: vi.fn(),
      removeListener: vi.fn(),
      addEventListener: vi.fn(),
      removeEventListener: vi.fn(),
      dispatchEvent: vi.fn(),
    } as MediaQueryList));

    await scrollTargetIntoView(target);

    expect(scrollIntoView).toHaveBeenCalledWith({
      behavior: 'auto',
      block: 'center',
      inline: 'nearest',
    });
  });

  it('scrollTargetIntoView does not call scrollIntoView when already fully visible', async () => {
    document.body.innerHTML = '<div id="target">Target</div>';
    const target = document.getElementById('target') as HTMLElement;
    const scrollIntoView = vi.fn();
    target.scrollIntoView = scrollIntoView;

    vi.spyOn(target, 'getBoundingClientRect').mockReturnValue({
      top: 100,
      left: 100,
      bottom: 200,
      right: 300,
      width: 200,
      height: 100,
      x: 100,
      y: 100,
      toJSON: () => ({}),
    } as DOMRect);

    Object.defineProperty(window, 'innerWidth', { value: 1200, configurable: true });
    Object.defineProperty(window, 'innerHeight', { value: 800, configurable: true });

    await scrollTargetIntoView(target);

    expect(scrollIntoView).not.toHaveBeenCalled();
  });

  it('findNextValidStepIndex skips steps with missing targets', () => {
    document.body.innerHTML = '<div data-tour="second">Second</div>';
    const steps: TourStep[] = [
      { id: 'one', target: '[data-tour="first"]', titleKey: 'a', bodyKey: 'a' },
      { id: 'two', target: '[data-tour="second"]', titleKey: 'b', bodyKey: 'b' },
    ];

    expect(findNextValidStepIndex(steps, 0)).toBe(1);
    expect(findNextValidStepIndex(steps, 1)).toBeNull();
  });

  it('prefersReducedMotion reads matchMedia', () => {
    vi.spyOn(window, 'matchMedia').mockReturnValue({
      matches: true,
      media: '(prefers-reduced-motion: reduce)',
      onchange: null,
      addListener: vi.fn(),
      removeListener: vi.fn(),
      addEventListener: vi.fn(),
      removeEventListener: vi.fn(),
      dispatchEvent: vi.fn(),
    } as MediaQueryList);

    expect(prefersReducedMotion()).toBe(true);
  });
});

describe('tourTooltip utilities', () => {
  it('centers tooltip when target rect is missing', () => {
    const position = getTooltipPosition(null, { width: 400, height: 800 }, 'ltr');
    expect(position.left).toBeGreaterThanOrEqual(16);
    expect(position.top).toBeGreaterThanOrEqual(16);
  });

  it('positions tooltip for RTL below target', () => {
    const position = getTooltipPosition(
      {
        top: 100,
        left: 200,
        bottom: 160,
        right: 400,
        width: 200,
        height: 60,
        x: 200,
        y: 100,
        toJSON: () => ({}),
      } as DOMRect,
      { width: 500, height: 800 },
      'rtl',
    );

    expect(position.top).toBe(172);
    expect(position.left).toBeLessThanOrEqual(500 - 320 - 16);
  });
});
