/**
 * Test Results Collector
 *
 * Single source of truth for test result tracking and output.
 * Supports JSON, JUnit XML, and console output formats.
 */

const fs = require('fs');
const path = require('path');

/**
 * Escape special characters for XML
 * @param {string} str - String to escape
 * @returns {string}
 */
function escapeXml(str) {
    if (!str) return '';
    return String(str)
        .replace(/&/g, '&amp;')
        .replace(/</g, '&lt;')
        .replace(/>/g, '&gt;')
        .replace(/"/g, '&quot;')
        .replace(/'/g, '&apos;');
}

class TestResults {
    /**
     * Create a new TestResults collector
     * @param {string} suiteName - Name of the test suite
     */
    constructor(suiteName) {
        this.suiteName = suiteName;
        this.tests = [];
        this.startTime = Date.now();
    }

    /**
     * Add a test result
     * @param {string} category - Test category/group
     * @param {string} name - Test name
     * @param {boolean} passed - Whether the test passed
     * @param {string} [message=''] - Result message or error
     * @param {number} [duration=0] - Test duration in ms
     */
    add(category, name, passed, message = '', duration = 0) {
        this.tests.push({
            category,
            name,
            passed,
            skipped: false,
            message,
            duration,
            timestamp: new Date().toISOString(),
        });

        const icon = passed ? '\x1b[32m\u2713\x1b[0m' : '\x1b[31m\u2717\x1b[0m';
        console.log(`  ${icon} [${category}] ${name}: ${message}`);
    }

    /**
     * Mark a test as skipped
     * @param {string} category - Test category/group
     * @param {string} name - Test name
     * @param {string} [reason=''] - Reason for skipping
     */
    skip(category, name, reason = '') {
        this.tests.push({
            category,
            name,
            passed: true,
            skipped: true,
            message: reason,
            duration: 0,
            timestamp: new Date().toISOString(),
        });

        console.log(`  \x1b[33m\u21B7\x1b[0m [${category}] ${name}: SKIPPED${reason ? ` - ${reason}` : ''}`);
    }

    /**
     * Run a test function and record the result
     * @param {string} category - Test category
     * @param {string} name - Test name
     * @param {Function} testFn - Async test function
     * @param {Object} [context] - Optional context (page, etc.) for screenshots on failure
     * @returns {Promise<boolean>} Whether the test passed
     */
    async run(category, name, testFn, context = {}) {
        const startTime = Date.now();
        try {
            const result = await testFn();
            const duration = Date.now() - startTime;

            // Handle test functions that return { passed, message }
            if (result && typeof result === 'object' && 'passed' in result) {
                this.add(category, name, result.passed, result.message || '', duration);
                return result.passed;
            }

            // Test passed if no error was thrown
            this.add(category, name, true, 'Passed', duration);
            return true;
        } catch (error) {
            const duration = Date.now() - startTime;

            this.add(category, name, false, error.message, duration);

            // Take screenshot on failure if page is available
            if (context.page && context.screenshotOnFailure !== false) {
                try {
                    const screenshotDir = path.join(__dirname, '..', 'screenshots');
                    if (!fs.existsSync(screenshotDir)) {
                        fs.mkdirSync(screenshotDir, { recursive: true });
                    }
                    const safeName = `${category}_${name}`.replace(/[^a-z0-9]/gi, '_');
                    const screenshotPath = path.join(screenshotDir, `failure_${safeName}_${Date.now()}.png`);
                    await context.page.screenshot({ path: screenshotPath, fullPage: true });
                    console.log(`    \x1b[90mScreenshot saved: ${screenshotPath}\x1b[0m`);
                } catch (screenshotError) {
                    console.log(`    \x1b[90mFailed to capture screenshot: ${screenshotError.message}\x1b[0m`);
                }
            }

            return false;
        }
    }

    /**
     * Get summary statistics
     * @returns {Object} Summary with total, passed, failed, skipped, duration
     */
    get summary() {
        const total = this.tests.length;
        const skipped = this.tests.filter(t => t.skipped).length;
        const passed = this.tests.filter(t => t.passed && !t.skipped).length;
        const failed = total - passed - skipped;
        const duration = Date.now() - this.startTime;

        return { total, passed, failed, skipped, duration };
    }

    /**
     * Get results as JSON object
     * @returns {Object}
     */
    toJSON() {
        return {
            suite: this.suiteName,
            summary: this.summary,
            tests: this.tests,
        };
    }

    /**
     * Get results as JUnit XML
     * @returns {string}
     */
    toJUnitXML() {
        const { summary } = this;

        // Group tests by category
        const testsByCategory = {};
        for (const test of this.tests) {
            if (!testsByCategory[test.category]) {
                testsByCategory[test.category] = [];
            }
            testsByCategory[test.category].push(test);
        }

        let xml = `<?xml version="1.0" encoding="UTF-8"?>\n`;
        xml += `<testsuites name="${escapeXml(this.suiteName)}" tests="${summary.total}" failures="${summary.failed}" skipped="${summary.skipped}" time="${(summary.duration / 1000).toFixed(3)}">\n`;

        for (const [category, tests] of Object.entries(testsByCategory)) {
            const catPassed = tests.filter(t => t.passed && !t.skipped).length;
            const catFailed = tests.filter(t => !t.passed).length;
            const catSkipped = tests.filter(t => t.skipped).length;
            const catDuration = tests.reduce((sum, t) => sum + (t.duration || 0), 0);

            xml += `  <testsuite name="${escapeXml(category)}" tests="${tests.length}" failures="${catFailed}" skipped="${catSkipped}" time="${(catDuration / 1000).toFixed(3)}">\n`;

            for (const test of tests) {
                xml += `    <testcase classname="${escapeXml(category)}" name="${escapeXml(test.name)}" time="${((test.duration || 0) / 1000).toFixed(3)}"`;

                if (test.skipped) {
                    xml += `>\n      <skipped${test.message ? ` message="${escapeXml(test.message)}"` : ''}/>\n    </testcase>\n`;
                } else if (!test.passed) {
                    xml += `>\n      <failure message="${escapeXml(test.message)}">${escapeXml(test.message)}</failure>\n    </testcase>\n`;
                } else {
                    xml += ` />\n`;
                }
            }

            xml += `  </testsuite>\n`;
        }

        xml += `</testsuites>\n`;
        return xml;
    }

    /**
     * Print summary to console
     */
    print() {
        const { summary } = this;

        console.log('\n' + '='.repeat(60));
        console.log(`TEST RESULTS: ${this.suiteName}`);
        console.log('='.repeat(60));
        console.log(`Total: ${summary.total} | Passed: \x1b[32m${summary.passed}\x1b[0m | Failed: \x1b[31m${summary.failed}\x1b[0m | Skipped: \x1b[33m${summary.skipped}\x1b[0m`);
        console.log(`Duration: ${(summary.duration / 1000).toFixed(2)}s`);
        console.log(`Pass Rate: ${summary.total > 0 ? ((summary.passed / (summary.total - summary.skipped)) * 100).toFixed(1) : 0}%`);

        if (summary.failed > 0) {
            console.log('\n\x1b[31mFailed tests:\x1b[0m');
            this.tests
                .filter(t => !t.passed && !t.skipped)
                .forEach(t => {
                    console.log(`  \x1b[31m\u2717\x1b[0m [${t.category}] ${t.name}: ${t.message}`);
                });
        }

        console.log('='.repeat(60) + '\n');
    }

    /**
     * Save results to file
     * @param {string} [dir] - Output directory
     * @param {Object} [options] - Save options
     * @param {boolean} [options.json=true] - Save JSON file
     * @param {boolean} [options.junit=true] - Save JUnit XML file
     */
    save(dir, options = {}) {
        const outputDir = dir || path.join(__dirname, '..', 'test-results');
        if (!fs.existsSync(outputDir)) {
            fs.mkdirSync(outputDir, { recursive: true });
        }

        const safeName = this.suiteName.replace(/[^a-z0-9]/gi, '-').toLowerCase();

        if (options.json !== false) {
            const jsonPath = path.join(outputDir, `${safeName}.json`);
            fs.writeFileSync(jsonPath, JSON.stringify(this.toJSON(), null, 2));
            console.log(`JSON results saved: ${jsonPath}`);
        }

        if (options.junit !== false || process.env.CI) {
            const xmlPath = path.join(outputDir, `${safeName}.xml`);
            fs.writeFileSync(xmlPath, this.toJUnitXML());
            console.log(`JUnit XML saved: ${xmlPath}`);
        }
    }

    /**
     * Get appropriate process exit code
     * @returns {number} 0 if all tests passed, 1 otherwise
     */
    exitCode() {
        const { failed, passed, total } = this.summary;
        if (failed > 0) return 1;
        return 0;
    }
}

module.exports = { TestResults, escapeXml };
