const fs = require("fs");
const crypto = require("crypto");
const { join } = require("path");

// Args
const on = process.argv[2] !== "off";

// -------- [Modifiers] -------- //

// Amends the file with the given entry point text and append text
// When "on" is true, the append text is added to the entry point,
// otherwise the append text is completely removed from the file
function amend(filePath, entryPointText, appendText) {
  const data = fs.readFileSync(filePath, "utf8");
  if (!data.includes(entryPointText)) {
    throw new Error("Entry point not found!", entryPointText);
  }
  // Ignore if the append text is already present
  if (on && data.includes(appendText)) {
    return;
  }
  // Add or clear according to on state
  const result = on
    ? data.replace(entryPointText, entryPointText + appendText)
    : data.replace(appendText, "");

  // Write
  fs.writeFileSync(filePath, result, "utf8");
}

// Bracket each injected block with a per-patch unique marker derived from a
// hash of `originalContent`. See 1.60.0.js for the full rationale — the legacy
// shared `/* checksumai */ ` marker caused OFF-mode `alwaysInjectScripts`
// (whose newContent collapses to the bare marker) to clobber the FIRST
// `/* checksumai */ ` it found, corrupting whichever patch happened to live
// at the lowest line number.
function replaceContent(filePath, originalContent, newContent) {
  const fileContent = fs.readFileSync(filePath, "utf8");

  const id = crypto
    .createHash("sha1")
    .update(originalContent)
    .digest("hex")
    .slice(0, 8);
  const openMarker = `/*checksumai:${id}*/`;
  const closeMarker = `/*checksumai:end:${id}*/`;
  const taggedNewContent = `${openMarker}${newContent}${closeMarker}`;

  if (on) {
    if (fileContent.includes(taggedNewContent)) {
      return;
    }
    if (!fileContent.includes(originalContent)) {
      return;
    }
    fs.writeFileSync(
      filePath,
      fileContent.replace(originalContent, taggedNewContent),
      "utf8"
    );
    return;
  }

  if (fileContent.includes(taggedNewContent)) {
    fs.writeFileSync(
      filePath,
      fileContent.replace(taggedNewContent, originalContent),
      "utf8"
    );
    return;
  }

  // Backward-compat: revert legacy single-marker form if present.
  const legacyTagged = `/* checksumai */ ${newContent}`;
  if (fileContent.includes(legacyTagged)) {
    fs.writeFileSync(
      filePath,
      fileContent.replace(legacyTagged, originalContent),
      "utf8"
    );
  }
}

function doesFileExist(filePath) {
  if (!fs.existsSync(filePath)) {
    console.warn("File not found", filePath);
    return false;
  }
  return true;
}

// -------- [Modifications] -------- //

// Remove conditions for injecting Playwright scripts
function alwaysInjectScripts(projectRoot) {
  const file = join(
    projectRoot,
    "node_modules/playwright-core/lib/server/browserContext.js"
  );
  if (!doesFileExist(file)) {
    return;
  }

  const originalContent = 'if ((0, import_debug.debugMode)() === "console")';

  const newContent = "";

  replaceContent(file, originalContent, newContent);
}

// Add implementation for generateSelectorAndLocator and inject to Playwright console API
function addGenerateSelectorAndLocator(projectRoot) {
  const file = join(
    projectRoot,
    "node_modules/playwright-core/lib/generated/injectedScriptSource.js"
  );
  if (!doesFileExist(file)) {
    return;
  }
  const entryPointText1 = "this._generateLocator(element, language),\\n      ";
  const appendText1 =
    "generateSelectorAndLocator: (element, language) => this._generateSelectorAndLocator(element, language),\\n asLocator,\\n     ";
  amend(file, entryPointText1, appendText1);

  const entryPointText2 = `return asLocator(language || "javascript", selector);\\n  }\\n  `;
  const appendText2 =
    '_generateSelectorAndLocator(element, language) {\\n    if (!(element instanceof Element))\\n      throw new Error(`Usage: playwright.locator(element).`);\\n    const selector = this._injectedScript.generateSelectorSimple(element);\\n    return {selector, locator: asLocator(language || \\"javascript\\", selector)};\\n  }\\n  ';
  amend(file, entryPointText2, appendText2);
}

// -------- [Runtime modifications] -------- //

