/**
 * LiveComponents E2E Tests - Partial Rendering & Fragments
 *
 * Tests for fragment-based partial rendering functionality:
 * - Fragment extraction and rendering
 * - DOM patching with focus preservation
 * - Multiple fragment updates in single request
 * - Error handling and fallback to full render
 *
 * @see src/Framework/LiveComponents/Services/FragmentExtractor.php
 * @see src/Framework/LiveComponents/Services/FragmentRenderer.php
 * @see resources/js/modules/DomPatcher.js
 */

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

test.describe('LiveComponents - Partial Rendering & Fragments', () => {
    test.beforeEach(async ({ page }) => {
        // Navigate to test page with LiveComponent
        await page.goto('/livecomponents/test');

        // Wait for LiveComponent to be initialized
        await page.waitForFunction(() => window.LiveComponent !== undefined);
    });

    test('should render initial component with fragments', async ({ page }) => {
        // Check that component has fragment markers
        const headerFragment = await page.locator('[data-lc-fragment="header"]');
        const contentFragment = await page.locator('[data-lc-fragment="content"]');
        const footerFragment = await page.locator('[data-lc-fragment="footer"]');

        await expect(headerFragment).toBeVisible();
        await expect(contentFragment).toBeVisible();
        await expect(footerFragment).toBeVisible();
    });

    test('should update single fragment without full re-render', async ({ page }) => {
        // Get initial content
        const contentFragment = page.locator('[data-lc-fragment="content"]');
        const initialContent = await contentFragment.textContent();

        // Get component ID
        const componentId = await page.locator('[data-component-id]').getAttribute('data-component-id');

        // Trigger action that updates only content fragment
        await page.click('[data-lc-action="updateContent"]');

        // Wait for fragment update
        await page.waitForResponse(response =>
            response.url().includes('/livecomponent') &&
            response.status() === 200
        );

        // Content should have changed
        const updatedContent = await contentFragment.textContent();
        expect(updatedContent).not.toBe(initialContent);

        // Header and footer should remain unchanged (no re-render)
        const headerElement = page.locator('[data-lc-fragment="header"]');
        const footerElement = page.locator('[data-lc-fragment="footer"]');

        // Elements should still have same references (not re-created)
        await expect(headerElement).toBeVisible();
        await expect(footerElement).toBeVisible();
    });

    test('should update multiple fragments in single request', async ({ page }) => {
        const headerFragment = page.locator('[data-lc-fragment="header"]');
        const contentFragment = page.locator('[data-lc-fragment="content"]');

        const initialHeader = await headerFragment.textContent();
        const initialContent = await contentFragment.textContent();

        // Trigger action that updates both header and content
        await page.click('[data-lc-action="updateHeaderAndContent"]');

        await page.waitForResponse(response =>
            response.url().includes('/livecomponent') &&
            response.status() === 200
        );

        // Both fragments should be updated
        const updatedHeader = await headerFragment.textContent();
        const updatedContent = await contentFragment.textContent();

        expect(updatedHeader).not.toBe(initialHeader);
        expect(updatedContent).not.toBe(initialContent);
    });

    test('should preserve focus during fragment updates', async ({ page }) => {
        // Find input in content fragment
        const input = page.locator('[data-lc-fragment="content"] input[name="search"]');

        await input.fill('test query');
        await input.focus();

        // Verify focus
        await expect(input).toBeFocused();

        // Trigger fragment update
        await page.click('[data-lc-action="updateContent"]');

        await page.waitForResponse(response =>
            response.url().includes('/livecomponent') &&
            response.status() === 200
        );

        // Focus should be preserved
        await expect(input).toBeFocused();

        // Value should be preserved
        await expect(input).toHaveValue('test query');
    });

    test('should preserve selection state during fragment updates', async ({ page }) => {
        const textarea = page.locator('[data-lc-fragment="content"] textarea');

        await textarea.fill('Hello World');

        // Select portion of text
        await textarea.evaluate((el) => {
            el.setSelectionRange(0, 5); // Select "Hello"
        });

        // Trigger fragment update
        await page.click('[data-lc-action="updateContent"]');

        await page.waitForResponse(response =>
            response.url().includes('/livecomponent') &&
            response.status() === 200
        );

        // Selection should be preserved
        const selectionStart = await textarea.evaluate((el) => el.selectionStart);
        const selectionEnd = await textarea.evaluate((el) => el.selectionEnd);

        expect(selectionStart).toBe(0);
        expect(selectionEnd).toBe(5);
    });

    test('should fall back to full render if fragment not found', async ({ page }) => {
        const component = page.locator('[data-component-id]');
        const initialHtml = await component.innerHTML();

        // Trigger action with non-existent fragment request
        await page.evaluate(() => {
            window.LiveComponent.executeAction(
                document.querySelector('[data-component-id]').dataset.componentId,
                'updateContent',
                {},
                ['non-existent-fragment']
            );
        });

        await page.waitForResponse(response =>
            response.url().includes('/livecomponent') &&
            response.status() === 200
        );

        // Should fall back to full render
        const updatedHtml = await component.innerHTML();
        expect(updatedHtml).not.toBe(initialHtml);

        // All fragments should still be present
        await expect(page.locator('[data-lc-fragment="header"]')).toBeVisible();
        await expect(page.locator('[data-lc-fragment="content"]')).toBeVisible();
        await expect(page.locator('[data-lc-fragment="footer"]')).toBeVisible();
    });

    test('should handle nested fragments correctly', async ({ page }) => {
        // Component with nested structure:
        // <div data-lc-fragment="parent">
        //   <div data-lc-fragment="child">...</div>
        // </div>

        const parentFragment = page.locator('[data-lc-fragment="parent"]');
        const childFragment = page.locator('[data-lc-fragment="child"]');

        const initialParent = await parentFragment.textContent();
        const initialChild = await childFragment.textContent();

        // Update only child fragment
        await page.click('[data-lc-action="updateChild"]');

        await page.waitForResponse(response =>
            response.url().includes('/livecomponent') &&
            response.status() === 200
        );

        // Child should be updated
        const updatedChild = await childFragment.textContent();
        expect(updatedChild).not.toBe(initialChild);

        // Parent container should remain (DOM patching, not replacement)
        await expect(parentFragment).toBeVisible();
    });

    test('should update attributes during fragment patch', async ({ page }) => {
        const contentFragment = page.locator('[data-lc-fragment="content"]');

        // Check initial class
        const initialClass = await contentFragment.getAttribute('class');

        // Trigger action that changes fragment attributes
        await page.click('[data-lc-action="toggleActive"]');

        await page.waitForResponse(response =>
            response.url().includes('/livecomponent') &&
            response.status() === 200
        );

        // Class should be updated
        const updatedClass = await contentFragment.getAttribute('class');
        expect(updatedClass).not.toBe(initialClass);
        expect(updatedClass).toContain('active');
    });

    test('should handle fragment updates with different element counts', async ({ page }) => {
        const listFragment = page.locator('[data-lc-fragment="list"]');

        // Initial list with 3 items
        let items = await listFragment.locator('li').count();
        expect(items).toBe(3);

        // Add item
        await page.click('[data-lc-action="addItem"]');

        await page.waitForResponse(response =>
            response.url().includes('/livecomponent') &&
            response.status() === 200
        );

        // Should have 4 items now
        items = await listFragment.locator('li').count();
        expect(items).toBe(4);

        // Remove item
        await page.click('[data-lc-action="removeItem"]');

        await page.waitForResponse(response =>
            response.url().includes('/livecomponent') &&
            response.status() === 200
        );

        // Should have 3 items again
        items = await listFragment.locator('li').count();
        expect(items).toBe(3);
    });

    test('should preserve event listeners during fragment updates', async ({ page }) => {
        // Click counter in fragment
        const button = page.locator('[data-lc-fragment="content"] button[data-click-counter]');
        const counter = page.locator('[data-lc-fragment="content"] .click-count');

        // Click button
        await button.click();
        await expect(counter).toHaveText('1');

        // Trigger fragment update
        await page.click('[data-lc-action="updateContent"]');

        await page.waitForResponse(response =>
            response.url().includes('/livecomponent') &&
            response.status() === 200
        );

        // Click button again - event listener should still work
        await button.click();
        await expect(counter).toHaveText('2');
    });

    test('should handle rapid fragment updates without conflicts', async ({ page }) => {
        const contentFragment = page.locator('[data-lc-fragment="content"]');

        // Trigger multiple updates rapidly
        await page.click('[data-lc-action="updateContent"]');
        await page.click('[data-lc-action="updateContent"]');
        await page.click('[data-lc-action="updateContent"]');

        // Wait for all responses
        await page.waitForLoadState('networkidle');

        // Fragment should be stable and visible
        await expect(contentFragment).toBeVisible();

        // Should have processed all updates
        const updateCount = await contentFragment.locator('.update-count').textContent();
        expect(parseInt(updateCount)).toBeGreaterThanOrEqual(3);
    });

    test('should emit fragment update events', async ({ page }) => {
        let fragmentUpdateEvents = [];

        // Listen for fragment update events
        await page.exposeFunction('onFragmentUpdate', (event) => {
            fragmentUpdateEvents.push(event);
        });

        await page.evaluate(() => {
            window.addEventListener('livecomponent:fragment-updated', (e) => {
                window.onFragmentUpdate(e.detail);
            });
        });

        // Trigger fragment update
        await page.click('[data-lc-action="updateContent"]');

        await page.waitForResponse(response =>
            response.url().includes('/livecomponent') &&
            response.status() === 200
        );

        // Wait for event
        await page.waitForTimeout(100);

        // Should have received fragment update event
        expect(fragmentUpdateEvents.length).toBeGreaterThan(0);
        expect(fragmentUpdateEvents[0]).toHaveProperty('fragmentName');
        expect(fragmentUpdateEvents[0].fragmentName).toBe('content');
    });

    test('should work with fragments containing special characters', async ({ page }) => {
        // Fragment with special characters in content
        const fragment = page.locator('[data-lc-fragment="special-chars"]');

        const initialContent = await fragment.innerHTML();

        // Update fragment
        await page.click('[data-lc-action="updateSpecialChars"]');

        await page.waitForResponse(response =>
            response.url().includes('/livecomponent') &&
            response.status() === 200
        );

        const updatedContent = await fragment.innerHTML();

        // Should properly handle HTML entities and special characters
        expect(updatedContent).not.toBe(initialContent);
        expect(updatedContent).toContain('&lt;'); // Escaped <
        expect(updatedContent).toContain('&amp;'); // Escaped &
    });

    test('should handle fragments with dynamic data-attributes', async ({ page }) => {
        const fragment = page.locator('[data-lc-fragment="dynamic"]');

        const initialDataValue = await fragment.getAttribute('data-custom-value');

        // Update fragment with new data attributes
        await page.click('[data-lc-action="updateDynamic"]');

        await page.waitForResponse(response =>
            response.url().includes('/livecomponent') &&
            response.status() === 200
        );

        const updatedDataValue = await fragment.getAttribute('data-custom-value');

        expect(updatedDataValue).not.toBe(initialDataValue);
    });
});
