/* eslint-disable @typescript-eslint/ban-ts-comment */
/* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable @typescript-eslint/no-unused-vars */

import path from "path";
import puppeteer, { executablePath } from "puppeteer-core";
import ansiC from "ansi-colors";
import { IBuiltConfig, IRunTime, ISummary } from "../../Types.js";
import { ChildProcess } from "child_process";
import fs, { watch } from "fs";

import {
  createLogStreams,
  filesHash,
  IOutputs,
  isValidUrl,
  pollForFile,
  puppeteerConfigs,
  writeFileAndCreateDir,
} from "../../PM/utils.js";

import { PM_0 } from "./PM_0.js";
import { makePrompt } from "./makePrompt.js";
import { getRunnables } from "./utils.js";

const changes: Record<string, string> = {};

export abstract class PM_1_WithProcesses extends PM_0 {
  summary: ISummary = {};

  webMetafileWatcher: fs.FSWatcher;
  nodeMetafileWatcher: fs.FSWatcher;
  importMetafileWatcher: fs.FSWatcher;
  pitonoMetafileWatcher: fs.FSWatcher;
  golangMetafileWatcher: fs.FSWatcher;

  ports: Record<number, string>;
  queue: string[];
  logStreams: Record<string, ReturnType<typeof createLogStreams>> = {};
  launchers: Record<string, () => void>;

  abstract launchNode(src: string, dest: string);
  abstract launchWeb(src: string, dest: string);
  abstract launchPure(src: string, dest: string);
  abstract launchPython(src: string, dest: string);
  abstract launchGolang(src: string, dest: string);
  abstract startBuildProcesses(): Promise<void>;

  abstract tscCheck({
    entrypoint,
    addableFiles,
    platform,
  }: {
    entrypoint: string;
    addableFiles: string[];
    platform: IRunTime;
  });

  abstract eslintCheck({
    entrypoint,
    addableFiles,
    platform,
  }: {
    entrypoint: string;
    addableFiles: string[];
    platform: IRunTime;
  });

  abstract onBuildDone(): void;

  runningProcesses: Map<string, ChildProcess | Promise<any>> = new Map();

  allProcesses: Map<
    string,
    {
      child?: ChildProcess;
      promise?: Promise<any>;
      status: "running" | "exited" | "error" | "completed";
      exitCode?: number;
      error?: string;
      command: string;
      pid?: number;
      timestamp: string;
      type: "process" | "promise";
      category: "aider" | "bdd-test" | "build-time" | "other";
      testName?: string;
      platform?: "node" | "web" | "pure" | "python" | "golang";
    }
  > = new Map();
  processLogs: Map<string, string[]> = new Map();

  clients: Set<any> = new Set();
  public fs: import("@isomorphic-git/lightning-fs");
  connected: boolean;

  constructor(configs: IBuiltConfig, name, mode) {
    super(configs, name, mode);

    console.log("mark5", configs);

    console.log("mark1", this.configs);

    this.configs.tests.forEach(([t, rt, tr, sidecars]) => {
      this.ensureSummaryEntry(t);
      sidecars.forEach(([sidecarName]) => {
        this.ensureSummaryEntry(sidecarName, true);
      });
    });

    this.launchers = {};
    this.ports = {};
    this.queue = [];

    this.configs.ports.forEach((element) => {
      this.ports[element] = ""; // set ports as open
    });
  }

  writeBigBoard = () => {
    // note: this path is different from the one used by front end
    const summaryPath = `./testeranto/reports/${this.projectName}/summary.json`;
    const summaryData = JSON.stringify(this.summary, null, 2);
    fs.writeFileSync(summaryPath, summaryData);

    // Broadcast the update
    this.webSocketBroadcastMessage({
      type: "summaryUpdate",
      data: this.summary,
    });
  };

  webSocketBroadcastMessage(message: any) {
    const data =
      typeof message === "string" ? message : JSON.stringify(message);
    this.clients.forEach((client) => {
      if (client.readyState === 1) {
        client.send(data);
      }
    });
  }

