import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
import { ref, nextTick } from 'vue';
import { useCountUp } from '../../resources/js/composables/useCountUp';

function mockMatchMedia(reduced: boolean): void {
    Object.defineProperty(window, 'matchMedia', {
        writable: true,
        configurable: true,
        value: vi.fn().mockImplementation((query: string) => ({
            matches: query.includes('reduce') ? reduced : false,
            media: query,
            onchange: null,
            addEventListener: vi.fn(),
            removeEventListener: vi.fn(),
            addListener: vi.fn(),
            removeListener: vi.fn(),
            dispatchEvent: vi.fn(),
        })),
    });
}

describe('useCountUp', () => {
    let originalRaf: typeof globalThis.requestAnimationFrame;
    let originalCaf: typeof globalThis.cancelAnimationFrame;

    beforeEach(() => {
        vi.useFakeTimers();
        mockMatchMedia(false);
        // RAF polyfill: jsdom's RAF does not advance synchronously with fake timers.
        // Route RAF through setTimeout so vi.advanceTimersByTimeAsync() advances it.
        originalRaf = globalThis.requestAnimationFrame;
        originalCaf = globalThis.cancelAnimationFrame;
        globalThis.requestAnimationFrame = ((cb: FrameRequestCallback) =>
            setTimeout(
                () => cb(performance.now()),
                16,
            ) as unknown as number) as typeof globalThis.requestAnimationFrame;
        globalThis.cancelAnimationFrame = ((id: number) =>
            clearTimeout(id as unknown as ReturnType<typeof setTimeout>)) as typeof globalThis.cancelAnimationFrame;
    });

    afterEach(() => {
        globalThis.requestAnimationFrame = originalRaf;
        globalThis.cancelAnimationFrame = originalCaf;
        vi.useRealTimers();
    });

    it('exposes initial display = 0 before target propagates', async () => {
        const target = ref(100);
        const { display } = useCountUp(target, { duration: 600 });
        expect(display.value).toBe(0);
    });

    it('animates from 0 to target over duration', async () => {
        const target = ref(100);
        const { display, start } = useCountUp(target, { duration: 600 });
        start();
        // step through animation
        await vi.advanceTimersByTimeAsync(300);
        expect(display.value).toBeGreaterThan(0);
        expect(display.value).toBeLessThan(100);
        await vi.advanceTimersByTimeAsync(400);
        expect(display.value).toBe(100);
    });

    it('respects prefers-reduced-motion (instant value, no animation)', async () => {
        mockMatchMedia(true);
        const target = ref(250);
        const { display, start } = useCountUp(target, { duration: 600 });
        start();
        await nextTick();
        expect(display.value).toBe(250);
    });

    it('re-animates when target value changes', async () => {
        const target = ref(50);
        const { display, start } = useCountUp(target, { duration: 400 });
        start();
        await vi.advanceTimersByTimeAsync(500);
        expect(display.value).toBe(50);

        target.value = 200;
        await nextTick();
        await vi.advanceTimersByTimeAsync(500);
        expect(display.value).toBe(200);
    });
});
