/**
 * Unit tests for HTMLPreservationEngine
 * Tests HTML preservation, sanitization, and attribute maintenance
 */
import { HTMLPreservationEngine } from './html-preservation.js';

// Mock DOM environment for testing
const mockDocument = {
    createElement: (tagName) => ({
        tagName: tagName.toUpperCase(),
        innerHTML: '',
        textContent: '',
        children: [],
        childNodes: [],
        attributes: [],
        classList: new Set(),
        
        // Mock methods
        appendChild: function(child) { this.children.push(child); },
        removeChild: function(child) { 
            const index = this.children.indexOf(child);
            if (index > -1) this.children.splice(index, 1);
        },
        replaceChild: function(newChild, oldChild) {
            const index = this.children.indexOf(oldChild);
            if (index > -1) this.children[index] = newChild;
        },
        querySelector: function(selector) { return null; },
        querySelectorAll: function(selector) { return []; },
        cloneNode: function(deep) { return mockDocument.createElement(tagName); },
        setAttribute: function(name, value) { this.attributes[name] = value; },
        getAttribute: function(name) { return this.attributes[name]; },
        removeAttribute: function(name) { delete this.attributes[name]; }
    }),
    createTextNode: (text) => ({
        nodeType: 3, // TEXT_NODE
        textContent: text
    })
};

global.document = mockDocument;
global.Node = {
    TEXT_NODE: 3,
    ELEMENT_NODE: 1
};

