import { describe, it, expect, vi, beforeEach } from "vitest";
import { ApiHelpers } from "../ApiHelpers";

// ── createActionWrapper ──────────────────────────────────────────────────────
describe("ApiHelpers.createActionWrapper", () => {
  beforeEach(() => {
    vi.useFakeTimers();
  });

  afterEach(() => {
    vi.useRealTimers();
  });

  it("calls the scene method with the primitive arg", () => {
    const scene = { move: vi.fn() };
    const wrapper = ApiHelpers.createActionWrapper(scene, "move", 0);
    const cb = vi.fn();
    wrapper(5, cb);
    expect(scene.move).toHaveBeenCalledWith(5);
  });

  it("unwraps interpreter object (arg.data) before passing to method", () => {
    const scene = { move: vi.fn() };
    const wrapper = ApiHelpers.createActionWrapper(scene, "move", 0);
    const cb = vi.fn();
    wrapper({ data: 42 }, cb);
    expect(scene.move).toHaveBeenCalledWith(42);
  });

  it("calls callback after animationDelay for sync method", () => {
    const scene = { move: vi.fn(() => undefined) };
    const wrapper = ApiHelpers.createActionWrapper(scene, "move", 50);
    const cb = vi.fn();
    wrapper(1, cb);
    expect(cb).not.toHaveBeenCalled();
    vi.advanceTimersByTime(50);
    expect(cb).toHaveBeenCalled();
  });

  it("calls callback after delay when method returns a resolved promise", async () => {
    const scene = { move: vi.fn(() => Promise.resolve()) };
    const wrapper = ApiHelpers.createActionWrapper(scene, "move", 30);
    const cb = vi.fn();
    wrapper(1, cb);
    await Promise.resolve(); // let microtask (then) run
    vi.advanceTimersByTime(30);
    expect(cb).toHaveBeenCalled();
  });

  it("calls callback after delay when method returns a rejected promise", async () => {
    const scene = { move: vi.fn(() => Promise.reject(new Error("fail"))) };
    const wrapper = ApiHelpers.createActionWrapper(scene, "move", 30);
    const cb = vi.fn();
    const spy = vi.spyOn(console, "error").mockImplementation(() => {});
    wrapper(1, cb);
    await Promise.resolve();
    await Promise.resolve();
    vi.advanceTimersByTime(30);
    expect(cb).toHaveBeenCalled();
    spy.mockRestore();
  });

  it("warns and calls callback when method throws synchronously", () => {
    const scene = { move: vi.fn(() => { throw new Error("boom"); }) };
    const wrapper = ApiHelpers.createActionWrapper(scene, "move", 10);
    const spy = vi.spyOn(console, "error").mockImplementation(() => {});
    const cb = vi.fn();
    wrapper(1, cb);
    vi.advanceTimersByTime(10);
    expect(cb).toHaveBeenCalled();
    spy.mockRestore();
  });

  it("warns and calls callback when method is not found on scene", () => {
    const scene = {};
    const wrapper = ApiHelpers.createActionWrapper(scene, "nonexistent", 10);
    const spy = vi.spyOn(console, "warn").mockImplementation(() => {});
    const cb = vi.fn();
    wrapper(1, cb);
    vi.advanceTimersByTime(10);
    expect(cb).toHaveBeenCalled();
    expect(spy).toHaveBeenCalledWith(expect.stringContaining("nonexistent"));
    spy.mockRestore();
  });

  it("uses default delay of 100ms", () => {
    const scene = { move: vi.fn() };
    const wrapper = ApiHelpers.createActionWrapper(scene, "move");
    const cb = vi.fn();
    wrapper(1, cb);
    vi.advanceTimersByTime(99);
    expect(cb).not.toHaveBeenCalled();
    vi.advanceTimersByTime(1);
    expect(cb).toHaveBeenCalled();
  });
});

// ── createConditionWrapper ───────────────────────────────────────────────────
describe("ApiHelpers.createConditionWrapper", () => {
  it("calls scene method and returns its value", () => {
    const scene = { isWall: vi.fn(() => true) };
    const wrapper = ApiHelpers.createConditionWrapper(scene, "isWall");
    expect(wrapper()).toBe(true);
    expect(scene.isWall).toHaveBeenCalled();
  });

  it("passes args through to the scene method", () => {
    const scene = { check: vi.fn((x, y) => x + y) };
    const wrapper = ApiHelpers.createConditionWrapper(scene, "check");
    expect(wrapper(2, 3)).toBe(5);
  });

  it("warns and returns false when method not found", () => {
    const scene = {};
    const spy = vi.spyOn(console, "warn").mockImplementation(() => {});
    const wrapper = ApiHelpers.createConditionWrapper(scene, "missing");
    expect(wrapper()).toBe(false);
    expect(spy).toHaveBeenCalledWith(expect.stringContaining("missing"));
    spy.mockRestore();
  });
});

