"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.BaseTestExecutor = void 0;
const playwright_1 = require("playwright");
const blocks_1 = require("../blocks");
const blocklyParser_1 = require("../utils/blocklyParser");
const variableResolver_1 = require("../utils/variableResolver");
/**
 * Base class for test execution providing shared functionality
 * for both server and CLI executors
 */
class BaseTestExecutor {
    constructor(options = {}) {
        this.browser = null;
        this.browserContext = null;
        this.page = null;
        this.plugins = new Map();
        this.procedures = new Map();
        this.options = {
            headless: true,
            timeout: 30000,
            ...options,
        };
        if (options.plugins) {
            options.plugins.forEach(plugin => {
                this.plugins.set(plugin.name, plugin);
            });
        }
    }
    /**
     * Initialize browser for test execution
     */
    async initialize() {
        if (this.options.testIdAttribute) {
            playwright_1.selectors.setTestIdAttribute(this.options.testIdAttribute);
        }
        this.browser = await playwright_1.chromium.launch({
            headless: this.options.headless,
        });
        this.browserContext = await this.browser.newContext();
        this.page = await this.browserContext.newPage();
        if (this.options.timeout) {
            this.page.setDefaultTimeout(this.options.timeout);
        }
    }
    /**
     * Cleanup browser resources
     */
    async cleanup() {
        if (this.page)
            await this.page.close();
        if (this.browserContext)
            await this.browserContext.close();
        if (this.browser)
            await this.browser.close();
        this.page = null;
        this.browserContext = null;
        this.browser = null;
    }
    /**
     * Check if a test file requires browser initialization
     */
    requiresBrowser(testFile) {
        const hasWebStep = (steps) => {
            return steps.some(step => step.type.startsWith('web_'));
        };
        const hasWebStepInState = (state) => {
            const steps = blocklyParser_1.BlocklyParser.extractSteps(state);
            return hasWebStep(steps);
        };
        // Check lifecycle hooks
        if (testFile.beforeAll && hasWebStepInState(testFile.beforeAll))
            return true;
        if (testFile.afterAll && hasWebStepInState(testFile.afterAll))
            return true;
        if (testFile.beforeEach && hasWebStepInState(testFile.beforeEach))
            return true;
        if (testFile.afterEach && hasWebStepInState(testFile.afterEach))
            return true;
        // Check all tests
        for (const test of testFile.tests) {
            if (hasWebStepInState(test.steps))
                return true;
            if (test.beforeEach && hasWebStepInState(test.beforeEach))
                return true;
            if (test.afterEach && hasWebStepInState(test.afterEach))
                return true;
        }
        return false;
    }
    /**
     * Register custom blocks from procedure definitions
     */
    registerCustomBlocksFromProcedures(procedures) {
        Object.entries(procedures).forEach(([name, proc]) => {
            (0, blocks_1.registerProcedure)(name, proc);
            this.procedures.set(name, proc);
            if (!proc.steps || proc.steps.length === 0)
                return;
            const blockType = `custom_${proc.name.toLowerCase().replace(/\s+/g, '_')}`;
            if ((0, blocks_1.getBlock)(blockType))
                return;
            const blockDef = {
                type: blockType,
                category: 'Custom',
                color: '#607D8B',
                tooltip: proc.description || `Custom block: ${proc.name}`,
                inputs: (proc.params || []).map(param => ({
                    name: param.name.toUpperCase(),
                    type: 'field',
                    fieldType: param.type === 'number' ? 'number' : 'text',
                    default: param.default,
                })),
                previousStatement: true,
                nextStatement: true,
                execute: async (params, context) => {
                    context.logger.info(`Executing custom block: ${proc.name}`);
                    (proc.params || []).forEach(p => {
                        const paramKey = p.name.toUpperCase();
                        let value = params[paramKey];
                        if (value !== undefined) {
                            if (typeof value === 'string') {
                                value = variableResolver_1.VariableResolver.resolve(value, context);
                            }
                            context.variables.set(p.name, value);
                            context.logger.debug(`Set procedure param: ${p.name} = "${value}"`);
                        }
                    });
                    return {
                        customBlock: true,
                        name: proc.name,
                        steps: proc.steps,
                    };
                },
            };
            (0, blocks_1.registerBlock)(blockDef);
        });
    }
    /**
     * Execute a single step
     */
    async runStep(step, context) {
        const startTime = Date.now();
        // Run beforeStep hooks
        for (const plugin of this.plugins.values()) {
            if (plugin.hooks?.beforeStep) {
                await plugin.hooks.beforeStep(context, step);
            }
        }
        let status = 'passed';
        let output;
        let error;
        try {
            const blockDef = (0, blocks_1.getBlock)(step.type);
            if (!blockDef) {
                throw new Error(`Unknown block type: ${step.type}`);
            }
            const resolvedParams = await this.resolveParams(step.params, context);
            output = await blockDef.execute(resolvedParams, context);
            // Handle custom blocks that return steps to execute
            if (output && typeof output === 'object' && 'customBlock' in output) {
                const customOutput = output;
                for (const childStep of customOutput.steps) {
                    const childResult = await this.runStep(childStep, context);
                    if (childResult.status === 'failed' || childResult.status === 'error') {
                        status = childResult.status;
                        error = childResult.error;
                        break;
                    }
                }
            }
            // Handle compound actions
            if (output && typeof output === 'object' && 'compoundAction' in output) {
                const compoundOutput = output;
                for (const childStep of compoundOutput.steps) {
                    const childResult = await this.runStep(childStep, context);
                    if (childResult.status === 'failed' || childResult.status === 'error') {
                        status = childResult.status;
                        error = childResult.error;
                        break;
                    }
                }
            }
            // Handle procedure calls
            if (output && typeof output === 'object' && 'procedureCall' in output) {
                const procOutput = output;
                for (const [argName, argValue] of Object.entries(procOutput.args)) {
                    let resolvedValue = argValue;
                    if (typeof argValue === 'string') {
                        resolvedValue = variableResolver_1.VariableResolver.resolve(argValue, context);
                    }
                    context.variables.set(argName, resolvedValue);
                }
                if (procOutput.procedure.steps) {
                    for (const childStep of procOutput.procedure.steps) {
                        const childResult = await this.runStep(childStep, context);
                        if (childResult.status === 'failed' || childResult.status === 'error') {
                            status = childResult.status;
                            error = childResult.error;
                            break;
                        }
                    }
                }
            }
        }
        catch (err) {
            status = 'failed';
            error = {
                message: err.message,
                stack: err.stack,
            };
        }
        // Capture screenshot on failure for web tests
        let screenshot;
        if (status === 'failed' && step.type.startsWith('web_') && context.page) {
            try {
                const page = context.page;
                const buffer = await page.screenshot({ fullPage: true });
                screenshot = buffer.toString('base64');
            }
            catch {
                // Ignore screenshot errors
            }
        }
        const result = {
            stepId: step.id,
            stepType: step.type,
            status,
            duration: Date.now() - startTime,
            output,
            error,
            screenshot,
        };
        // Run afterStep hooks
        for (const plugin of this.plugins.values()) {
            if (plugin.hooks?.afterStep) {
                await plugin.hooks.afterStep(context, step, result);
            }
        }
        return result;
    }
    /**
     * Resolve step parameters, executing nested blocks
     */
    async resolveParams(params, context) {
        const resolved = {};
        for (const [key, value] of Object.entries(params)) {
            if (value && typeof value === 'object' && 'type' in value && 'id' in value) {
                // This is a connected block - execute it to get the value
                const nestedStep = value;
                const blockDef = (0, blocks_1.getBlock)(nestedStep.type);
                if (blockDef) {
                    const nestedParams = await this.resolveParams(nestedStep.params || {}, context);
                    resolved[key] = await blockDef.execute(nestedParams, context);
                }
            }
            else {
                resolved[key] = value;
            }
        }
        return resolved;
    }
    /**
     * Create a logger instance
     */
    createLogger() {
        return {
            info: (message, data) => {
                console.log(`[INFO] ${message}`, data !== undefined ? data : '');
            },
            warn: (message, data) => {
                console.warn(`[WARN] ${message}`, data !== undefined ? data : '');
            },
            error: (message, data) => {
                console.error(`[ERROR] ${message}`, data !== undefined ? data : '');
            },
            debug: (message, data) => {
                if (process.env.DEBUG) {
                    console.debug(`[DEBUG] ${message}`, data !== undefined ? data : '');
                }
            },
        };
    }
    /**
     * Resolve variable defaults from globals format
     */
    resolveVariableDefaults(vars) {
        return (0, variableResolver_1.resolveVariableDefaults)(vars);
    }
    /**
     * Extract steps from Blockly state
     */
    extractStepsFromBlocklyState(state) {
        return blocklyParser_1.BlocklyParser.extractSteps(state);
    }
}
exports.BaseTestExecutor = BaseTestExecutor;
