import { describe, it, expect, beforeEach, vi } from 'vitest';
import { waitFor } from '@testing-library/react';
import { renderHookWithClient } from '@/test/utils';

vi.mock('@/lib/api', () => ({
    api: { get: vi.fn(), post: vi.fn(), patch: vi.fn(), put: vi.fn(), delete: vi.fn() },
    ApiError: class extends Error {},
}));

const drive = vi.fn();
const destroyHandlers: Array<() => void> = [];

vi.mock('driver.js', () => ({
    driver: vi.fn((config: { onDestroyed?: () => void }) => {
        if (config.onDestroyed) destroyHandlers.push(config.onDestroyed);
        return { drive };
    }),
}));
vi.mock('driver.js/dist/driver.css', () => ({}));

import { driver } from 'driver.js';
import { api } from '@/lib/api';
import { useTour } from '@/hooks/useTour';
import { useTourProgress } from '@/hooks/useTourProgress';
import { appointmentsTour } from '@/lib/tour/tours/appointments';

const get = api.get as ReturnType<typeof vi.fn>;
const post = api.post as ReturnType<typeof vi.fn>;
const driverMock = driver as unknown as ReturnType<typeof vi.fn>;

function mountAppointmentsAnchors() {
    document.body.innerHTML = appointmentsTour.steps
        .map((s) => `<div data-tour="${s.anchor}"></div>`)
        .join('');
}

beforeEach(() => {
    vi.useFakeTimers({ shouldAdvanceTime: true });
    document.body.innerHTML = '';
    destroyHandlers.length = 0;
    get.mockReset();
    post.mockReset();
    drive.mockReset();
    driverMock.mockClear();
});

describe('useTourProgress', () => {
    it('نقشهٔ دیده‌شده‌ها را از سرور می‌خواند', async () => {
        get.mockResolvedValue({ data: { seen: { appointments: 2 } } });

        const { result } = renderHookWithClient(() => useTourProgress());

        await waitFor(() => expect(result.current.isReady).toBe(true));
        expect(result.current.isSeen('appointments', 2)).toBe(true);
        expect(result.current.isSeen('appointments', 3)).toBe(false);
        expect(result.current.isSeen('patients', 1)).toBe(false);
    });

    it('ثبت دیده‌شدن، کش را بدون درخواست دوباره به‌روز می‌کند', async () => {
        get.mockResolvedValue({ data: { seen: {} } });
        post.mockResolvedValue({ data: { tourId: 'appointments', version: 1 } });

        const { result } = renderHookWithClient(() => useTourProgress());
        await waitFor(() => expect(result.current.isReady).toBe(true));

        result.current.markSeen('appointments', 1);

        await waitFor(() => expect(result.current.isSeen('appointments', 1)).toBe(true));
        expect(post).toHaveBeenCalledWith('/api/v1/my/tours/appointments/seen', { version: 1 });
        expect(get).toHaveBeenCalledTimes(1);
    });
});

