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

/**
 * Animation Timing Consistency Property Tests
 *
 * Property-based tests for animation timing consistency.
 * Verifies that all CSS animations and transitions use design token timing values
 * and that prefers-reduced-motion media query is properly implemented.
 *
 * **Validates: Requirements 8.1, 8.5**
 */

import * as fc from 'fast-check';
import * as fs from 'fs';
import * as path from 'path';

/**
 * Valid animation timing tokens from design-tokens.css.
 * These are the only allowed duration values for animations/transitions.
 */
const VALID_DURATION_TOKENS = ['--duration-fast', '--duration-normal', '--duration-slow', '--duration-slower', '--duration-instant'] as const;

/**
 * Valid duration fallback values (in ms) that correspond to design tokens.
 * Used when CSS uses var() with fallback values.
 */
const VALID_DURATION_VALUES_MS = ['0ms', '150ms', '250ms', '350ms', '500ms'] as const;

/**
 * Animation class names defined in animations.css that should use design tokens.
 */
const ANIMATION_CLASSES = [
    'message-enter',
    'button-press',
    'fade-in',
    'slide-up-fade',
    'slide-down-fade',
    'scale-in',
] as const;

/**
 * Keyframe animation names that have specific timing requirements.
 */
const KEYFRAME_ANIMATIONS = [
    'message-enter',
    'typing-pulse',
    'button-press',
    'cursor-blink',
    'spin',
    'fade-in',
    'slide-up-fade',
    'slide-down-fade',
    'scale-in',
    'pulse',
] as const;

/**
 * Parse CSS file and extract animation-related declarations.
 */
