import { describe, it, expect, beforeEach, vi } from 'vitest';
import { act, cleanup, render, screen } from '@testing-library/react';
import { Reveal } from '../Reveal.js';
import { StaggerList } from '../StaggerList.js';
import { NumberFlow } from '../NumberFlow.js';
import { TiltCard } from '../TiltCard.js';
import { ScrollProgress } from '../ScrollProgress.js';
import { SPRINGS, prefersReducedMotion } from '../utils.js';

describe('utils', () => {
  it('exposes 4 spring presets as cubic-bezier strings', () => {
    expect(Object.keys(SPRINGS)).toHaveLength(4);
    for (const v of Object.values(SPRINGS)) {
      expect(v).toMatch(/^cubic-bezier/);
    }
  });
  it('prefersReducedMotion handles missing matchMedia gracefully', () => {
    const orig = window.matchMedia;
    // @ts-expect-error force delete for test
    delete (window as { matchMedia?: unknown }).matchMedia;
    expect(prefersReducedMotion()).toBe(false);
    window.matchMedia = orig;
  });
});

describe('Reveal', () => {
  beforeEach(() => cleanup());

  it('renders children and exposes data-visible attribute', () => {
    render(
      <Reveal disableMotion>
        <span data-testid="child">hi</span>
      </Reveal>,
    );
    expect(screen.getByTestId('child')).toBeDefined();
    const wrap = screen.getByTestId('bl-reveal');
    expect(wrap.getAttribute('data-visible')).toBe('true');
  });

  it('starts hidden when IntersectionObserver is unavailable (graceful fallback)', () => {
    const orig = (globalThis as { IntersectionObserver?: unknown }).IntersectionObserver;
    // @ts-expect-error force delete
    delete (globalThis as { IntersectionObserver?: unknown }).IntersectionObserver;
    render(<Reveal><span>x</span></Reveal>);
    // Fallback path sets visible=true so SSR / non-IO browsers still render.
    expect(screen.getByTestId('bl-reveal').getAttribute('data-visible')).toBe('true');
    (globalThis as { IntersectionObserver?: unknown }).IntersectionObserver = orig;
  });

  it('forwards inline style after motion vars', () => {
    render(
      <Reveal disableMotion style={{ background: 'red' }}>
        <span>x</span>
      </Reveal>,
    );
    expect(screen.getByTestId('bl-reveal').style.background).toBe('red');
  });
});

describe('StaggerList', () => {
  beforeEach(() => cleanup());

  it('wraps each child in a Reveal with increasing delay', () => {
    render(
      <StaggerList disableMotion stagger={50} initialDelay={10}>
        <div data-testid="i1">1</div>
        <div data-testid="i2">2</div>
        <div data-testid="i3">3</div>
      </StaggerList>,
    );
    const list = screen.getByTestId('bl-stagger-list');
    expect(list.children).toHaveLength(3);
    expect(screen.getByTestId('i3')).toBeDefined();
  });

  it('supports element override via `as`', () => {
    render(
      <StaggerList disableMotion as="ul">
        <li>a</li>
        <li>b</li>
      </StaggerList>,
    );
    expect(screen.getByTestId('bl-stagger-list').tagName.toLowerCase()).toBe('ul');
  });
});

describe('NumberFlow', () => {
  beforeEach(() => cleanup());

  it('snaps to value when disableMotion=true', () => {
    render(<NumberFlow value={1234} disableMotion />);
    expect(screen.getByTestId('bl-number-flow').textContent).toBe('1,234');
  });

  it('formats with decimals + prefix/suffix', () => {
    render(<NumberFlow value={42.5} decimals={1} prefix="$" suffix=" USD" disableMotion />);
    expect(screen.getByTestId('bl-number-flow').textContent).toBe('$42.5 USD');
  });

  it('uses custom format function', () => {
    render(<NumberFlow value={5} format={(n) => `≈${n.toFixed(0)} items`} disableMotion />);
    expect(screen.getByTestId('bl-number-flow').textContent).toBe('≈5 items');
  });

  it('exposes data-target equal to the latest target value', () => {
    render(<NumberFlow value={7} disableMotion />);
    expect(screen.getByTestId('bl-number-flow').getAttribute('data-target')).toBe('7');
  });
});

describe('TiltCard', () => {
  beforeEach(() => cleanup());

  it('renders children and attaches mousemove/mouseleave handlers', () => {
    render(
      <TiltCard>
        <span data-testid="tilt-child">card body</span>
      </TiltCard>,
    );
    expect(screen.getByTestId('tilt-child')).toBeDefined();
    const el = screen.getByTestId('bl-tilt-card');
    // Glare overlay rendered by default.
    expect(el.lastElementChild?.getAttribute('aria-hidden')).toBe('true');
  });

  it('hides glare overlay when disabled via prop', () => {
    render(
      <TiltCard glare={0}>
        <span data-testid="tc-child">x</span>
      </TiltCard>,
    );
    const el = screen.getByTestId('bl-tilt-card');
    // Without glare, the only child is the user's content — no aria-hidden overlay sibling.
    const ariaHiddenSiblings = Array.from(el.children).filter(
      c => c.getAttribute('aria-hidden') === 'true',
    );
    expect(ariaHiddenSiblings).toHaveLength(0);
  });
});

