// web-test cli/exec-context v1.1 — buildContext + executeScript для run/exec/test
// Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import { readFileSync, writeFileSync } from 'fs';
import { resolve, dirname } from 'path';
import { fileURLToPath } from 'url';
import * as browser from '../browser.mjs';
import { elapsed, slugify } from './util.mjs';

const __dirname = dirname(fileURLToPath(import.meta.url));
const DEFAULT_ERROR_SHOT_PATH = resolve(__dirname, '..', '..', 'error-shot.png');

// Where 1C-error screenshots go. The default is a single fixed file: fine for
// interactive `exec`/`run` (last error wins, easy to find), wrong for a test run
// where every test needs its own file inside reportDir. cmdTest overrides this.
let errorShotDir = null;
let errorShotSeq = 0;

export function setErrorShotDir(dir) {
  errorShotDir = dir;
  errorShotSeq = 0;
}

function nextErrorShotPath(label) {
  if (!errorShotDir) return DEFAULT_ERROR_SHOT_PATH;
  return resolve(errorShotDir, `onec-error-${++errorShotSeq}-${slugify(label || 'shot')}.png`);
}

/**
 * Build a per-context wrapper: same shape as buildContext output, but every call
 * is prefixed with `setActiveContext(name)` so the test can interleave actions
 * across contexts (`ctx.a.click(...); ctx.b.click(...)`).
 */
export function buildScopedContext(name) {
  const inner = buildContext({ noRecord: false });
  const scoped = {};
  for (const [k, v] of Object.entries(inner)) {
    if (typeof v === 'function') {
      scoped[k] = async (...args) => {
        await browser.setActiveContext(name);
        return v(...args);
      };
    } else {
      scoped[k] = v;
    }
  }
  return scoped;
}

export function buildContext({ noRecord = false } = {}) {
  const ctx = {};
  for (const [k, v] of Object.entries(browser)) {
    if (k !== 'default') ctx[k] = v;
  }
  ctx.writeFileSync = writeFileSync;
  ctx.readFileSync = readFileSync;

  // --no-record: stub recording/narration functions to return safe defaults
  if (noRecord) {
    const noop = async () => {};
    ctx.startRecording = noop;
    ctx.stopRecording = async () => ({ file: null, duration: 0, size: 0 });
    ctx.addNarration = async () => ({ file: null, duration: 0, size: 0, captions: 0 });
    for (const fn of ['showCaption', 'hideCaption']) {
      ctx[fn] = noop;
    }
    ctx.isRecording = () => false;
    ctx.getCaptions = () => [];
  }

  // Wrap action functions to auto-detect 1C errors (modal, balloon)
  // and stop execution immediately with diagnostic info
  const ACTION_FNS = [
    'clickElement', 'fillFields', 'fillField', 'selectValue', 'fillTableRow',
    'deleteTableRow', 'openCommand', 'navigateSection', 'navigateLink', 'openFile',
    'closeForm', 'filterList', 'unfilterList'
  ];
  for (const name of ACTION_FNS) {
    if (typeof ctx[name] !== 'function') continue;
    const orig = ctx[name];
    ctx[name] = async (...args) => {
      const result = await orig(...args);
      const errors = result?.errors;
      if (errors?.modal || errors?.balloon) {
        // Screenshot while the error modal is still visible (before fetchErrorStack closes it)
        let errorShot;
        try {
          const png = await ctx.screenshot();
          errorShot = nextErrorShotPath(name);
          writeFileSync(errorShot, png);
        } catch {}
        // Try to fetch call stack for modal errors before throwing
        let stack = null;
        if (errors?.modal && typeof ctx.fetchErrorStack === 'function') {
          try {
            stack = await ctx.fetchErrorStack(errors.modal.formNum, errors.modal.hasReport);
          } catch { /* don't fail if stack fetch fails */ }
        }
        const msg = errors.modal?.message || errors.balloon?.message || 'Unknown 1C error';
        const err = new Error(msg);
        err.onecError = { step: name, args, errors, formState: result, stack, screenshot: errorShot };
        throw err;
      }
      return result;
    };
  }

  return ctx;
}

export async function executeScript(code, { noRecord } = {}) {
  const output = [];
  const origLog = console.log;
  const origErr = console.error;
  console.log = (...a) => output.push(a.map(String).join(' '));
  console.error = (...a) => output.push('[ERR] ' + a.map(String).join(' '));

  const t0 = Date.now();
  try {
    const ctx = buildContext({ noRecord });

    // Normalize Windows backslash paths to prevent JS parse errors
    // (e.g. C:\Users\... → \u triggers "Invalid Unicode escape sequence")
    code = code.replace(/[A-Za-z]:\\[^\s'"`;\n)}\]]+/g, m => m.replace(/\\/g, '/'));

    const AsyncFunction = Object.getPrototypeOf(async function(){}).constructor;
    const fn = new AsyncFunction(...Object.keys(ctx), code);
    await fn(...Object.values(ctx));

    console.log = origLog;
    console.error = origErr;
    return { ok: true, output: output.join('\n'), elapsed: elapsed(t0) };
  } catch (e) {
    console.log = origLog;
    console.error = origErr;

    // Auto-stop recording if active (prevents "Already recording" on next exec)
    if (browser.isRecording()) {
      try { await browser.stopRecording(); } catch {}
    }

    // Error screenshot (skip if already taken before fetchErrorStack closed the modal)
    let shotFile = e.onecError?.screenshot;
    if (!shotFile) {
      try {
        const png = await browser.screenshot();
        shotFile = nextErrorShotPath('script');
        writeFileSync(shotFile, png);
      } catch {}
    }

    const result = { ok: false, error: e.message, output: output.join('\n'), screenshot: shotFile, elapsed: elapsed(t0) };

    // Enrich with 1C error context if available
    if (e.onecError) {
      result.step = e.onecError.step;
      result.stepArgs = e.onecError.args;
      result.onecErrors = e.onecError.errors;
      result.formState = e.onecError.formState;
      if (e.onecError.stack) result.stack = e.onecError.stack;
    }

    return result;
  }
}