  addPromiseProcess(
    processId: string,
    promise: Promise<any>,
    command: string,
    category: "aider" | "bdd-test" | "build-time" | "other" = "other",
    testName?: string,
    platform?: "node" | "web" | "pure" | "python" | "golang",
    onResolve?: (result: any) => void,
    onReject?: (error: any) => void
  ) {
    this.runningProcesses.set(processId, promise);
    this.allProcesses.set(processId, {
      promise,
      status: "running",
      command,
      timestamp: new Date().toISOString(),
      type: "promise",
      category,
      testName,
      platform,
    });

    // Initialize logs for this process
    this.processLogs.set(processId, []);

    const startMessage = `Starting: ${command}`;
    const logs = this.processLogs.get(processId) || [];
    logs.push(startMessage);
    this.processLogs.set(processId, logs);

    this.webSocketBroadcastMessage({
      type: "processStarted",
      processId,
      command,
      timestamp: new Date().toISOString(),
      logs: [startMessage],
    });

    promise
      .then((result) => {
        this.runningProcesses.delete(processId);
        const processInfo = this.allProcesses.get(processId);
        if (processInfo) {
          this.allProcesses.set(processId, {
            ...processInfo,
            status: "completed",
            exitCode: 0,
          });
        }

        // Add log entry for process completion
        const successMessage = `Completed successfully with result: ${JSON.stringify(
          result
        )}`;
        const currentLogs = this.processLogs.get(processId) || [];
        currentLogs.push(successMessage);
        this.processLogs.set(processId, currentLogs);

        this.webSocketBroadcastMessage({
          type: "processExited",
          processId,
          exitCode: 0,
          timestamp: new Date().toISOString(),
          logs: [successMessage],
        });
        if (onResolve) onResolve(result);
      })
      .catch((error) => {
        this.runningProcesses.delete(processId);

        const processInfo = this.allProcesses.get(processId);
        if (processInfo) {
          this.allProcesses.set(processId, {
            ...processInfo,
            status: "error",
            error: error.message,
          });
        }

        const errorMessage = `Failed with error: ${error.message}`;
        const currentLogs = this.processLogs.get(processId) || [];
        currentLogs.push(errorMessage);
        this.processLogs.set(processId, currentLogs);

        this.webSocketBroadcastMessage({
          type: "processError",
          processId,
          error: error.message,
          timestamp: new Date().toISOString(),
          logs: [errorMessage],
        });
        if (onReject) onReject(error);
      });

    return processId;
  }

  getProcessesByCategory(
    category: "aider" | "bdd-test" | "build-time" | "other"
  ) {
    return Array.from(this.allProcesses.entries())
      .filter(([id, procInfo]) => procInfo.category === category)
      .map(([id, procInfo]) => ({
        processId: id,
        command: procInfo.command,
        pid: procInfo.pid,
        status: procInfo.status,
        exitCode: procInfo.exitCode,
        error: procInfo.error,
        timestamp: procInfo.timestamp,
        category: procInfo.category,
        testName: procInfo.testName,
        platform: procInfo.platform,
        logs: this.processLogs.get(id) || [],
      }));
  }

  getBDDTestProcesses() {
    return this.getProcessesByCategory("bdd-test");
  }

  getBuildTimeProcesses() {
    return this.getProcessesByCategory("build-time");
  }

  getAiderProcesses() {
    return this.getProcessesByCategory("aider");
  }

  getProcessesByTestName(testName: string) {
    return Array.from(this.allProcesses.entries())
      .filter(([id, procInfo]) => procInfo.testName === testName)
      .map(([id, procInfo]) => ({
        processId: id,
        command: procInfo.command,
        pid: procInfo.pid,
        status: procInfo.status,
        exitCode: procInfo.exitCode,
        error: procInfo.error,
        timestamp: procInfo.timestamp,
        category: procInfo.category,
        testName: procInfo.testName,
        platform: procInfo.platform,
        logs: this.processLogs.get(id) || [],
      }));
  }

