/**
 * DomPatcher Tests
 *
 * Tests for lightweight DOM patching functionality used by LiveComponent
 * for efficient fragment-based partial rendering.
 */

import { DomPatcher, domPatcher } from '../../../../resources/js/modules/livecomponent/DomPatcher.js';

describe('DomPatcher', () => {
    let patcher;

    beforeEach(() => {
        patcher = new DomPatcher();
        document.body.innerHTML = '';
    });

    describe('patchFragment', () => {
        it('patches a single fragment successfully', () => {
            // Setup container with fragment
            const container = document.createElement('div');
            container.innerHTML = `
                <div data-lc-fragment="user-stats">
                    <h3>Statistics</h3>
                    <p>Count: 5</p>
                </div>
            `;
            document.body.appendChild(container);

            // Patch fragment with new content
            const newHtml = `
                <div data-lc-fragment="user-stats">
                    <h3>Statistics</h3>
                    <p>Count: 10</p>
                </div>
            `;

            const result = patcher.patchFragment(container, 'user-stats', newHtml);

            expect(result).toBe(true);
            expect(container.querySelector('[data-lc-fragment="user-stats"] p').textContent).toBe('Count: 10');
        });

        it('returns false when fragment not found', () => {
            const container = document.createElement('div');
            container.innerHTML = '<div>No fragments here</div>';

            const result = patcher.patchFragment(container, 'nonexistent', '<div data-lc-fragment="nonexistent">Test</div>');

            expect(result).toBe(false);
        });

        it('returns false for invalid HTML', () => {
            const container = document.createElement('div');
            container.innerHTML = '<div data-lc-fragment="test">Test</div>';

            const result = patcher.patchFragment(container, 'test', '');

            expect(result).toBe(false);
        });

        it('returns false when fragment name mismatch', () => {
            const container = document.createElement('div');
            container.innerHTML = '<div data-lc-fragment="fragment-a">Test</div>';

            const newHtml = '<div data-lc-fragment="fragment-b">Updated</div>';
            const result = patcher.patchFragment(container, 'fragment-a', newHtml);

            expect(result).toBe(false);
        });
    });

    describe('patchFragments', () => {
        it('patches multiple fragments at once', () => {
            const container = document.createElement('div');
            container.innerHTML = `
                <div data-lc-fragment="header">
                    <h1>Old Header</h1>
                </div>
                <div data-lc-fragment="content">
                    <p>Old Content</p>
                </div>
                <div data-lc-fragment="footer">
                    <p>Old Footer</p>
                </div>
            `;

            const fragments = {
                'header': '<div data-lc-fragment="header"><h1>New Header</h1></div>',
                'content': '<div data-lc-fragment="content"><p>New Content</p></div>'
            };

            const results = patcher.patchFragments(container, fragments);

            expect(results.header).toBe(true);
            expect(results.content).toBe(true);
            expect(container.querySelector('[data-lc-fragment="header"] h1').textContent).toBe('New Header');
            expect(container.querySelector('[data-lc-fragment="content"] p').textContent).toBe('New Content');
            expect(container.querySelector('[data-lc-fragment="footer"] p').textContent).toBe('Old Footer');
        });

        it('returns success status for each fragment', () => {
            const container = document.createElement('div');
            container.innerHTML = '<div data-lc-fragment="exists">Test</div>';

            const fragments = {
                'exists': '<div data-lc-fragment="exists">Updated</div>',
                'missing': '<div data-lc-fragment="missing">Not found</div>'
            };

            const results = patcher.patchFragments(container, fragments);

            expect(results.exists).toBe(true);
            expect(results.missing).toBe(false);
        });
    });

    describe('patchAttributes', () => {
        it('updates changed attributes', () => {
            const oldElement = document.createElement('div');
            oldElement.setAttribute('class', 'old-class');
            oldElement.setAttribute('data-value', '5');

            const newElement = document.createElement('div');
            newElement.setAttribute('class', 'new-class');
            newElement.setAttribute('data-value', '10');

            patcher.patchAttributes(oldElement, newElement);

            expect(oldElement.getAttribute('class')).toBe('new-class');
            expect(oldElement.getAttribute('data-value')).toBe('10');
        });

        it('adds new attributes', () => {
            const oldElement = document.createElement('div');
            oldElement.setAttribute('class', 'test');

            const newElement = document.createElement('div');
            newElement.setAttribute('class', 'test');
            newElement.setAttribute('data-new', 'value');

            patcher.patchAttributes(oldElement, newElement);

            expect(oldElement.getAttribute('data-new')).toBe('value');
        });

        it('removes attributes that no longer exist', () => {
            const oldElement = document.createElement('div');
            oldElement.setAttribute('class', 'test');
            oldElement.setAttribute('data-old', 'value');

            const newElement = document.createElement('div');
            newElement.setAttribute('class', 'test');

            patcher.patchAttributes(oldElement, newElement);

            expect(oldElement.hasAttribute('data-old')).toBe(false);
            expect(oldElement.getAttribute('class')).toBe('test');
        });

        it('does not update unchanged attributes', () => {
            const oldElement = document.createElement('div');
            oldElement.setAttribute('class', 'unchanged');
            const setAttributeSpy = jest.spyOn(oldElement, 'setAttribute');

            const newElement = document.createElement('div');
            newElement.setAttribute('class', 'unchanged');

            patcher.patchAttributes(oldElement, newElement);

            expect(setAttributeSpy).not.toHaveBeenCalledWith('class', 'unchanged');
        });
    });

    describe('patchChildren', () => {
        it('appends new child nodes', () => {
            const oldElement = document.createElement('div');
            oldElement.innerHTML = '<p>First</p>';

            const newElement = document.createElement('div');
            newElement.innerHTML = '<p>First</p><p>Second</p>';

            patcher.patchChildren(oldElement, newElement);

            expect(oldElement.children.length).toBe(2);
            expect(oldElement.children[1].textContent).toBe('Second');
        });

        it('removes deleted child nodes', () => {
            const oldElement = document.createElement('div');
            oldElement.innerHTML = '<p>First</p><p>Second</p>';

            const newElement = document.createElement('div');
            newElement.innerHTML = '<p>First</p>';

            patcher.patchChildren(oldElement, newElement);

            expect(oldElement.children.length).toBe(1);
            expect(oldElement.children[0].textContent).toBe('First');
        });

        it('updates text node content', () => {
            const oldElement = document.createElement('div');
            oldElement.textContent = 'Old text';

            const newElement = document.createElement('div');
            newElement.textContent = 'New text';

            patcher.patchChildren(oldElement, newElement);

            expect(oldElement.textContent).toBe('New text');
        });

        it('recursively patches nested elements', () => {
            const oldElement = document.createElement('div');
            oldElement.innerHTML = '<div><p>Old</p></div>';

            const newElement = document.createElement('div');
            newElement.innerHTML = '<div><p>New</p></div>';

            patcher.patchChildren(oldElement, newElement);

            expect(oldElement.querySelector('p').textContent).toBe('New');
        });

        it('replaces elements with different tags', () => {
            const oldElement = document.createElement('div');
            oldElement.innerHTML = '<p>Content</p>';

            const newElement = document.createElement('div');
            newElement.innerHTML = '<span>Content</span>';

            patcher.patchChildren(oldElement, newElement);

            expect(oldElement.children[0].tagName).toBe('SPAN');
        });
    });

    describe('shouldPatch', () => {
        it('returns false for different node types', () => {
            const textNode = document.createTextNode('text');
            const elementNode = document.createElement('div');

            expect(patcher.shouldPatch(textNode, elementNode)).toBe(false);
        });

        it('returns true for text nodes', () => {
            const oldText = document.createTextNode('old');
            const newText = document.createTextNode('new');

            expect(patcher.shouldPatch(oldText, newText)).toBe(true);
        });

        it('returns false for different element tags', () => {
            const divElement = document.createElement('div');
            const spanElement = document.createElement('span');

            expect(patcher.shouldPatch(divElement, spanElement)).toBe(false);
        });

        it('returns true for same element tags', () => {
            const div1 = document.createElement('div');
            const div2 = document.createElement('div');

            expect(patcher.shouldPatch(div1, div2)).toBe(true);
        });

        it('respects data-lc-key for element identity', () => {
            const element1 = document.createElement('div');
            element1.setAttribute('data-lc-key', 'key-1');

            const element2 = document.createElement('div');
            element2.setAttribute('data-lc-key', 'key-2');

            expect(patcher.shouldPatch(element1, element2)).toBe(false);
        });

        it('patches elements with matching data-lc-key', () => {
            const element1 = document.createElement('div');
            element1.setAttribute('data-lc-key', 'same-key');

            const element2 = document.createElement('div');
            element2.setAttribute('data-lc-key', 'same-key');

            expect(patcher.shouldPatch(element1, element2)).toBe(true);
        });

        it('uses id as fallback key', () => {
            const element1 = document.createElement('div');
            element1.id = 'element-1';

            const element2 = document.createElement('div');
            element2.id = 'element-2';

            expect(patcher.shouldPatch(element1, element2)).toBe(false);
        });
    });

    describe('preserveFocus', () => {
        it('preserves focused input element', () => {
            const container = document.createElement('div');
            const input = document.createElement('input');
            input.id = 'test-input';
            input.value = 'Test';
            container.appendChild(input);
            document.body.appendChild(container);

            input.focus();
            input.setSelectionRange(2, 4);

            const restoreFocus = patcher.preserveFocus(container);

            // Simulate patching that might lose focus
            document.body.focus();

            restoreFocus();

            expect(document.activeElement).toBe(input);
            expect(input.selectionStart).toBe(2);
            expect(input.selectionEnd).toBe(4);
        });

        it('returns no-op when no element is focused', () => {
            const container = document.createElement('div');
            document.body.appendChild(container);

            const restoreFocus = patcher.preserveFocus(container);

            expect(typeof restoreFocus).toBe('function');
            expect(() => restoreFocus()).not.toThrow();
        });

        it('returns no-op when focused element is outside container', () => {
            const container = document.createElement('div');
            const outsideInput = document.createElement('input');
            document.body.appendChild(container);
            document.body.appendChild(outsideInput);

            outsideInput.focus();

            const restoreFocus = patcher.preserveFocus(container);

            expect(typeof restoreFocus).toBe('function');
            expect(() => restoreFocus()).not.toThrow();
        });

        it('handles focus restoration failure gracefully', () => {
            const container = document.createElement('div');
            const input = document.createElement('input');
            input.name = 'test';
            container.appendChild(input);
            document.body.appendChild(container);

            input.focus();

            const restoreFocus = patcher.preserveFocus(container);

            // Remove the input before restoration
            container.removeChild(input);

            expect(() => restoreFocus()).not.toThrow();
        });
    });

    describe('getElementSelector', () => {
        it('uses id as primary selector', () => {
            const element = document.createElement('div');
            element.id = 'unique-id';
            element.name = 'test-name';

            expect(patcher.getElementSelector(element)).toBe('#unique-id');
        });

        it('uses name as fallback selector', () => {
            const element = document.createElement('input');
            element.name = 'field-name';

            expect(patcher.getElementSelector(element)).toBe('[name="field-name"]');
        });

        it('uses data-lc-key as selector', () => {
            const element = document.createElement('div');
            element.setAttribute('data-lc-key', 'item-123');

            expect(patcher.getElementSelector(element)).toBe('[data-lc-key="item-123"]');
        });

        it('falls back to tag name', () => {
            const element = document.createElement('span');

            expect(patcher.getElementSelector(element)).toBe('span');
        });
    });

    describe('singleton instance', () => {
        it('exports singleton domPatcher instance', () => {
            expect(domPatcher).toBeInstanceOf(DomPatcher);
        });

        it('singleton is the same instance', () => {
            const instance1 = domPatcher;
            const instance2 = domPatcher;

            expect(instance1).toBe(instance2);
        });
    });

    describe('complex patching scenarios', () => {
        it('patches nested fragments with multiple changes', () => {
            const container = document.createElement('div');
            container.innerHTML = `
                <div data-lc-fragment="card">
                    <h2 class="old-title">Old Title</h2>
                    <div class="content">
                        <p>Old paragraph 1</p>
                        <p>Old paragraph 2</p>
                    </div>
                    <button data-action="old">Old Button</button>
                </div>
            `;

            const newHtml = `
                <div data-lc-fragment="card">
                    <h2 class="new-title">New Title</h2>
                    <div class="content">
                        <p>New paragraph 1</p>
                        <p>New paragraph 2</p>
                        <p>New paragraph 3</p>
                    </div>
                    <button data-action="new">New Button</button>
                </div>
            `;

            patcher.patchFragment(container, 'card', newHtml);

            const fragment = container.querySelector('[data-lc-fragment="card"]');
            expect(fragment.querySelector('h2').textContent).toBe('New Title');
            expect(fragment.querySelector('h2').className).toBe('new-title');
            expect(fragment.querySelectorAll('.content p').length).toBe(3);
            expect(fragment.querySelector('button').getAttribute('data-action')).toBe('new');
        });

        it('handles whitespace and formatting changes', () => {
            const container = document.createElement('div');
            container.innerHTML = '<div data-lc-fragment="test"><p>Content</p></div>';

            const newHtml = `
                <div data-lc-fragment="test">
                    <p>Content</p>
                </div>
            `;

            const result = patcher.patchFragment(container, 'test', newHtml);

            expect(result).toBe(true);
            expect(container.querySelector('p').textContent).toBe('Content');
        });

        it('preserves event listeners on patched elements', () => {
            const container = document.createElement('div');
            container.innerHTML = '<div data-lc-fragment="test"><button id="btn">Click</button></div>';
            document.body.appendChild(container);

            const button = container.querySelector('#btn');
            let clicked = false;
            button.addEventListener('click', () => { clicked = true; });

            const newHtml = '<div data-lc-fragment="test"><button id="btn">Updated</button></div>';
            patcher.patchFragment(container, 'test', newHtml);

            // Event listener is preserved because element is patched, not replaced
            container.querySelector('#btn').click();
            expect(clicked).toBe(true);
        });
    });

    describe('performance characteristics', () => {
        it('patches large fragment efficiently', () => {
            const container = document.createElement('div');
            const itemCount = 100;

            // Build initial list
            const items = [];
            for (let i = 0; i < itemCount; i++) {
                items.push(`<li data-lc-key="item-${i}">Item ${i}</li>`);
            }
            container.innerHTML = `<ul data-lc-fragment="list">${items.join('')}</ul>`;

            // Update half the items
            const updatedItems = [];
            for (let i = 0; i < itemCount; i++) {
                if (i % 2 === 0) {
                    updatedItems.push(`<li data-lc-key="item-${i}">Updated Item ${i}</li>`);
                } else {
                    updatedItems.push(`<li data-lc-key="item-${i}">Item ${i}</li>`);
                }
            }
            const newHtml = `<ul data-lc-fragment="list">${updatedItems.join('')}</ul>`;

            const startTime = performance.now();
            patcher.patchFragment(container, 'list', newHtml);
            const duration = performance.now() - startTime;

            // Should complete in reasonable time (< 100ms for 100 items)
            expect(duration).toBeLessThan(100);

            // Verify updates applied
            const firstItem = container.querySelector('[data-lc-key="item-0"]');
            expect(firstItem.textContent).toBe('Updated Item 0');
        });
    });
});
