import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
import { viewTransition } from './view-transition.js';
import { _resetMotionCache } from '../motion.js';
import {
  mountHost,
  connectTrait,
  spyEvent,
  resetDOM,
  expectValidSchema,
  wait,
} from '../test-helpers.js';

describe('view-transition', () => {
  beforeEach(() => {
    resetDOM();
    _resetMotionCache();
    // happy-dom has no startViewTransition; ensure we start clean each test.
    delete document.startViewTransition;
  });

  afterEach(() => {
    delete document.startViewTransition;
    _resetMotionCache();
  });

  it('exposes a valid schema', () => {
    expectValidSchema(viewTransition);
    expect(viewTransition.schema.name).toBe('view-transition');
    expect(viewTransition.schema.category).toBe('motion-positioning');
    expect(viewTransition.schema.events).toContain('view-transition-start');
    expect(viewTransition.schema.events).toContain('view-transition-end');
    expect(viewTransition.schema.config).toContain('data-view-transition-name');
    expect(viewTransition.schema.config).toContain('data-view-transition-duration');
    expect(viewTransition.schema.config).toContain('data-view-transition-easing');
  });

  it('writes view-transition-name to the host inline style', () => {
    const host = mountHost('div', { 'data-view-transition-name': 'card-42' });
    connectTrait(viewTransition, host);
    expect(host.style.viewTransitionName).toBe('card-42');
  });

  it('writes --vt-duration / --vt-easing custom props', () => {
    const host = mountHost('div', {
      'data-view-transition-name': 'hero',
      'data-view-transition-duration': '450ms',
      'data-view-transition-easing': 'cubic-bezier(0.2, 0.8, 0.2, 1)',
    });
    connectTrait(viewTransition, host);
    expect(host.style.getPropertyValue('--vt-duration')).toBe('450ms');
    expect(host.style.getPropertyValue('--vt-easing')).toBe(
      'cubic-bezier(0.2, 0.8, 0.2, 1)',
    );
  });

  it('omits CSS props when their data attrs are absent', () => {
    const host = mountHost('div');
    connectTrait(viewTransition, host);
    // happy-dom returns undefined for unset viewTransitionName; real browsers
    // return ''. Either is acceptable — the contract is "nothing was set."
    expect(host.style.viewTransitionName || '').toBe('');
    expect(host.style.getPropertyValue('--vt-duration')).toBe('');
    expect(host.style.getPropertyValue('--vt-easing')).toBe('');
  });

  it('installs host.startTransition() method', () => {
    const host = mountHost();
    expect(typeof host.startTransition).toBe('undefined');
    connectTrait(viewTransition, host);
    expect(typeof host.startTransition).toBe('function');
  });

  it('removes host.startTransition() on disconnect', () => {
    const host = mountHost();
    const inst = connectTrait(viewTransition, host);
    expect(typeof host.startTransition).toBe('function');
    inst.disconnect(host);
    expect(host.startTransition).toBeUndefined();
  });

  it('runs the callback when the API is missing (feature-detect fallback)', () => {
    const host = mountHost();
    connectTrait(viewTransition, host);
    let ran = false;
    host.startTransition(() => { ran = true; });
    expect(ran).toBe(true);
  });

  it('fires start + end events when the API is missing', async () => {
    const host = mountHost();
    connectTrait(viewTransition, host);
    const startSpy = spyEvent(host, 'view-transition-start');
    const endSpy = spyEvent(host, 'view-transition-end');
    host.startTransition(() => {});
    // start fires synchronously; end fires on the next microtask.
    expect(startSpy.count).toBe(1);
    await Promise.resolve();
    expect(endSpy.count).toBe(1);
  });

  it('toggles data-view-transition-active across start → end', async () => {
    const host = mountHost();
    connectTrait(viewTransition, host);
    expect(host.hasAttribute('data-view-transition-active')).toBe(false);
    host.startTransition(() => {
      // Mutation runs while the attribute is set.
      expect(host.hasAttribute('data-view-transition-active')).toBe(true);
    });
    // The end is queued on a microtask in the fallback path.
    await Promise.resolve();
    expect(host.hasAttribute('data-view-transition-active')).toBe(false);
  });

  it('returns a finished-promise even in the fallback path', async () => {
    const host = mountHost();
    connectTrait(viewTransition, host);
    const transition = host.startTransition(() => {});
    expect(transition).toBeTruthy();
    expect(transition.finished).toBeInstanceOf(Promise);
    await expect(transition.finished).resolves.toBeUndefined();
  });

  it('uses real document.startViewTransition when available', () => {
    const finished = Promise.resolve();
    const fakeTransition = { finished, ready: finished, updateCallbackDone: finished };
    const stub = vi.fn((cb) => { cb(); return fakeTransition; });
    document.startViewTransition = stub;

    const host = mountHost();
    connectTrait(viewTransition, host);
    const cb = vi.fn();
    const result = host.startTransition(cb);

    expect(stub).toHaveBeenCalledTimes(1);
    expect(cb).toHaveBeenCalledTimes(1);
    expect(result).toBe(fakeTransition);
  });

  it('fires end after the real transition resolves', async () => {
    let resolveFinished;
    const finished = new Promise((r) => { resolveFinished = r; });
    document.startViewTransition = (cb) => {
      cb();
      return { finished, ready: finished, updateCallbackDone: finished };
    };

    const host = mountHost();
    connectTrait(viewTransition, host);
    const endSpy = spyEvent(host, 'view-transition-end');

    host.startTransition(() => {});
    // End hasn't fired yet — the transition is still in flight.
    expect(endSpy.count).toBe(0);

    resolveFinished();
    await Promise.resolve();
    await Promise.resolve();
    expect(endSpy.count).toBe(1);
  });

  it('fires end even if the real transition rejects (user-skipped)', async () => {
    const finished = Promise.reject(new Error('skipped'));
    // Pre-attach a noop catch so the rejection isn't unhandled in the test runner.
    finished.catch(() => {});
    document.startViewTransition = (cb) => {
      cb();
      return { finished, ready: finished, updateCallbackDone: finished };
    };

    const host = mountHost();
    connectTrait(viewTransition, host);
    const endSpy = spyEvent(host, 'view-transition-end');

    host.startTransition(() => {});
    await Promise.resolve();
    await Promise.resolve();
    expect(endSpy.count).toBe(1);
  });

  it('runs callback synchronously even with a missing/invalid argument', () => {
    const host = mountHost();
    connectTrait(viewTransition, host);
    expect(() => host.startTransition()).not.toThrow();
    expect(() => host.startTransition('not a function')).not.toThrow();
  });

  it('honors prefers-reduced-motion: reduce — synchronous lifecycle, no animation', async () => {
    // Force prefersReducedMotion() to return true.
    const origMatchMedia = window.matchMedia;
    window.matchMedia = (q) => ({
      matches: typeof q === 'string' && q.includes('prefers-reduced-motion'),
      media: q,
      addEventListener() {},
      removeEventListener() {},
      addListener() {},
      removeListener() {},
      dispatchEvent() { return false; },
      onchange: null,
    });
    _resetMotionCache();

    // Even if the API is "available", reduced-motion takes the fallback path.
    document.startViewTransition = vi.fn();

    const host = mountHost();
    connectTrait(viewTransition, host);
    const startSpy = spyEvent(host, 'view-transition-start');
    const endSpy = spyEvent(host, 'view-transition-end');
    let ran = false;
    host.startTransition(() => { ran = true; });

    expect(ran).toBe(true);
    expect(startSpy.count).toBe(1);
    await Promise.resolve();
    expect(endSpy.count).toBe(1);
    expect(document.startViewTransition).not.toHaveBeenCalled();

    window.matchMedia = origMatchMedia;
    _resetMotionCache();
  });

  it('disconnect clears active attr and restores inline styles', () => {
    const host = mountHost('div', {
      'data-view-transition-name': 'card-9',
      'data-view-transition-duration': '300ms',
    });
    const inst = connectTrait(viewTransition, host);
    // Force the attribute on so disconnect's cleanup is observable.
    host.setAttribute('data-view-transition-active', '');
    inst.disconnect(host);

    expect(host.hasAttribute('data-view-transition-active')).toBe(false);
    expect(host.style.viewTransitionName || '').toBe('');
    expect(host.style.getPropertyValue('--vt-duration')).toBe('');
  });

  it('does not clobber an existing host.startTransition; restores it on disconnect', () => {
    const host = mountHost();
    const sentinel = () => 'pre-existing';
    host.startTransition = sentinel;
    const inst = connectTrait(viewTransition, host);
    expect(host.startTransition).not.toBe(sentinel); // trait took over
    inst.disconnect(host);
    expect(host.startTransition).toBe(sentinel); // restored
  });

  it('connect → disconnect → connect-again does not throw', () => {
    const host = mountHost('div', { 'data-view-transition-name': 'a' });
    const inst1 = connectTrait(viewTransition, host);
    inst1.disconnect(host);
    expect(() => {
      const inst2 = connectTrait(viewTransition, host);
      inst2.disconnect(host);
    }).not.toThrow();
  });

  it('two parallel hosts each get their own startTransition', () => {
    const a = mountHost();
    const b = mountHost();
    const instA = connectTrait(viewTransition, a);
    const instB = connectTrait(viewTransition, b);
    expect(typeof a.startTransition).toBe('function');
    expect(typeof b.startTransition).toBe('function');
    expect(a.startTransition).not.toBe(b.startTransition);
    instA.disconnect(a);
    instB.disconnect(b);
  });
});
