/**
 * LiveComponents Security E2E Tests
 *
 * Tests security features and protections:
 * - CSRF token validation for state-changing actions
 * - Rate limiting for action calls
 * - Idempotency for critical operations
 * - Input sanitization and XSS prevention
 * - Authorization checks for protected actions
 * - Session security and token rotation
 *
 * Run with: npx playwright test security.spec.js
 */

import { test, expect } from '@playwright/test';

test.describe('LiveComponents Security Tests', () => {
    test.beforeEach(async ({ page }) => {
        await page.goto('https://localhost/livecomponents/test/security');
        await page.waitForFunction(() => window.LiveComponents !== undefined);
    });

    test.describe('CSRF Protection', () => {
        test('should include CSRF token in action requests', async ({ page }) => {
            // Monitor network requests
            const actionRequests = [];

            page.on('request', request => {
                if (request.url().includes('/live-component/')) {
                    actionRequests.push({
                        url: request.url(),
                        headers: request.headers(),
                        postData: request.postData()
                    });
                }
            });

            // Trigger action
            await page.click('button#trigger-action');

            // Wait for request to complete
            await page.waitForTimeout(1000);

            // Verify at least one action request was made
            expect(actionRequests.length).toBeGreaterThan(0);

            // Verify CSRF token present in request
            const actionRequest = actionRequests[0];

            // Check for CSRF token in headers or POST data
            const hasCsrfHeader = 'x-csrf-token' in actionRequest.headers ||
                                  'x-xsrf-token' in actionRequest.headers;

            const hasCsrfInBody = actionRequest.postData &&
                                  actionRequest.postData.includes('_csrf_token');

            expect(hasCsrfHeader || hasCsrfInBody).toBe(true);
        });

        test('should reject action without CSRF token', async ({ page }) => {
            // Intercept and remove CSRF token
            await page.route('**/live-component/**', async (route) => {
                const request = route.request();

                // Remove CSRF token from headers
                const headers = { ...request.headers() };
                delete headers['x-csrf-token'];
                delete headers['x-xsrf-token'];

                // Remove from POST data if present
                let postData = request.postData();
                if (postData) {
                    postData = postData.replace(/_csrf_token=[^&]+&?/, '');
                }

                await route.continue({
                    headers,
                    postData
                });
            });

            // Trigger action
            await page.click('button#trigger-action');

            // Wait for error
            await page.waitForTimeout(1000);

            // Verify error response
            const errorMessage = await page.locator('.error-message').textContent();
            expect(errorMessage).toContain('CSRF');
        });

        test('should reject action with invalid CSRF token', async ({ page }) => {
            // Intercept and replace CSRF token with invalid one
            await page.route('**/live-component/**', async (route) => {
                const request = route.request();

                const headers = {
                    ...request.headers(),
                    'x-csrf-token': 'invalid-token-12345'
                };

                await route.continue({ headers });
            });

            // Trigger action
            await page.click('button#trigger-action');

            // Wait for error
            await page.waitForTimeout(1000);

            // Verify error response
            const errorMessage = await page.locator('.error-message').textContent();
            expect(errorMessage).toContain('CSRF');
        });

        test('should rotate CSRF token after usage', async ({ page }) => {
            // Get initial CSRF token
            const initialToken = await page.evaluate(() => {
                return document.querySelector('meta[name="csrf-token"]')?.content;
            });

            expect(initialToken).toBeTruthy();

            // Trigger action
            await page.click('button#trigger-action');

            // Wait for action to complete
            await page.waitForTimeout(1000);

            // Get new CSRF token
            const newToken = await page.evaluate(() => {
                return document.querySelector('meta[name="csrf-token"]')?.content;
            });

            // Tokens should be different after action
            // (depending on framework configuration)
            // For now, just verify new token exists
            expect(newToken).toBeTruthy();
        });
    });

    test.describe('Rate Limiting', () => {
        test('should enforce rate limit on rapid action calls', async ({ page }) => {
            // Trigger action rapidly 20 times
            const results = [];

            for (let i = 0; i < 20; i++) {
                try {
                    await page.click('button#trigger-action');
                    await page.waitForTimeout(50); // Small delay

                    const success = await page.locator('.action-success').isVisible();
                    results.push({ success, attempt: i + 1 });
                } catch (error) {
                    results.push({ success: false, attempt: i + 1, error: error.message });
                }
            }

            // Some requests should be rate limited
            const successCount = results.filter(r => r.success).length;

            // Expect rate limiting to kick in (not all 20 succeed)
            expect(successCount).toBeLessThan(20);

            // Verify rate limit error message
            const rateLimitError = await page.locator('.rate-limit-error').isVisible();
            expect(rateLimitError).toBe(true);
        });

        test('should provide retry-after information when rate limited', async ({ page }) => {
            // Trigger rapid requests to hit rate limit
            for (let i = 0; i < 15; i++) {
                await page.click('button#trigger-action');
                await page.waitForTimeout(20);
            }

            // Wait for rate limit error
            await page.waitForSelector('.rate-limit-error', { timeout: 5000 });

            // Verify retry-after information
            const retryAfter = await page.locator('[data-retry-after]').getAttribute('data-retry-after');

            expect(retryAfter).toBeTruthy();
            expect(parseInt(retryAfter)).toBeGreaterThan(0);

            console.log(`Rate limit retry-after: ${retryAfter} seconds`);
        });

        test('should reset rate limit after cooldown period', async ({ page }) => {
            // Hit rate limit
            for (let i = 0; i < 15; i++) {
                await page.click('button#trigger-action');
                await page.waitForTimeout(20);
            }

            // Verify rate limited
            await expect(page.locator('.rate-limit-error')).toBeVisible();

            // Get retry-after duration
            const retryAfter = await page.locator('[data-retry-after]').getAttribute('data-retry-after');
            const waitTime = parseInt(retryAfter) * 1000 + 1000; // Add 1 second buffer

            console.log(`Waiting ${waitTime}ms for rate limit reset...`);

            // Wait for cooldown
            await page.waitForTimeout(waitTime);

            // Try action again
            await page.click('button#trigger-action');

            // Should succeed now
            await expect(page.locator('.action-success')).toBeVisible();
        });

        test('should have separate rate limits per action type', async ({ page }) => {
            // Hit rate limit on action A
            for (let i = 0; i < 15; i++) {
                await page.click('button#action-a');
                await page.waitForTimeout(20);
            }

            // Verify action A is rate limited
            await expect(page.locator('.rate-limit-error-a')).toBeVisible();

            // Action B should still work
            await page.click('button#action-b');

            await expect(page.locator('.action-success-b')).toBeVisible();
        });

        test('should apply IP-based rate limiting', async ({ page, context }) => {
            // Create multiple browser contexts (simulate multiple users from same IP)
            const context2 = await page.context().browser().newContext();
            const page2 = await context2.newPage();

            await page2.goto('https://localhost/livecomponents/test/security');
            await page2.waitForFunction(() => window.LiveComponents !== undefined);

            // User 1: Trigger many requests
            for (let i = 0; i < 10; i++) {
                await page.click('button#trigger-action');
                await page.waitForTimeout(20);
            }

            // User 2: Should also be affected by IP-based limit
            for (let i = 0; i < 10; i++) {
                await page2.click('button#trigger-action');
                await page2.waitForTimeout(20);
            }

            // Both should eventually be rate limited (shared IP limit)
            const user1Limited = await page.locator('.rate-limit-error').isVisible();
            const user2Limited = await page2.locator('.rate-limit-error').isVisible();

            expect(user1Limited || user2Limited).toBe(true);

            await context2.close();
        });
    });

    test.describe('Idempotency', () => {
        test('should handle duplicate action calls idempotently', async ({ page }) => {
            // Get initial state
            const initialValue = await page.locator('#counter-value').textContent();
            const initialCount = parseInt(initialValue);

            // Trigger increment action twice with same idempotency key
            const idempotencyKey = 'test-key-12345';

            await page.evaluate((key) => {
                window.LiveComponents.get('counter:test').call('increment', {}, {
                    idempotencyKey: key
                });
            }, idempotencyKey);

            await page.waitForTimeout(500);

            // Same idempotency key - should be ignored
            await page.evaluate((key) => {
                window.LiveComponents.get('counter:test').call('increment', {}, {
                    idempotencyKey: key
                });
            }, idempotencyKey);

            await page.waitForTimeout(500);

            // Value should only increment once
            const finalValue = await page.locator('#counter-value').textContent();
            const finalCount = parseInt(finalValue);

            expect(finalCount).toBe(initialCount + 1);
        });

        test('should allow different idempotency keys', async ({ page }) => {
            const initialValue = await page.locator('#counter-value').textContent();
            const initialCount = parseInt(initialValue);

            // First action with idempotency key 1
            await page.evaluate(() => {
                window.LiveComponents.get('counter:test').call('increment', {}, {
                    idempotencyKey: 'key-1'
                });
            });

            await page.waitForTimeout(500);

            // Second action with idempotency key 2 (different key)
            await page.evaluate(() => {
                window.LiveComponents.get('counter:test').call('increment', {}, {
                    idempotencyKey: 'key-2'
                });
            });

            await page.waitForTimeout(500);

            // Both should execute (different keys)
            const finalValue = await page.locator('#counter-value').textContent();
            const finalCount = parseInt(finalValue);

            expect(finalCount).toBe(initialCount + 2);
        });

        test('should expire idempotency keys after TTL', async ({ page }) => {
            const idempotencyKey = 'expiring-key';

            // First action
            await page.evaluate((key) => {
                window.LiveComponents.get('counter:test').call('increment', {}, {
                    idempotencyKey: key
                });
            }, idempotencyKey);

            await page.waitForTimeout(500);

            const afterFirstValue = await page.locator('#counter-value').textContent();
            const afterFirstCount = parseInt(afterFirstValue);

            // Wait for idempotency key to expire (assume 5 second TTL)
            await page.waitForTimeout(6000);

            // Same key should work again after expiration
            await page.evaluate((key) => {
                window.LiveComponents.get('counter:test').call('increment', {}, {
                    idempotencyKey: key
                });
            }, idempotencyKey);

            await page.waitForTimeout(500);

            const finalValue = await page.locator('#counter-value').textContent();
            const finalCount = parseInt(finalValue);

            expect(finalCount).toBe(afterFirstCount + 1);
        });

        test('should return cached result for duplicate idempotent action', async ({ page }) => {
            const idempotencyKey = 'cached-result-key';

            // First action
            const result1 = await page.evaluate((key) => {
                return window.LiveComponents.get('counter:test').call('increment', {}, {
                    idempotencyKey: key
                });
            }, idempotencyKey);

            await page.waitForTimeout(500);

            // Duplicate action (same key)
            const result2 = await page.evaluate((key) => {
                return window.LiveComponents.get('counter:test').call('increment', {}, {
                    idempotencyKey: key
                });
            }, idempotencyKey);

            // Results should be identical (cached)
            expect(result2).toEqual(result1);
        });
    });

    test.describe('Input Sanitization & XSS Prevention', () => {
        test('should sanitize HTML in action parameters', async ({ page }) => {
            // Attempt XSS injection via action parameter
            const xssPayload = '<script>alert("XSS")</script>';

            await page.evaluate((payload) => {
                window.LiveComponents.get('text:test').call('setText', {
                    text: payload
                });
            }, xssPayload);

            await page.waitForTimeout(500);

            // Verify script tag is sanitized/escaped
            const displayedText = await page.locator('#text-display').textContent();

            // Should display as text, not execute
            expect(displayedText).toContain('script');
            expect(displayedText).not.toContain('<script>');

            // Verify no script execution
            const alertShown = await page.evaluate(() => {
                return window.__alertCalled === true;
            });

            expect(alertShown).toBeFalsy();
        });

        test('should escape HTML entities in rendered content', async ({ page }) => {
            const maliciousInput = '<img src=x onerror=alert("XSS")>';

            await page.evaluate((input) => {
                window.LiveComponents.get('text:test').call('setText', {
                    text: input
                });
            }, maliciousInput);

            await page.waitForTimeout(500);

            // Check that img tag is escaped
            const innerHTML = await page.locator('#text-display').innerHTML();

            // Should be escaped HTML entities
            expect(innerHTML).toContain('&lt;img');
            expect(innerHTML).not.toContain('<img');
        });

        test('should prevent JavaScript injection in fragment updates', async ({ page }) => {
            const jsPayload = 'javascript:alert("XSS")';

            await page.evaluate((payload) => {
                window.LiveComponents.get('link:test').call('setLink', {
                    url: payload
                });
            }, jsPayload);

            await page.waitForTimeout(500);

            // Verify link is sanitized
            const linkHref = await page.locator('#link-display').getAttribute('href');

            // Should not contain javascript: protocol
            expect(linkHref).not.toContain('javascript:');
        });

        test('should validate and sanitize file paths', async ({ page }) => {
            // Path traversal attempt
            const maliciousPath = '../../../etc/passwd';

            await page.evaluate((path) => {
                window.LiveComponents.get('file:test').call('loadFile', {
                    path: path
                });
            }, maliciousPath);

            await page.waitForTimeout(500);

            // Should reject or sanitize path
            const errorMessage = await page.locator('.error-message').textContent();

            expect(errorMessage).toContain('Invalid path');
        });

        test('should prevent SQL injection in search parameters', async ({ page }) => {
            const sqlInjection = "'; DROP TABLE users; --";

            await page.evaluate((query) => {
                window.LiveComponents.get('search:test').call('search', {
                    query: query
                });
            }, sqlInjection);

            await page.waitForTimeout(500);

            // Search should handle safely (parameterized queries)
            // No error should occur, but SQL shouldn't execute
            const results = await page.locator('.search-results').isVisible();

            expect(results).toBe(true); // Search completed without error
        });
    });

    test.describe('Authorization', () => {
        test('should reject unauthorized action calls', async ({ page }) => {
            // Attempt to call admin-only action without auth
            await page.evaluate(() => {
                window.LiveComponents.get('admin:test').call('deleteAllData', {});
            });

            await page.waitForTimeout(500);

            // Should be rejected
            const errorMessage = await page.locator('.authorization-error').textContent();

            expect(errorMessage).toContain('Unauthorized');
        });

        test('should allow authorized action calls', async ({ page }) => {
            // Login first
            await page.fill('#username', 'admin');
            await page.fill('#password', 'password');
            await page.click('#login-btn');

            await page.waitForSelector('.logged-in-indicator');

            // Now try admin action
            await page.evaluate(() => {
                window.LiveComponents.get('admin:test').call('performAdminAction', {});
            });

            await page.waitForTimeout(500);

            // Should succeed
            const successMessage = await page.locator('.action-success').isVisible();

            expect(successMessage).toBe(true);
        });

        test('should respect role-based authorization', async ({ page }) => {
            // Login as regular user
            await page.fill('#username', 'user');
            await page.fill('#password', 'password');
            await page.click('#login-btn');

            await page.waitForSelector('.logged-in-indicator');

            // Try admin action - should fail
            await page.evaluate(() => {
                window.LiveComponents.get('admin:test').call('performAdminAction', {});
            });

            await page.waitForTimeout(500);

            const errorMessage = await page.locator('.authorization-error').textContent();

            expect(errorMessage).toContain('Insufficient permissions');
        });
    });

    test.describe('Session Security', () => {
        test('should invalidate session after logout', async ({ page }) => {
            // Login
            await page.fill('#username', 'user');
            await page.fill('#password', 'password');
            await page.click('#login-btn');

            await page.waitForSelector('.logged-in-indicator');

            // Perform authenticated action
            await page.evaluate(() => {
                window.LiveComponents.get('user:test').call('getUserData', {});
            });

            await page.waitForTimeout(500);

            const beforeLogout = await page.locator('.user-data').isVisible();
            expect(beforeLogout).toBe(true);

            // Logout
            await page.click('#logout-btn');

            await page.waitForSelector('.logged-out-indicator');

            // Try same action - should fail
            await page.evaluate(() => {
                window.LiveComponents.get('user:test').call('getUserData', {});
            });

            await page.waitForTimeout(500);

            const errorMessage = await page.locator('.authorization-error').textContent();

            expect(errorMessage).toContain('Unauthorized');
        });

        test('should detect session hijacking attempts', async ({ page, context }) => {
            // Login in first tab
            await page.fill('#username', 'user');
            await page.fill('#password', 'password');
            await page.click('#login-btn');

            await page.waitForSelector('.logged-in-indicator');

            // Get session cookie
            const cookies = await context.cookies();
            const sessionCookie = cookies.find(c => c.name === 'session_id');

            expect(sessionCookie).toBeTruthy();

            // Create new context with different IP/user agent
            const newContext = await page.context().browser().newContext({
                userAgent: 'Mozilla/5.0 (Different Browser)'
            });

            const newPage = await newContext.newPage();

            // Set stolen cookie
            await newContext.addCookies([sessionCookie]);

            await newPage.goto('https://localhost/livecomponents/test/security');

            // Try to use session from different context
            await newPage.evaluate(() => {
                window.LiveComponents.get('user:test').call('getUserData', {});
            });

            await newPage.waitForTimeout(500);

            // Should detect session hijacking and reject
            const errorMessage = await newPage.locator('.security-error').textContent();

            expect(errorMessage).toContain('Session invalid');

            await newContext.close();
        });

        test('should enforce session timeout', async ({ page }) => {
            // Login
            await page.fill('#username', 'user');
            await page.fill('#password', 'password');
            await page.click('#login-btn');

            await page.waitForSelector('.logged-in-indicator');

            // Wait for session timeout (assume 5 minute timeout in test environment)
            console.log('Waiting for session timeout (5 minutes)...');
            await page.waitForTimeout(5 * 60 * 1000 + 5000); // 5 min + buffer

            // Try action - should fail due to timeout
            await page.evaluate(() => {
                window.LiveComponents.get('user:test').call('getUserData', {});
            });

            await page.waitForTimeout(500);

            const errorMessage = await page.locator('.session-expired-error').textContent();

            expect(errorMessage).toContain('Session expired');
        });
    });

    test.describe('Content Security Policy', () => {
        test('should have CSP headers set', async ({ page }) => {
            const response = await page.goto('https://localhost/livecomponents/test/security');

            const cspHeader = response.headers()['content-security-policy'];

            expect(cspHeader).toBeTruthy();
            console.log('CSP Header:', cspHeader);
        });

        test('should block inline scripts via CSP', async ({ page }) => {
            // Attempt to inject inline script
            const consoleErrors = [];

            page.on('console', msg => {
                if (msg.type() === 'error') {
                    consoleErrors.push(msg.text());
                }
            });

            await page.evaluate(() => {
                const script = document.createElement('script');
                script.textContent = 'alert("Inline script executed")';
                document.body.appendChild(script);
            });

            await page.waitForTimeout(500);

            // CSP should block inline script
            const hasCspError = consoleErrors.some(err =>
                err.includes('Content Security Policy') || err.includes('CSP')
            );

            expect(hasCspError).toBe(true);
        });
    });
});