function expect(projectRoot) {
  const file = join(
    projectRoot,
    "node_modules/playwright/lib/matchers/expect.js"
  );
  if (!doesFileExist(file)) {
    return;
  }
  let originalContent, newContent;

  // originalContent = `return (...args) => {
  //     const testInfo = (0, _globals.currentTestInfo)();`;
  // newContent = `return (...args) => {
  //     let noSoft = false;
  //     if (args.find(arg=>arg==='no-soft')){
  //       noSoft = true;
  //       args.pop();
  //     }
  //     const testInfo = (0, _globals.currentTestInfo)();`;
  // replaceContent(file, originalContent, newContent);

  // originalContent = `step.complete({
  //         error
  //       })`;
  // newContent = `step.complete({
  //         error,
  //         noSoft
  //       })`;
  // replaceContent(file, originalContent, newContent);

  // originalContent = `if (this._info.isSoft) testInfo._failWithError(error);else throw error;`;
  // newContent = `if (this._info.isSoft && !noSoft) testInfo._failWithError(error);else throw error;`;
  // replaceContent(file, originalContent, newContent);
}

function testInfo(projectRoot) {
  const file = join(
    projectRoot,
    "node_modules/playwright/lib/worker/testInfo.js"
  );
  if (!doesFileExist(file)) {
    return;
  }
  let originalContent, newContent;
  let entryPointText, appendText;

  // originalContent = `const filteredStack = (0, _util.filteredStackTrace)((0, _utils.captureRawStack)());`;
  // newContent = `const filteredStack = (0, _util.filteredStackTrace)((0, _utils.captureRawStack)().filter(s=>!s.includes('@checksum-ai/runtime')));`;
  // replaceContent(file, originalContent, newContent);

  entryPointText = `location = location || filteredStack[0];`;
  appendText = `\nif (this._checksumInternal) {
      location = undefined;
      this._checksumInternal = false;
    }
    if (this._checksumNoLocation){
      location = undefined;
    }`;
  amend(file, entryPointText, appendText);

  originalContent = `if (childStep.error && childStep.infectParentStepsWithError) {`;
  newContent = `if (childStep.error && childStep.infectParentStepsWithError && !step.preventInfectParentStepsWithError) {`;
  replaceContent(file, originalContent, newContent);

  originalContent = `_failWithError(error) {`;
  newContent = `addError(error, message) {
    const serialized = (0, import_util2.testInfoError)(error);
    serialized.message = [message, serialized.message].join('\\n\\n');
    serialized.stack = [message, serialized.stack].join('\\n\\n');
    const step = error[stepSymbol];
    if (step && step.boxedStack) serialized.stack = \`\${error.name}: \${error.message}\\n\${(0, import_utils.stringifyStackFrames)(step.boxedStack).join('\\n')}\`;
    this.errors.push(serialized);
  }
  _failWithError(error) {`;
  replaceContent(file, originalContent, newContent);
}

function testType(projectRoot) {
  const file = join(
    projectRoot,
    "node_modules/playwright/lib/common/testType.js"
  );
  if (!doesFileExist(file)) {
    return;
  }

  entryPointText = `return await (0, import_utils.currentZone)().with("stepZone", step).run(async () => {`;
  // entryPointText = `return await _utils.zones.run('stepZone', step, async () => {`;
  appendText = `\nif (options.obtainStep){
        options.obtainStep(step);
      }`;
  amend(file, entryPointText, appendText);
}

function indexContent(projectRoot) {
  const file = join(projectRoot, "node_modules/playwright/lib/index.js");
  if (!doesFileExist(file)) {
    return;
  }
  let originalContent, newContent;
  originalContent = `const browser = await playwright[browserName].launch();`;
  newContent = `
    let browser = playwright[browserName];
    try {
      const { playwrightExtra } = testInfo?.project?.use || {};
      if (playwrightExtra && playwrightExtra?.length) {
        const pw = require("playwright-extra")
        const PupeteerExtraPlugin = require("puppeteer-extra-plugin").PuppeteerExtraPlugin
        const chromium = pw.chromium;
        
        playwrightExtra.forEach((plugin, i) => {
          try {
            if(!(plugin instanceof PupeteerExtraPlugin)){
              console.warn(\`Plugin at index \${i} is not an instance of PupeteerExtraPlugin\`);
            }
            chromium.use(plugin);
          } catch (e) {
            console.warn(e);
          }
        });
        browser = chromium;
      }
    } catch (e) {
      console.warn(
        "CHECKSUM: Failed to load Playwright Extra, using Playwright instead.",
        e
      );
    }
    browser = await browser.launch();
  `;
  replaceContent(file, originalContent, newContent);
}

