/**
 * CoreArchitectureTests - Tests for core architecture components
 * Validates Module.js, EventBus.js, ModuleLoader.js, Router.js, Application.js
 */

import { TestFramework } from '../TestFramework.js';

// We'll dynamically import core modules to avoid dependency issues
let Module, EventBus, ModuleLoader, Router, Application;

const testFramework = new TestFramework();

// Test Module.js - Abstract base class
testFramework.describe('Module.js - Abstract Base Class', function() {

    this.beforeAll(async () => {
        try {
            Module = (await import('../../core/Module.js')).default;
        } catch (error) {
            console.warn('Could not import Module.js, using mock for tests');
            Module = class MockModule {
                constructor(name, dependencies) {
                    if (this.constructor === Module) {
                        throw new Error('Module is abstract and cannot be instantiated directly');
                    }
                    this.name = name;
                    this.dependencies = dependencies || [];
                    this._initialized = false;
                    this._destroyed = false;
                }

                _validateNotDestroyed() {
                    if (this._destroyed) throw new Error('Module has been destroyed');
                }

                _setInitialized() { this._initialized = true; }
                _setDestroyed() { this._destroyed = true; }

                async init() { throw new Error('init() must be implemented by subclass'); }
                async destroy() { throw new Error('destroy() must be implemented by subclass'); }
            };
        }
    });

    this.it('should not allow direct instantiation of abstract Module class', (assert) => {
        assert.assertThrows(
            () => new Module('test', []),
            'Module is abstract and cannot be instantiated directly',
            'Module class should be abstract'
        );
    });

    this.it('should allow subclass instantiation with proper parameters', (assert) => {
        class TestModule extends Module {
            constructor(name, dependencies) {
                super(name, dependencies);
                Object.seal(this);
            }
            async init() { this._setInitialized(); }
            async destroy() { this._setDestroyed(); }
        }

        const testModule = new TestModule('testModule', ['eventBus']);

        assert.assertEqual(testModule.name, 'testModule');
        assert.assertContains(testModule.dependencies, 'eventBus');
        assert.assertFalse(testModule._initialized);
        assert.assertFalse(testModule._destroyed);
    });

    this.it('should enforce abstract method implementation', (assert) => {
        class IncompleteModule extends Module {
            constructor(name, dependencies) {
                super(name, dependencies);
            }
            // Missing init() and destroy() implementations
        }

        const incompleteModule = new IncompleteModule('incomplete', []);

        assert.assertThrows(
            () => incompleteModule.init(),
            'Module incomplete: init() method must be implemented'
        );

        assert.assertThrows(
            () => incompleteModule.destroy(),
            'Module incomplete: destroy() method must be implemented'
        );
    });

    this.it('should handle module lifecycle correctly', async (assert) => {
        class LifecycleModule extends Module {
            constructor(name, dependencies) {
                super(name, dependencies);
                Object.seal(this);
            }

            async init() {
                this._validateNotDestroyed();
                this._setInitialized();
            }

            async destroy() {
                this._validateNotDestroyed();
                this._setDestroyed();
            }
        }

        const module = new LifecycleModule('lifecycle', []);

        // Initially not initialized or destroyed
        assert.assertFalse(module._initialized);
        assert.assertFalse(module._destroyed);

        // After init
        await module.init();
        assert.assertTrue(module._initialized);
        assert.assertFalse(module._destroyed);

        // After destroy
        await module.destroy();
        assert.assertTrue(module._destroyed);

        // Should not allow operations after destroy
        assert.assertThrows(
            () => module.init(),
            'Module has been destroyed'
        );
    });

    this.it('should prevent modification after sealing', (assert) => {
        class SealedModule extends Module {
            constructor(name, dependencies) {
                super(name, dependencies);
                Object.seal(this);
            }
            async init() { this._setInitialized(); }
            async destroy() { this._setDestroyed(); }
        }

        const module = new SealedModule('sealed', []);

        // Try to add new properties (should fail silently in non-strict mode)
        module.newProperty = 'should not work';
        assert.assertUndefined(module.newProperty, 'Sealed object should not accept new properties');
    });
});

