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

// A controllable stand-in for SvelteKit's `navigating` store: `null` when
// idle, a Navigation-like object while a client-side navigation is pending.
// Built via vi.hoisted (a minimal writable, so no import is needed before
// the hoisted vi.mock factory runs).
const { navigating } = vi.hoisted(() => {
    let value: unknown = null;
    const subs = new Set<(v: unknown) => void>();
    return {
        navigating: {
            subscribe(fn: (v: unknown) => void) {
                subs.add(fn);
                fn(value);
                return () => subs.delete(fn);
            },
            set(v: unknown) {
                value = v;
                subs.forEach((fn) => fn(value));
            }
        }
    };
});

vi.mock('$app/stores', () => ({ navigating }));

import NavProgress from './NavProgress.svelte';

const nav = (path: string) => ({ from: null, to: { url: new URL(`http://x${path}`) } });

afterEach(() => {
    cleanup();
    navigating.set(null);
});

describe('NavProgress', () => {
    it('renders a decorative bar carrying the nav-progress testid', () => {
        render(NavProgress);
        const bar = screen.getByTestId('nav-progress');
        expect(bar).toBeTruthy();
        expect(bar.getAttribute('aria-hidden')).toBe('true');
    });

    it('is idle when not navigating', () => {
        render(NavProgress);
        const bar = screen.getByTestId('nav-progress');
        expect(bar.dataset.phase).toBe('idle');
        expect(bar.classList.contains('loading')).toBe(false);
    });

    it('enters the loading phase while a navigation is pending', () => {
        render(NavProgress);
        navigating.set(nav('/manga/1'));
        flushSync();
        const bar = screen.getByTestId('nav-progress');
        expect(bar.dataset.phase).toBe('loading');
        expect(bar.classList.contains('loading')).toBe(true);
    });

    it('snaps to done then returns to idle once navigation settles', () => {
        vi.useFakeTimers();
        try {
            render(NavProgress);
            navigating.set(nav('/manga/1'));
            flushSync();
            expect(screen.getByTestId('nav-progress').dataset.phase).toBe('loading');

            navigating.set(null);
            flushSync();
            // Completes to full and fades, rather than snapping away instantly.
            expect(screen.getByTestId('nav-progress').dataset.phase).toBe('done');

            vi.advanceTimersByTime(300);
            flushSync();
            expect(screen.getByTestId('nav-progress').dataset.phase).toBe('idle');
        } finally {
            vi.useRealTimers();
        }
    });

    it('restarts the bar when a new navigation supersedes a pending one', () => {
        render(NavProgress);
        navigating.set(nav('/manga/1'));
        flushSync();
        const first = screen.getByTestId('nav-progress');

        // A second navigation begins before the first settles — the store
        // stays truthy the whole time, just swapping objects.
        navigating.set(nav('/manga/2'));
        flushSync();
        const second = screen.getByTestId('nav-progress');

        expect(second.dataset.phase).toBe('loading');
        // {#key run} remounts the element so the trickle animation replays
        // rather than staying parked at its previous width.
        expect(second).not.toBe(first);
    });
});
