/**
 * TestFramework - Lightweight testing framework for the modular architecture
 * Tests core architecture and modules with detailed reporting
 */

class TestFramework {
    constructor() {
        this.tests = [];
        this.results = [];
        this.suites = new Map();
        this.currentSuite = null;
        this.totalTests = 0;
        this.passedTests = 0;
        this.failedTests = 0;
        this.startTime = null;
        this.endTime = null;

        // Test states
        this.STATES = {
            PENDING: 'pending',
            RUNNING: 'running',
            PASSED: 'passed',
            FAILED: 'failed',
            SKIPPED: 'skipped'
        };
    }

    /**
     * Create a test suite
     * @param {string} suiteName - Name of the test suite
     * @param {Function} callback - Function containing tests
     */
    describe(suiteName, callback) {
        console.log(`📝 Setting up test suite: ${suiteName}`);

        const suite = {
            name: suiteName,
            tests: [],
            beforeEach: null,
            afterEach: null,
            beforeAll: null,
            afterAll: null,
            results: {
                total: 0,
                passed: 0,
                failed: 0,
                skipped: 0
            }
        };

        this.suites.set(suiteName, suite);
        this.currentSuite = suite;

        // Execute the callback to register tests
        if (callback && typeof callback === 'function') {
            callback.call(this);
        }

        this.currentSuite = null;
        return suite;
    }

    /**
     * Register a test
     * @param {string} testName - Name of the test
     * @param {Function} testFunction - Test function to execute
     */
    it(testName, testFunction) {
        if (!this.currentSuite) {
            throw new Error('Test must be inside a describe block');
        }

        const test = {
            id: `${this.currentSuite.name}::${testName}`,
            name: testName,
            suite: this.currentSuite.name,
            fn: testFunction,
            state: this.STATES.PENDING,
            error: null,
            duration: 0,
            timestamp: null
        };

        this.currentSuite.tests.push(test);
        this.tests.push(test);
        return test;
    }

    /**
     * Setup function to run before each test in current suite
     * @param {Function} fn - Setup function
     */
    beforeEach(fn) {
        if (!this.currentSuite) {
            throw new Error('beforeEach must be inside a describe block');
        }
        this.currentSuite.beforeEach = fn;
    }

    /**
     * Teardown function to run after each test in current suite
     * @param {Function} fn - Teardown function
     */
    afterEach(fn) {
        if (!this.currentSuite) {
            throw new Error('afterEach must be inside a describe block');
        }
        this.currentSuite.afterEach = fn;
    }

    /**
     * Setup function to run before all tests in current suite
     * @param {Function} fn - Setup function
     */
    beforeAll(fn) {
        if (!this.currentSuite) {
            throw new Error('beforeAll must be inside a describe block');
        }
        this.currentSuite.beforeAll = fn;
    }

    /**
     * Teardown function to run after all tests in current suite
     * @param {Function} fn - Teardown function
     */
    afterAll(fn) {
        if (!this.currentSuite) {
            throw new Error('afterAll must be inside a describe block');
        }
        this.currentSuite.afterAll = fn;
    }

    /**
     * Run all registered tests
     * @returns {Promise<Object>} - Test results summary
     */
    async runAll() {
        console.log('🚀 Starting Test Framework execution...');
        this.startTime = Date.now();

        // Reset counters
        this.totalTests = this.tests.length;
        this.passedTests = 0;
        this.failedTests = 0;
        this.results = [];

        // Run each suite
        for (const [suiteName, suite] of this.suites) {
            await this._runSuite(suite);
        }

        this.endTime = Date.now();
        const duration = this.endTime - this.startTime;

        const summary = {
            total: this.totalTests,
            passed: this.passedTests,
            failed: this.failedTests,
            skipped: this.totalTests - this.passedTests - this.failedTests,
            duration: duration,
            suites: Array.from(this.suites.values()).map(suite => ({
                name: suite.name,
                results: suite.results
            })),
            details: this.results
        };

        this._printSummary(summary);
        return summary;
    }

    /**
     * Run a specific test suite
     * @param {Object} suite - Test suite to run
     * @private
     */
    async _runSuite(suite) {
        console.log(`\n📦 Running suite: ${suite.name}`);

        try {
            // Run beforeAll if exists
            if (suite.beforeAll) {
                await suite.beforeAll();
            }

            // Run each test in the suite
            for (const test of suite.tests) {
                await this._runTest(test, suite);
            }

            // Run afterAll if exists
            if (suite.afterAll) {
                await suite.afterAll();
            }

        } catch (error) {
            console.error(`❌ Suite ${suite.name} setup/teardown failed:`, error);
        }
    }