describe('HTMLPreservationEngine', () => {
    let engine;
    
    beforeEach(() => {
        engine = new HTMLPreservationEngine();
    });

    describe('Content Extraction', () => {
        test('should extract content with preservation metadata', () => {
            const mockElement = createMockElement('p', {
                innerHTML: 'Hello <strong class="emph">world</strong>!',
                classes: ['insertr'],
                attributes: { id: 'test-element' }
            });

            const extracted = engine.extractForEditing(mockElement);
            
            expect(extracted.html).toBe('Hello <strong class="emph">world</strong>!');
            expect(extracted.text).toBe('Hello world!');
            expect(extracted.containerAttributes.class).toBe('insertr');
            expect(extracted.containerAttributes.id).toBe('test-element');
            expect(extracted.elementTag).toBe('p');
            expect(extracted.hasNestedElements).toBe(true);
        });

        test('should handle simple text content', () => {
            const mockElement = createMockElement('p', {
                innerHTML: 'Simple text content',
                textContent: 'Simple text content'
            });

            const extracted = engine.extractForEditing(mockElement);
            
            expect(extracted.html).toBe('Simple text content');
            expect(extracted.text).toBe('Simple text content');
            expect(extracted.hasNestedElements).toBe(false);
        });
    });

    describe('HTML Validation and Sanitization', () => {
        test('should validate safe HTML', () => {
            const safeHTML = 'Hello <strong class="emph">world</strong>!';
            expect(engine.isValidHTML(safeHTML)).toBe(true);
        });

        test('should reject dangerous HTML', () => {
            const dangerousHTML = 'Hello <script>alert("xss")</script> world!';
            expect(engine.isValidHTML(dangerousHTML)).toBe(false);
        });

        test('should sanitize HTML by removing dangerous elements', () => {
            const unsafeHTML = 'Hello <script>alert("xss")</script><strong>safe</strong>';
            const sanitized = engine.validateAndSanitizeHTML(unsafeHTML);
            
            expect(sanitized).not.toContain('<script>');
            expect(sanitized).toContain('<strong>safe</strong>');
        });

        test('should preserve allowed attributes', () => {
            const htmlWithAttrs = '<a href="#test" class="fancy" data-track="click">Link</a>';
            const sanitized = engine.validateAndSanitizeHTML(htmlWithAttrs);
            
            expect(sanitized).toContain('href="#test"');
            expect(sanitized).toContain('class="fancy"');
            expect(sanitized).toContain('data-track="click"');
        });

        test('should remove dangerous href attributes', () => {
            const htmlWithDangerousHref = '<a href="javascript:alert(1)">Bad Link</a>';
            const sanitized = engine.validateAndSanitizeHTML(htmlWithDangerousHref);
            
            expect(sanitized).not.toContain('javascript:');
        });
    });

    describe('Attribute Preservation', () => {
        test('should extract all element attributes', () => {
            const mockElement = createMockElement('div', {
                attributes: {
                    class: 'insertr test',
                    id: 'unique-id',
                    'data-value': '123',
                    'aria-label': 'Test element'
                }
            });

            const attributes = engine.extractElementAttributes(mockElement);
            
            expect(attributes.class).toBe('insertr test');
            expect(attributes.id).toBe('unique-id');
            expect(attributes['data-value']).toBe('123');
            expect(attributes['aria-label']).toBe('Test element');
        });

        test('should restore element attributes correctly', () => {
            const mockElement = createMockElement('div');
            const attributes = {
                class: 'restored',
                id: 'restored-id',
                'data-test': 'value'
            };

            engine.restoreElementAttributes(mockElement, attributes);
            
            expect(mockElement.attributes.class).toBe('restored');
            expect(mockElement.attributes.id).toBe('restored-id');
            expect(mockElement.attributes['data-test']).toBe('value');
        });
    });

    describe('Content Application', () => {
        test('should apply HTML content while preserving container attributes', () => {
            const mockElement = createMockElement('p', {
                classes: ['insertr', 'special'],
                attributes: { id: 'preserved-id' }
            });

            const newHTML = 'Updated <strong class="emph">content</strong>!';
            const success = engine.applyFromEditing(mockElement, newHTML);
            
            expect(success).toBe(true);
            expect(mockElement.innerHTML).toBe('Updated <strong class="emph">content</strong>!');
            // Container attributes should remain unchanged
            expect(mockElement.classList.has('insertr')).toBe(true);
            expect(mockElement.classList.has('special')).toBe(true);
        });

        test('should handle invalid HTML gracefully', () => {
            const mockElement = createMockElement('p');
            const invalidHTML = '<script>malicious()</script>';
            
            const success = engine.applyFromEditing(mockElement, invalidHTML);
            
            // Should handle gracefully (either sanitize or reject)
            expect(typeof success).toBe('boolean');
        });
    });

    describe('Plain Text Extraction', () => {
        test('should extract plain text preserving structure', () => {
            const mockElement = createMockElement('p', {
                textContent: 'Hello world and welcome!'
            });

            const plainText = engine.extractPlainTextWithStructure(mockElement);
            expect(plainText).toBe('Hello world and welcome!');
        });

        test('should handle complex nested content', () => {
            const mockElement = createMockElement('p', {
                children: [
                    { nodeType: 3, textContent: 'Hello ' },
                    { nodeType: 1, textContent: 'world' },
                    { nodeType: 3, textContent: ' and welcome!' }
                ],
                childNodes: [
                    { nodeType: 3, textContent: 'Hello ' },
                    { nodeType: 1, textContent: 'world' },
                    { nodeType: 3, textContent: ' and welcome!' }
                ]
            });

            const plainText = engine.extractPlainTextWithStructure(mockElement);
            expect(plainText).toBe('Hello world and welcome!');
        });
    });

    describe('Editable Content Preparation', () => {
        test('should prepare content for safe editing', () => {
            const mockElement = createMockElement('p', {
                innerHTML: 'Hello <strong class="emph">world</strong>!',
                children: [{ tagName: 'STRONG' }]
            });

            const prepared = engine.prepareForEditing(mockElement);
            
            expect(prepared.html).toBe('Hello <strong class="emph">world</strong>!');
            expect(prepared.editableHTML).toBeDefined();
            expect(prepared.isComplex).toBe(true);
            expect(prepared.originalHTML).toBe('Hello <strong class="emph">world</strong>!');
        });
    });

    describe('Content Finalization', () => {
        test('should finalize string content', () => {
            const mockElement = createMockElement('p');
            const editedContent = 'New <strong>content</strong>';
            
            const success = engine.finalizeEditing(mockElement, editedContent);
            expect(success).toBe(true);
        });

        test('should finalize object content', () => {
            const mockElement = createMockElement('p');
            const editedContent = {
                html: 'New <strong>content</strong>',
                text: 'New content'
            };
            
            const success = engine.finalizeEditing(mockElement, editedContent);
            expect(success).toBe(true);
        });

        test('should handle invalid content gracefully', () => {
            const mockElement = createMockElement('p');
            const invalidContent = null;
            
            const success = engine.finalizeEditing(mockElement, invalidContent);
            expect(success).toBe(false);
        });
    });

    describe('Security and Safety', () => {
        test('should allow safe tags', () => {
            const safeTags = ['strong', 'em', 'a', 'span', 'p', 'div', 'h1', 'h2', 'h3'];
            safeTags.forEach(tag => {
                expect(engine.allowedTags.has(tag)).toBe(true);
            });
        });

        test('should allow safe attributes', () => {
            const safeAttrs = ['class', 'id', 'href', 'title', 'data-test', 'aria-label'];
            safeAttrs.forEach(attr => {
                expect(
                    engine.allowedAttributes.has(attr) || 
                    attr.startsWith('data-') || 
                    attr.startsWith('aria-')
                ).toBe(true);
            });
        });

        test('should create safe editable copy', () => {
            const unsafeHTML = '<p>Safe content</p><script>alert("unsafe")</script>';
            const safeCopy = engine.createEditableCopy(unsafeHTML);
            
            expect(safeCopy).toContain('<p>Safe content</p>');
            expect(safeCopy).not.toContain('<script>');
        });
    });

    describe('Merge with Preservation', () => {
        test('should merge content while preserving specific elements', () => {
            const originalHTML = '<p>Hello <span class="preserve">world</span>!</p>';
            const editedHTML = '<p>Hi <span class="preserve">universe</span>!</p>';
            const preserveSelectors = ['.preserve'];
            
            const merged = engine.mergeWithPreservation(originalHTML, editedHTML, preserveSelectors);
            
            // Should preserve the original .preserve element
            expect(merged).toContain('world'); // Original preserved content
        });
    });
});

