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

/**
 * Memory Service Property Tests
 *
 * Property-based tests for MemoryService round-trip persistence.
 */

import * as fc from 'fast-check';
import type { ConversationMessage, ConversationTurn } from '../../types/chat.types';

/**
 * Mock DynamoDB storage for testing.
 */
class MockDynamoDBStorage {
    private storage: Map<string, { messages: ConversationMessage[] }> = new Map();

    async get(sessionId: string): Promise<ConversationMessage[]> {
        const item = this.storage.get(sessionId);
        return item?.messages ?? [];
    }

    async append(sessionId: string, turn: ConversationTurn): Promise<void> {
        const timestamp = new Date().toISOString();
        const existing = this.storage.get(sessionId) ?? { messages: [] };

        const userMessage: ConversationMessage = {
            role: 'user',
            content: turn.user,
            timestamp,
        };

        const assistantMessage: ConversationMessage = {
            role: 'assistant',
            content: turn.assistant,
            timestamp,
        };

        existing.messages.push(userMessage, assistantMessage);
        this.storage.set(sessionId, existing);
    }

    clear(): void {
        this.storage.clear();
    }
}

describe('Memory Service Properties', () => {
    let mockStorage: MockDynamoDBStorage;

    beforeEach(() => {
        mockStorage = new MockDynamoDBStorage();
    });

    /**
     * Feature: ai-chat-template, Property 8: Session Memory Round-Trip Persistence
     *
     * For any valid sessionId and conversation turn (user message + assistant response),
     * appending the turn to DynamoDB and then retrieving the session SHALL return a
     * messages array containing the appended turn with correct role and content values.
     *
     * **Validates: Requirements 5.4, 5.5**
     */
    describe('Property 8: Session Memory Round-Trip Persistence', () => {
        it('should persist and retrieve conversation turns with correct content', async () => {
            await fc.assert(
                fc.asyncProperty(
                    fc.string({ minLength: 1, maxLength: 50 }).filter((s) => s.trim().length > 0),
                    fc.string({ minLength: 1, maxLength: 500 }).filter((s) => s.trim().length > 0),
                    fc.string({ minLength: 1, maxLength: 500 }).filter((s) => s.trim().length > 0),
                    async (sessionId, userMessage, assistantMessage) => {
                        // Clear storage at the start of each iteration
                        mockStorage.clear();

                        // Append a conversation turn
                        await mockStorage.append(sessionId, {
                            user: userMessage,
                            assistant: assistantMessage,
                        });

                        // Retrieve the session
                        const messages = await mockStorage.get(sessionId);

                        // Verify messages were persisted (exactly 2 for one turn)
                        expect(messages.length).toBe(2);

                        // Verify content matches
                        expect(messages[0].role).toBe('user');
                        expect(messages[0].content).toBe(userMessage);

                        expect(messages[1].role).toBe('assistant');
                        expect(messages[1].content).toBe(assistantMessage);
                    }
                ),
                { numRuns: 100 }
            );
        });

        it('should return empty array for non-existent sessions', async () => {
            await fc.assert(
                fc.asyncProperty(
                    fc.string({ minLength: 1, maxLength: 50 }).filter((s) => s.trim().length > 0),
                    async (sessionId) => {
                        // Get messages for a session that doesn't exist
                        const messages = await mockStorage.get(sessionId);

                        // Should return empty array, not null or undefined
                        expect(Array.isArray(messages)).toBe(true);
                        expect(messages.length).toBe(0);
                    }
                ),
                { numRuns: 100 }
            );
        });

        it('should preserve message order across multiple turns', async () => {
            await fc.assert(
                fc.asyncProperty(
                    fc.string({ minLength: 1, maxLength: 50 }).filter((s) => s.trim().length > 0),
                    fc.array(
                        fc.record({
                            user: fc.string({ minLength: 1, maxLength: 200 }).filter((s) => s.trim().length > 0),
                            assistant: fc.string({ minLength: 1, maxLength: 200 }).filter((s) => s.trim().length > 0),
                        }),
                        { minLength: 1, maxLength: 5 }
                    ),
                    async (sessionId, turns) => {
                        // Clear storage at the start of each iteration
                        mockStorage.clear();

                        // Append multiple turns
                        for (const turn of turns) {
                            await mockStorage.append(sessionId, turn);
                        }

                        // Retrieve all messages
                        const messages = await mockStorage.get(sessionId);

                        // Verify total message count (2 per turn)
                        expect(messages.length).toBe(turns.length * 2);

                        // Verify alternating user/assistant pattern
                        for (let i = 0; i < messages.length; i += 2) {
                            expect(messages[i].role).toBe('user');
                            expect(messages[i + 1].role).toBe('assistant');
                        }

                        // Verify content order matches input order
                        for (let i = 0; i < turns.length; i++) {
                            expect(messages[i * 2].content).toBe(turns[i].user);
                            expect(messages[i * 2 + 1].content).toBe(turns[i].assistant);
                        }
                    }
                ),
                { numRuns: 100 }
            );
        });

        it('should include timestamps in all messages', async () => {
            await fc.assert(
                fc.asyncProperty(
                    fc.string({ minLength: 1, maxLength: 50 }).filter((s) => s.trim().length > 0),
                    fc.string({ minLength: 1, maxLength: 200 }).filter((s) => s.trim().length > 0),
                    fc.string({ minLength: 1, maxLength: 200 }).filter((s) => s.trim().length > 0),
                    async (sessionId, userMessage, assistantMessage) => {
                        // Clear storage at the start of each iteration
                        mockStorage.clear();

                        await mockStorage.append(sessionId, {
                            user: userMessage,
                            assistant: assistantMessage,
                        });

                        const messages = await mockStorage.get(sessionId);

                        // All messages should have timestamps
                        for (const message of messages) {
                            expect(message.timestamp).toBeDefined();
                            expect(typeof message.timestamp).toBe('string');
                            // Timestamp should be valid ISO format
                            expect(() => new Date(message.timestamp)).not.toThrow();
                        }
                    }
                ),
                { numRuns: 100 }
            );
        });

        it('should isolate sessions from each other', async () => {
            await fc.assert(
                fc.asyncProperty(
                    fc.string({ minLength: 1, maxLength: 50 }).filter((s) => s.trim().length > 0),
                    fc.string({ minLength: 1, maxLength: 50 }).filter((s) => s.trim().length > 0),
                    fc.string({ minLength: 1, maxLength: 200 }).filter((s) => s.trim().length > 0),
                    fc.string({ minLength: 1, maxLength: 200 }).filter((s) => s.trim().length > 0),
                    async (sessionId1, sessionId2, userMessage, assistantMessage) => {
                        // Clear storage at the start of each iteration to ensure isolation
                        mockStorage.clear();

                        // Skip if session IDs are the same
                        if (sessionId1 === sessionId2) return;

                        // Append to first session only
                        await mockStorage.append(sessionId1, {
                            user: userMessage,
                            assistant: assistantMessage,
                        });

                        // Second session should still be empty
                        const messages2 = await mockStorage.get(sessionId2);
                        expect(messages2.length).toBe(0);

                        // First session should have the messages
                        const messages1 = await mockStorage.get(sessionId1);
                        expect(messages1.length).toBe(2);
                    }
                ),
                { numRuns: 100 }
            );
        });
    });
});
