"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    var desc = Object.getOwnPropertyDescriptor(m, k);
    if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
      desc = { enumerable: true, get: function() { return m[k]; } };
    }
    Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
    Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
    o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
    var ownKeys = function(o) {
        ownKeys = Object.getOwnPropertyNames || function (o) {
            var ar = [];
            for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
            return ar;
        };
        return ownKeys(o);
    };
    return function (mod) {
        if (mod && mod.__esModule) return mod;
        var result = {};
        if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
        __setModuleDefault(result, mod);
        return result;
    };
})();
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.startServer = startServer;
const express_1 = __importDefault(require("express"));
const cors_1 = __importDefault(require("cors"));
const path_1 = __importDefault(require("path"));
const fs_1 = __importDefault(require("fs"));
const executor_1 = require("./executor");
const reporters_1 = require("../cli/reporters");
// Read version from package.json
function getVersion() {
    try {
        // Method 1: Try require.resolve to find package.json (works for global/local installs)
        try {
            const pkgPath = require.resolve('@testsmith/testblocks/package.json');
            const pkg = JSON.parse(fs_1.default.readFileSync(pkgPath, 'utf-8'));
            if (pkg.version)
                return pkg.version;
        }
        catch {
            // Package might not be installed under this name, try relative paths
        }
        // Method 2: Try relative paths from this file
        const possiblePaths = [
            path_1.default.join(__dirname, '../../package.json'), // dist/server -> package.json
            path_1.default.join(__dirname, '../../../package.json'), // nested node_modules
            path_1.default.join(__dirname, '../../../../package.json'), // scoped package in node_modules
        ];
        for (const pkgPath of possiblePaths) {
            if (fs_1.default.existsSync(pkgPath)) {
                const pkg = JSON.parse(fs_1.default.readFileSync(pkgPath, 'utf-8'));
                // Verify it's our package
                if (pkg.name === '@testsmith/testblocks' && pkg.version) {
                    return pkg.version;
                }
            }
        }
    }
    catch {
        // Ignore errors
    }
    return '0.0.0';
}
const VERSION = getVersion();
const plugins_1 = require("./plugins");
const globals_1 = require("./globals");
const codegenManager_1 = require("./codegenManager");
/**
 * Merge folder hooks into a test file.
 * Folder hooks are ordered from outermost to innermost folder.
 * - beforeAll: run parent hooks first, then child hooks, then test file hooks
 * - afterAll: run test file hooks first, then child hooks, then parent hooks
 * - beforeEach/afterEach: same pattern
 */