function channelOwner(projectRoot) {
  const file = join(
    projectRoot,
    "node_modules/playwright-core/lib/client/channelOwner.js"
  );
  if (!doesFileExist(file)) {
    return;
  }
  let originalContent, newContent;
  let entryPointText, appendText;

  entryPointText = `async _wrapApiCall(func, options) {`;

  appendText = `\nif (this._checksumInternal){
      options = options || {};
      options.internal = true;
    }`;
  amend(file, entryPointText, appendText);

  entryPointText = `const apiZone = { title: options?.title, apiName: stackTrace.apiName, frames: stackTrace.frames, internal: options?.internal ?? false, reported: false, userData: void 0, stepId: void 0 };`;

  appendText = `\nif (!apiZone.internal && this._checksumTitle){
      apiZone.apiName = this._checksumTitle;
      this._checksumTitle = undefined;
    }
    if (!apiZone.apiName){
      options = options || {};
      options.internal = true;
      apiZone.internal = true;
      apiZone.reported = true;
    }
    if (apiZone.apiName && apiZone.apiName.startsWith('proxy')) {
      apiZone.apiName = apiZone.apiName.replace('proxy', 'page');
    }`;
  amend(file, entryPointText, appendText);
}

function stackTrace(projectRoot) {
  const file = join(
    projectRoot,
    "node_modules/playwright-core/lib/utils/isomorphic/stackTrace.js"
  );
  if (!doesFileExist(file)) {
    return;
  }

  let originalContent, newContent;

  // Create a regex for getting a file for each line in the stacktrace that overrides the original regex
  // This regex is only used for getting files, the original regex is sitll used for the rest of the content
  originalContent = `let file = match[7];`;
  const fileRe = /(\/.*?\.[a-zA-Z0-9]+)(?=:\d+:\d+)/;
  newContent = `
  const fileRe = new RegExp(${JSON.stringify(fileRe.source)}, "${
    fileRe.flags
  }");
  const m = fileRe.exec(match[0] ?? "");
  let file = m ? m[1] : undefined;
  `;
  replaceContent(file, originalContent, newContent);

  // Filter out checksum-ai/runtime from stack traces
  originalContent = `return stack.split("\\n");`;
  newContent = `return stack.split("\\n").filter(s=>!s.includes('@checksum-ai/runtime'));`;
  replaceContent(file, originalContent, newContent);
}

function reportTraceFile(projectRoot) {
  const file = join(
    projectRoot,
    "node_modules/playwright/lib/reporters/html.js"
  );
  if (!doesFileExist(file)) {
    return;
  }

  let originalContent, newContent;

  originalContent = `const buffer = import_fs.default.readFileSync(a.path);`;
  newContent = `let buffer = import_fs.default.readFileSync(a.path);
          if (a.name === "trace") {
            let retries = 2;
            while (!buffer.slice(0,100).toString().startsWith("checksum-playwright-trace") && retries > 0) {
              Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 5000);
              buffer = import_fs.default.readFileSync(a.path)
              retries--;
            }
          }`;
  replaceContent(file, originalContent, newContent);
}

