"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.buildPlanningMessage = buildPlanningMessage;
const env_1 = require("@mablhq-internal/runtime/env/env");
const Timers = __importStar(require("timers"));
const inquirer = __importStar(require("inquirer"));
const pluralize_1 = __importDefault(require("pluralize"));
const uuid_1 = require("uuid");
const mablApiClientFactory_1 = require("@mablhq-internal/runtime/api/mablApiClientFactory");
const util_1 = require("../../../commandUtil/util");
const cliUtil_1 = require("@mablhq-internal/runtime/core/cliUtil");
const mablApi_1 = require("@mablhq-internal/runtime/mablApi");
const index_1 = require("@mablhq-internal/runtime/proxy/index");
const RichPromise_1 = __importDefault(require("@mablhq-internal/runtime/util/RichPromise"));
const pureUtil_1 = require("@mablhq-internal/runtime/util/pureUtil");
const constants_1 = require("@mablhq-internal/runtime/core/constants");
const constants_2 = require("../../../constants");
const loggingProvider_1 = require("@mablhq-internal/runtime/providers/logging/loggingProvider");
const CommandArgCredentialsId = 'credentialsId';
const CommandArgUseLink = 'useLink';
const CommandArgNoPlan = 'no-plan';
exports.command = 'selenium';
exports.describe = 'Import an existing Selenium-based test';
exports.builder = (yargs) => {
    yargs
        .option(constants_2.CommandArgAuto, {
        default: false,
        describe: 'Submit the captured session to the cloud authoring agent without prompting',
        required: false,
        requiresArg: false,
        type: 'boolean',
    })
        .option(constants_2.CommandArgDebug, {
        default: false,
        describe: 'Enable debug mode',
        hidden: true,
        required: false,
        requiresArg: false,
        type: 'boolean',
    })
        .option(constants_2.CommandArgMulti, {
        default: false,
        describe: 'Capture multiple tests at once',
        required: false,
        requiresArg: false,
        type: 'boolean',
    })
        .option(constants_2.CommandArgName, {
        alias: constants_2.CommandArgAliases.Name,
        describe: 'The name to give this test in mabl',
        nargs: 1,
        required: false,
        type: 'string',
    })
        .option(constants_1.CommandArgPortNumber, {
        default: 8889,
        describe: 'The port on which the Selenium proxy should listen',
        min: 1,
        max: 65535,
        nargs: 1,
        required: false,
        type: 'number',
    })
        .option(constants_1.CommandArgWorkspaceId, {
        alias: constants_2.CommandArgAliases.WorkspaceId,
        describe: 'Workspace into which tests should be imported',
        nargs: 1,
        required: false,
        requiresArg: true,
        type: 'string',
    })
        .option(CommandArgCredentialsId, {
        describe: 'Workspace credential ID to bind to the imported test. Pass when the captured login uses secrets from an external vault that do not match any mabl credential.',
        nargs: 1,
        required: false,
        requiresArg: true,
        type: 'string',
    })
        .option(CommandArgUseLink, {
        default: false,
        describe: 'Bias the planner toward applications and environments configured for the Link Agent',
        required: false,
        requiresArg: false,
        type: 'boolean',
    })
        .option(CommandArgNoPlan, {
        default: false,
        describe: 'Skip the planning agent and submit the captured session directly to the test authoring agent',
        required: false,
        requiresArg: false,
        type: 'boolean',
    });
};
exports.handler = (0, cliUtil_1.failWrapper)(importTest);
async function importTest(parsed) {
    const workspaceId = await (0, util_1.getWorkspaceId)(parsed);
    const apiClient = await mablApiClientFactory_1.MablApiClientFactory.createApiClient();
    const captures = await captureSessions(parsed.port, parsed.debug, parsed.multi);
    if (captures.length === 0) {
        return Promise.reject(new Error('No commands were captured.'));
    }
    loggingProvider_1.logger.info(`Captured ${(0, pluralize_1.default)('test', captures.length, true)}.`);
    if (!parsed.autoSave && !(await confirmSubmission(captures.length))) {
        loggingProvider_1.logger.info('Exiting without submitting.');
        return;
    }
    const usePlanning = !parsed['no-plan'];
    for (let index = 0; index < captures.length; index++) {
        const captured = captures[index];
        const label = captures.length > 1 ? ` (${index + 1} of ${captures.length})` : '';
        loggingProvider_1.logger.info(`Submitting session${label} to mabl for test generation${usePlanning ? ' (planning enabled)' : ''}...`);
        const result = await submitCapturedSession({
            apiClient,
            workspaceId,
            captured,
            usePlanning,
            explicitTestName: parsed.name,
            explicitCredentialsId: parsed.credentialsId,
            useLink: parsed.useLink,
        });
        loggingProvider_1.logger.info(`Test authoring session${label} initiated: session=${result.session_id} instance=${result.instance_id ?? 'pending'}`);
        loggingProvider_1.logger.info(`${env_1.BASE_APP_URL}/workspaces/${workspaceId}/agents/tasks/${result.session_id}`);
    }
}
async function confirmSubmission(capturedCount) {
    const answer = await inquirer.prompt([
        {
            type: 'confirm',
            name: 'submit',
            message: `Submit ${(0, pluralize_1.default)('captured session', capturedCount, true)} to mabl for test authoring?`,
            default: true,
        },
    ]);
    return answer.submit;
}
async function captureSessions(port, debug, multi) {
    const captures = [];
    const importCancelPromise = new RichPromise_1.default();
    const sessionClosedPromise = new RichPromise_1.default();
    const proxy = new index_1.SeleniumProxy({
        port,
        debug,
        agentVersion: (0, pureUtil_1.getCliVersion)(),
        onSessionComplete: (session) => {
            captures.push(session);
            if (multi) {
                loggingProvider_1.logger.info(`Detected end of test ${captures.length} with ${(0, pluralize_1.default)('step', session.steps.length, true)}.`);
                loggingProvider_1.logger.info('Press CTRL+C to end multiple test import.');
            }
            if (sessionClosedPromise.isPending()) {
                sessionClosedPromise.resolve();
            }
        },
    });
    process.once('SIGINT', () => {
        loggingProvider_1.logger.logNewLine();
        importCancelPromise.resolve();
    });
    await proxy.start();
    loggingProvider_1.logger.info(`Configure your test to use a selenium proxy at localhost:${port} and run the test now.`);
    if (multi) {
        loggingProvider_1.logger.info('Press CTRL+C to end multiple test import.');
        await importCancelPromise;
    }
    else {
        loggingProvider_1.logger.info('Import will end automatically when the Selenium session is closed or CTRL+C is pressed');
        await Promise.race([importCancelPromise, sessionClosedPromise]);
    }
    await new Promise((resolve) => {
        Timers.setImmediate(async () => {
            try {
                await proxy.stop();
            }
            catch (error) {
                if (debug) {
                    loggingProvider_1.logger.error(`Error stopping selenium proxy: ${error}`);
                }
            }
            resolve();
        });
    });
    const stragglerCapture = proxy.finalize();
    if (stragglerCapture && !captures.includes(stragglerCapture)) {
        captures.push(stragglerCapture);
    }
    return captures;
}
async function submitCapturedSession(params) {
    if (params.usePlanning) {
        const result = await submitWithPlanning(params);
        if (result) {
            return result;
        }
        loggingProvider_1.logger.info('Planning session did not produce a test_information artifact; falling back to direct submission.');
    }
    return submitWithoutPlanning(params);
}
async function submitWithPlanning(params) {
    const { apiClient, workspaceId, captured } = params;
    const trainingSessionId = `selenium-import-${workspaceId}-${Date.now()}`;
    loggingProvider_1.logger.info('Initiating test planning session...');
    const planningSession = await apiClient.initiateTestPlanningSession(workspaceId, {
        training_session_id: trainingSessionId,
    });
    loggingProvider_1.logger.info('Generating test plan from captured session...');
    const [intent, testName] = await Promise.all([
        summarize(apiClient, workspaceId, captured, INTENT_CONTEXT),
        params.explicitTestName?.trim() ||
            summarize(apiClient, workspaceId, captured, NAME_CONTEXT),
    ]);
    const userMessage = {
        id: (0, uuid_1.v4)(),
        role: mablApi_1.MessageRole.User,
        timestamp: Date.now(),
        parts: [
            {
                type: mablApi_1.MessagePartType.Text,
                text: buildPlanningMessage(captured, testName, intent, params),
            },
        ],
    };
    const planningResponse = await apiClient.executeTestPlanNextStep(workspaceId, {
        agent_session_id: planningSession.session_id,
        new_messages: [userMessage],
    });
    if (!hasTestInformationArtifact(planningResponse.new_artifacts)) {
        return undefined;
    }
    loggingProvider_1.logger.info('Initiating cloud test authoring session...');
    return apiClient.initiateTestAuthoringSession(workspaceId, {
        is_cloud: true,
        parent_session_id: planningSession.session_id,
    });
}
async function submitWithoutPlanning(params) {
    const { apiClient, workspaceId, captured } = params;
    loggingProvider_1.logger.info('Generating test information from captured session...');
    const [intent, testName, description] = await Promise.all([
        summarize(apiClient, workspaceId, captured, INTENT_CONTEXT),
        params.explicitTestName?.trim() ||
            summarize(apiClient, workspaceId, captured, NAME_CONTEXT),
        summarize(apiClient, workspaceId, captured, DESCRIPTION_CONTEXT),
    ]);
    const testInformation = {
        test_case: intent || description || testName,
        name: testName,
        description,
        url_override: captured.initialUrl,
    };
    const request = {
        is_cloud: true,
        test_information: testInformation,
    };
    return apiClient.initiateTestAuthoringSession(workspaceId, request);
}
function hasTestInformationArtifact(artifacts) {
    return Boolean(artifacts?.some((artifact) => artifact.name === 'test_information'));
}
const INTENT_CONTEXT = 'Generate the intent (what it is trying to accomplish) for a Selenium test that performs these actions. The intent should be a concise one-sentence description.';
const NAME_CONTEXT = 'Generate a short, descriptive test name (under 60 characters) for a Selenium test that performs these actions. Return only the test name, nothing else. Use title case.';
const DESCRIPTION_CONTEXT = 'Generate a description (no more than one paragraph) for a Selenium test that performs these actions.';
async function summarize(apiClient, workspaceId, captured, context) {
    const request = {
        input: summarizationInput(captured),
        context,
    };
    try {
        const result = await apiClient.summarizeText(workspaceId, request);
        return result.summary?.trim() ?? '';
    }
    catch (error) {
        loggingProvider_1.logger.debug(`Summarization failed (${context}): ${error}`);
        return '';
    }
}
function summarizationInput(captured) {
    const lines = [];
    if (captured.initialUrl) {
        lines.push(`Starting URL: ${captured.initialUrl}`);
    }
    lines.push(`Commands: ${JSON.stringify(captured.summary.commandCounts)}`);
    lines.push(`Pages visited: ${JSON.stringify(captured.summary.uniqueUrls)}`);
    for (const step of captured.steps) {
        lines.push(step.parameters
            ? `${step.command} ${JSON.stringify(step.parameters)}`
            : step.command);
    }
    return lines.join('\n');
}
function buildPlanningMessage(captured, testName, intent, params) {
    const lines = [];
    lines.push('The mabl CLI captured the WebDriver session below from a customer-owned');
    lines.push('Selenium test. The user has approved importing it as a mabl test and');
    lines.push('cannot answer follow-up questions, so plan the test in this same turn.');
    lines.push('');
    lines.push('## Test intent');
    lines.push(intent || '(intent summary unavailable)');
    lines.push('');
    lines.push('## Capture details');
    if (captured.initialUrl) {
        lines.push(`- Recording started at: ${captured.initialUrl}`);
    }
    const hosts = uniqueHosts(captured);
    if (hosts.length > 0) {
        lines.push(`- Hosts contacted during recording: ${hosts.join(', ')}`);
    }
    lines.push(`- Steps captured: ${captured.summary.totalSteps}`);
    lines.push('');
    lines.push('## Selecting the target');
    lines.push('');
    lines.push('Inspect the applications and environments already in your context. Pick');
    lines.push('the application + environment whose deployment URL best matches the hosts');
    lines.push("above (usually a hostname match). Pass that deployment's `deployment_id`");
    lines.push('to `save_test_information` — it will resolve the application, environment,');
    lines.push('and target URL from the deployment.');
    lines.push('');
    lines.push('Only fall back to `url_override` if no deployment in the workspace matches.');
    lines.push('Default to the captured initial URL, since that is where the test was');
    lines.push('designed to start. Trim it to the host root only when the captured path is');
    lines.push("clearly an intermediate redirect rather than the application's entry point");
    lines.push('(for example, an external identity-provider page the user was bounced');
    lines.push('through during login).');
    lines.push('');
    if (params.useLink) {
        lines.push('The user has flagged this import as requiring a Link Agent. Among matching');
        lines.push('applications, prefer an environment that is listed as Link Agent enabled.');
        lines.push('');
    }
    if (params.explicitCredentialsId) {
        lines.push('## Credentials');
        lines.push('');
        lines.push(`The user has explicitly specified credentials_id=\`${params.explicitCredentialsId}\``);
        lines.push('for this import. You MUST pass exactly this `credentials_id` to');
        lines.push('`save_test_information`. Do not pick a different credential from the');
        lines.push('workspace context, and do not omit it.');
        lines.push('');
    }
    lines.push('## Credentials in login steps');
    lines.push('');
    lines.push('The captured recording includes the literal username and password the user');
    lines.push('typed into the login form. These typically come from a vault or');
    lines.push('environment-specific config and almost never match the workspace credential');
    lines.push('the test will be bound to. In `test-outline.md`, when you describe steps');
    lines.push("that type into a username or password input, reference the bound credential's");
    lines.push('variables instead of the captured literal values — e.g.');
    lines.push('"Enter `{{@app.defaults.username}}` into the username field" and');
    lines.push('"Enter `{{@app.defaults.password}}` into the password field". Use the variable');
    lines.push('references for **BOTH** fields, even if the captured username looks like a real');
    lines.push("login. Without this the test will run with one credential's password and a");
    lines.push("different account's username and fail at the login step.");
    lines.push('');
    lines.push('## What the test must do');
    lines.push('');
    lines.push('The captured steps are the user actions that lead to the validations');
    lines.push('the test must perform.');
    lines.push('');
    lines.push('## In this same turn');
    lines.push('');
    lines.push(`1. Call \`save_test_information\` with \`name: "${testName}"\`, a`);
    lines.push('   `test_case` you derive from the intent, and either');
    lines.push('   `deployment_id` (preferred) or `url_override` (fallback).');
    lines.push('2. Write the plan to `test-outline.md` — one task per meaningful user');
    lines.push('   action (navigate, click, type, assert).');
    lines.push('3. Call `send_response` with `filename: "test-outline.md"`.');
    lines.push('');
    lines.push('## Captured session (JSON)');
    lines.push(JSON.stringify(captured, undefined, 2));
    return lines.join('\n');
}
function uniqueHosts(captured) {
    const hosts = new Set();
    const addHost = (url) => {
        if (!url) {
            return;
        }
        try {
            const parsed = new URL(url);
            if (parsed.protocol === 'http:' || parsed.protocol === 'https:') {
                hosts.add(parsed.host);
            }
        }
        catch {
        }
    };
    addHost(captured.initialUrl);
    for (const step of captured.steps) {
        addHost(step.pageContext?.url);
    }
    return [...hosts];
}
