/**
 * @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 MessageBubble component.
 *
 * **Property 2: Message Role-Based Rendering**
 * **Validates: Requirements 2.1, 2.3, 2.5, 2.8**
 *
 * Property: For any message with role 'user' or 'assistant', the MessageBubble
 * component SHALL render with a CSS class containing the role name, SHALL include
 * a timestamp element, and assistant messages SHALL include an avatar element
 * while user messages SHALL NOT.
 *
 * @module components/chat/__tests__/MessageBubble.property.test
 */

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

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

/**
 * Arbitrary generator for valid ISO timestamps.
 * Uses integer-based timestamp generation to avoid invalid date issues.
 */
const timestampArbitrary = (): fc.Arbitrary<string> => {
    const minTimestamp = new Date('2020-01-01').getTime();
    const maxTimestamp = new Date('2030-12-31').getTime();
    return fc
        .integer({ min: minTimestamp, max: maxTimestamp })
        .map((ts) => new Date(ts).toISOString());
};

/**
 * Arbitrary generator for user messages.
 */
const userMessageArbitrary = (): fc.Arbitrary<Message> => {
    return fc.record({
        id: fc.uuid(),
        role: fc.constant('user' as const),
        content: fc.string({ minLength: 1, maxLength: 500 }),
        timestamp: timestampArbitrary(),
        isStreaming: fc.constant(false),
    });
};

/**
 * Arbitrary generator for assistant messages.
 */
const assistantMessageArbitrary = (): fc.Arbitrary<Message> => {
    return fc.record({
        id: fc.uuid(),
        role: fc.constant('assistant' as const),
        content: fc.string({ minLength: 1, maxLength: 500 }),
        timestamp: timestampArbitrary(),
        isStreaming: fc.boolean(),
    });
};

/**
 * Arbitrary generator for any valid message (user or assistant).
 */
const anyMessageArbitrary = (): fc.Arbitrary<Message> => {
    return fc.oneof(userMessageArbitrary(), assistantMessageArbitrary());
};