  getProcessesByPlatform(
    platform: "node" | "web" | "pure" | "pitono" | "golang"
  ) {
    return Array.from(this.allProcesses.entries())
      .filter(([id, procInfo]) => procInfo.platform === platform)
      .map(([id, procInfo]) => ({
        processId: id,
        command: procInfo.command,
        pid: procInfo.pid,
        status: procInfo.status,
        exitCode: procInfo.exitCode,
        error: procInfo.error,
        timestamp: procInfo.timestamp,
        category: procInfo.category,
        testName: procInfo.testName,
        platform: procInfo.platform,
        logs: this.processLogs.get(id) || [],
      }));
  }

  bddTestIsRunning(src: string) {
    // @ts-ignore
    this.summary[src] = {
      prompt: "?",
      runTimeErrors: "?",
      staticErrors: "?",
      typeErrors: "?",
      failingFeatures: {},
    };
  }

  async metafileOutputs(platform: IRunTime) {
    let metafilePath: string;
    if (platform === "python") {
      metafilePath = `./testeranto/metafiles/python/core.json`;
    } else {
      metafilePath = `./testeranto/metafiles/${platform}/${this.projectName}.json`;
    }

    // Ensure the metafile exists
    if (!fs.existsSync(metafilePath)) {
      console.log(
        ansiC.yellow(`Metafile not found at ${metafilePath}, skipping`)
      );
      return;
    }

    let metafile;
    try {
      const fileContent = fs.readFileSync(metafilePath).toString();
      const parsedData = JSON.parse(fileContent);
      // Handle different metafile structures
      if (platform === "python") {
        // Pitono metafile might be the entire content or have a different structure
        metafile = parsedData.metafile || parsedData;
      } else {
        metafile = parsedData.metafile;
      }
      if (!metafile) {
        console.log(
          ansiC.yellow(ansiC.inverse(`No metafile found in ${metafilePath}`))
        );
        return;
      }
      // console.log(
      //   ansiC.blue(
      //     `Found metafile for ${platform} with ${
      //       Object.keys(metafile.outputs || {}).length
      //     } outputs`
      //   )
      // );
    } catch (error) {
      console.error(`Error reading metafile at ${metafilePath}:`, error);
      return;
    }

    const outputs: IOutputs = metafile.outputs;

    // Check if outputs exists and is an object
    // if (!outputs || typeof outputs !== "object") {
    //   console.log(
    //     ansiC.yellow(
    //       ansiC.inverse(`No outputs found in metafile at ${metafilePath}`)
    //     )
    //   );
    //   return;
    // }

    // @ts-ignore
    Object.keys(outputs).forEach(async (k) => {
      const pattern = `testeranto/bundles/${platform}/${this.projectName}/${this.configs.src}`;
      if (!k.startsWith(pattern)) {
        return;
      }

      // // @ts-ignore
      const output = outputs[k];
      // Check if the output entry exists and has inputs
      // if (!output || !output.inputs) {
      //   return;
      // }

      // @ts-ignore
      const addableFiles = Object.keys(output.inputs).filter((i) => {
        if (!fs.existsSync(i)) return false;
        if (i.startsWith("node_modules")) return false;
        if (i.startsWith("./node_modules")) return false;

        return true;
      });

      const f = `${k.split(".").slice(0, -1).join(".")}/`;

      if (!fs.existsSync(f)) {
        fs.mkdirSync(f, { recursive: true });
      }

      // @ts-ignore
      let entrypoint = output.entryPoint;

      if (entrypoint) {
        // Normalize the entrypoint path to ensure consistent comparison
        entrypoint = path.normalize(entrypoint);

        const changeDigest = await filesHash(addableFiles);

        if (changeDigest === changes[entrypoint]) {
          // skip
        } else {
          changes[entrypoint] = changeDigest;

          // Run appropriate static analysis based on platform
          if (
            platform === "node" ||
            platform === "web" ||
            platform === "pure"
          ) {
            this.tscCheck({ entrypoint, addableFiles, platform });
            this.eslintCheck({ entrypoint, addableFiles, platform });
          } else if (platform === "python") {
            this.pythonLintCheck(entrypoint, addableFiles);
            this.pythonTypeCheck(entrypoint, addableFiles);
          }

          makePrompt(
            this.summary,
            this.projectName,
            entrypoint,
            addableFiles,
            platform
          );

          const testName = this.findTestNameByEntrypoint(entrypoint, platform);
          if (testName) {
            console.log(
              ansiC.green(
                ansiC.inverse(
                  `Source files changed, re-queueing test: ${testName}`
                )
              )
            );
            this.addToQueue(testName, platform);
          } else {
            console.error(
              `Could not find test for entrypoint: ${entrypoint} (${platform})`
            );
            process.exit(-1);
          }
        }
      }
    });
  }

