/**
 * @jest-environment jsdom
 */

/*
 * Runtime Web – Unified Serverless Framework for Full-Stack TypeScript Applications
 * Documentation: https://runtimeweb.com/docs/getting-started
 *
 * @template: ai-chat
 */

/**
 * Property-based tests for useChat hook error handling.
 *
 * Feature: ai-chat-template
 * - Property 11: Error Display on Request Failure
 * - Property 12: Error State Cleared on Success
 * - Property 13: Message Preserved on Error for Retry
 *
 * Validates: Requirements 6.1, 6.3, 6.5, 6.6
 *
 * @module hooks/__tests__/useChat.property.test
 */

import * as fc from 'fast-check';
import { renderHook, act } from '@testing-library/react';
import { useChat } from '../useChat';
import { chatApiService } from '../../services';
import type { ChatResponse } from '../../types/chat.types';
import type { BehaviorMode } from '../../types/ui.types';

// Mock the chatApiService module
jest.mock('../../services', () => ({
    chatApiService: {
        sendMessage: jest.fn(),
        sendMessageStream: jest.fn(),
    },
}));

const mockedChatApiService = chatApiService as jest.Mocked<typeof chatApiService>;


/**
 * Arbitrary generators for test data.
 */
const arbitraries = {
    /** Valid session ID matching the pattern session-{timestamp}-{random} */
    sessionId: fc.string({ minLength: 1, maxLength: 50 }).map((s) => `session-${s.replace(/[^a-z0-9]/gi, 'x')}`),

    /** Non-empty message content */
    messageContent: fc.string({ minLength: 1, maxLength: 500 }).filter((s) => s.trim().length > 0),

    /** Valid behavior modes */
    behaviorMode: fc.constantFrom<BehaviorMode>('precise', 'balanced', 'creative'),

    /** Network error messages */
    networkError: fc.constantFrom(
        'Network request failed',
        'Failed to fetch',
        'net::ERR_CONNECTION_REFUSED',
        'net::ERR_INTERNET_DISCONNECTED',
        'TypeError: Failed to fetch',
    ),

    /** HTTP error status codes */
    httpErrorStatus: fc.constantFrom(400, 401, 403, 404, 500, 502, 503),

    /** HTTP error messages */
    httpErrorMessage: fc.constantFrom(
        'Bad Request',
        'Unauthorized',
        'Forbidden',
        'Not Found',
        'Internal Server Error',
        'Bad Gateway',
        'Service Unavailable',
    ),
};

/**
 * Creates a mock successful response.
 */
function createSuccessResponse(sessionId: string): ChatResponse {
    return {
        sessionId,
        response: 'This is a successful AI response.',
        timestamp: new Date().toISOString(),
    };
}

/**
 * Creates a mock async generator that throws an error.
 */
async function* createFailingStreamGenerator(): AsyncGenerator<never> {
    throw new Error('Streaming not available');
}

