import { describe, it, expect, vi, afterEach, beforeEach } from 'vitest';
import { render, screen, cleanup } from '@testing-library/svelte';
import TapZone from './TapZone.svelte';

// jsdom does not implement PointerEvent. Construct a MouseEvent of the
// correct type and decorate it with the `pointerType` + coords the
// component reads — Svelte forwards it to the onpointerdown handler
// unchanged, and the guard `e.pointerType === 'touch'` reads our
// property.
function pointerEvent(
    type: string,
    init: { pointerType?: string; clientX?: number; clientY?: number } = {}
): Event {
    const ev = new MouseEvent(type, {
        bubbles: true,
        clientX: init.clientX,
        clientY: init.clientY
    });
    if (init.pointerType != null) {
        Object.defineProperty(ev, 'pointerType', {
            value: init.pointerType,
            configurable: true
        });
    }
    return ev;
}

function pointerDown(
    el: Element,
    init: { pointerType: string; clientX: number; clientY: number }
) {
    el.dispatchEvent(pointerEvent('pointerdown', init));
}

function pointerMove(
    el: Element,
    init: { clientX: number; clientY: number }
) {
    el.dispatchEvent(pointerEvent('pointermove', init));
}

function pointerUp(el: Element) {
    el.dispatchEvent(pointerEvent('pointerup'));
}

afterEach(() => cleanup());

describe('TapZone', () => {
    it('renders three labelled buttons (Previous / Toggle / Next)', () => {
        render(TapZone, {
            props: { onPrev: () => {}, onNext: () => {}, onToggle: () => {} }
        });
        expect(screen.getByRole('button', { name: 'Previous page' })).toBeTruthy();
        expect(screen.getByRole('button', { name: 'Toggle controls' })).toBeTruthy();
        expect(screen.getByRole('button', { name: 'Next page' })).toBeTruthy();
    });

    it('fires onPrev when the left zone is tapped', () => {
        const onPrev = vi.fn();
        render(TapZone, { props: { onPrev, onNext: () => {}, onToggle: () => {} } });
        screen.getByRole('button', { name: 'Previous page' }).click();
        expect(onPrev).toHaveBeenCalledOnce();
    });

    it('fires onNext when the right zone is tapped', () => {
        const onNext = vi.fn();
        render(TapZone, { props: { onPrev: () => {}, onNext, onToggle: () => {} } });
        screen.getByRole('button', { name: 'Next page' }).click();
        expect(onNext).toHaveBeenCalledOnce();
    });

    it('fires onToggle when the center zone is tapped', () => {
        const onToggle = vi.fn();
        render(TapZone, { props: { onPrev: () => {}, onNext: () => {}, onToggle } });
        screen.getByRole('button', { name: 'Toggle controls' }).click();
        expect(onToggle).toHaveBeenCalledOnce();
    });

    it('exposes zone-level testids derived from the wrapper testid', () => {
        render(TapZone, {
            props: {
                onPrev: () => {},
                onNext: () => {},
                onToggle: () => {},
                testid: 'reader-tap'
            }
        });
        expect(screen.getByTestId('reader-tap')).toBeTruthy();
        expect(screen.getByTestId('reader-tap-left')).toBeTruthy();
        expect(screen.getByTestId('reader-tap-center')).toBeTruthy();
        expect(screen.getByTestId('reader-tap-right')).toBeTruthy();
    });

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

        it('fires onLongPress with anchor coords after 450ms touch hold', () => {
            const onLongPress = vi.fn();
            const onNext = vi.fn();
            render(TapZone, {
                props: {
                    onPrev: () => {},
                    onNext,
                    onToggle: () => {},
                    onLongPress
                }
            });
            const right = screen.getByTestId('tap-zone-right');
            pointerDown(right, {
                pointerType: 'touch',
                clientX: 200,
                clientY: 300
            });
            vi.advanceTimersByTime(500);
            expect(onLongPress).toHaveBeenCalledWith({ x: 200, y: 300 });
            // The synthesized click that follows the long-press is
            // suppressed so the next-page handler doesn't fire.
            right.click();
            expect(onNext).not.toHaveBeenCalled();
        });

        it('does not fire onLongPress for mouse pointers', () => {
            const onLongPress = vi.fn();
            render(TapZone, {
                props: {
                    onPrev: () => {},
                    onNext: () => {},
                    onToggle: () => {},
                    onLongPress
                }
            });
            const center = screen.getByTestId('tap-zone-center');
            pointerDown(center, {
                pointerType: 'mouse',
                clientX: 100,
                clientY: 100
            });
            vi.advanceTimersByTime(1000);
            expect(onLongPress).not.toHaveBeenCalled();
        });

        it('cancels long-press when pointer moves beyond tolerance', () => {
            const onLongPress = vi.fn();
            render(TapZone, {
                props: {
                    onPrev: () => {},
                    onNext: () => {},
                    onToggle: () => {},
                    onLongPress
                }
            });
            const right = screen.getByTestId('tap-zone-right');
            pointerDown(right, {
                pointerType: 'touch',
                clientX: 100,
                clientY: 100
            });
            pointerMove(right, { clientX: 200, clientY: 100 });
            vi.advanceTimersByTime(500);
            expect(onLongPress).not.toHaveBeenCalled();
        });

        it('cancels long-press on pointer up before threshold', () => {
            const onLongPress = vi.fn();
            render(TapZone, {
                props: {
                    onPrev: () => {},
                    onNext: () => {},
                    onToggle: () => {},
                    onLongPress
                }
            });
            const center = screen.getByTestId('tap-zone-center');
            pointerDown(center, {
                pointerType: 'touch',
                clientX: 100,
                clientY: 100
            });
            pointerUp(center);
            vi.advanceTimersByTime(500);
            expect(onLongPress).not.toHaveBeenCalled();
        });

        it('regular click still fires onNext when no long-press handler', () => {
            const onNext = vi.fn();
            render(TapZone, {
                props: {
                    onPrev: () => {},
                    onNext,
                    onToggle: () => {}
                }
            });
            screen.getByTestId('tap-zone-right').click();
            expect(onNext).toHaveBeenCalledOnce();
        });

        it('suppression auto-expires so the next legitimate tap fires after a slide-off long-press', () => {
            // Scenario: user long-presses, finger slides off the zone
            // onto the opened sheet, releases there. No click is
            // synthesized on the zone, so without the expiry the
            // suppression would leak and eat the next tap. The 500ms
            // expiry inside the long-press callback caps the
            // suppression window.
            const onLongPress = vi.fn();
            const onNext = vi.fn();
            render(TapZone, {
                props: {
                    onPrev: () => {},
                    onNext,
                    onToggle: () => {},
                    onLongPress
                }
            });
            const right = screen.getByTestId('tap-zone-right');
            pointerDown(right, {
                pointerType: 'touch',
                clientX: 100,
                clientY: 100
            });
            // Long-press fires.
            vi.advanceTimersByTime(450);
            expect(onLongPress).toHaveBeenCalledOnce();
            // Drain the suppression-expiry window without a click —
            // simulates the user sliding off and releasing elsewhere.
            vi.advanceTimersByTime(600);
            // Fresh tap on the zone — must NOT be eaten.
            right.click();
            expect(onNext).toHaveBeenCalledOnce();
        });
    });
});