  private findTestNameByEntrypoint(
    entrypoint: string,
    platform: IRunTime
  ): string | null {
    const runnables = getRunnables(this.configs.tests, this.projectName);

    let entryPointsMap: Record<string, string>;

    switch (platform) {
      case "node":
        entryPointsMap = runnables.nodeEntryPoints;
        break;
      case "web":
        entryPointsMap = runnables.webEntryPoints;
        break;
      case "pure":
        entryPointsMap = runnables.pureEntryPoints;
        break;
      case "python":
        entryPointsMap = runnables.pythonEntryPoints;
        break;
      case "golang":
        entryPointsMap = runnables.golangEntryPoints;
        break;
      default:
        throw "wtf";
    }

    if (!entryPointsMap) {
      console.error("idk");
    }

    if (!entryPointsMap[entrypoint]) {
      console.error(`${entrypoint} not found`);
    }

    return entryPointsMap[entrypoint];
  }

  async pythonLintCheck(entrypoint: string, addableFiles: string[]) {
    const reportDest = `testeranto/reports/${this.projectName}/${entrypoint
      .split(".")
      .slice(0, -1)
      .join(".")}/python`;

    if (!fs.existsSync(reportDest)) {
      fs.mkdirSync(reportDest, { recursive: true });
    }

    const lintErrorsPath = `${reportDest}/lint_errors.txt`;

    try {
      // Use flake8 for Python linting
      const { spawn } = await import("child_process");
      const child = spawn("flake8", [entrypoint, "--max-line-length=88"], {
        stdio: ["pipe", "pipe", "pipe"],
      });

      let stderr = "";
      child.stderr.on("data", (data) => {
        stderr += data.toString();
      });

      let stdout = "";
      child.stdout.on("data", (data) => {
        stdout += data.toString();
      });

      return new Promise<void>((resolve) => {
        child.on("close", () => {
          const output = stdout + stderr;
          if (output.trim()) {
            fs.writeFileSync(lintErrorsPath, output);
            this.summary[entrypoint].staticErrors = output.split("\n").length;
          } else {
            if (fs.existsSync(lintErrorsPath)) {
              fs.unlinkSync(lintErrorsPath);
            }
            this.summary[entrypoint].staticErrors = 0;
          }
          resolve();
        });
      });
    } catch (error) {
      console.error(`Error running flake8 on ${entrypoint}:`, error);
      fs.writeFileSync(
        lintErrorsPath,
        `Error running flake8: ${error.message}`
      );
      this.summary[entrypoint].staticErrors = -1;
    }
  }

  async pythonTypeCheck(entrypoint: string, addableFiles: string[]) {
    const reportDest = `testeranto/reports/${this.projectName}/${entrypoint
      .split(".")
      .slice(0, -1)
      .join(".")}/python`;

    if (!fs.existsSync(reportDest)) {
      fs.mkdirSync(reportDest, { recursive: true });
    }

    const typeErrorsPath = `${reportDest}/type_errors.txt`;

    try {
      // Use mypy for Python type checking
      const { spawn } = await import("child_process");
      const child = spawn("mypy", [entrypoint], {
        stdio: ["pipe", "pipe", "pipe"],
      });

      let stderr = "";
      child.stderr.on("data", (data) => {
        stderr += data.toString();
      });

      let stdout = "";
      child.stdout.on("data", (data) => {
        stdout += data.toString();
      });

      return new Promise<void>((resolve) => {
        child.on("close", () => {
          const output = stdout + stderr;
          if (output.trim()) {
            fs.writeFileSync(typeErrorsPath, output);
            this.summary[entrypoint].typeErrors = output.split("\n").length;
          } else {
            if (fs.existsSync(typeErrorsPath)) {
              fs.unlinkSync(typeErrorsPath);
            }
            this.summary[entrypoint].typeErrors = 0;
          }
          resolve();
        });
      });
    } catch (error) {
      console.error(`Error running mypy on ${entrypoint}:`, error);
      fs.writeFileSync(typeErrorsPath, `Error running mypy: ${error.message}`);
      this.summary[entrypoint].typeErrors = -1;
    }
  }