describe('ScrollProgress', () => {
  beforeEach(() => cleanup());

  it('renders a fixed bar with the given thickness + color', () => {
    render(<ScrollProgress thickness={5} color="hotpink" />);
    const bar = screen.getByTestId('bl-scroll-progress');
    expect(bar.style.position).toBe('fixed');
    expect(bar.style.height).toBe('5px');
  });

  it('updates the inner transform on window scroll', () => {
    render(<ScrollProgress />);
    const bar = screen.getByTestId('bl-scroll-progress');
    const fill = bar.firstElementChild as HTMLElement;
    expect(fill.style.transform).toBe('scaleX(0)');
    // Simulate scroll — we can't mutate scrollTop on document.documentElement
    // in happy-dom in all builds, so just verify the listener was attached
    // and dispatching scroll doesn't throw.
    act(() => {
      window.dispatchEvent(new Event('scroll'));
    });
  });

  it('cleans up listeners on unmount (no leak)', () => {
    const add = vi.spyOn(window, 'addEventListener');
    const remove = vi.spyOn(window, 'removeEventListener');
    const { unmount } = render(<ScrollProgress />);
    unmount();
    expect(remove).toHaveBeenCalled();
    add.mockRestore();
    remove.mockRestore();
  });
});

// ── Wave 13.D spatial primitives ────────────────────────────────────────

import { Spotlight } from '../Spotlight.js';
import { Magnetic } from '../Magnetic.js';
import { MeshBackground } from '../MeshBackground.js';

describe('Spotlight', () => {
  beforeEach(() => cleanup());
  it('renders children + data-testid', () => {
    render(<Spotlight><span data-testid="child" /></Spotlight>);
    expect(screen.getByTestId('bl-spotlight')).toBeDefined();
    expect(screen.getByTestId('child')).toBeDefined();
  });
  it('disableMotion=true renders the reduced-motion (centered) gradient', () => {
    render(<Spotlight disableMotion><span /></Spotlight>);
    const el = screen.getByTestId('bl-spotlight');
    expect(el.getAttribute('data-reduced')).toBe('true');
  });
  it('updates --bl-spot-x / --bl-spot-y on pointermove', () => {
    render(<Spotlight><span /></Spotlight>);
    const el = screen.getByTestId('bl-spotlight') as HTMLElement;
    const evt = new Event('pointermove') as PointerEvent;
    Object.assign(evt, { clientX: 120, clientY: 80 });
    el.dispatchEvent(evt);
    // Property updates are best-effort; we just assert no throw.
    expect(el).toBeDefined();
  });
});

describe('Magnetic', () => {
  beforeEach(() => cleanup());
  it('renders children inline-block', () => {
    render(<Magnetic><button>hi</button></Magnetic>);
    const el = screen.getByTestId('bl-magnetic') as HTMLElement;
    expect(el.style.display).toBe('inline-block');
  });
  it('disableMotion bypasses transform transition', () => {
    render(<Magnetic disableMotion><button>hi</button></Magnetic>);
    const el = screen.getByTestId('bl-magnetic') as HTMLElement;
    expect(el.style.transition).toBe('none');
  });
});

describe('MeshBackground', () => {
  beforeEach(() => cleanup());
  it('renders children + records mood + reduced data attrs', () => {
    render(
      <MeshBackground mood="focus" disableMotion>
        <h1 data-testid="hero">Title</h1>
      </MeshBackground>,
    );
    const el = screen.getByTestId('bl-mesh-background');
    expect(el.getAttribute('data-mood')).toBe('focus');
    expect(el.getAttribute('data-reduced')).toBe('true');
    expect(screen.getByTestId('hero').textContent).toBe('Title');
  });
  it('accepts custom 4-stop palette', () => {
    render(
      <MeshBackground
        colors={['red', 'orange', 'yellow', 'green']}
        disableMotion
      >
        <span />
      </MeshBackground>,
    );
    // Just ensure it doesn't throw + renders the wrapper.
    expect(screen.getByTestId('bl-mesh-background')).toBeDefined();
  });
});

// ── Wave 13.D.1 / .5 ────────────────────────────────────────────────────

import { Parallax } from '../Parallax.js';
import { TiltGallery, type TiltGalleryItem } from '../TiltGallery.js';

describe('Parallax', () => {
  beforeEach(() => cleanup());
  it('records axis + reduced data attributes', () => {
    render(
      <Parallax axis="x" disableMotion>
        <span data-testid="px-child" />
      </Parallax>,
    );
    const el = screen.getByTestId('bl-parallax');
    expect(el.getAttribute('data-axis')).toBe('x');
    expect(el.getAttribute('data-reduced')).toBe('true');
  });
  it('renders children + does not throw on scroll', () => {
    render(<Parallax><span data-testid="px-child2" /></Parallax>);
    // No setState in the listener; just confirm the listener is attached + safe.
    window.dispatchEvent(new Event('scroll'));
    expect(screen.getByTestId('px-child2')).toBeDefined();
  });
});

const TILT_ITEMS: TiltGalleryItem[] = [
  { id: 'tg-1', content: <span data-testid="tg-c1">A</span>, caption: 'one' },
  { id: 'tg-2', content: <span data-testid="tg-c2">B</span> },
  { id: 'tg-3', content: <span data-testid="tg-c3">C</span> },
];

describe('TiltGallery', () => {
  beforeEach(() => cleanup());
  it('renders one tile per item', () => {
    render(<TiltGallery items={TILT_ITEMS} disableMotion />);
    expect(screen.getAllByTestId('bl-tilt-gallery-tile')).toHaveLength(3);
  });
  it('records reduced data attribute on the region', () => {
    render(<TiltGallery items={TILT_ITEMS} disableMotion />);
    expect(screen.getByTestId('bl-tilt-gallery').getAttribute('data-reduced')).toBe('true');
  });
  it('uses aria-label for the region', () => {
    render(<TiltGallery items={TILT_ITEMS} ariaLabel="Demo gallery" disableMotion />);
    expect(screen.getByTestId('bl-tilt-gallery').getAttribute('aria-label')).toBe('Demo gallery');
  });
});
