import { describe, it, expect } from "bun:test";
import { runCode } from "./run-code.ts";
import type { IClient } from "../client-like.ts";

function createMockClient(
  overrides?: Partial<Pick<IClient, "callTool" | "listTools">>,
): IClient {
  return {
    callTool: overrides?.callTool ?? (async () => ({ content: [] })),
    listTools: overrides?.listTools ?? (async () => ({ tools: [] })),
    listResources: async () => ({ resources: [] }),
    readResource: async () => ({ contents: [] }),
    listResourceTemplates: async () => ({ resourceTemplates: [] }),
    listPrompts: async () => ({ prompts: [] }),
    getPrompt: async () => ({ messages: [] }),
    getServerCapabilities: () => undefined,
    getInstructions: () => undefined,
    close: async () => {},
  } as unknown as IClient;
}

describe("runCode", () => {
  describe("basic code execution", () => {
    it("returns the value from the default export function", async () => {
      const result = await runCode({
        client: createMockClient(),
        code: `export default async (client) => { return 42; }`,
        timeoutMs: 5000,
      });

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

    it("returns object values", async () => {
      const result = await runCode({
        client: createMockClient(),
        code: `export default async (client) => { return { hello: "world", n: 123 }; }`,
        timeoutMs: 5000,
      });

      expect(result.error).toBeUndefined();
      expect(result.returnValue).toEqual({ hello: "world", n: 123 });
    });

    it("returns null when function returns nothing", async () => {
      const result = await runCode({
        client: createMockClient(),
        code: `export default async (client) => {}`,
        timeoutMs: 5000,
      });

      expect(result.error).toBeUndefined();
      // undefined becomes null in QuickJS dump
      expect(result.returnValue).toBeUndefined();
    });
  });

  describe("console log capture", () => {
    it("captures console.log output", async () => {
      const result = await runCode({
        client: createMockClient(),
        code: `export default async (client) => {
          console.log("hello");
          console.log("world");
          return "done";
        }`,
        timeoutMs: 5000,
      });

      expect(result.error).toBeUndefined();
      expect(result.consoleLogs).toHaveLength(2);
      expect(result.consoleLogs[0]).toEqual({
        type: "log",
        content: "hello",
      });
      expect(result.consoleLogs[1]).toEqual({
        type: "log",
        content: "world",
      });
    });

    it("captures console.warn and console.error", async () => {
      const result = await runCode({
        client: createMockClient(),
        code: `export default async (client) => {
          console.warn("warning");
          console.error("error");
          return true;
        }`,
        timeoutMs: 5000,
      });

      expect(result.error).toBeUndefined();
      expect(result.consoleLogs).toHaveLength(2);
      expect(result.consoleLogs[0].type).toBe("warn");
      expect(result.consoleLogs[1].type).toBe("error");
    });
  });

  describe("client method invocation", () => {
    it("calls client.callTool from sandbox code", async () => {
      const client = createMockClient({
        callTool: async (params) => ({
          content: [
            {
              type: "text" as const,
              text: JSON.stringify({
                sum:
                  (params.arguments?.a as number) +
                  (params.arguments?.b as number),
              }),
            },
          ],
        }),
      });

      const result = await runCode({
        client,
        code: `export default async (client) => {
          const result = await client.callTool({ name: "myTool", arguments: { a: 3, b: 4 } });
          return result;
        }`,
        timeoutMs: 5000,
      });

      expect(result.error).toBeUndefined();
      expect(result.returnValue).toEqual({
        content: [{ type: "text", text: '{"sum":7}' }],
      });
    });

    it("calls client.listTools from sandbox code", async () => {
      const client = createMockClient({
        listTools: async () => ({
          tools: [
            {
              name: "tool_a",
              description: "Tool A",
              inputSchema: { type: "object" as const, properties: {} },
            },
          ],
        }),
      });

      const result = await runCode({
        client,
        code: `export default async (client) => {
          const { tools } = await client.listTools();
          return tools.map(t => t.name);
        }`,
        timeoutMs: 5000,
      });

      expect(result.error).toBeUndefined();
      expect(result.returnValue).toEqual(["tool_a"]);
    });

    it("handles client method errors gracefully", async () => {
      const client = createMockClient({
        callTool: async () => {
          throw new Error("tool failed");
        },
      });

      const result = await runCode({
        client,
        code: `export default async (client) => {
          try {
            await client.callTool({ name: "failingTool", arguments: {} });
          } catch (e) {
            return "caught error";
          }
        }`,
        timeoutMs: 5000,
      });

      expect(result.error).toBeUndefined();
      expect(result.returnValue).toBe("caught error");
    });

    it("propagates error for unknown tool calls", async () => {
      const client = createMockClient({
        callTool: async (params) => {
          throw new Error(`Tool "${params.name}" not found`);
        },
      });

      const result = await runCode({
        client,
        code: `export default async (client) => {
          const result = await client.callTool({ name: "UNKNOWN_TOOL", arguments: {} });
          return result;
        }`,
        timeoutMs: 5000,
      });

      expect(result.error).toBeDefined();
      expect(result.error).toContain("UNKNOWN_TOOL");
    });
  });

  describe("timeout enforcement", () => {
    it("returns error when code exceeds timeout", async () => {
      const result = await runCode({
        client: createMockClient(),
        code: `export default async (client) => {
          let x = 0;
          while(true) { x++; }
          return x;
        }`,
        timeoutMs: 200,
      });

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

  describe("non-function default export", () => {
    it("returns error when default export is not a function", async () => {
      const result = await runCode({
        client: createMockClient(),
        code: `const value = 42; export default value;`,
        timeoutMs: 5000,
      });

      expect(result.error).toBeDefined();
      expect(result.error).toContain("must export default a function");
    });

    it("returns error for object default export", async () => {
      const result = await runCode({
        client: createMockClient(),
        code: `export default { key: "value" };`,
        timeoutMs: 5000,
      });

      expect(result.error).toBeDefined();
      expect(result.error).toContain("must export default a function");
    });
  });

  describe("memory/stack limits are configurable", () => {
    it("accepts custom memoryLimitBytes without error", async () => {
      const result = await runCode({
        client: createMockClient(),
        code: `export default async (client) => "ok";`,
        timeoutMs: 5000,
        memoryLimitBytes: 16 * 1024 * 1024,
      });

      expect(result.error).toBeUndefined();
      expect(result.returnValue).toBe("ok");
    });

    it("accepts custom stackSizeBytes without error", async () => {
      const result = await runCode({
        client: createMockClient(),
        code: `export default async (client) => "ok";`,
        timeoutMs: 5000,
        stackSizeBytes: 256 * 1024,
      });

      expect(result.error).toBeUndefined();
      expect(result.returnValue).toBe("ok");
    });

    it("uses defaults when limits are not provided", async () => {
      const result = await runCode({
        client: createMockClient(),
        code: `export default async (client) => "ok";`,
        timeoutMs: 5000,
      });

      expect(result.error).toBeUndefined();
      expect(result.returnValue).toBe("ok");
    });
  });

  describe("host promise resolves after runtime disposal", () => {
    it("does not crash when host function resolves after sandbox is disposed", async () => {
      // Simulate the production scenario: client.callTool takes a long time
      // (e.g., slow DB query) and the sandbox timeout fires first, disposing
      // the QuickJS runtime. The pending .then() callback must not call
      // ctx.newString() on a dead context.
      const client = createMockClient({
        callTool: () =>
          // This promise resolves AFTER the sandbox timeout (200ms),
          // so by the time .then() fires the runtime is already disposed.
          new Promise((resolve) =>
            setTimeout(
              () =>
                resolve({ content: [{ type: "text" as const, text: "late" }] }),
              1000,
            ),
          ),
      });

      const result = await runCode({
        client,
        code: `export default async (client) => {
          const result = await client.callTool({ name: "slowTool", arguments: {} });
          return result;
        }`,
        timeoutMs: 200,
      });

      // Should get a timeout error, NOT a process crash from QuickJSUseAfterFree
      expect(result.error).toBeDefined();

      // Wait for the delayed host promise to resolve — this is when the
      // use-after-free would fire if the bug still exists.
      await new Promise((resolve) => setTimeout(resolve, 1500));
    });
  });

  describe("syntax errors", () => {
    it("returns error for invalid JavaScript", async () => {
      const result = await runCode({
        client: createMockClient(),
        code: `export default async (client) => { invalid syntax here !!!`,
        timeoutMs: 5000,
      });

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