  async start() {
    // Wait for build processes to complete first
    try {
      await this.startBuildProcesses();

      // Generate Python metafile if there are Python tests
      const pythonTests = this.configs.tests.filter(
        (test) => test[1] === "python"
      );
      if (pythonTests.length > 0) {
        const { generatePitonoMetafile, writePitonoMetafile } = await import(
          "../../utils/pitonoMetafile.js"
        );
        const entryPoints = pythonTests.map((test) => test[0]);
        const metafile = await generatePitonoMetafile(
          this.projectName,
          entryPoints
        );
        writePitonoMetafile(this.projectName, metafile);
      }

      this.onBuildDone();
    } catch (error) {
      console.error("Build processes failed:", error);
      return;
    }

    // Continue with the rest of the setup after builds are done
    this.mapping().forEach(async ([command, func]) => {
      globalThis[command] = func;
    });

    if (!fs.existsSync(`testeranto/reports/${this.projectName}`)) {
      fs.mkdirSync(`testeranto/reports/${this.projectName}`);
    }

    try {
      this.browser = await puppeteer.launch(puppeteerConfigs);
    } catch (e) {
      console.error(e);
      console.error(
        "could not start chrome via puppeter. Check this path: ",
        executablePath
      );
    }

    const runnables = getRunnables(this.configs.tests, this.projectName);
    const {
      nodeEntryPoints,
      webEntryPoints,
      pureEntryPoints,
      pythonEntryPoints,
      golangEntryPoints,
    } = runnables;

    // Add all tests to the queue
    [
      ["node", nodeEntryPoints],
      ["web", webEntryPoints],
      ["pure", pureEntryPoints],
      ["python", pythonEntryPoints],
      ["golang", golangEntryPoints],
    ].forEach(([runtime, entryPoints]: [IRunTime, Record<string, string>]) => {
      Object.keys(entryPoints).forEach((entryPoint) => {
        // Create the report directory
        const reportDest = `testeranto/reports/${this.projectName}/${entryPoint
          .split(".")
          .slice(0, -1)
          .join(".")}/${runtime}`;
        if (!fs.existsSync(reportDest)) {
          fs.mkdirSync(reportDest, { recursive: true });
        }

        // Add to the processing queue
        this.addToQueue(entryPoint, runtime);
      });
    });

    // Set up metafile watchers for each runtime
    const runtimeConfigs = [
      ["node", nodeEntryPoints],
      ["web", webEntryPoints],
      ["pure", pureEntryPoints],
      ["python", pythonEntryPoints],
      ["golang", golangEntryPoints],
    ];

    for (const [runtime, entryPoints] of runtimeConfigs) {
      if (Object.keys(entryPoints).length === 0) continue;

      // For python, the metafile path is different
      let metafile: string;
      if (runtime === "python") {
        metafile = `./testeranto/metafiles/${runtime}/core.json`;
      } else {
        metafile = `./testeranto/metafiles/${runtime}/${this.projectName}.json`;
      }

      // Ensure the directory exists
      const metafileDir = metafile.split("/").slice(0, -1).join("/");
      if (!fs.existsSync(metafileDir)) {
        fs.mkdirSync(metafileDir, { recursive: true });
      }

      try {
        // For python, we may need to generate the metafile first
        if (runtime === "python" && !fs.existsSync(metafile)) {
          const { generatePitonoMetafile, writePitonoMetafile } = await import(
            "../../utils/pitonoMetafile.js"
          );
          const entryPointList = Object.keys(entryPoints);
          if (entryPointList.length > 0) {
            const metafileData = await generatePitonoMetafile(
              this.projectName,
              entryPointList
            );
            writePitonoMetafile(this.projectName, metafileData);
          }
        }

        await pollForFile(metafile);
        // console.log("Found metafile for", runtime, metafile);

        // Set up watcher for the metafile with debouncing
        let timeoutId: NodeJS.Timeout;
        const watcher = watch(metafile, async (e, filename) => {
          // Debounce to avoid multiple rapid triggers
          clearTimeout(timeoutId);
          timeoutId = setTimeout(async () => {
            console.log(
              ansiC.yellow(ansiC.inverse(`< ${e} ${filename} (${runtime})`))
            );
            try {
              await this.metafileOutputs(runtime as IRunTime);
              // After processing metafile changes, check the queue to run tests
              console.log(
                ansiC.blue(
                  `Metafile processed, checking queue for tests to run`
                )
              );
              this.checkQueue();
            } catch (error) {
              console.error(`Error processing metafile changes:`, error);
            }
          }, 300); // 300ms debounce
        });

        // Store the watcher based on runtime
        switch (runtime) {
          case "node":
            this.nodeMetafileWatcher = watcher;
            break;
          case "web":
            this.webMetafileWatcher = watcher;
            break;
          case "pure":
            this.importMetafileWatcher = watcher;
            break;
          case "python":
            this.pitonoMetafileWatcher = watcher;
            break;
          case "golang":
            this.golangMetafileWatcher = watcher;
            break;
        }

        // Read the metafile immediately
        await this.metafileOutputs(runtime as IRunTime);
      } catch (error) {
        console.error(`Error setting up watcher for ${runtime}:`, error);
      }
    }
  }