function htmlReporter(projectRoot) {
  const file = join(
    projectRoot,
    "node_modules/playwright/lib/reporters/html.js"
  );
  if (!doesFileExist(file)) {
    return;
  }

  let originalContent, newContent;

  // Filter out runtime files from snippet generation
  originalContent = `function createSnippets(stepsInFile) {
  for (const file of stepsInFile.keys()) {
    let source;
    try {
      source = import_fs.default.readFileSync(file, "utf-8") + "\\n//";
    } catch (e) {
      continue;
    }`;
  newContent = `function createSnippets(stepsInFile) {
  for (const file of stepsInFile.keys()) {
    // Skip runtime files to reduce report size
    if (file.includes('@checksum-ai/runtime') || file.includes('node_modules')) {
      continue;
    }
    let source;
    try {
      source = import_fs.default.readFileSync(file, "utf-8") + "\\n//";
    } catch (e) {
      continue;
    }`;
  replaceContent(file, originalContent, newContent);

  // Also filter from error codeframe generation
  originalContent = `function createErrorCodeframe(message, location) {
  let source;
  try {
    source = import_fs.default.readFileSync(location.file, "utf-8") + "\\n//";
  } catch (e) {
    return;
  }`;
  newContent = `function createErrorCodeframe(message, location) {
  // Skip runtime files to reduce report size
  if (location.file && (location.file.includes('@checksum-ai/runtime') || location.file.includes('node_modules'))) {
    return;
  }
  let source;
  try {
    source = import_fs.default.readFileSync(location.file, "utf-8") + "\\n//";
  } catch (e) {
    return;
  }`;
  replaceContent(file, originalContent, newContent);

  // Filter out steps with locations in runtime/node_modules files completely
  originalContent = `_createTestStep(dedupedStep, result) {
    const { step, duration, count } = dedupedStep;
    const skipped = dedupedStep.step.annotations?.find((a) => a.type === "skip");
    let title = step.title;
    if (skipped)
      title = \`\${title} (skipped\${skipped.description ? ": " + skipped.description : ""})\`;
    const testStep = {
      title,
      startTime: step.startTime.toISOString(),
      duration,
      steps: dedupeSteps(step.steps).map((s) => this._createTestStep(s, result)),
      attachments: step.attachments.map((s) => {
        const index = result.attachments.indexOf(s);
        if (index === -1)
          throw new Error("Unexpected, attachment not found");
        return index;
      }),
      location: this._relativeLocation(step.location),
      error: step.error?.message,
      count,
      skipped: !!skipped
    };
    if (step.location)
      this._stepsInFile.set(step.location.file, testStep);
    return testStep;
  }`;
  newContent = `_createTestStep(dedupedStep, result) {
    const { step, duration, count } = dedupedStep;
    // Skip "Evaluate" steps with locations in runtime/node_modules files
    if (step.location && step.title === "Evaluate" && (step.location.file.includes('@checksum-ai/runtime') || step.location.file.includes('node_modules'))) {
      // Return null to indicate this step should be filtered out
      return null;
    }
    const skipped = dedupedStep.step.annotations?.find((a) => a.type === "skip");
    let title = step.title;
    if (skipped)
      title = \`\${title} (skipped\${skipped.description ? ": " + skipped.description : ""})\`;
    const testStep = {
      title,
      startTime: step.startTime.toISOString(),
      duration,
      steps: dedupeSteps(step.steps).map((s) => this._createTestStep(s, result)).filter(s => s !== null),
      attachments: step.attachments.map((s) => {
        const index = result.attachments.indexOf(s);
        if (index === -1)
          throw new Error("Unexpected, attachment not found");
        return index;
      }),
      location: this._relativeLocation(step.location),
      error: step.error?.message,
      count,
      skipped: !!skipped
    };
    if (step.location)
      this._stepsInFile.set(step.location.file, testStep);
    return testStep;
  }`;
  replaceContent(file, originalContent, newContent);

  // Also filter steps when creating test results
  originalContent = `steps: dedupeSteps(result.steps).map((s) => this._createTestStep(s, result)),`;
  newContent = `steps: dedupeSteps(result.steps).map((s) => this._createTestStep(s, result)).filter(s => s !== null),`;
  replaceContent(file, originalContent, newContent);

  // Normalize locations for steps from runtime/index.js - hide location if it's index.js from runtime
  originalContent = `_relativeLocation(location) {
    if (!location)
      return void 0;
    const file = (0, import_utils.toPosixPath)(import_path.default.relative(this._config.rootDir, location.file));
    return {
      file,
      line: location.line,
      column: location.column
    };
  }`;
  newContent = `_relativeLocation(location) {
    if (!location)
      return void 0;
    // Hide location for steps from runtime/index.js to reduce clutter
    if (location.file && location.file.includes('@checksum-ai/runtime') && location.file.endsWith('index.js')) {
      return void 0;
    }
    const file = (0, import_utils.toPosixPath)(import_path.default.relative(this._config.rootDir, location.file));
    return {
      file,
      line: location.line,
      column: location.column
    };
  }`;
  replaceContent(file, originalContent, newContent);
}

// -------- [Run] -------- //

const isRuntime = true || process.env.RUNTIME === "true";

function run(projectPath) {
  try {
    if (fs.existsSync(projectPath)) {
      alwaysInjectScripts(projectPath);
      addGenerateSelectorAndLocator(projectPath);
      if (isRuntime) {
        expect(projectPath);
        testInfo(projectPath);
        testType(projectPath);
        channelOwner(projectPath);
        stackTrace(projectPath);
        indexContent(projectPath);
        reportTraceFile(projectPath);
        htmlReporter(projectPath);
      }
    } else {
      console.warn("Project path not found", projectPath);
    }
  } catch (e) {
    // ignore
    console.error(e);
  }
}

module.exports = run;
