import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import { renderHook, act, waitFor } from '@testing-library/react';
import { useBuilderChat } from '../useBuilderChat';
import axios from 'axios';

// Capture the options passed into useBuilderPusher so we can invoke its
// event handlers directly, bypassing the real Pusher/Echo machinery.
// eslint-disable-next-line @typescript-eslint/no-explicit-any
let capturedPusherOpts: any = null;

vi.mock('@/hooks/useBuilderPusher', () => ({
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    useBuilderPusher: (opts: any) => {
        capturedPusherOpts = opts;
        return { subscribe: vi.fn(), unsubscribe: vi.fn() };
    },
}));

// eslint-disable-next-line @typescript-eslint/no-explicit-any
let capturedReverbOpts: any = null;

vi.mock('@/hooks/useBuilderReverb', () => ({
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    useBuilderReverb: (opts: any) => {
        capturedReverbOpts = opts;

        return { subscribe: vi.fn(), unsubscribe: vi.fn() };
    },
}));

vi.mock('axios');

// Global route mock
(global as unknown as { route: (name: string, params?: Record<string, unknown>) => string }).route = (name: string, params?: Record<string, unknown>) => {
    if (name === 'builder.stream' && params?.project) {
        return `/builder/${params.project}/stream`;
    }
    if (name === 'builder.start' && params?.project) {
        return `/builder/${params.project}/start`;
    }
    if (name === 'builder.complete' && params?.project) {
        return `/builder/${params.project}/complete`;
    }
    return `/${name}`;
};

// Mock pusherConfig for tests (no `provider` key -> pusher path is active)
const mockPusherConfig = {
    key: 'test-key',
    cluster: 'mt1',
    forceTLS: true,
    channelPrefix: 'private-project.',
};