  async stop() {
    console.log(ansiC.inverse("Testeranto-Run is shutting down gracefully..."));
    this.mode = "once";
    this.nodeMetafileWatcher.close();
    this.webMetafileWatcher.close();
    this.importMetafileWatcher.close();
    if (this.pitonoMetafileWatcher) {
      this.pitonoMetafileWatcher.close();
    }

    // if (this.gitWatcher) {
    //   this.gitWatcher.close();
    // }

    // if (this.gitWatchTimeout) {
    //   clearTimeout(this.gitWatchTimeout);
    // }

    Object.values(this.logStreams || {}).forEach((logs) => logs.closeAll());

    // if (this.wss) {
    //   this.wss.close(() => {
    //     console.log("WebSocket server closed");
    //   });
    // }

    this.clients.forEach((client) => {
      client.terminate();
    });
    this.clients.clear();

    // if (this.httpServer) {
    //   this.httpServer.close(() => {
    //     console.log("HTTP server closed");
    //   });
    // }
    this.checkForShutdown();
  }

  receiveFeaturesV2 = (
    reportDest: string,
    srcTest: string,
    platform: IRunTime
  ) => {
    const featureDestination = path.resolve(
      process.cwd(),
      "reports",
      "features",
      "strings",
      srcTest.split(".").slice(0, -1).join(".") + ".features.txt"
    );

    const testReportPath = `${reportDest}/tests.json`;
    if (!fs.existsSync(testReportPath)) {
      console.error(`tests.json not found at: ${testReportPath}`);
      return;
    }

    const testReport = JSON.parse(fs.readFileSync(testReportPath, "utf8"));

    // Add full path information to each test
    if (testReport.tests) {
      testReport.tests.forEach((test) => {
        // Add the full path to each test
        test.fullPath = path.resolve(process.cwd(), srcTest);
      });
    }

    // Add full path to the report itself
    testReport.fullPath = path.resolve(process.cwd(), srcTest);

    // Write the modified report back
    fs.writeFileSync(testReportPath, JSON.stringify(testReport, null, 2));

    testReport.features
      .reduce(async (mm, featureStringKey) => {
        const accum = await mm;

        const isUrl = isValidUrl(featureStringKey);

        if (isUrl) {
          const u = new URL(featureStringKey);

          if (u.protocol === "file:") {
            accum.files.push(u.pathname);
          } else if (u.protocol === "http:" || u.protocol === "https:") {
            const newPath = `${process.cwd()}/testeranto/features/external/${
              u.hostname
            }${u.pathname}`;

            const body = await this.configs.featureIngestor(featureStringKey);

            writeFileAndCreateDir(newPath, body);
            accum.files.push(newPath);
          }
        } else {
          await fs.promises.mkdir(path.dirname(featureDestination), {
            recursive: true,
          });

          accum.strings.push(featureStringKey);
        }

        return accum;
      }, Promise.resolve({ files: [] as string[], strings: [] as string[] }))

      .then(({ files }: { files: string[]; strings: string[] }) => {
        // Markdown files must be referenced in the prompt but string style features are already present in the tests.json file

        fs.writeFileSync(
          `testeranto/reports/${this.projectName}/${srcTest
            .split(".")
            .slice(0, -1)
            .join(".")}/${platform}/featurePrompt.txt`,
          files
            .map((f) => {
              return `/read ${f}`;
            })
            .join("\n")
        );
      });

    testReport.givens.forEach((g) => {
      if (g.failed === true) {
        // @ts-ignore
        this.summary[srcTest].failingFeatures[g.key] = g.features;
      }
    });

    this.writeBigBoard();
  };

