import { $ } from "bun";
import path from "node:path";
import c from "picocolors";

const basePath = new URL(import.meta.resolve("../../")).pathname.slice(0, -1);

async function run(
   cmd: string[] | string,
   opts: Bun.SpawnOptions.OptionsObject & {},
   onChunk: (chunk: string, resolve: (data: any) => void, reject: (err: Error) => void) => void,
): Promise<{ proc: Bun.Subprocess; data: any }> {
   return new Promise((resolve, reject) => {
      const proc = Bun.spawn(Array.isArray(cmd) ? cmd : cmd.split(" "), {
         ...opts,
         stdout: "pipe",
         stderr: "pipe",
      });

      // Read from stdout
      const reader = (proc.stdout as ReadableStream).getReader();
      const decoder = new TextDecoder();

      // Function to read chunks
      let resolveCalled = false;
      (async () => {
         try {
            while (true) {
               const { done, value } = await reader.read();
               if (done) break;

               const text = decoder.decode(value);
               if (!resolveCalled) {
                  console.info(c.dim(text.replace(/\n$/, "")));
               }
               onChunk(
                  text,
                  (data) => {
                     resolve({ proc, data });
                     resolveCalled = true;
                  },
                  reject,
               );
            }
         } catch (err) {
            reject(err);
         }
      })();

      proc.exited.then((code) => {
         if (code !== 0 && code !== 130) {
            throw new Error(`Process exited with code ${code}`);
         }
      });
   });
}

const adapters = {
   node: {
      dir: path.join(basePath, "examples/node"),
      clean: async function () {
         const cwd = path.relative(process.cwd(), this.dir);
         await $`cd ${cwd} && rm -rf uploads data.db && mkdir -p uploads`;
      },
      start: async function () {
         return await run(
            "npm run start",
            {
               cwd: this.dir,
            },
            (chunk, resolve, reject) => {
               const regex = /running on (http:\/\/.*)\n/;
               if (regex.test(chunk)) {
                  resolve(chunk.match(regex)?.[1]);
               }
            },
         );
      },
   },
   bun: {
      dir: path.join(basePath, "examples/bun"),
      clean: async function () {
         const cwd = path.relative(process.cwd(), this.dir);
         await $`cd ${cwd} && rm -rf uploads data.db && mkdir -p uploads`;
      },
      start: async function () {
         return await run(
            "npm run start",
            {
               cwd: this.dir,
            },
            (chunk, resolve, reject) => {
               const regex = /running on (http:\/\/.*)\n/;
               if (regex.test(chunk)) {
                  resolve(chunk.match(regex)?.[1]);
               }
            },
         );
      },
   },
   cloudflare: {
      dir: path.join(basePath, "examples/cloudflare-worker"),
      clean: async function () {
         const cwd = path.relative(process.cwd(), this.dir);
         await $`cd ${cwd} && rm -rf .wrangler node_modules/.cache node_modules/.mf`;
      },
      start: async function () {
         return await run(
            "npm run dev",
            {
               cwd: this.dir,
            },
            (chunk, resolve, reject) => {
               const regex = /Ready on (http:\/\/.*)/;
               if (regex.test(chunk)) {
                  resolve(chunk.match(regex)?.[1]);
               }
            },
         );
      },
   },
   "react-router": {
      dir: path.join(basePath, "examples/react-router"),
      clean: async function () {
         const cwd = path.relative(process.cwd(), this.dir);
         await $`cd ${cwd} && rm -rf .react-router data.db`;
         await $`cd ${cwd} && rm -rf public/uploads && mkdir -p public/uploads`;
      },
      start: async function () {
         return await run(
            "npm run dev",
            {
               cwd: this.dir,
            },
            (chunk, resolve, reject) => {
               const regex = /Local.*?(http:\/\/.*)\//;
               if (regex.test(chunk)) {
                  resolve(chunk.match(regex)?.[1]);
               }
            },
         );
      },
   },
   nextjs: {
      dir: path.join(basePath, "examples/nextjs"),
      env: "TEST_TIMEOUT=20000",
      clean: async function () {
         const cwd = path.relative(process.cwd(), this.dir);
         await $`cd ${cwd} && rm -rf .nextjs data.db`;
         await $`cd ${cwd} && rm -rf public/uploads && mkdir -p public/uploads`;
      },
      start: async function () {
         return await run(
            "npm run dev",
            {
               cwd: this.dir,
            },
            (chunk, resolve, reject) => {
               const regex = /Local.*?(http:\/\/.*)\n/;
               if (regex.test(chunk)) {
                  resolve(chunk.match(regex)?.[1]);
               }
            },
         );
      },
   },
   astro: {
      dir: path.join(basePath, "examples/astro"),
      clean: async function () {
         const cwd = path.relative(process.cwd(), this.dir);
         await $`cd ${cwd} && rm -rf .astro data.db`;
         await $`cd ${cwd} && rm -rf public/uploads && mkdir -p public/uploads`;
      },
      start: async function () {
         return await run(
            "npm run dev",
            {
               cwd: this.dir,
            },
            (chunk, resolve, reject) => {
               const regex = /Local.*?(http:\/\/.*)\//;
               if (regex.test(chunk)) {
                  resolve(chunk.match(regex)?.[1]);
               }
            },
         );
      },
   },
} as const;

async function testAdapter(name: keyof typeof adapters) {
   const config = adapters[name];
   console.info("adapter", c.cyan(name));
   await config.clean();

   const { proc, data } = await config.start();
   console.info("proc:", proc.pid, "data:", c.cyan(data));
   //proc.kill();process.exit(0);

   const add_env = "env" in config && config.env ? config.env : "";
   await $`TEST_URL=${data} TEST_ADAPTER=${name} ${add_env} bun run test:e2e`;
   console.info("DONE!");

   while (!proc.killed) {
      proc.kill("SIGINT");
      await Bun.sleep(250);
      console.info("Waiting for process to exit...");
   }
}

// run with: TEST_ADAPTER=astro bun run e2e/adapters.ts
// (modify `test:e2e` to `test:e2e:ui` to see the UI)
if (process.env.TEST_ADAPTER) {
   await testAdapter(process.env.TEST_ADAPTER as any);
} else {
   for (const [name] of Object.entries(adapters)) {
      await testAdapter(name as any);
   }
}