function parseAnimationCss(cssContent: string): {
    animationDurations: Map<string, string>;
    transitionDurations: Map<string, string>;
    hasReducedMotionQuery: boolean;
    reducedMotionContent: string;
    keyframeNames: Set<string>;
} {
    const animationDurations = new Map<string, string>();
    const transitionDurations = new Map<string, string>();
    let hasReducedMotionQuery = false;
    let reducedMotionContent = '';
    const keyframeNames = new Set<string>();

    // Extract keyframe names
    const keyframeMatches = cssContent.matchAll(/@keyframes\s+([a-zA-Z0-9-]+)\s*\{/g);
    for (const match of keyframeMatches) {
        keyframeNames.add(match[1]);
    }

    // Extract animation declarations with their selectors
    // Match: .selector { animation: name duration ... }
    const animationPattern = /\.([a-zA-Z0-9_-]+)\s*\{[^}]*animation\s*:\s*([^;]+);/g;
    const animationMatches = cssContent.matchAll(animationPattern);
    for (const match of animationMatches) {
        const selector = match[1];
        const animationValue = match[2];
        animationDurations.set(selector, animationValue);
    }

    // Extract animation-duration declarations
    const animationDurationPattern = /\.([a-zA-Z0-9_-]+)\s*\{[^}]*animation-duration\s*:\s*([^;]+);/g;
    const durationMatches = cssContent.matchAll(animationDurationPattern);
    for (const match of durationMatches) {
        const selector = match[1];
        const durationValue = match[2];
        animationDurations.set(`${selector}__duration`, durationValue);
    }

    // Extract transition declarations
    const transitionPattern = /\.([a-zA-Z0-9_-]+)\s*\{[^}]*transition\s*:\s*([^;]+);/g;
    const transitionMatches = cssContent.matchAll(transitionPattern);
    for (const match of transitionMatches) {
        const selector = match[1];
        const transitionValue = match[2];
        transitionDurations.set(selector, transitionValue);
    }

    // Check for prefers-reduced-motion media query
    const reducedMotionMatch = cssContent.match(/@media\s*\(\s*prefers-reduced-motion\s*:\s*reduce\s*\)\s*\{([\s\S]*?)\n\}/);
    if (reducedMotionMatch) {
        hasReducedMotionQuery = true;
        reducedMotionContent = reducedMotionMatch[1];
    }

    return {
        animationDurations,
        transitionDurations,
        hasReducedMotionQuery,
        reducedMotionContent,
        keyframeNames,
    };
}

/**
 * Check if a duration value uses a valid design token or fallback.
 */
function usesValidDurationToken(value: string): boolean {
    // Check if it uses a CSS variable with valid token name
    for (const token of VALID_DURATION_TOKENS) {
        if (value.includes(token)) {
            return true;
        }
    }

    // Check if it uses a valid fallback value
    for (const validMs of VALID_DURATION_VALUES_MS) {
        if (value.includes(validMs)) {
            return true;
        }
    }

    // Special cases: infinite animations (typing, cursor, spin) don't need duration tokens
    // as they run continuously
    if (value.includes('infinite')) {
        // For infinite animations, check if duration is reasonable (1s, 1.4s, 2s are common)
        const infiniteDurations = ['1s', '1.4s', '2s'];
        for (const dur of infiniteDurations) {
            if (value.includes(dur)) {
                return true;
            }
        }
    }

    return false;
}

/**
 * Load the animations.css file content.
 */
function loadAnimationsCss(): string {
    const cssPath = path.resolve(__dirname, '../animations.css');
    return fs.readFileSync(cssPath, 'utf-8');
}

/**
 * Load the chat.css file content for focus-visible verification.
 */
function loadChatCss(): string {
    const cssPath = path.resolve(__dirname, '../chat.css');
    return fs.readFileSync(cssPath, 'utf-8');
}

/**
 * Load the design-tokens.css file content.
 */
function loadDesignTokensCss(): string {
    const cssPath = path.resolve(__dirname, '../design-tokens.css');
    return fs.readFileSync(cssPath, 'utf-8');
}

describe('Animation Timing Consistency Property Tests', () => {
    let animationsCss: string;
    let chatCss: string;
    let designTokensCss: string;
    let parsedAnimations: ReturnType<typeof parseAnimationCss>;

    beforeAll(() => {
        animationsCss = loadAnimationsCss();
        chatCss = loadChatCss();
        designTokensCss = loadDesignTokensCss();
        parsedAnimations = parseAnimationCss(animationsCss);
    });

    /**
     * Feature: ai-chat-ui-redesign, Property 7: Animation Timing Consistency
     *
     * For any CSS animation or transition in the design system, the duration SHALL be
     * one of the defined timing tokens (--duration-fast, --duration-normal, --duration-slow).
     * For any interactive element, focus-visible styles SHALL be defined.
     *
     * **Validates: Requirements 8.1, 8.5**
     */
    describe('Property 7: Animation Timing Consistency', () => {
        describe('Animation Duration Token Usage', () => {
            it('should define all expected keyframe animations', async () => {
                await fc.assert(
                    fc.asyncProperty(fc.constantFrom(...KEYFRAME_ANIMATIONS), async (animationName) => {
                        expect(parsedAnimations.keyframeNames.has(animationName)).toBe(true);
                    }),
                    { numRuns: KEYFRAME_ANIMATIONS.length }
                );
            });

            it('should use design token durations for animation classes', async () => {
                await fc.assert(
                    fc.asyncProperty(fc.constantFrom(...ANIMATION_CLASSES), async (className) => {
                        const animationValue = parsedAnimations.animationDurations.get(className);
                        if (animationValue) {
                            const usesToken = usesValidDurationToken(animationValue);
                            expect(usesToken).toBe(true);
                        }
                        // If no animation value found, the class might be defined differently
                        // which is acceptable as long as it exists in the CSS
                        expect(animationsCss).toContain(`.${className}`);
                    }),
                    { numRuns: ANIMATION_CLASSES.length }
                );
            });

            it('should have animation durations within acceptable range (150ms-500ms for micro-interactions)', async () => {
                await fc.assert(
                    fc.asyncProperty(fc.constant(null), async () => {
                        // Verify design tokens define appropriate duration values
                        expect(designTokensCss).toContain('--duration-fast: 150ms');
                        expect(designTokensCss).toContain('--duration-normal: 250ms');
                        expect(designTokensCss).toContain('--duration-slow: 350ms');
                    }),
                    { numRuns: 1 }
                );
            });
        });

        describe('Reduced Motion Accessibility', () => {
            it('should have prefers-reduced-motion media query', async () => {
                await fc.assert(
                    fc.asyncProperty(fc.constant(null), async () => {
                        expect(parsedAnimations.hasReducedMotionQuery).toBe(true);
                        expect(animationsCss).toContain('prefers-reduced-motion: reduce');
                    }),
                    { numRuns: 1 }
                );
            });

            it('should disable or minimize animations in reduced motion mode', async () => {
                await fc.assert(
                    fc.asyncProperty(fc.constant(null), async () => {
                        const reducedMotion = parsedAnimations.reducedMotionContent;

                        // Should set animation-duration to minimal value
                        expect(reducedMotion).toContain('animation-duration: 0.01ms');

                        // Should set transition-duration to minimal value
                        expect(reducedMotion).toContain('transition-duration: 0.01ms');

                        // Should limit animation iterations
                        expect(reducedMotion).toContain('animation-iteration-count: 1');
                    }),
                    { numRuns: 1 }
                );
            });

            it('should apply reduced motion to all elements via universal selector', async () => {
                await fc.assert(
                    fc.asyncProperty(fc.constant(null), async () => {
                        const reducedMotion = parsedAnimations.reducedMotionContent;

                        // Should use universal selector for broad coverage
                        expect(reducedMotion).toMatch(/\*[\s,]/);
                        expect(reducedMotion).toContain('*::before');
                        expect(reducedMotion).toContain('*::after');
                    }),
                    { numRuns: 1 }
                );
            });

            it('should preserve essential feedback indicators in reduced motion mode', async () => {
                await fc.assert(
                    fc.asyncProperty(fc.constant(null), async () => {
                        const reducedMotion = parsedAnimations.reducedMotionContent;

                        // Typing indicator should have alternative visual feedback
                        expect(reducedMotion).toContain('.typing-indicator__dot');

                        // Cursor should remain visible
                        expect(reducedMotion).toContain('.cursor-blink');
                    }),
                    { numRuns: 1 }
                );
            });
        });

        describe('Easing Function Consistency', () => {
            it('should define standard easing functions in design tokens', async () => {
                await fc.assert(
                    fc.asyncProperty(fc.constant(null), async () => {
                        // Verify easing tokens exist
                        expect(designTokensCss).toContain('--ease-in:');
                        expect(designTokensCss).toContain('--ease-out:');
                        expect(designTokensCss).toContain('--ease-in-out:');
                    }),
                    { numRuns: 1 }
                );
            });

            it('should use design token easing functions in animations', async () => {
                await fc.assert(
                    fc.asyncProperty(fc.constant(null), async () => {
                        // Animations should reference easing tokens or use valid cubic-bezier
                        const easingReferences = animationsCss.match(/var\(--ease-[a-z-]+/g) || [];
                        const cubicBezierUsage = animationsCss.match(/cubic-bezier\([^)]+\)/g) || [];

                        // Should have easing function usage
                        expect(easingReferences.length + cubicBezierUsage.length).toBeGreaterThan(0);
                    }),
                    { numRuns: 1 }
                );
            });
        });

        describe('Animation Performance', () => {
            it('should use GPU-accelerated properties (transform, opacity) for animations', async () => {
                await fc.assert(
                    fc.asyncProperty(fc.constant(null), async () => {
                        // Check keyframe animations use transform and opacity
                        // These are GPU-accelerated and avoid layout thrashing

                        // message-enter should use transform and opacity
                        expect(animationsCss).toMatch(/@keyframes message-enter[\s\S]*?transform/);
                        expect(animationsCss).toMatch(/@keyframes message-enter[\s\S]*?opacity/);

                        // fade-in should use opacity
                        expect(animationsCss).toMatch(/@keyframes fade-in[\s\S]*?opacity/);

                        // slide-up-fade should use transform and opacity
                        expect(animationsCss).toMatch(/@keyframes slide-up-fade[\s\S]*?transform/);
                        expect(animationsCss).toMatch(/@keyframes slide-up-fade[\s\S]*?opacity/);
                    }),
                    { numRuns: 1 }
                );
            });

            it('should not animate layout-triggering properties in keyframes', async () => {
                await fc.assert(
                    fc.asyncProperty(fc.constant(null), async () => {
                        // Extract all keyframe blocks
                        const keyframeBlocks = animationsCss.match(/@keyframes\s+[a-zA-Z0-9-]+\s*\{[\s\S]*?\n\}/g) || [];

                        for (const block of keyframeBlocks) {
                            // Should not animate width, height, margin, padding, top, left, etc.
                            // These cause layout recalculation
                            expect(block).not.toMatch(/\bwidth\s*:/);
                            expect(block).not.toMatch(/\bheight\s*:/);
                            expect(block).not.toMatch(/\bmargin\s*:/);
                            expect(block).not.toMatch(/\bpadding\s*:/);
                            expect(block).not.toMatch(/\btop\s*:/);
                            expect(block).not.toMatch(/\bleft\s*:/);
                            expect(block).not.toMatch(/\bright\s*:/);
                            expect(block).not.toMatch(/\bbottom\s*:/);
                        }
                    }),
                    { numRuns: 1 }
                );
            });
        });

        describe('Focus Visible Styles', () => {
            it('should define focus ring tokens in design system', async () => {
                await fc.assert(
                    fc.asyncProperty(fc.constant(null), async () => {
                        // Design tokens should define focus ring colors
                        expect(designTokensCss).toContain('--color-focus-ring');
                        expect(designTokensCss).toContain('--shadow-input-focus');
                        expect(designTokensCss).toContain('--shadow-button-focus');
                    }),
                    { numRuns: 1 }
                );
            });

            it('should have focus styles for interactive elements in chat.css', async () => {
                await fc.assert(
                    fc.asyncProperty(fc.constant(null), async () => {
                        // Interactive elements should have focus states
                        // Check for :focus pseudo-class usage
                        expect(chatCss).toContain(':focus');

                        // Input textarea should have focus styling
                        expect(chatCss).toMatch(/\.input-area__textarea:focus/);
                    }),
                    { numRuns: 1 }
                );
            });

            it('should define border-focus color token for focus states', async () => {
                await fc.assert(
                    fc.asyncProperty(fc.constant(null), async () => {
                        // Design tokens should have focus border color
                        expect(designTokensCss).toContain('--color-border-focus');

                        // Both light and dark themes should define focus colors
                        const darkThemeMatch = designTokensCss.match(/@media\s*\(\s*prefers-color-scheme\s*:\s*dark\s*\)\s*\{[\s\S]*?\}/);
                        expect(darkThemeMatch).not.toBeNull();
                        if (darkThemeMatch) {
                            expect(darkThemeMatch[0]).toContain('--color-border-focus');
                        }
                    }),
                    { numRuns: 1 }
                );
            });
        });

        describe('Animation Timing Requirements Compliance', () => {
            it('should have micro-interaction durations between 200-300ms as per Requirement 8.1', async () => {
                await fc.assert(
                    fc.asyncProperty(fc.constant(null), async () => {
                        // Requirement 8.1: 200-300ms for micro-interactions
                        // --duration-normal (250ms) falls within this range
                        expect(designTokensCss).toContain('--duration-normal: 250ms');

                        // Verify animations use this token
                        expect(animationsCss).toContain('var(--duration-normal');
                    }),
                    { numRuns: 1 }
                );
            });

            it('should have larger transition durations between 300-500ms as per Requirement 8.1', async () => {
                await fc.assert(
                    fc.asyncProperty(fc.constant(null), async () => {
                        // Requirement 8.1: 300-500ms for larger transitions
                        // --duration-slow (350ms) and --duration-slower (500ms) cover this
                        expect(designTokensCss).toContain('--duration-slow: 350ms');
                        expect(designTokensCss).toContain('--duration-slower: 500ms');

                        // Verify slide-up-fade uses slow duration for larger transitions
                        expect(animationsCss).toContain('var(--duration-slow');
                    }),
                    { numRuns: 1 }
                );
            });
        });
    });
});