  addToQueue(src: string, runtime: IRunTime) {
    // Ensure we're using the original test source path, not a bundle path
    // The src parameter might be a bundle path from metafile changes
    // We need to find the corresponding test source path

    // First, check if this looks like a bundle path (contains 'testeranto/bundles')
    if (src.includes("testeranto/bundles")) {
      // Try to find the original test name that corresponds to this bundle
      const runnables = getRunnables(this.configs.tests, this.projectName);
      const allEntryPoints = [
        ...Object.entries(runnables.nodeEntryPoints),
        ...Object.entries(runnables.webEntryPoints),
        ...Object.entries(runnables.pureEntryPoints),
        ...Object.entries(runnables.pythonEntryPoints),
        ...Object.entries(runnables.golangEntryPoints),
      ];

      // Normalize the source path for comparison
      const normalizedSrc = path.normalize(src);

      for (const [testName, bundlePath] of allEntryPoints) {
        const normalizedBundlePath = path.normalize(bundlePath as string);
        // Check if the source path ends with the bundle path
        if (normalizedSrc.endsWith(normalizedBundlePath)) {
          // Use the original test name instead of the bundle path
          src = testName;
          break;
        }
      }
    }

    // First, clean up any existing processes for this test
    this.cleanupTestProcesses(src);

    // Add the test to the queue (using the original test source path)
    // Make sure we don't add duplicates
    if (!this.queue.includes(src)) {
      this.queue.push(src);
      console.log(
        ansiC.green(
          ansiC.inverse(`Added ${src} (${runtime}) to the processing queue`)
        )
      );
      // Try to process the queue
      this.checkQueue();
    } else {
      console.log(
        ansiC.yellow(
          ansiC.inverse(`Test ${src} is already in the queue, skipping`)
        )
      );
    }
  }

  private cleanupTestProcesses(testName: string) {
    // Find and clean up any running processes for this test
    const processesToCleanup: string[] = [];

    // Find all process IDs that match this test name
    for (const [processId, processInfo] of this.allProcesses.entries()) {
      if (
        processInfo.testName === testName &&
        processInfo.status === "running"
      ) {
        processesToCleanup.push(processId);
      }
    }

    // Clean up each process
    processesToCleanup.forEach((processId) => {
      const processInfo = this.allProcesses.get(processId);
      if (processInfo) {
        // Kill child process if it exists
        if (processInfo.child) {
          try {
            processInfo.child.kill();
          } catch (error) {
            console.error(`Error killing process ${processId}:`, error);
          }
        }

        // Update process status
        this.allProcesses.set(processId, {
          ...processInfo,
          status: "exited",
          exitCode: -1,
          error: "Killed due to source file change",
        });

        // Remove from running processes
        this.runningProcesses.delete(processId);

        // Broadcast process exit
        this.webSocketBroadcastMessage({
          type: "processExited",
          processId,
          exitCode: -1,
          timestamp: new Date().toISOString(),
          logs: ["Process killed due to source file change"],
        });
      }
    });
  }