describe('MessageBubble Property Tests', () => {
    /**
     * **Property 2: Message Role-Based Rendering**
     * **Validates: Requirements 2.1, 2.3, 2.5, 2.8**
     */
    describe('Property 2: Message Role-Based Rendering', () => {
        /**
         * Validates: Requirement 2.1
         * For any message with role 'user' or 'assistant', the component SHALL
         * render with a CSS class containing the role name.
         */
        it('renders with CSS class containing the role name for any message', () => {
            fc.assert(
                fc.property(anyMessageArbitrary(), (message) => {
                    const { container, unmount } = render(<MessageBubble message={message} />);

                    const bubbleElement = container.querySelector('.message-bubble');
                    expect(bubbleElement).not.toBeNull();

                    // Verify role-specific CSS class exists
                    expect(bubbleElement?.classList.contains(`message-bubble--${message.role}`)).toBe(true);

                    // Verify data-role attribute matches
                    expect(bubbleElement?.getAttribute('data-role')).toBe(message.role);

                    unmount();
                }),
                { numRuns: 100 },
            );
        });

        /**
         * Validates: Requirement 2.3
         * For any message, the component SHALL include a timestamp element.
         */
        it('includes a timestamp element for any message', () => {
            fc.assert(
                fc.property(anyMessageArbitrary(), (message) => {
                    const { unmount } = render(<MessageBubble message={message} />);

                    const timestampElement = screen.getByTestId('message-bubble-timestamp');
                    expect(timestampElement).toBeInTheDocument();

                    // 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();

                    unmount();
                }),
                { numRuns: 100 },
            );
        });

        /**
         * Validates: Requirement 2.8
         * For assistant messages, the component SHALL include an avatar element.
         */
        it('includes an avatar element for assistant messages', () => {
            fc.assert(
                fc.property(assistantMessageArbitrary(), (message) => {
                    const { unmount } = render(<MessageBubble message={message} showAvatar={true} />);

                    const avatarElement = screen.getByTestId('message-bubble-avatar');
                    expect(avatarElement).toBeInTheDocument();

                    // Verify avatar icon is present
                    const avatarIcon = avatarElement.querySelector('.message-bubble__avatar-icon');
                    expect(avatarIcon).not.toBeNull();

                    unmount();
                }),
                { numRuns: 100 },
            );
        });

        /**
         * Validates: Requirement 2.8 (inverse)
         * For user messages, the component SHALL NOT include an avatar element.
         */
        it('does NOT include an avatar element for user messages', () => {
            fc.assert(
                fc.property(userMessageArbitrary(), (message) => {
                    const { container, unmount } = render(<MessageBubble message={message} showAvatar={true} />);

                    // Avatar should not be present for user messages
                    const avatarElement = container.querySelector('[data-testid="message-bubble-avatar"]');
                    expect(avatarElement).toBeNull();

                    unmount();
                }),
                { numRuns: 100 },
            );
        });

        /**
         * Validates: Requirements 2.1, 2.3, 2.8 combined
         * User messages: role class + timestamp + NO avatar
         */
        it('user messages have role class, timestamp, and NO avatar', () => {
            fc.assert(
                fc.property(userMessageArbitrary(), (message) => {
                    const { container, unmount } = render(<MessageBubble message={message} />);

                    // Role class
                    const bubbleElement = container.querySelector('.message-bubble');
                    expect(bubbleElement?.classList.contains('message-bubble--user')).toBe(true);

                    // Timestamp
                    const timestampElement = screen.getByTestId('message-bubble-timestamp');
                    expect(timestampElement).toBeInTheDocument();

                    // NO avatar
                    const avatarElement = container.querySelector('[data-testid="message-bubble-avatar"]');
                    expect(avatarElement).toBeNull();

                    unmount();
                }),
                { numRuns: 100 },
            );
        });

        /**
         * Validates: Requirements 2.1, 2.3, 2.8 combined
         * Assistant messages: role class + timestamp + avatar
         */
        it('assistant messages have role class, timestamp, and avatar', () => {
            fc.assert(
                fc.property(assistantMessageArbitrary(), (message) => {
                    const { container, unmount } = render(<MessageBubble message={message} />);

                    // Role class
                    const bubbleElement = container.querySelector('.message-bubble');
                    expect(bubbleElement?.classList.contains('message-bubble--assistant')).toBe(true);

                    // Timestamp
                    const timestampElement = screen.getByTestId('message-bubble-timestamp');
                    expect(timestampElement).toBeInTheDocument();

                    // Avatar present
                    const avatarElement = screen.getByTestId('message-bubble-avatar');
                    expect(avatarElement).toBeInTheDocument();

                    unmount();
                }),
                { numRuns: 100 },
            );
        });

        /**
         * Validates: Requirement 2.5 (streaming indicator)
         * Assistant messages with isStreaming=true SHALL show streaming cursor.
         */
        it('assistant messages with isStreaming show streaming cursor', () => {
            const streamingMessageArbitrary = fc.record({
                id: fc.uuid(),
                role: fc.constant('assistant' as const),
                content: fc.string({ minLength: 1, maxLength: 500 }),
                timestamp: timestampArbitrary(),
                isStreaming: fc.constant(true),
            });

            fc.assert(
                fc.property(streamingMessageArbitrary, (message) => {
                    const { container, unmount } = render(<MessageBubble message={message} />);

                    // Streaming cursor should be present
                    const cursorElement = screen.getByTestId('streaming-cursor');
                    expect(cursorElement).toBeInTheDocument();

                    // data-streaming attribute should be set
                    const bubbleElement = container.querySelector('.message-bubble');
                    expect(bubbleElement?.getAttribute('data-streaming')).toBe('true');

                    unmount();
                }),
                { numRuns: 50 },
            );
        });

        /**
         * Validates: showAvatar prop behavior
         * When showAvatar=false, assistant messages SHALL NOT show avatar.
         */
        it('assistant messages without showAvatar do NOT show avatar', () => {
            fc.assert(
                fc.property(assistantMessageArbitrary(), (message) => {
                    const { container, unmount } = render(<MessageBubble message={message} showAvatar={false} />);

                    // Avatar should not be present when showAvatar=false
                    const avatarElement = container.querySelector('[data-testid="message-bubble-avatar"]');
                    expect(avatarElement).toBeNull();

                    // But role class and timestamp should still be present
                    const bubbleElement = container.querySelector('.message-bubble');
                    expect(bubbleElement?.classList.contains('message-bubble--assistant')).toBe(true);

                    const timestampElement = screen.getByTestId('message-bubble-timestamp');
                    expect(timestampElement).toBeInTheDocument();

                    unmount();
                }),
                { numRuns: 50 },
            );
        });

        /**
         * Validates: Message content is preserved
         * For any message, the content SHALL be rendered in the component.
         */
        it('message content is preserved in rendering', () => {
            fc.assert(
                fc.property(anyMessageArbitrary(), (message) => {
                    const { container, unmount } = render(<MessageBubble message={message} />);

                    const contentElement = container.querySelector('.message-bubble__content');
                    expect(contentElement).not.toBeNull();

                    // Content should be present in the rendered output
                    expect(contentElement?.textContent).toContain(message.content);

                    unmount();
                }),
                { numRuns: 100 },
            );
        });

        /**
         * Validates: Role classes are mutually exclusive
         * A message SHALL have exactly one role class (user XOR assistant).
         */
        it('role classes are mutually exclusive', () => {
            fc.assert(
                fc.property(anyMessageArbitrary(), (message) => {
                    const { container, unmount } = render(<MessageBubble message={message} />);

                    const bubbleElement = container.querySelector('.message-bubble');
                    const hasUserClass = bubbleElement?.classList.contains('message-bubble--user');
                    const hasAssistantClass = bubbleElement?.classList.contains('message-bubble--assistant');

                    // XOR: exactly one should be true
                    expect(hasUserClass !== hasAssistantClass).toBe(true);

                    // The class should match the role
                    if (message.role === 'user') {
                        expect(hasUserClass).toBe(true);
                        expect(hasAssistantClass).toBe(false);
                    } else {
                        expect(hasUserClass).toBe(false);
                        expect(hasAssistantClass).toBe(true);
                    }

                    unmount();
                }),
                { numRuns: 100 },
            );
        });
    });
});