function mergeFolderHooksIntoTestFile(testFile, folderHooks) {
    if (!folderHooks || folderHooks.length === 0) {
        return testFile;
    }
    // Collect all steps from folder hooks (parent to child order is already provided)
    const beforeAllSteps = [];
    const afterAllSteps = [];
    const beforeEachSteps = [];
    const afterEachSteps = [];
    // Parent to child order for beforeAll/beforeEach
    for (const hooks of folderHooks) {
        if (hooks.beforeAll)
            beforeAllSteps.push(...hooks.beforeAll);
        if (hooks.beforeEach)
            beforeEachSteps.push(...hooks.beforeEach);
    }
    // Child to parent order for afterAll/afterEach
    for (let i = folderHooks.length - 1; i >= 0; i--) {
        const hooks = folderHooks[i];
        if (hooks.afterAll)
            afterAllSteps.unshift(...hooks.afterAll);
        if (hooks.afterEach)
            afterEachSteps.unshift(...hooks.afterEach);
    }
    // Merge with test file hooks
    const merged = {
        ...testFile,
        beforeAll: [
            ...beforeAllSteps,
            ...(testFile.beforeAll || []),
        ],
        afterAll: [
            ...(testFile.afterAll || []),
            ...afterAllSteps,
        ],
        beforeEach: [
            ...beforeEachSteps,
            ...(testFile.beforeEach || []),
        ],
        afterEach: [
            ...(testFile.afterEach || []),
            ...afterEachSteps,
        ],
    };
    return merged;
}
async function startServer(options = {}) {
    const port = options.port || 3000;
    const workingDir = process.cwd();
    // Set directories
    const pluginsDir = options.pluginsDir || path_1.default.join(workingDir, 'plugins');
    const globalsDir = options.globalsDir || workingDir;
    (0, plugins_1.setPluginsDirectory)(pluginsDir);
    (0, globals_1.setGlobalsDirectory)(globalsDir);
    // Load plugins and globals
    try {
        await (0, plugins_1.loadAllPlugins)();
        (0, plugins_1.initializeServerPlugins)();
        (0, globals_1.initializeGlobalsAndSnippets)();
    }
    catch (err) {
        console.error('Failed to load plugins:', err);
    }
    const app = (0, express_1.default)();
    app.use((0, cors_1.default)());
    app.use(express_1.default.json({ limit: '10mb' }));
    // Serve static client files
    const clientDir = path_1.default.join(__dirname, '..', 'client');
    if (fs_1.default.existsSync(clientDir)) {
        app.use(express_1.default.static(clientDir));
    }
    // Health check
    app.get('/api/health', (_req, res) => {
        res.json({ status: 'ok', version: VERSION });
    });
    // Version endpoint
    app.get('/api/version', (_req, res) => {
        res.json({ version: VERSION });
    });
    // List available plugins (with full block definitions for client registration)
    app.get('/api/plugins', (_req, res) => {
        const available = (0, plugins_1.discoverPlugins)();
        const loaded = (0, plugins_1.getServerPlugins)().map(p => ({
            name: p.name,
            version: p.version,
            description: p.description,
            blocks: p.blocks, // Include full block definitions
        }));
        res.json({
            directory: (0, plugins_1.getPluginsDirectory)(),
            available,
            loaded,
        });
    });
    // Load specific plugins
    app.post('/api/plugins/load', async (req, res) => {
        try {
            const { plugins } = req.body;
            await (0, plugins_1.loadTestFilePlugins)(plugins);
            res.json({ loaded: plugins });
        }
        catch (error) {
            res.status(500).json({ error: error.message });
        }
    });
    // Get globals and snippets
    app.get('/api/globals', (_req, res) => {
        const globals = (0, globals_1.getGlobals)();
        const globalProcedures = (0, globals_1.getGlobalProcedures)();
        const snippets = (0, globals_1.getAllSnippets)().map(s => ({
            name: s.name,
            description: s.description,
            category: s.category,
            params: s.params,
            stepCount: s.steps.length,
        }));
        // Convert procedures to a format the client can use for blocks
        const procedures = Object.entries(globalProcedures).map(([, proc]) => ({
            name: proc.name,
            description: proc.description,
            category: 'Custom',
            params: proc.params,
            stepCount: proc.steps?.length || 0,
        }));
        res.json({
            directory: (0, globals_1.getGlobalsDirectory)(),
            globals,
            snippets,
            procedures,
            testIdAttribute: (0, globals_1.getTestIdAttribute)(),
        });
    });
    // Update test ID attribute
    app.put('/api/globals/test-id-attribute', (req, res) => {
        const { testIdAttribute } = req.body;
        if (!testIdAttribute || typeof testIdAttribute !== 'string') {
            return res.status(400).json({ error: 'testIdAttribute is required and must be a string' });
        }
        (0, globals_1.setTestIdAttribute)(testIdAttribute);
        res.json({ testIdAttribute: (0, globals_1.getTestIdAttribute)() });
    });
    // Run tests
    app.post('/api/run', async (req, res) => {
        try {
            const { testFile, folderHooks } = req.body;
            if (!testFile || !testFile.tests) {
                return res.status(400).json({ error: 'Invalid test file format' });
            }
            console.log(`Running ${testFile.tests.length} tests from "${testFile.name}"...`);
            // Merge folder hooks into test file
            const mergedTestFile = mergeFolderHooksIntoTestFile(testFile, folderHooks || []);
            const globalVars = (0, globals_1.getGlobalVariables)();
            const globalProcs = (0, globals_1.getGlobalProcedures)();
            const testIdAttr = (0, globals_1.getTestIdAttribute)();
            const executor = new executor_1.TestExecutor({
                headless: req.query.headless !== 'false',
                timeout: Number(req.query.timeout) || 30000,
                variables: globalVars,
                procedures: globalProcs,
                testIdAttribute: testIdAttr,
                baseDir: globalsDir,
            });
            const results = await executor.runTestFile(mergedTestFile);
            const passed = results.filter(r => r.status === 'passed').length;
            const failed = results.filter(r => r.status === 'failed').length;
            console.log(`Results: ${passed} passed, ${failed} failed`);
            res.json(results);
        }
        catch (error) {
            const err = error;
            console.error('Test execution failed:', err.message);
            res.status(500).json({
                error: 'Test execution failed',
                message: err.message,
            });
        }
    });
    // Run a single test
    app.post('/api/run/:testId', async (req, res) => {
        try {
            const { testFile, folderHooks } = req.body;
            const { testId } = req.params;
            const test = testFile.tests.find(t => t.id === testId);
            if (!test) {
                return res.status(404).json({ error: `Test not found: ${testId}` });
            }
            // Merge folder hooks for beforeEach/afterEach
            const mergedTestFile = mergeFolderHooksIntoTestFile(testFile, folderHooks || []);
            const globalVars = (0, globals_1.getGlobalVariables)();
            const globalProcs = (0, globals_1.getGlobalProcedures)();
            const testIdAttr = (0, globals_1.getTestIdAttribute)();
            const executor = new executor_1.TestExecutor({
                headless: req.query.headless !== 'false',
                timeout: Number(req.query.timeout) || 30000,
                variables: globalVars,
                procedures: globalProcs,
                testIdAttribute: testIdAttr,
                baseDir: globalsDir,
            });
            // Register file-level procedures (overrides globals)
            if (mergedTestFile.procedures) {
                executor.registerProcedures(mergedTestFile.procedures);
            }
            await executor.initialize();
            const result = await executor.runTest(test, mergedTestFile.variables);
            await executor.cleanup();
            res.json(result);
        }
        catch (error) {
            console.error('Test execution failed:', error);
            res.status(500).json({
                error: 'Test execution failed',
                message: error.message,
            });
        }
    });
    // Validate test file
    app.post('/api/validate', (req, res) => {
        try {
            const testFile = req.body;
            const errors = [];
            if (!testFile.version) {
                errors.push('Missing version field');
            }
            if (!testFile.name) {
                errors.push('Missing name field');
            }
            if (!testFile.tests || !Array.isArray(testFile.tests)) {
                errors.push('Missing or invalid tests array');
            }
            else {
                testFile.tests.forEach((test, index) => {
                    if (!test.id) {
                        errors.push(`Test at index ${index} is missing an id`);
                    }
                    if (!test.name) {
                        errors.push(`Test at index ${index} is missing a name`);
                    }
                });
            }
            if (errors.length > 0) {
                res.status(400).json({ valid: false, errors });
            }
            else {
                res.json({ valid: true });
            }
        }
        catch (error) {
            res.status(400).json({
                valid: false,
                errors: ['Invalid JSON: ' + error.message],
            });
        }
    });
    // Recording endpoints
    app.post('/api/record/start', async (req, res) => {
        try {
            const { url, testIdAttribute } = req.body;
            if (!url) {
                return res.status(400).json({ error: 'URL is required' });
            }
            console.log(`Starting recording session for URL: ${url}`);
            const sessionId = await codegenManager_1.codegenManager.startRecording(url, {
                testIdAttribute: testIdAttribute || undefined,
            });
            res.json({
                sessionId,
                status: 'running',
            });
        }
        catch (error) {
            console.error('Failed to start recording:', error);
            res.status(500).json({
                error: 'Failed to start recording',
                message: error.message,
            });
        }
    });
    app.post('/api/record/stop', async (req, res) => {
        try {
            const { sessionId } = req.body;
            if (!sessionId) {
                return res.status(400).json({ error: 'Session ID is required' });
            }
            console.log(`Stopping recording session: ${sessionId}`);
            const steps = await codegenManager_1.codegenManager.stopRecording(sessionId);
            res.json({
                status: 'completed',
                steps,
            });
            setTimeout(() => {
                codegenManager_1.codegenManager.cleanup(sessionId);
            }, 5000);
        }
        catch (error) {
            console.error('Failed to stop recording:', error);
            res.status(500).json({
                error: 'Failed to stop recording',
                message: error.message,
            });
        }
    });
    app.get('/api/record/status/:sessionId', (req, res) => {
        const { sessionId } = req.params;
        const session = codegenManager_1.codegenManager.getStatus(sessionId);
        if (!session) {
            return res.status(404).json({ error: 'Session not found' });
        }
        res.json({
            sessionId: session.id,
            status: session.status,
            url: session.url,
            error: session.error,
        });
    });
    // Report generation
    app.post('/api/reports/html', (req, res) => {
        try {
            const { testFile, results } = req.body;
            if (!testFile || !results) {
                return res.status(400).json({ error: 'testFile and results are required' });
            }
            const timestamp = new Date().toISOString();
            const reportData = {
                timestamp,
                summary: {
                    totalTests: results.length,
                    passed: results.filter(r => r.status === 'passed').length,
                    failed: results.filter(r => r.status !== 'passed').length,
                    duration: results.reduce((sum, r) => sum + r.duration, 0),
                },
                testFiles: [{
                        file: testFile.name || 'TestBlocks Test',
                        testFile,
                        results,
                    }],
            };
            const html = (0, reporters_1.generateHTMLReport)(reportData);
            const filename = `report-${(0, reporters_1.getTimestamp)()}.html`;
            res.setHeader('Content-Type', 'text/html');
            res.setHeader('Content-Disposition', `attachment; filename="${filename}"`);
            res.send(html);
        }
        catch (error) {
            console.error('Failed to generate HTML report:', error);
            res.status(500).json({
                error: 'Failed to generate report',
                message: error.message,
            });
        }
    });
    app.post('/api/reports/junit', (req, res) => {
        try {
            const { testFile, results } = req.body;
            if (!testFile || !results) {
                return res.status(400).json({ error: 'testFile and results are required' });
            }
            const timestamp = new Date().toISOString();
            const reportData = {
                timestamp,
                summary: {
                    totalTests: results.length,
                    passed: results.filter(r => r.status === 'passed').length,
                    failed: results.filter(r => r.status !== 'passed').length,
                    duration: results.reduce((sum, r) => sum + r.duration, 0),
                },
                testFiles: [{
                        file: testFile.name || 'TestBlocks Test',
                        testFile,
                        results,
                    }],
            };
            const xml = (0, reporters_1.generateJUnitXML)(reportData);
            const filename = `junit-${(0, reporters_1.getTimestamp)()}.xml`;
            res.setHeader('Content-Type', 'application/xml');
            res.setHeader('Content-Disposition', `attachment; filename="${filename}"`);
            res.send(xml);
        }
        catch (error) {
            console.error('Failed to generate JUnit report:', error);
            res.status(500).json({
                error: 'Failed to generate report',
                message: error.message,
            });
        }
    });
    // Serve index.html for all non-API routes (SPA support)
    app.get('*', (_req, res) => {
        const indexPath = path_1.default.join(clientDir, 'index.html');
        if (fs_1.default.existsSync(indexPath)) {
            res.sendFile(indexPath);
        }
        else {
            res.status(404).json({ error: 'Web UI not found. Make sure testblocks is properly installed.' });
        }
    });
    // Cleanup handlers
    process.on('SIGTERM', () => {
        console.log('Received SIGTERM, cleaning up...');
        codegenManager_1.codegenManager.cleanupAll();
        process.exit(0);
    });
    process.on('SIGINT', () => {
        console.log('Received SIGINT, cleaning up...');
        codegenManager_1.codegenManager.cleanupAll();
        process.exit(0);
    });
    // Start server
    app.listen(port, () => {
        console.log(`\nTestBlocks Web UI running at http://localhost:${port}\n`);
        console.log('Directories:');
        console.log(`  Working directory: ${workingDir}`);
        console.log(`  Plugins: ${pluginsDir}`);
        console.log(`  Globals: ${globalsDir}`);
        console.log('\nPress Ctrl+C to stop\n');
        // Open browser if requested
        if (options.open) {
            const url = `http://localhost:${port}`;
            const command = process.platform === 'darwin' ? 'open' :
                process.platform === 'win32' ? 'start' : 'xdg-open';
            Promise.resolve().then(() => __importStar(require('child_process'))).then(cp => {
                cp.exec(`${command} ${url}`);
            });
        }
    });
}