// Helper function to create mock DOM elements for testing
function createMockElement(tagName, options = {}) {
    const element = {
        tagName: tagName.toUpperCase(),
        innerHTML: options.innerHTML || '',
        textContent: options.textContent || options.innerHTML?.replace(/<[^>]*>/g, '') || '',
        children: options.children || [],
        childNodes: options.childNodes || options.children || [],
        classList: new Set(options.classes || []),
        attributes: { ...options.attributes } || {},
        
        // Mock methods
        querySelector: () => null,
        querySelectorAll: () => [],
        appendChild: function(child) { this.children.push(child); },
        removeChild: function(child) { 
            const index = this.children.indexOf(child);
            if (index > -1) this.children.splice(index, 1);
        },
        replaceWith: function(newElement) {
            // Mock implementation
        },
        cloneNode: function(deep) {
            return createMockElement(tagName, options);
        },
        setAttribute: function(name, value) {
            this.attributes[name] = value;
        },
        getAttribute: function(name) {
            return this.attributes[name];
        },
        removeAttribute: function(name) {
            delete this.attributes[name];
        }
    };

    // Add length property to children for proper iteration
    Object.defineProperty(element.children, 'length', {
        get: function() { return this.filter(Boolean).length; }
    });

    // Add classList methods
    element.classList.has = (className) => element.classList.has(className);
    element.classList.contains = (className) => element.classList.has(className);
    element.classList.add = (className) => element.classList.add(className);

    return element;
}