// ── createHighlightWrapper ───────────────────────────────────────────────────
describe("ApiHelpers.createHighlightWrapper", () => {
  it("calls scene.highlightBlock with string id", () => {
    const scene = { highlightBlock: vi.fn() };
    const wrapper = ApiHelpers.createHighlightWrapper(scene);
    wrapper("block-1");
    expect(scene.highlightBlock).toHaveBeenCalledWith("block-1");
  });

  it("converts non-string id to string", () => {
    const scene = { highlightBlock: vi.fn() };
    const wrapper = ApiHelpers.createHighlightWrapper(scene);
    wrapper(42);
    expect(scene.highlightBlock).toHaveBeenCalledWith("42");
  });

  it("uses empty string when id is falsy", () => {
    const scene = { highlightBlock: vi.fn() };
    const wrapper = ApiHelpers.createHighlightWrapper(scene);
    wrapper(null);
    expect(scene.highlightBlock).toHaveBeenCalledWith("");
  });

  it("falls back to scene.workspace.highlightBlock when scene.highlightBlock absent", () => {
    const workspaceHighlight = vi.fn();
    const scene = { workspace: { highlightBlock: workspaceHighlight } };
    const wrapper = ApiHelpers.createHighlightWrapper(scene);
    wrapper("b");
    expect(workspaceHighlight).toHaveBeenCalledWith("b");
  });

  it("sets scene.highlightPause to true when property exists", () => {
    const scene = { highlightBlock: vi.fn(), highlightPause: false };
    const wrapper = ApiHelpers.createHighlightWrapper(scene);
    wrapper("b");
    expect(scene.highlightPause).toBe(true);
  });

  it("does not set highlightPause when property is undefined", () => {
    const scene = { highlightBlock: vi.fn() };
    const wrapper = ApiHelpers.createHighlightWrapper(scene);
    wrapper("b");
    expect(scene.highlightPause).toBeUndefined();
  });

  it("does nothing when neither highlightBlock nor workspace.highlightBlock exists", () => {
    const scene = {};
    const wrapper = ApiHelpers.createHighlightWrapper(scene);
    expect(() => wrapper("b")).not.toThrow();
  });
});

// ── registerFunction ─────────────────────────────────────────────────────────
describe("ApiHelpers.registerFunction", () => {
  function makeInterpreter() {
    return {
      createAsyncFunction: vi.fn((fn) => ({ async: true, fn })),
      createNativeFunction: vi.fn((fn) => ({ native: true, fn })),
      setProperty: vi.fn(),
    };
  }

  it("registers a sync (native) function when isAsync=false", () => {
    const interp = makeInterpreter();
    const scope = {};
    const fn = vi.fn();
    ApiHelpers.registerFunction(interp, scope, "move", fn, false);
    expect(interp.createNativeFunction).toHaveBeenCalledWith(fn);
    expect(interp.setProperty).toHaveBeenCalledWith(scope, "move", expect.anything());
  });

  it("registers an async function when isAsync=true", () => {
    const interp = makeInterpreter();
    const scope = {};
    const fn = vi.fn();
    ApiHelpers.registerFunction(interp, scope, "move", fn, true);
    expect(interp.createAsyncFunction).toHaveBeenCalledWith(fn);
  });

  it("defaults isAsync to false", () => {
    const interp = makeInterpreter();
    ApiHelpers.registerFunction(interp, {}, "fn", vi.fn());
    expect(interp.createNativeFunction).toHaveBeenCalled();
  });
});

// ── registerMultipleFunctions ────────────────────────────────────────────────
describe("ApiHelpers.registerMultipleFunctions", () => {
  function makeInterpreter() {
    return {
      createAsyncFunction: vi.fn((fn) => fn),
      createNativeFunction: vi.fn((fn) => fn),
      setProperty: vi.fn(),
    };
  }

  it("registers each function in the array", () => {
    const interp = makeInterpreter();
    const scope = {};
    const fn1 = vi.fn();
    const fn2 = vi.fn();
    ApiHelpers.registerMultipleFunctions(interp, scope, [
      { name: "move", wrapper: fn1 },
      { name: "turn", wrapper: fn2, isAsync: true },
    ]);
    expect(interp.setProperty).toHaveBeenCalledTimes(2);
    expect(interp.createNativeFunction).toHaveBeenCalledWith(fn1);
    expect(interp.createAsyncFunction).toHaveBeenCalledWith(fn2);
  });

  it("does nothing for empty array", () => {
    const interp = makeInterpreter();
    ApiHelpers.registerMultipleFunctions(interp, {}, []);
    expect(interp.setProperty).not.toHaveBeenCalled();
  });
});
