/**
 * @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 MessageList component.
 *
 * Feature: ai-chat-template, Property 2: Message List Renders All Messages with Role Distinction
 * Validates: Requirements 2.1, 2.2, 2.4
 *
 * @module components/chat/__tests__/MessageList.property.test
 */

import * as React from 'react';
import * as fc from 'fast-check';
import { render, cleanup } from '@testing-library/react';
import { MessageList } from '../MessageList';
import type { Message } from '../../../types';

// Mock scrollIntoView which is not available in jsdom
beforeAll(() => {
    Element.prototype.scrollIntoView = jest.fn();
});

afterEach(() => {
    cleanup();
});

/**
 * Creates a message arbitrary with unique IDs using an index-based approach.
 * This ensures each message in an array has a unique ID.
 */
function createMessageWithIndex(index: number): fc.Arbitrary<Message> {
    // Use integer-based timestamp generation to avoid invalid date issues
    const minTimestamp = new Date('2020-01-01').getTime();
    const maxTimestamp = new Date('2030-12-31').getTime();

    return fc.record({
        id: fc.constant(`msg-${index}-${Date.now()}-${Math.random().toString(36).substring(2, 9)}`),
        role: fc.constantFrom('user', 'assistant') as fc.Arbitrary<'user' | 'assistant'>,
        content: fc.string({ minLength: 1, maxLength: 200 }),
        timestamp: fc
            .integer({ min: minTimestamp, max: maxTimestamp })
            .map((ts) => new Date(ts).toISOString()),
        isStreaming: fc.constant(false),
    });
}

/**
 * Arbitrary generator for arrays of messages with unique IDs and mixed roles.
 * Uses fc.tuple to ensure each message gets a unique index-based ID.
 */
const uniqueMessagesArbitrary = (minLength: number, maxLength: number): fc.Arbitrary<Message[]> => {
    return fc
        .integer({ min: minLength, max: maxLength })
        .chain((length) => {
            const messageArbitraries = Array.from({ length }, (_, i) => createMessageWithIndex(i));
            return fc.tuple(...messageArbitraries);
        })
        .map((messages) => messages as Message[]);
};

/**
 * Arbitrary generator for arrays of messages with mixed roles (at least one of each).
 */
const mixedRolesMessagesArbitrary = uniqueMessagesArbitrary(2, 10).filter((messages) => {
    const hasUser = messages.some((m) => m.role === 'user');
    const hasAssistant = messages.some((m) => m.role === 'assistant');
    return hasUser && hasAssistant;
});