// Test EventBus.js - Event communication system
testFramework.describe('EventBus.js - Event Communication System', function() {

    this.beforeAll(async () => {
        try {
            EventBus = (await import('../../core/EventBus.js')).default;
        } catch (error) {
            console.warn('Could not import EventBus.js, using mock for tests');
            EventBus = class MockEventBus {
                constructor() {
                    this.events = new Map();
                    this.registeredModules = new Set();
                    this.eventHistory = [];
                }

                registerModule(moduleName) {
                    this.registeredModules.add(moduleName);
                }

                on(eventName, callback, moduleName) {
                    if (!this.registeredModules.has(moduleName)) {
                        throw new Error('EventBus requires module registration before use');
                    }
                    if (!this.events.has(eventName)) {
                        this.events.set(eventName, []);
                    }
                    this.events.get(eventName).push({ callback, moduleName });
                }

                emit(eventName, data, senderModule) {
                    const event = { eventName, data, senderModule, timestamp: Date.now() };
                    this.eventHistory.push(event);

                    if (this.events.has(eventName)) {
                        this.events.get(eventName).forEach(({ callback }) => {
                            try { callback(event); } catch (error) { console.error(error); }
                        });
                    }
                }

                off(eventName, callback, moduleName) {
                    if (this.events.has(eventName)) {
                        const listeners = this.events.get(eventName);
                        this.events.set(eventName, listeners.filter(
                            listener => !(listener.callback === callback && listener.moduleName === moduleName)
                        ));
                    }
                }

                getEventHistory() { return this.eventHistory; }
            };
        }
    });

    this.it('should require module registration before event usage', (assert) => {
        const eventBus = new EventBus();

        assert.assertThrows(
            () => eventBus.on('test:event', () => {}, 'unregisteredModule'),
            'Module unregisteredModule is not registered with EventBus'
        );
    });

    this.it('should allow event registration and emission after module registration', (assert) => {
        const eventBus = new EventBus();
        let eventReceived = false;
        let eventData = null;

        // Register module
        const testModule = { name: 'testModule' };
        eventBus.registerModule(testModule);

        // Register event listener
        eventBus.on('test:event', (event) => {
            eventReceived = true;
            eventData = event.data;
        }, 'testModule');

        // Emit event
        eventBus.emit('test:event', { message: 'hello' }, 'testModule');

        assert.assertTrue(eventReceived, 'Event should have been received');
        assert.assertEqual(eventData.message, 'hello', 'Event data should be correct');
    });

    this.it('should maintain event history', (assert) => {
        const eventBus = new EventBus();

        const historyModule = { name: 'historyModule' };
        eventBus.registerModule(historyModule);
        eventBus.emit('history:test1', { id: 1 }, 'historyModule');
        eventBus.emit('history:test2', { id: 2 }, 'historyModule');

        const history = eventBus.getEventHistory();
        assert.assertEqual(history.length, 2, 'Should have 2 events in history');
        assert.assertEqual(history[0].eventName, 'history:test1');
        assert.assertEqual(history[1].eventName, 'history:test2');
    });

    this.it('should allow event listener removal', (assert) => {
        const eventBus = new EventBus();
        let callCount = 0;

        const removalModule = { name: 'removalModule' };
        eventBus.registerModule(removalModule);

        const listener = () => { callCount++; };
        eventBus.on('removal:test', listener, 'removalModule');

        // Emit once
        eventBus.emit('removal:test', {}, 'removalModule');
        assert.assertEqual(callCount, 1, 'Should have been called once');

        // Remove listener and emit again
        eventBus.off('removal:test', listener, 'removalModule');
        eventBus.emit('removal:test', {}, 'removalModule');
        assert.assertEqual(callCount, 1, 'Should still be 1 after removal');
    });

    this.it('should handle errors in event callbacks gracefully', (assert) => {
        const eventBus = new EventBus();
        let goodListenerCalled = false;

        const errorModule = { name: 'errorModule' };
        eventBus.registerModule(errorModule);

        // Add a listener that throws
        eventBus.on('error:test', () => {
            throw new Error('Test error');
        }, 'errorModule');

        // Add a good listener
        eventBus.on('error:test', () => {
            goodListenerCalled = true;
        }, 'errorModule');

        // This should not throw, even with error in first listener
        assert.assertDoesNotThrow(
            () => eventBus.emit('error:test', {}, 'errorModule'),
            'EventBus should handle listener errors gracefully'
        );

        assert.assertTrue(goodListenerCalled, 'Good listener should still be called');
    });
});

