import { describe, expect, it } from "@effect/vitest";
import * as Data from "effect/Data";
import * as Effect from "effect/Effect";

import type { SandboxToolInvoker } from "@executor-js/codemode-core";
import { makeQuickJsExecutor } from "./index";

class UnknownToolError extends Data.TaggedError("UnknownToolError")<{
  readonly path: string;
}> {}

const makeTestInvoker = (
  handlers: Record<string, (args: unknown) => unknown>,
): SandboxToolInvoker => ({
  invoke: ({ path, args }) => {
    const handler = handlers[path];
    if (!handler) {
      return Effect.fail(new UnknownToolError({ path }));
    }
    return Effect.try({
      try: () => handler(args),
      catch: (error) => error,
    });
  },
});

const executor = makeQuickJsExecutor({ timeoutMs: 5_000 });

describe("quickjs executor", () => {
  it.effect("runs plain code", () =>
    Effect.gen(function* () {
      const result = yield* executor.execute(`return 1 + 2`, makeTestInvoker({}));
      expect(result.result).toBe(3);
      expect(result.error).toBeUndefined();
    }),
  );

  it.effect("recovers prose-wrapped fenced async arrow input", () =>
    Effect.gen(function* () {
      const result = yield* executor.execute(
        ["Use this snippet.", "", "```ts", "async () => 42", "```"].join("\n"),
        makeTestInvoker({}),
      );

      expect(result.result).toBe(42);
      expect(result.error).toBeUndefined();
    }),
  );

  it.effect("invokes a tool and returns its result", () =>
    Effect.gen(function* () {
      const invoker = makeTestInvoker({
        "math.add": (args) => {
          const { a, b } = args as { a: number; b: number };
          return { sum: a + b };
        },
      });

      const result = yield* executor.execute(
        `
        const res = await tools.math.add({ a: 5, b: 3 });
        return res.sum;
        `,
        invoker,
      );

      expect(result.error).toBeUndefined();
      expect(result.result).toBe(8);
    }),
  );

  it.effect("invokes multiple tools in sequence", () =>
    Effect.gen(function* () {
      const invoker = makeTestInvoker({
        "users.get": (args) => {
          const { id } = args as { id: number };
          return { id, name: `User ${id}` };
        },
        "users.greet": (args) => {
          const { name } = args as { name: string };
          return { message: `Hello, ${name}!` };
        },
      });

      const result = yield* executor.execute(
        `
        const user = await tools.users.get({ id: 42 });
        const greeting = await tools.users.greet({ name: user.name });
        return greeting.message;
        `,
        invoker,
      );

      expect(result.error).toBeUndefined();
      expect(result.result).toBe("Hello, User 42!");
    }),
  );

  it.effect("handles tool errors", () =>
    Effect.gen(function* () {
      const invoker = makeTestInvoker({
        "db.query": () => {
          throw new Error("connection refused");
        },
      });

      const result = yield* executor.execute(
        `
        try {
          await tools.db.query({ sql: "SELECT 1" });
          return "should not reach";
        } catch (e) {
          return "caught: " + e.message;
        }
        `,
        invoker,
      );

      expect(result.error).toBeUndefined();
      expect(result.result).toContain("caught:");
    }),
  );

  it.effect("internal defects reach the sandbox as an opaque generic only", () =>
    Effect.gen(function* () {
      // Plugin defect carrying sensitive context. The bridge's reject
      // path must strip everything except the canonical
      // "Internal tool error [<corrId>]" shape — or fall back to the
      // bare generic if the upstream invoker hasn't already stamped
      // the correlation id (this test exercises the latter path
      // because it bypasses makeExecutorToolInvoker).
      const invoker: SandboxToolInvoker = {
        invoke: () =>
          Effect.fail(
            Object.assign(
              new Error("Authorization: Bearer SECRET_TOKEN_xyz failed against host 10.0.0.5"),
              {
                stack: "Error\n    at /home/svc/executor/packages/plugins/foo:142:11",
              },
            ) as never,
          ),
      };

      const result = yield* executor.execute(
        `
        try {
          await tools.leaky.call({});
          return "should not reach";
        } catch (e) {
          return e.message;
        }
        `,
        invoker,
      );

      expect(result.error).toBeUndefined();
      const message = String(result.result);
      // Either the canonical opaque generic with a correlation id, or
      // the bare fallback. Neither must contain any sensitive context.
      expect(
        message === "Internal tool error" || /^Internal tool error \[[0-9a-f]{8}\]$/.test(message),
      ).toBe(true);
      expect(message).not.toContain("SECRET_TOKEN_xyz");
      expect(message).not.toContain("Authorization");
      expect(message).not.toContain("10.0.0.5");
      expect(message).not.toContain("packages/plugins");
    }),
  );

  it.effect("handles unknown tool path", () =>
    Effect.gen(function* () {
      const invoker = makeTestInvoker({});

      const result = yield* executor.execute(
        `
        try {
          await tools.nonexistent.thing({ x: 1 });
          return "should not reach";
        } catch (e) {
          return "caught: " + e.message;
        }
        `,
        invoker,
      );

      expect(result.error).toBeUndefined();
      expect(result.result).toContain("caught:");
    }),
  );

  it.effect("captures console.log output", () =>
    Effect.gen(function* () {
      const result = yield* executor.execute(
        `
        console.log("hello from sandbox");
        console.warn("a warning");
        return "done";
        `,
        makeTestInvoker({}),
      );

      expect(result.error).toBeUndefined();
      expect(result.result).toBe("done");
      expect(result.logs).toContainEqual("[log] hello from sandbox");
      expect(result.logs).toContainEqual("[warn] a warning");
    }),
  );

  it.effect("applies a memory limit by default", () =>
    Effect.gen(function* () {
      const defaultExecutor = makeQuickJsExecutor({ timeoutMs: 5_000 });

      const result = yield* defaultExecutor.execute(
        `
        return new ArrayBuffer(128 * 1024 * 1024).byteLength;
        `,
        makeTestInvoker({}),
      );

      expect(result.result).toBeNull();
      expect(result.error).toBeDefined();
    }),
  );

  it.effect("passes tool result into next tool call", () =>
    Effect.gen(function* () {
      const invoker = makeTestInvoker({
        "stripe.customers.list": () => ({
          data: [
            { id: "cus_1", email: "alice@example.com" },
            { id: "cus_2", email: "bob@example.com" },
          ],
        }),
        "stripe.invoices.create": (args) => {
          const { customer, amount } = args as { customer: string; amount: number };
          return { id: "inv_1", customer, amount };
        },
      });

      const result = yield* executor.execute(
        `
        const customers = await tools.stripe.customers.list();
        const invoice = await tools.stripe.invoices.create({
          customer: customers.data[0].id,
          amount: 5000,
        });
        return invoice;
        `,
        invoker,
      );

      expect(result.error).toBeUndefined();
      expect(result.result).toEqual({
        id: "inv_1",
        customer: "cus_1",
        amount: 5000,
      });
    }),
  );
});
