import { describe, it, expect, beforeEach } from 'vitest';
import { layoutAnimation } from './layout-animation.js';
import { mountHost, connectTrait, spyEvent, resetDOM, raf } from '../test-helpers.js';

describe('layout-animation', () => {
  beforeEach(resetDOM);

  it('connect does not throw and registers no leftover attribute', () => {
    const host = mountHost();
    const inst = connectTrait(layoutAnimation, host);
    expect(host.hasAttribute('data-layout-animation-active')).toBe(false);
    inst.disconnect(host);
  });

  it('disconnect cleans up the active attribute and is idempotent', () => {
    const host = mountHost();
    const inst = connectTrait(layoutAnimation, host);
    host.setAttribute('data-layout-animation-active', '');
    inst.disconnect(host);
    expect(host.hasAttribute('data-layout-animation-active')).toBe(false);
    // A second disconnect must not throw.
    expect(() => inst.disconnect(host)).not.toThrow();
  });

  it('reads data-layout-animate-duration + data-layout-animate-easing without throwing', () => {
    const host = mountHost('div', {
      'data-layout-animate-duration': '500',
      'data-layout-animate-easing': 'ease-in-out',
    });
    expect(() => connectTrait(layoutAnimation, host)).not.toThrow();
  });

  it('handles a missing duration / easing — defaults apply silently', () => {
    const host = mountHost();
    expect(() => connectTrait(layoutAnimation, host)).not.toThrow();
  });

  it('exposes the expected schema surface', () => {
    expect(layoutAnimation.schema.name).toBe('layout-animation');
    expect(layoutAnimation.schema.category).toBe('motion-positioning');
    expect(layoutAnimation.schema.attributes).toContain('data-layout-animation-active');
    expect(layoutAnimation.schema.events).toContain('layout-animation-done');
    expect(layoutAnimation.schema.config).toEqual([
      'data-layout-animate-duration',
      'data-layout-animate-easing',
      'data-layout-animate-trigger',
    ]);
  });

  it('parent-childList mutation invokes the FLIP path without throwing', async () => {
    // Build a parent + sibling so we can mutate child order.
    const parent = document.createElement('div');
    document.body.appendChild(parent);
    const sib = document.createElement('div');
    const host = document.createElement('div');
    parent.appendChild(sib);
    parent.appendChild(host);

    const inst = layoutAnimation();
    inst.connect(host);

    // Reorder siblings — should drive the MutationObserver callback.
    expect(() => parent.insertBefore(host, sib)).not.toThrow();

    // Allow the MutationObserver microtask + rAF capture to settle.
    await Promise.resolve();
    await raf();

    inst.disconnect(host);
    expect(host.hasAttribute('data-layout-animation-active')).toBe(false);
  });

  it('attribute-trigger flip is wired (toggling data-layout-animate-trigger does not throw)', async () => {
    const host = mountHost();
    const inst = connectTrait(layoutAnimation, host);

    // Set the trigger — observer should fire a flip(); no DOM rect change in
    // the test env means it short-circuits to "no meaningful delta", which we
    // assert by absence of throws + no leftover active attribute after settle.
    host.setAttribute('data-layout-animate-trigger', '1');

    await Promise.resolve();
    await raf();

    inst.disconnect(host);
    expect(host.hasAttribute('data-layout-animation-active')).toBe(false);
  });

  it('layout-animation-done event listener attaches without error', () => {
    const host = mountHost();
    const spy = spyEvent(host, 'layout-animation-done');
    connectTrait(layoutAnimation, host);
    // We don't assert spy.count > 0 here — the test environment (happy-dom)
    // returns zero rects, so the FLIP path short-circuits at the no-delta
    // gate. The contract we care about is that subscribing doesn't throw.
    expect(spy.count).toBe(0);
  });

  it('two parallel instances on different hosts do not interfere', () => {
    const a = mountHost();
    const b = mountHost();
    const ia = connectTrait(layoutAnimation, a);
    const ib = connectTrait(layoutAnimation, b);
    expect(() => ia.disconnect(a)).not.toThrow();
    expect(() => ib.disconnect(b)).not.toThrow();
  });

  it('disconnect cancels any scheduled snapshot capture', () => {
    const host = mountHost();
    const inst = connectTrait(layoutAnimation, host);
    // Disconnect synchronously after connect — exercises the rAF-cancel branch.
    expect(() => inst.disconnect(host)).not.toThrow();
  });
});