    /**
     * Run a single test
     * @param {Object} test - Test to run
     * @param {Object} suite - Suite containing the test
     * @private
     */
    async _runTest(test, suite) {
        const startTime = Date.now();
        test.state = this.STATES.RUNNING;
        test.timestamp = new Date().toISOString();

        try {
            // Run beforeEach if exists
            if (suite.beforeEach) {
                await suite.beforeEach();
            }

            // Run the actual test
            await test.fn(this._createAssertions());

            // Test passed
            test.state = this.STATES.PASSED;
            test.duration = Date.now() - startTime;
            this.passedTests++;
            suite.results.passed++;

            console.log(`  ✅ ${test.name} (${test.duration}ms)`);

            // Run afterEach if exists
            if (suite.afterEach) {
                await suite.afterEach();
            }

        } catch (error) {
            // Test failed
            test.state = this.STATES.FAILED;
            test.error = {
                message: error.message,
                stack: error.stack,
                name: error.name
            };
            test.duration = Date.now() - startTime;
            this.failedTests++;
            suite.results.failed++;

            console.error(`  ❌ ${test.name} (${test.duration}ms)`);
            console.error(`     Error: ${error.message}`);

            // Run afterEach even if test failed
            try {
                if (suite.afterEach) {
                    await suite.afterEach();
                }
            } catch (teardownError) {
                console.error(`     Teardown error: ${teardownError.message}`);
            }
        }

        suite.results.total++;
        this.results.push({
            id: test.id,
            name: test.name,
            suite: test.suite,
            state: test.state,
            duration: test.duration,
            error: test.error,
            timestamp: test.timestamp
        });
    }

    /**
     * Create assertion helpers for tests
     * @returns {Object} - Assertion methods
     * @private
     */
    _createAssertions() {
        return {
            /**
             * Assert that a condition is true
             * @param {boolean} condition - Condition to test
             * @param {string} message - Error message if assertion fails
             */
            assertTrue: (condition, message = 'Expected condition to be true') => {
                if (!condition) {
                    throw new AssertionError(message);
                }
            },

            /**
             * Assert that a condition is false
             * @param {boolean} condition - Condition to test
             * @param {string} message - Error message if assertion fails
             */
            assertFalse: (condition, message = 'Expected condition to be false') => {
                if (condition) {
                    throw new AssertionError(message);
                }
            },

            /**
             * Assert that two values are equal
             * @param {*} actual - Actual value
             * @param {*} expected - Expected value
             * @param {string} message - Error message if assertion fails
             */
            assertEqual: (actual, expected, message = `Expected ${actual} to equal ${expected}`) => {
                if (actual !== expected) {
                    throw new AssertionError(message);
                }
            },

            /**
             * Assert that two values are not equal
             * @param {*} actual - Actual value
             * @param {*} notExpected - Value that should not match
             * @param {string} message - Error message if assertion fails
             */
            assertNotEqual: (actual, notExpected, message = `Expected ${actual} not to equal ${notExpected}`) => {
                if (actual === notExpected) {
                    throw new AssertionError(message);
                }
            },

            /**
             * Assert that a value is null
             * @param {*} value - Value to test
             * @param {string} message - Error message if assertion fails
             */
            assertNull: (value, message = 'Expected value to be null') => {
                if (value !== null) {
                    throw new AssertionError(message);
                }
            },

            /**
             * Assert that a value is not null
             * @param {*} value - Value to test
             * @param {string} message - Error message if assertion fails
             */
            assertNotNull: (value, message = 'Expected value not to be null') => {
                if (value === null) {
                    throw new AssertionError(message);
                }
            },

            /**
             * Assert that a value is undefined
             * @param {*} value - Value to test
             * @param {string} message - Error message if assertion fails
             */
            assertUndefined: (value, message = 'Expected value to be undefined') => {
                if (value !== undefined) {
                    throw new AssertionError(message);
                }
            },

            /**
             * Assert that a value is defined
             * @param {*} value - Value to test
             * @param {string} message - Error message if assertion fails
             */
            assertDefined: (value, message = 'Expected value to be defined') => {
                if (value === undefined) {
                    throw new AssertionError(message);
                }
            },

            /**
             * Assert that a function throws an error
             * @param {Function} fn - Function that should throw
             * @param {string} expectedError - Expected error message (optional)
             * @param {string} message - Error message if assertion fails
             */
            assertThrows: (fn, expectedError = null, message = 'Expected function to throw an error') => {
                let threwError = false;
                let actualError = null;

                try {
                    fn();
                } catch (error) {
                    threwError = true;
                    actualError = error;
                }

                if (!threwError) {
                    throw new AssertionError(message);
                }

                if (expectedError && actualError.message !== expectedError) {
                    throw new AssertionError(`Expected error "${expectedError}" but got "${actualError.message}"`);
                }
            },

            /**
             * Assert that a function does not throw an error
             * @param {Function} fn - Function that should not throw
             * @param {string} message - Error message if assertion fails
             */
            assertDoesNotThrow: (fn, message = 'Expected function not to throw an error') => {
                try {
                    fn();
                } catch (error) {
                    throw new AssertionError(`${message}. Got: ${error.message}`);
                }
            },

            /**
             * Assert that an object has a property
             * @param {Object} obj - Object to check
             * @param {string} property - Property name
             * @param {string} message - Error message if assertion fails
             */
            assertHasProperty: (obj, property, message = `Expected object to have property "${property}"`) => {
                if (!(property in obj)) {
                    throw new AssertionError(message);
                }
            },

            /**
             * Assert that a value is an instance of a specific type
             * @param {*} value - Value to check
             * @param {Function} type - Constructor function
             * @param {string} message - Error message if assertion fails
             */
            assertInstanceOf: (value, type, message = `Expected value to be instance of ${type.name}`) => {
                if (!(value instanceof type)) {
                    throw new AssertionError(message);
                }
            },

            /**
             * Assert that an array contains a specific value
             * @param {Array} array - Array to search
             * @param {*} value - Value to find
             * @param {string} message - Error message if assertion fails
             */
            assertContains: (array, value, message = `Expected array to contain ${value}`) => {
                if (!Array.isArray(array) || !array.includes(value)) {
                    throw new AssertionError(message);
                }
            }
        };
    }

