import { describe, it, expect, beforeEach, vi } from 'vitest';
import { longPress } from './long-press.js';
import { mountHost, connectTrait, spyEvent, resetDOM, wait } from '../test-helpers.js';

function pointerDown(host, x = 0, y = 0) {
  host.dispatchEvent(new PointerEvent('pointerdown', { clientX: x, clientY: y, bubbles: true }));
}
function pointerUp(host, x = 0, y = 0) {
  host.dispatchEvent(new PointerEvent('pointerup', { clientX: x, clientY: y, bubbles: true }));
}
function pointerMove(host, x, y) {
  host.dispatchEvent(new PointerEvent('pointermove', { clientX: x, clientY: y, bubbles: true }));
}
function pointerLeave(host) {
  host.dispatchEvent(new PointerEvent('pointerleave', { bubbles: true }));
}

describe('long-press', () => {
  beforeEach(() => {
    resetDOM();
    vi.useFakeTimers();
  });

  it('sets data-long-press-active on pointerdown', () => {
    const host = mountHost('button', {
      'data-long-press-duration': '100',
      'data-long-press-progress-interval': '20',
    });
    connectTrait(longPress, host);
    pointerDown(host);
    expect(host.hasAttribute('data-long-press-active')).toBe(true);
    expect(host.getAttribute('data-long-press-progress')).toBe('0');
  });

  it('fires "long-press" after the configured duration elapses', () => {
    const host = mountHost('button', {
      'data-long-press-duration': '100',
      'data-long-press-progress-interval': '20',
    });
    connectTrait(longPress, host);
    const spy = spyEvent(host, 'long-press');
    pointerDown(host);
    vi.advanceTimersByTime(100);
    expect(spy.count).toBe(1);
    expect(host.hasAttribute('data-long-press-fired')).toBe(true);
  });

  it('emits "long-press-progress" at intervals while held', () => {
    const host = mountHost('button', {
      'data-long-press-duration': '100',
      'data-long-press-progress-interval': '20',
    });
    connectTrait(longPress, host);
    const spy = spyEvent(host, 'long-press-progress');
    pointerDown(host);
    vi.advanceTimersByTime(60); // 3 ticks at 20ms
    expect(spy.count).toBeGreaterThanOrEqual(2);
    // Each tick should report progress in [0, 1].
    for (const detail of spy.captures) {
      expect(detail.progress).toBeGreaterThanOrEqual(0);
      expect(detail.progress).toBeLessThanOrEqual(1);
    }
  });

  it('fires "long-press-cancelled" when pointerup arrives early', () => {
    const host = mountHost('button', {
      'data-long-press-duration': '200',
      'data-long-press-progress-interval': '50',
    });
    connectTrait(longPress, host);
    const fireSpy = spyEvent(host, 'long-press');
    const cancelSpy = spyEvent(host, 'long-press-cancelled');
    pointerDown(host);
    vi.advanceTimersByTime(80); // well short of 200
    pointerUp(host);
    expect(fireSpy.count).toBe(0);
    expect(cancelSpy.count).toBe(1);
    expect(cancelSpy.last.reason).toBe('release');
  });

  it('fires "long-press-cancelled" on pointerleave', () => {
    const host = mountHost('button', {
      'data-long-press-duration': '200',
    });
    connectTrait(longPress, host);
    const cancelSpy = spyEvent(host, 'long-press-cancelled');
    pointerDown(host);
    vi.advanceTimersByTime(40);
    pointerLeave(host);
    expect(cancelSpy.count).toBe(1);
    expect(cancelSpy.last.reason).toBe('leave');
    expect(host.hasAttribute('data-long-press-active')).toBe(false);
  });

  it('cancels when pointermove exceeds the tolerance', () => {
    const host = mountHost('button', {
      'data-long-press-duration': '300',
      'data-long-press-tolerance': '8',
    });
    connectTrait(longPress, host);
    const cancelSpy = spyEvent(host, 'long-press-cancelled');
    pointerDown(host, 0, 0);
    pointerMove(host, 20, 0); // > 8px
    expect(cancelSpy.count).toBe(1);
    expect(cancelSpy.last.reason).toBe('move');
  });

  it('does NOT cancel when pointermove stays within tolerance', () => {
    const host = mountHost('button', {
      'data-long-press-duration': '100',
      'data-long-press-tolerance': '8',
    });
    connectTrait(longPress, host);
    const cancelSpy = spyEvent(host, 'long-press-cancelled');
    const fireSpy = spyEvent(host, 'long-press');
    pointerDown(host, 0, 0);
    pointerMove(host, 5, 5); // hypot ~7.07, under 8
    vi.advanceTimersByTime(100);
    expect(cancelSpy.count).toBe(0);
    expect(fireSpy.count).toBe(1);
  });

  it('respects [disabled] — no listeners fire, no attributes set', () => {
    const host = mountHost('button', {
      disabled: '',
      'data-long-press-duration': '50',
    });
    connectTrait(longPress, host);
    const fireSpy = spyEvent(host, 'long-press');
    pointerDown(host);
    expect(host.hasAttribute('data-long-press-active')).toBe(false);
    vi.advanceTimersByTime(100);
    expect(fireSpy.count).toBe(0);
  });

  it('suppresses the synthetic click that follows a committed long-press', () => {
    const host = mountHost('button', {
      'data-long-press-duration': '100',
    });
    connectTrait(longPress, host);
    let clickHits = 0;
    host.addEventListener('click', () => clickHits++);
    pointerDown(host);
    vi.advanceTimersByTime(100);
    pointerUp(host);
    host.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true }));
    expect(clickHits).toBe(0);
  });

  it('does NOT suppress click after a cancelled press', () => {
    const host = mountHost('button', {
      'data-long-press-duration': '300',
    });
    connectTrait(longPress, host);
    let clickHits = 0;
    host.addEventListener('click', () => clickHits++);
    pointerDown(host);
    vi.advanceTimersByTime(60);
    pointerUp(host); // early release → cancelled
    host.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true }));
    expect(clickHits).toBe(1);
  });

  it('uses default duration (600ms) when attribute missing', () => {
    const host = mountHost('button');
    connectTrait(longPress, host);
    const spy = spyEvent(host, 'long-press');
    pointerDown(host);
    vi.advanceTimersByTime(599);
    expect(spy.count).toBe(0);
    vi.advanceTimersByTime(2);
    expect(spy.count).toBe(1);
  });

  it('uses default tolerance (8px) when attribute missing', () => {
    const host = mountHost('button', { 'data-long-press-duration': '300' });
    connectTrait(longPress, host);
    const cancelSpy = spyEvent(host, 'long-press-cancelled');
    pointerDown(host, 0, 0);
    pointerMove(host, 7, 0); // under default 8
    expect(cancelSpy.count).toBe(0);
    pointerMove(host, 9, 0); // over default 8
    expect(cancelSpy.count).toBe(1);
  });

  it('disconnect clears active timers + attributes mid-press', () => {
    const host = mountHost('button', {
      'data-long-press-duration': '200',
      'data-long-press-progress-interval': '50',
    });
    const inst = connectTrait(longPress, host);
    pointerDown(host);
    expect(host.hasAttribute('data-long-press-active')).toBe(true);
    inst.disconnect(host);
    expect(host.hasAttribute('data-long-press-active')).toBe(false);
    expect(host.hasAttribute('data-long-press-progress')).toBe(false);
    // After disconnect, advancing time must not fire long-press.
    const fireSpy = spyEvent(host, 'long-press');
    vi.advanceTimersByTime(500);
    expect(fireSpy.count).toBe(0);
  });

  it('reconnect after disconnect picks up fresh state', () => {
    const host = mountHost('button', {
      'data-long-press-duration': '100',
    });
    const inst = connectTrait(longPress, host);
    pointerDown(host);
    inst.disconnect(host);

    connectTrait(longPress, host);
    const spy = spyEvent(host, 'long-press');
    pointerDown(host);
    vi.advanceTimersByTime(100);
    expect(spy.count).toBe(1);
  });

  it('schema declares the input-interaction category and full attr/event/config sets', () => {
    expect(longPress.schema.name).toBe('long-press');
    expect(longPress.schema.category).toBe('input-interaction');
    expect(longPress.schema.attributes).toContain('data-long-press-active');
    expect(longPress.schema.attributes).toContain('data-long-press-progress');
    expect(longPress.schema.attributes).toContain('data-long-press-fired');
    expect(longPress.schema.events).toEqual(
      expect.arrayContaining(['long-press', 'long-press-cancelled', 'long-press-progress']),
    );
    expect(longPress.schema.config).toEqual(
      expect.arrayContaining([
        'data-long-press-duration',
        'data-long-press-tolerance',
        'data-long-press-progress-interval',
      ]),
    );
  });

  it('contextmenu is suppressed while a press is in flight', () => {
    const host = mountHost('button', { 'data-long-press-duration': '200' });
    connectTrait(longPress, host);
    pointerDown(host);
    const ev = new Event('contextmenu', { bubbles: true, cancelable: true });
    host.dispatchEvent(ev);
    expect(ev.defaultPrevented).toBe(true);
  });
});