describe('MessageList Property Tests', () => {
    /**
     * Feature: ai-chat-template, Property 2: Message List Renders All Messages with Role Distinction
     * Validates: Requirements 2.1, 2.2, 2.4
     *
     * Property: For any array of messages with mixed user/assistant roles,
     * the MessageList component SHALL render each message with a CSS class
     * or attribute that distinguishes the role, and each rendered message
     * SHALL include its timestamp.
     */
    describe('Property 2: Message List Renders All Messages with Role Distinction', () => {
        it('renders all messages in the array', () => {
            fc.assert(
                fc.property(uniqueMessagesArbitrary(1, 15), (messages) => {
                    const { container } = render(<MessageList messages={messages} isLoading={false} />);

                    // Each message should be rendered as a MessageItem
                    const messageItems = container.querySelectorAll('.message-item');
                    expect(messageItems.length).toBe(messages.length);
                }),
                { numRuns: 100 },
            );
        });

        it('each message has role-distinguishing CSS class', () => {
            fc.assert(
                fc.property(uniqueMessagesArbitrary(1, 10), (messages) => {
                    const { container } = render(<MessageList messages={messages} isLoading={false} />);

                    messages.forEach((message) => {
                        // Find the message item by data-message-id attribute
                        const messageElement = container.querySelector(`[data-message-id="${message.id}"]`);
                        expect(messageElement).not.toBeNull();

                        // Verify data-role attribute for role distinction (on message-item wrapper)
                        expect(messageElement?.getAttribute('data-role')).toBe(message.role);

                        // Verify role-specific CSS class exists on the inner message-bubble
                        const bubbleElement = messageElement?.querySelector('.message-bubble');
                        expect(bubbleElement?.classList.contains(`message-bubble--${message.role}`)).toBe(true);
                    });
                }),
                { numRuns: 100 },
            );
        });

        it('each message displays its timestamp', () => {
            fc.assert(
                fc.property(uniqueMessagesArbitrary(1, 10), (messages) => {
                    const { container } = render(<MessageList messages={messages} isLoading={false} />);

                    messages.forEach((message) => {
                        // Find the message item
                        const messageElement = container.querySelector(`[data-message-id="${message.id}"]`);
                        expect(messageElement).not.toBeNull();

                        // Find the timestamp element within the message-bubble
                        const timestampElement = messageElement?.querySelector('.message-bubble__timestamp');
                        expect(timestampElement).not.toBeNull();

                        // Verify the datetime attribute matches the message timestamp
                        expect(timestampElement?.getAttribute('datetime')).toBe(message.timestamp);

                        // Verify timestamp text is rendered (formatted time)
                        expect(timestampElement?.textContent).toBeTruthy();
                    });
                }),
                { numRuns: 100 },
            );
        });

        it('user and assistant messages have distinct styling', () => {
            fc.assert(
                fc.property(mixedRolesMessagesArbitrary, (messages) => {
                    const { container } = render(<MessageList messages={messages} isLoading={false} />);

                    // Find message-bubbles with role-specific classes
                    const userBubbles = container.querySelectorAll('.message-bubble--user');
                    const assistantBubbles = container.querySelectorAll('.message-bubble--assistant');

                    // Count should match the input
                    const expectedUserCount = messages.filter((m) => m.role === 'user').length;
                    const expectedAssistantCount = messages.filter((m) => m.role === 'assistant').length;

                    expect(userBubbles.length).toBe(expectedUserCount);
                    expect(assistantBubbles.length).toBe(expectedAssistantCount);

                    // Verify no message-bubble has both classes (mutually exclusive)
                    const allBubbles = container.querySelectorAll('.message-bubble');
                    allBubbles.forEach((bubble) => {
                        const isUser = bubble.classList.contains('message-bubble--user');
                        const isAssistant = bubble.classList.contains('message-bubble--assistant');
                        // XOR: exactly one should be true
                        expect(isUser !== isAssistant).toBe(true);
                    });
                }),
                { numRuns: 100 },
            );
        });

        it('messages are rendered in the correct order', () => {
            fc.assert(
                fc.property(uniqueMessagesArbitrary(1, 10), (messages) => {
                    const { container } = render(<MessageList messages={messages} isLoading={false} />);

                    const messageItems = container.querySelectorAll('.message-item');

                    // Verify order matches input array
                    messages.forEach((message, index) => {
                        const renderedId = messageItems[index]?.getAttribute('data-message-id');
                        expect(renderedId).toBe(message.id);
                    });
                }),
                { numRuns: 100 },
            );
        });

        it('empty message array shows welcome message', () => {
            fc.assert(
                fc.property(fc.constant([] as Message[]), (messages) => {
                    const { container } = render(<MessageList messages={messages} isLoading={false} />);

                    // Should show welcome message for empty state
                    const welcomeMessage = container.querySelector('.welcome-message');
                    expect(welcomeMessage).not.toBeNull();

                    // Should not render any message items
                    const messageItems = container.querySelectorAll('.message-item');
                    expect(messageItems.length).toBe(0);
                }),
                { numRuns: 10 },
            );
        });

        it('message content is preserved in rendering', () => {
            fc.assert(
                fc.property(uniqueMessagesArbitrary(1, 10), (messages) => {
                    const { container } = render(<MessageList messages={messages} isLoading={false} />);

                    messages.forEach((message) => {
                        const messageElement = container.querySelector(`[data-message-id="${message.id}"]`);
                        expect(messageElement).not.toBeNull();

                        // Content should be present in the rendered output
                        // Note: We check textContent includes the message content
                        // (it may also include timestamp text)
                        const contentElement = messageElement?.querySelector('.message-bubble__content');
                        expect(contentElement?.textContent).toContain(message.content);
                    });
                }),
                { numRuns: 100 },
            );
        });
    });
});