    /**
     * Print test summary
     * @param {Object} summary - Test results summary
     * @private
     */
    _printSummary(summary) {
        console.log('\n' + '='.repeat(60));
        console.log('📊 TEST SUMMARY');
        console.log('='.repeat(60));

        const passRate = summary.total > 0 ? Math.round((summary.passed / summary.total) * 100) : 0;
        const status = summary.failed === 0 ? '✅ ALL TESTS PASSED' : '❌ SOME TESTS FAILED';

        console.log(`${status}`);
        console.log(`Total Tests: ${summary.total}`);
        console.log(`Passed: ${summary.passed} (${passRate}%)`);
        console.log(`Failed: ${summary.failed}`);
        console.log(`Skipped: ${summary.skipped}`);
        console.log(`Duration: ${summary.duration}ms`);

        // Suite breakdown
        console.log('\n📦 SUITE BREAKDOWN:');
        summary.suites.forEach(suite => {
            const suitePassRate = suite.results.total > 0 ?
                Math.round((suite.results.passed / suite.results.total) * 100) : 0;
            console.log(`  ${suite.name}: ${suite.results.passed}/${suite.results.total} (${suitePassRate}%)`);
        });

        // Failed tests details
        if (summary.failed > 0) {
            console.log('\n❌ FAILED TESTS:');
            this.results
                .filter(result => result.state === this.STATES.FAILED)
                .forEach(result => {
                    console.log(`  ${result.suite}::${result.name}`);
                    console.log(`    Error: ${result.error.message}`);
                });
        }

        console.log('='.repeat(60));
    }

    /**
     * Get test results as JSON
     * @returns {Object} - Complete test results
     */
    getResults() {
        return {
            summary: {
                total: this.totalTests,
                passed: this.passedTests,
                failed: this.failedTests,
                skipped: this.totalTests - this.passedTests - this.failedTests,
                duration: this.endTime - this.startTime
            },
            suites: Array.from(this.suites.entries()).map(([name, suite]) => ({
                name,
                results: suite.results,
                tests: suite.tests.map(test => ({
                    name: test.name,
                    state: test.state,
                    duration: test.duration,
                    error: test.error
                }))
            })),
            results: this.results
        };
    }
}

/**
 * Custom assertion error
 */
class AssertionError extends Error {
    constructor(message) {
        super(message);
        this.name = 'AssertionError';
    }
}

// Export for use in tests
export { TestFramework, AssertionError };