// Test Module Isolation
testFramework.describe('Module Isolation and Architecture Integrity', function() {

    this.it('should enforce zero direct dependencies between modules', (assert) => {
        // This test validates that modules can only communicate via EventBus

        class ModuleA extends (Module || class {}) {
            constructor() {
                super('moduleA', ['eventBus']);
                this.eventBus = null;
                this.receivedMessages = [];
                Object.seal(this);
            }

            setEventBus(eventBus) {
                this.eventBus = eventBus;
                const moduleA = { name: 'moduleA' };
                this.eventBus.registerModule(moduleA);
                this.eventBus.on('moduleB:message', (event) => {
                    this.receivedMessages.push(event.data);
                }, 'moduleA');
            }

            async init() { this._setInitialized && this._setInitialized(); }
            async destroy() { this._setDestroyed && this._setDestroyed(); }

            sendMessage(message) {
                this.eventBus.emit('moduleA:message', { message }, 'moduleA');
            }
        }

        class ModuleB extends (Module || class {}) {
            constructor() {
                super('moduleB', ['eventBus']);
                this.eventBus = null;
                this.receivedMessages = [];
                Object.seal(this);
            }

            setEventBus(eventBus) {
                this.eventBus = eventBus;
                const moduleB = { name: 'moduleB' };
                this.eventBus.registerModule(moduleB);
                this.eventBus.on('moduleA:message', (event) => {
                    this.receivedMessages.push(event.data);
                    // Respond back
                    this.eventBus.emit('moduleB:message', { response: 'received' }, 'moduleB');
                }, 'moduleB');
            }

            async init() { this._setInitialized && this._setInitialized(); }
            async destroy() { this._setDestroyed && this._setDestroyed(); }
        }

        const eventBus = new EventBus();
        const moduleA = new ModuleA();
        const moduleB = new ModuleB();

        moduleA.setEventBus(eventBus);
        moduleB.setEventBus(eventBus);

        // ModuleA sends message to ModuleB via EventBus
        moduleA.sendMessage('hello');

        assert.assertEqual(moduleB.receivedMessages.length, 1, 'ModuleB should receive message via EventBus');
        assert.assertEqual(moduleA.receivedMessages.length, 1, 'ModuleA should receive response via EventBus');
        assert.assertEqual(moduleB.receivedMessages[0].message, 'hello');
        assert.assertEqual(moduleA.receivedMessages[0].response, 'received');
    });

    this.it('should prevent external modification of module internals', (assert) => {
        class ProtectedModule extends (Module || class {}) {
            constructor() {
                super('protected', []);
                // Private data should be truly private (WeakMap would be ideal)
                this._privateData = { secret: 'hidden' };
                Object.seal(this);
            }

            async init() { this._setInitialized && this._setInitialized(); }
            async destroy() { this._setDestroyed && this._setDestroyed(); }

            getPublicData() {
                return { name: this.name };
            }
        }

        const module = new ProtectedModule();

        // Should not be able to add new properties
        module.newProperty = 'should not work';
        assert.assertUndefined(module.newProperty, 'Should not be able to add properties to sealed module');

        // Should not be able to modify existing properties (in strict mode)
        const originalName = module.name;
        module.name = 'modified';
        // In some environments this might work due to non-strict mode, but architecture should prevent it

        // Should be able to call public methods
        const publicData = module.getPublicData();
        assert.assertDefined(publicData.name, 'Public method should work');
    });
});

export { testFramework as CoreArchitectureTests };