describe('useTour', () => {
    it('تور دیده‌نشده را بعد از آماده شدن صفحه خودکار اجرا می‌کند', async () => {
        get.mockResolvedValue({ data: { seen: {} } });
        mountAppointmentsAnchors();

        renderHookWithClient(() => useTour('appointments', { ready: true }));

        await waitFor(() => expect(driverMock).toHaveBeenCalled());
        await vi.advanceTimersByTimeAsync(400);

        expect(drive).toHaveBeenCalledTimes(1);
        expect(driverMock.mock.calls[0][0].steps).toHaveLength(appointmentsTour.steps.length);
    });

    it('شمارندهٔ استپ‌ها با ارقام فارسی است', async () => {
        get.mockResolvedValue({ data: { seen: {} } });
        mountAppointmentsAnchors();

        renderHookWithClient(() => useTour('appointments', { ready: true }));
        await vi.advanceTimersByTimeAsync(400);
        await waitFor(() => expect(drive).toHaveBeenCalled());

        const steps = driverMock.mock.calls[0][0].steps;
        expect(steps[0].popover.progressText).toBe('۱ از ۸');
        expect(steps[7].popover.progressText).toBe('۸ از ۸');
    });

    it('رندرهای پیاپی قبل از شلیک تایمر، اجرای خودکار را لغو نمی‌کنند', async () => {
        get.mockResolvedValue({ data: { seen: {} } });
        mountAppointmentsAnchors();

        const { rerender } = renderHookWithClient(() => useTour('appointments', { ready: true }));

        // هر رندر تابع start تازه‌ای می‌سازد؛ اگر وابستگیِ effect باشد، cleanup
        // تایمر را پاک می‌کند و تور هرگز اجرا نمی‌شود.
        for (let i = 0; i < 5; i++) rerender();

        await vi.advanceTimersByTimeAsync(400);
        await waitFor(() => expect(drive).toHaveBeenCalledTimes(1));
    });

    it('استپِ بدون المان را حذف می‌کند', async () => {
        get.mockResolvedValue({ data: { seen: {} } });
        document.body.innerHTML = '<div data-tour="appointments-stats"></div>';

        renderHookWithClient(() => useTour('appointments', { ready: true }));

        await vi.advanceTimersByTimeAsync(400);
        await waitFor(() => expect(drive).toHaveBeenCalled());

        expect(driverMock.mock.calls[0][0].steps).toHaveLength(1);
    });

    it('تور دیده‌شده خودکار اجرا نمی‌شود', async () => {
        get.mockResolvedValue({ data: { seen: { appointments: appointmentsTour.version } } });
        mountAppointmentsAnchors();

        const { result } = renderHookWithClient(() => useTour('appointments', { ready: true }));

        await vi.advanceTimersByTimeAsync(400);
        expect(drive).not.toHaveBeenCalled();

        // ولی دکمهٔ راهنما همچنان کار می‌کند
        result.current.start();
        expect(drive).toHaveBeenCalledTimes(1);
    });

    it('تا وقتی وضعیت از سرور نیامده اجرا نمی‌شود', async () => {
        get.mockRejectedValue(new Error('network down'));
        mountAppointmentsAnchors();

        renderHookWithClient(() => useTour('appointments', { ready: true }));

        await vi.advanceTimersByTimeAsync(400);
        expect(drive).not.toHaveBeenCalled();
    });

    it('صفحه‌ای که هنوز آماده نیست تور نمی‌گیرد', async () => {
        get.mockResolvedValue({ data: { seen: {} } });
        mountAppointmentsAnchors();

        renderHookWithClient(() => useTour('appointments', { ready: false }));

        await vi.advanceTimersByTimeAsync(400);
        expect(drive).not.toHaveBeenCalled();
    });

    it('شناسهٔ ناشناس نه دکمه دارد نه اجرا', async () => {
        get.mockResolvedValue({ data: { seen: {} } });

        const { result } = renderHookWithClient(() => useTour('nope', { ready: true }));

        await vi.advanceTimersByTimeAsync(400);
        expect(result.current.available).toBe(false);

        result.current.start();
        expect(drive).not.toHaveBeenCalled();
    });

    it('وقتی هیچ المانی روی صفحه نیست، اجرا نمی‌شود', async () => {
        get.mockResolvedValue({ data: { seen: {} } });

        const { result } = renderHookWithClient(() => useTour('appointments', { ready: true }));
        await waitFor(() => expect(result.current.available).toBe(true));

        result.current.start();
        expect(drive).not.toHaveBeenCalled();
    });

    it('بستن تور، دیده‌شدن را ثبت می‌کند', async () => {
        get.mockResolvedValue({ data: { seen: {} } });
        post.mockResolvedValue({ data: { tourId: 'appointments', version: appointmentsTour.version } });
        mountAppointmentsAnchors();

        renderHookWithClient(() => useTour('appointments', { ready: true }));
        await vi.advanceTimersByTimeAsync(400);
        await waitFor(() => expect(destroyHandlers).toHaveLength(1));

        destroyHandlers[0]();

        await waitFor(() =>
            expect(post).toHaveBeenCalledWith('/api/v1/my/tours/appointments/seen', {
                version: appointmentsTour.version,
            }),
        );
    });
});