describe('useChat Property Tests - Error Handling', () => {
    beforeEach(() => {
        jest.clearAllMocks();
    });


    /**
     * Feature: ai-chat-template, Property 11: Error Display on Request Failure
     * Validates: Requirements 6.1, 6.5
     *
     * Property: For any failed request (network error or HTTP 4xx/5xx),
     * the Chat UI SHALL display an error message component, and the error
     * message SHALL be user-friendly (not raw error codes or stack traces).
     */
    describe('Property 11: Error Display on Request Failure', () => {
        it('sets error state when network request fails', async () => {
            await fc.assert(
                fc.asyncProperty(
                    arbitraries.sessionId,
                    arbitraries.messageContent,
                    arbitraries.behaviorMode,
                    arbitraries.networkError,
                    async (sessionId, message, behavior, errorMessage) => {
                        // Setup: Both streaming and non-streaming fail with network error
                        mockedChatApiService.sendMessageStream.mockImplementation(createFailingStreamGenerator);
                        mockedChatApiService.sendMessage.mockRejectedValue(new Error(errorMessage));

                        const { result } = renderHook(() => useChat(sessionId));

                        // Initially no error
                        expect(result.current.error).toBeNull();

                        // Send message that will fail
                        await act(async () => {
                            await result.current.sendMessage(message, behavior);
                        });

                        // Error state should be set
                        expect(result.current.error).not.toBeNull();
                        expect(result.current.error).toBeInstanceOf(Error);
                        expect(result.current.error?.message).toBeTruthy();
                    },
                ),
                { numRuns: 100 },
            );
        });

        it('sets error state when HTTP request returns error status', async () => {
            await fc.assert(
                fc.asyncProperty(
                    arbitraries.sessionId,
                    arbitraries.messageContent,
                    arbitraries.behaviorMode,
                    arbitraries.httpErrorStatus,
                    arbitraries.httpErrorMessage,
                    async (sessionId, message, behavior, status, errorMsg) => {
                        // Setup: Both streaming and non-streaming fail with HTTP error
                        mockedChatApiService.sendMessageStream.mockImplementation(createFailingStreamGenerator);
                        mockedChatApiService.sendMessage.mockRejectedValue(
                            new Error(`Request failed: ${status} - ${errorMsg}`),
                        );

                        const { result } = renderHook(() => useChat(sessionId));

                        await act(async () => {
                            await result.current.sendMessage(message, behavior);
                        });

                        // Error state should be set
                        expect(result.current.error).not.toBeNull();
                        expect(result.current.error?.message).toBeTruthy();
                    },
                ),
                { numRuns: 100 },
            );
        });

        it('error message is user-friendly (not raw stack traces)', async () => {
            await fc.assert(
                fc.asyncProperty(
                    arbitraries.sessionId,
                    arbitraries.messageContent,
                    arbitraries.behaviorMode,
                    async (sessionId, message, behavior) => {
                        // Setup: Fail with a technical error
                        mockedChatApiService.sendMessageStream.mockImplementation(createFailingStreamGenerator);
                        mockedChatApiService.sendMessage.mockRejectedValue(new Error('Connection timeout'));

                        const { result } = renderHook(() => useChat(sessionId));

                        await act(async () => {
                            await result.current.sendMessage(message, behavior);
                        });

                        // Error should exist and be an Error instance
                        expect(result.current.error).toBeInstanceOf(Error);

                        // Error message should not contain stack trace indicators
                        const errMessage = result.current.error?.message || '';
                        expect(errMessage).not.toMatch(/at\s+\w+\s+\(/); // No "at FunctionName ("
                        expect(errMessage).not.toMatch(/\.ts:\d+:\d+/); // No file:line:col
                        expect(errMessage).not.toMatch(/\.js:\d+:\d+/); // No file:line:col
                    },
                ),
                { numRuns: 100 },
            );
        });

        it('isLoading is false after error occurs', async () => {
            await fc.assert(
                fc.asyncProperty(
                    arbitraries.sessionId,
                    arbitraries.messageContent,
                    arbitraries.behaviorMode,
                    async (sessionId, message, behavior) => {
                        mockedChatApiService.sendMessageStream.mockImplementation(createFailingStreamGenerator);
                        mockedChatApiService.sendMessage.mockRejectedValue(new Error('Request failed'));

                        const { result } = renderHook(() => useChat(sessionId));

                        await act(async () => {
                            await result.current.sendMessage(message, behavior);
                        });

                        // Loading should be false after error
                        expect(result.current.isLoading).toBe(false);
                    },
                ),
                { numRuns: 100 },
            );
        });
    });


    /**
     * Feature: ai-chat-template, Property 12: Error State Cleared on Success
     * Validates: Requirements 6.6
     *
     * Property: For any error state followed by a successful request,
     * the Chat UI SHALL clear the error state and the error message
     * component SHALL no longer be rendered.
     */
    describe('Property 12: Error State Cleared on Success', () => {
        it('error state is cleared after successful request', async () => {
            await fc.assert(
                fc.asyncProperty(
                    arbitraries.sessionId,
                    arbitraries.messageContent,
                    arbitraries.messageContent,
                    arbitraries.behaviorMode,
                    async (sessionId, failingMessage, successMessage, behavior) => {
                        const { result } = renderHook(() => useChat(sessionId));

                        // First request fails
                        mockedChatApiService.sendMessageStream.mockImplementation(createFailingStreamGenerator);
                        mockedChatApiService.sendMessage.mockRejectedValue(new Error('First request failed'));

                        await act(async () => {
                            await result.current.sendMessage(failingMessage, behavior);
                        });

                        // Verify error state is set
                        expect(result.current.error).not.toBeNull();

                        // Second request succeeds
                        mockedChatApiService.sendMessage.mockResolvedValue(createSuccessResponse(sessionId));

                        await act(async () => {
                            await result.current.sendMessage(successMessage, behavior);
                        });

                        // Error state should be cleared
                        expect(result.current.error).toBeNull();
                    },
                ),
                { numRuns: 100 },
            );
        });

        it('error state is cleared when clearError is called', async () => {
            await fc.assert(
                fc.asyncProperty(
                    arbitraries.sessionId,
                    arbitraries.messageContent,
                    arbitraries.behaviorMode,
                    async (sessionId, message, behavior) => {
                        const { result } = renderHook(() => useChat(sessionId));

                        // Request fails
                        mockedChatApiService.sendMessageStream.mockImplementation(createFailingStreamGenerator);
                        mockedChatApiService.sendMessage.mockRejectedValue(new Error('Request failed'));

                        await act(async () => {
                            await result.current.sendMessage(message, behavior);
                        });

                        // Verify error state is set
                        expect(result.current.error).not.toBeNull();

                        // Clear error
                        act(() => {
                            result.current.clearError();
                        });

                        // Error state should be cleared
                        expect(result.current.error).toBeNull();
                    },
                ),
                { numRuns: 100 },
            );
        });

        it('multiple consecutive errors followed by success clears error', async () => {
            await fc.assert(
                fc.asyncProperty(
                    arbitraries.sessionId,
                    fc.array(arbitraries.messageContent, { minLength: 1, maxLength: 5 }),
                    arbitraries.messageContent,
                    arbitraries.behaviorMode,
                    async (sessionId, failingMessages, successMessage, behavior) => {
                        const { result } = renderHook(() => useChat(sessionId));

                        // Multiple failing requests
                        mockedChatApiService.sendMessageStream.mockImplementation(createFailingStreamGenerator);
                        mockedChatApiService.sendMessage.mockRejectedValue(new Error('Request failed'));

                        for (const msg of failingMessages) {
                            await act(async () => {
                                await result.current.sendMessage(msg, behavior);
                            });
                            expect(result.current.error).not.toBeNull();
                        }

                        // Successful request
                        mockedChatApiService.sendMessage.mockResolvedValue(createSuccessResponse(sessionId));

                        await act(async () => {
                            await result.current.sendMessage(successMessage, behavior);
                        });

                        // Error should be cleared
                        expect(result.current.error).toBeNull();
                    },
                ),
                { numRuns: 50 },
            );
        });
    });


    /**
     * Feature: ai-chat-template, Property 13: Message Preserved on Error for Retry
     * Validates: Requirements 6.3
     *
     * Property: For any failed request, the user's original message content
     * SHALL be preserved in state, and invoking the retry action SHALL
     * resend the same message content.
     */
    describe('Property 13: Message Preserved on Error for Retry', () => {
        it('lastRequest is preserved after failed request', async () => {
            await fc.assert(
                fc.asyncProperty(
                    arbitraries.sessionId,
                    arbitraries.messageContent,
                    arbitraries.behaviorMode,
                    async (sessionId, message, behavior) => {
                        mockedChatApiService.sendMessageStream.mockImplementation(createFailingStreamGenerator);
                        mockedChatApiService.sendMessage.mockRejectedValue(new Error('Request failed'));

                        const { result } = renderHook(() => useChat(sessionId));

                        await act(async () => {
                            await result.current.sendMessage(message, behavior);
                        });

                        // Verify error occurred
                        expect(result.current.error).not.toBeNull();

                        // Clear mocks to track retry call
                        mockedChatApiService.sendMessage.mockClear();
                        mockedChatApiService.sendMessageStream.mockClear();

                        // Setup for retry (still fails, but we want to verify the message)
                        mockedChatApiService.sendMessageStream.mockImplementation(createFailingStreamGenerator);
                        mockedChatApiService.sendMessage.mockRejectedValue(new Error('Retry failed'));

                        // Retry should use the same message
                        await act(async () => {
                            await result.current.retryLastMessage();
                        });

                        // Verify sendMessageStream was called (streaming is tried first)
                        expect(mockedChatApiService.sendMessageStream).toHaveBeenCalledWith(
                            expect.objectContaining({
                                sessionId,
                                message,
                                behavior,
                            }),
                        );
                    },
                ),
                { numRuns: 100 },
            );
        });

        it('retry sends exact same message content and behavior', async () => {
            await fc.assert(
                fc.asyncProperty(
                    arbitraries.sessionId,
                    arbitraries.messageContent,
                    arbitraries.behaviorMode,
                    async (sessionId, message, behavior) => {
                        mockedChatApiService.sendMessageStream.mockImplementation(createFailingStreamGenerator);
                        mockedChatApiService.sendMessage.mockRejectedValue(new Error('Request failed'));

                        const { result } = renderHook(() => useChat(sessionId));

                        // First attempt fails
                        await act(async () => {
                            await result.current.sendMessage(message, behavior);
                        });

                        // Setup successful retry
                        mockedChatApiService.sendMessage.mockResolvedValue(createSuccessResponse(sessionId));

                        // Retry
                        await act(async () => {
                            await result.current.retryLastMessage();
                        });

                        // Verify the retry used exact same parameters
                        // Note: sendMessageStream is called first (streaming attempt)
                        const streamCalls = mockedChatApiService.sendMessageStream.mock.calls;
                        const lastStreamCall = streamCalls[streamCalls.length - 1];

                        expect(lastStreamCall[0]).toEqual({
                            sessionId,
                            message,
                            behavior,
                        });
                    },
                ),
                { numRuns: 100 },
            );
        });

        it('successful retry clears error and adds messages', async () => {
            await fc.assert(
                fc.asyncProperty(
                    arbitraries.sessionId,
                    arbitraries.messageContent,
                    arbitraries.behaviorMode,
                    async (sessionId, message, behavior) => {
                        mockedChatApiService.sendMessageStream.mockImplementation(createFailingStreamGenerator);
                        mockedChatApiService.sendMessage.mockRejectedValue(new Error('Request failed'));

                        const { result } = renderHook(() => useChat(sessionId));

                        // First attempt fails
                        await act(async () => {
                            await result.current.sendMessage(message, behavior);
                        });

                        expect(result.current.error).not.toBeNull();
                        const messagesAfterError = result.current.messages.length;

                        // Setup successful retry
                        mockedChatApiService.sendMessage.mockResolvedValue(createSuccessResponse(sessionId));

                        // Retry succeeds
                        await act(async () => {
                            await result.current.retryLastMessage();
                        });

                        // Error should be cleared
                        expect(result.current.error).toBeNull();

                        // Messages should include user message and assistant response
                        expect(result.current.messages.length).toBeGreaterThan(messagesAfterError);

                        // Find the user message with our content
                        const userMessages = result.current.messages.filter(
                            (m) => m.role === 'user' && m.content === message,
                        );
                        expect(userMessages.length).toBeGreaterThanOrEqual(1);
                    },
                ),
                { numRuns: 100 },
            );
        });

        it('retryLastMessage does nothing when no previous request exists', async () => {
            await fc.assert(
                fc.asyncProperty(arbitraries.sessionId, async (sessionId) => {
                    const { result } = renderHook(() => useChat(sessionId));

                    // No previous request, retry should do nothing
                    await act(async () => {
                        await result.current.retryLastMessage();
                    });

                    // No error, no messages, no loading
                    expect(result.current.error).toBeNull();
                    expect(result.current.messages).toHaveLength(0);
                    expect(result.current.isLoading).toBe(false);

                    // API should not have been called
                    expect(mockedChatApiService.sendMessage).not.toHaveBeenCalled();
                    expect(mockedChatApiService.sendMessageStream).not.toHaveBeenCalled();
                }),
                { numRuns: 100 },
            );
        });

        it('user message is removed from messages array on error', async () => {
            await fc.assert(
                fc.asyncProperty(
                    arbitraries.sessionId,
                    arbitraries.messageContent,
                    arbitraries.behaviorMode,
                    async (sessionId, message, behavior) => {
                        mockedChatApiService.sendMessageStream.mockImplementation(createFailingStreamGenerator);
                        mockedChatApiService.sendMessage.mockRejectedValue(new Error('Request failed'));

                        const { result } = renderHook(() => useChat(sessionId));

                        // Initial state: no messages
                        expect(result.current.messages).toHaveLength(0);

                        // Send message that fails
                        await act(async () => {
                            await result.current.sendMessage(message, behavior);
                        });

                        // After error, user message should be removed
                        // (so user can retry without duplicate messages)
                        const userMessagesWithContent = result.current.messages.filter(
                            (m) => m.role === 'user' && m.content === message,
                        );
                        expect(userMessagesWithContent).toHaveLength(0);
                    },
                ),
                { numRuns: 100 },
            );
        });
    });


    /**
     * Additional edge case tests for error handling robustness.
     */
    describe('Error Handling Edge Cases', () => {
        it('handles non-Error thrown values gracefully', async () => {
            await fc.assert(
                fc.asyncProperty(
                    arbitraries.sessionId,
                    arbitraries.messageContent,
                    arbitraries.behaviorMode,
                    fc.oneof(fc.string(), fc.integer(), fc.constant(null), fc.constant(undefined)),
                    async (sessionId, message, behavior, thrownValue) => {
                        mockedChatApiService.sendMessageStream.mockImplementation(createFailingStreamGenerator);
                        mockedChatApiService.sendMessage.mockRejectedValue(thrownValue);

                        const { result } = renderHook(() => useChat(sessionId));

                        await act(async () => {
                            await result.current.sendMessage(message, behavior);
                        });

                        // Should still set an Error object
                        expect(result.current.error).toBeInstanceOf(Error);
                        expect(result.current.isLoading).toBe(false);
                    },
                ),
                { numRuns: 50 },
            );
        });

        it('streaming failure falls back to non-streaming', async () => {
            await fc.assert(
                fc.asyncProperty(
                    arbitraries.sessionId,
                    arbitraries.messageContent,
                    arbitraries.behaviorMode,
                    async (sessionId, message, behavior) => {
                        // Streaming fails, non-streaming succeeds
                        mockedChatApiService.sendMessageStream.mockImplementation(createFailingStreamGenerator);
                        mockedChatApiService.sendMessage.mockResolvedValue(createSuccessResponse(sessionId));

                        const { result } = renderHook(() => useChat(sessionId));

                        await act(async () => {
                            await result.current.sendMessage(message, behavior);
                        });

                        // Should succeed via fallback
                        expect(result.current.error).toBeNull();
                        expect(result.current.messages.length).toBeGreaterThan(0);

                        // Should have both user and assistant messages
                        const roles = result.current.messages.map((m) => m.role);
                        expect(roles).toContain('user');
                        expect(roles).toContain('assistant');
                    },
                ),
                { numRuns: 100 },
            );
        });
    });
});