describe('useBuilderChat pusher event handlers', () => {
    const mockedAxios = vi.mocked(axios);

    beforeEach(() => {
        capturedPusherOpts = null;
        capturedReverbOpts = null;

        vi.mocked(axios.post).mockReset();
        vi.mocked(axios.get).mockReset();

        vi.mocked(localStorage.getItem).mockReturnValue(null);
        vi.mocked(localStorage.setItem).mockReset();
        vi.mocked(localStorage.removeItem).mockReset();

        // Mock CSRF token
        const metaElement = document.createElement('meta');
        metaElement.setAttribute('name', 'csrf-token');
        metaElement.setAttribute('content', 'test-csrf-token');
        document.head.appendChild(metaElement);
    });

    afterEach(() => {
        vi.restoreAllMocks();
        document.head.innerHTML = '';
    });

    it('handleSummarizationComplete adds a compaction activity message', () => {
        const { result } = renderHook(() => useBuilderChat(1, { pusherConfig: mockPusherConfig }));

        expect(capturedPusherOpts).not.toBeNull();

        act(() => {
            capturedPusherOpts.onSummarizationComplete({ turns_compacted: 8, reduction_percent: 42.6 });
        });

        const activityMessage = result.current.messages.find(
            m => m.type === 'activity' && m.activityType === 'compacting',
        );

        expect(activityMessage).toBeDefined();
        expect(activityMessage?.content).toBe('Compressed 8 turns (43% reduction)');
    });

    it('handleWsReconnected is a no-op when not running', async () => {
        const { result } = renderHook(() => useBuilderChat(1, { pusherConfig: mockPusherConfig }));

        expect(result.current.progress.status).toBe('idle');

        await act(async () => {
            await capturedPusherOpts.onReconnected();
        });

        expect(mockedAxios.get).not.toHaveBeenCalled();
        expect(result.current.progress.status).toBe('idle');
    });

    it('handleWsReconnected polls and marks completed when running', async () => {
        mockedAxios.post.mockResolvedValueOnce({ data: { session_id: 'sess-1', builder_id: 1 } });

        const { result } = renderHook(() => useBuilderChat(1, { pusherConfig: mockPusherConfig }));

        await act(async () => {
            await result.current.sendMessage('build');
        });

        act(() => {
            capturedPusherOpts.onStatus({ status: 'running', message: '' });
        });

        expect(result.current.progress.status).toBe('running');

        mockedAxios.get.mockResolvedValueOnce({ data: { status: 'completed' } });

        await act(async () => {
            await capturedPusherOpts.onReconnected();
        });

        expect(mockedAxios.get).toHaveBeenCalledWith(expect.stringContaining('/status?quick=1'));

        await waitFor(() => {
            expect(result.current.progress.status).toBe('completed');
        });
        expect(result.current.progress.hasFileChanges).toBe(true);
    });

    it('handleWsReconnected marks failed when the poll reports failed', async () => {
        mockedAxios.post.mockResolvedValueOnce({ data: { session_id: 'sess-1', builder_id: 1 } });

        const { result } = renderHook(() => useBuilderChat(1, { pusherConfig: mockPusherConfig }));

        await act(async () => {
            await result.current.sendMessage('build');
        });

        act(() => {
            capturedPusherOpts.onStatus({ status: 'running', message: '' });
        });

        mockedAxios.get.mockResolvedValueOnce({ data: { status: 'failed' } });

        await act(async () => {
            await capturedPusherOpts.onReconnected();
        });

        await waitFor(() => {
            expect(result.current.progress.status).toBe('failed');
        });
    });

    it('handleWsReconnected silently ignores a failed poll request', async () => {
        mockedAxios.post.mockResolvedValueOnce({ data: { session_id: 'sess-1', builder_id: 1 } });

        const { result } = renderHook(() => useBuilderChat(1, { pusherConfig: mockPusherConfig }));

        await act(async () => {
            await result.current.sendMessage('build');
        });

        act(() => {
            capturedPusherOpts.onStatus({ status: 'running', message: '' });
        });

        mockedAxios.get.mockRejectedValueOnce(new Error('network error'));

        await act(async () => {
            await capturedPusherOpts.onReconnected();
        });

        expect(result.current.progress.status).toBe('running');
    });

    // -----------------------------------------------------------------------
    // A rejected private subscription must reach the user
    //
    // The chat enters "running" the moment POST /start succeeds — that state is
    // driven by progress.status, not the socket — so a dead channel looks like a
    // working build with zero thinking bubbles, actions or messages.
    // -----------------------------------------------------------------------
    it('forwards a rejected private subscription from both broadcasters', () => {
        const onSubscriptionError = vi.fn();

        renderHook(() => useBuilderChat(1, { pusherConfig: mockPusherConfig, onSubscriptionError }));

        expect(capturedPusherOpts.onSubscriptionError).toBeTypeOf('function');
        expect(capturedReverbOpts.onSubscriptionError).toBeTypeOf('function');

        act(() => {
            capturedPusherOpts.onSubscriptionError(
                'Could not connect to live build updates. You may need to sign in again.',
            );
        });

        expect(onSubscriptionError).toHaveBeenCalledWith(
            'Could not connect to live build updates. You may need to sign in again.',
        );
    });

    /**
     * The copy is already user-facing and translated by the consumer — running
     * it through the raw-provider-error sanitizer would match no pattern and
     * collapse it to "Something went wrong. Please try again."
     */
    it('does not sanitize the subscription error message', () => {
        const onSubscriptionError = vi.fn();
        const errorSanitizer = vi.fn(() => 'Something went wrong. Please try again.');

        renderHook(() =>
            useBuilderChat(1, { pusherConfig: mockPusherConfig, onSubscriptionError, errorSanitizer })
        );

        act(() => {
            capturedPusherOpts.onSubscriptionError(
                'Could not connect to live build updates. The build is still running and will finish in the background.',
            );
        });

        expect(errorSanitizer).not.toHaveBeenCalled();
        expect(onSubscriptionError).toHaveBeenCalledWith(
            'Could not connect to live build updates. The build is still running and will finish in the background.',
        );
    });

    // -----------------------------------------------------------------------
    // J2 — the version-drift 503 carries a server-translated, actionable message
    //
    // 503 means the build service is too old to deliver live progress; it
    // persists until an admin updates it, so "Please try again" is actively
    // wrong. Like the 409 conflict, the server message must pass through.
    // -----------------------------------------------------------------------
    it('passes a 503 server message through untouched', async () => {
        const serverMessage =
            'The build service is out of date and cannot deliver live build progress. Please contact the site administrator.';
        const errorSanitizer = vi.fn(() => 'Something went wrong. Please try again.');
        const onError = vi.fn();

        mockedAxios.post.mockRejectedValueOnce({
            isAxiosError: true,
            response: { status: 503, data: { error: serverMessage } },
            message: 'Request failed with status code 503',
        });
        vi.mocked(axios.isAxiosError).mockReturnValue(true);

        const { result } = renderHook(() =>
            useBuilderChat(1, { pusherConfig: mockPusherConfig, errorSanitizer, onError })
        );

        await act(async () => {
            await result.current.sendMessage('build');
        });

        expect(errorSanitizer).not.toHaveBeenCalled();
        expect(onError).toHaveBeenCalledWith(serverMessage);
        expect(result.current.progress.error).toBe(serverMessage);
    });
});