  checkQueue() {
    // Process all items in the queue
    while (this.queue.length > 0) {
      const x = this.queue.pop();
      if (!x) continue;

      // Check if this test is already running
      let isRunning = false;
      for (const processInfo of this.allProcesses.values()) {
        if (processInfo.testName === x && processInfo.status === "running") {
          isRunning = true;
          break;
        }
      }

      if (isRunning) {
        console.log(
          ansiC.yellow(
            `Skipping ${x} - already running, will be re-queued when current run completes`
          )
        );
        continue;
      }

      const test = this.configs.tests.find((t) => t[0] === x);
      if (!test) {
        console.error(`test is undefined ${x}`);
        continue;
      }

      // Get the appropriate launcher based on the runtime type
      const runtime = test[1];

      const runnables = getRunnables(this.configs.tests, this.projectName);
      let dest: string;

      switch (runtime) {
        case "node":
          dest = runnables.nodeEntryPoints[x];
          if (dest) {
            this.launchNode(x, dest);
          } else {
            console.error(`No destination found for node test: ${x}`);
          }
          break;
        case "web":
          dest = runnables.webEntryPoints[x];
          if (dest) {
            this.launchWeb(x, dest);
          } else {
            console.error(`No destination found for web test: ${x}`);
          }
          break;
        case "pure":
          dest = runnables.pureEntryPoints[x];
          if (dest) {
            this.launchPure(x, dest);
          } else {
            console.error(`No destination found for pure test: ${x}`);
          }
          break;
        case "python":
          dest = runnables.pythonEntryPoints[x];
          if (dest) {
            this.launchPython(x, dest);
          } else {
            console.error(`No destination found for python test: ${x}`);
          }
          break;
        case "golang":
          dest = runnables.golangEntryPoints[x];
          if (dest) {
            // For Golang, we need to build and run the executable
            // The dest is the path to the generated wrapper file
            this.launchGolang(x, dest);
          } else {
            console.error(`No destination found for golang test: ${x}`);
          }
          break;
        default:
          console.error(`Unknown runtime: ${runtime} for test ${x}`);
          break;
      }
    }

    if (this.queue.length === 0) {
      console.log(ansiC.inverse(`The queue is empty`));
    }
  }

  checkForShutdown = () => {
    this.checkQueue();

    console.log(
      ansiC.inverse(
        `The following jobs are awaiting resources: ${JSON.stringify(
          this.queue
        )}`
      )
    );
    console.log(
      ansiC.inverse(`The status of ports: ${JSON.stringify(this.ports)}`)
    );

    this.writeBigBoard();

    if (this.mode === "dev") return;

    let inflight = false;

    Object.keys(this.summary).forEach((k) => {
      if (this.summary[k].prompt === "?") {
        console.log(ansiC.blue(ansiC.inverse(`🕕 prompt ${k}`)));
        inflight = true;
      }
    });

    Object.keys(this.summary).forEach((k) => {
      if (this.summary[k].runTimeErrors === "?") {
        console.log(ansiC.blue(ansiC.inverse(`🕕 runTimeError ${k}`)));
        inflight = true;
      }
    });

    Object.keys(this.summary).forEach((k) => {
      if (this.summary[k].staticErrors === "?") {
        console.log(ansiC.blue(ansiC.inverse(`🕕 staticErrors ${k}`)));
        inflight = true;
      }
    });

    Object.keys(this.summary).forEach((k) => {
      if (this.summary[k].typeErrors === "?") {
        console.log(ansiC.blue(ansiC.inverse(`🕕 typeErrors ${k}`)));
        inflight = true;
      }
    });

    this.writeBigBoard();

    if (!inflight) {
      if (this.browser) {
        if (this.browser) {
          this.browser.disconnect().then(() => {
            console.log(
              ansiC.inverse(`${this.projectName} has been tested. Goodbye.`)
            );
            process.exit();
          });
        }
      }
    }
  };

  private ensureSummaryEntry(src: string, isSidecar = false) {
    if (!this.summary[src]) {
      // @ts-ignore
      this.summary[src] = {
        typeErrors: undefined,
        staticErrors: undefined,
        runTimeErrors: undefined,
        prompt: undefined,
        failingFeatures: {},
      };
      if (isSidecar) {
        // Sidecars don't need all fields
        // delete this.summary[src].runTimeError;
        // delete this.summary[src].prompt;
      }
    }
    return this.summary[src];
  }
}
