import { afterEach, describe, expect, it } from "vitest";

if (!(globalThis as any).document) {
  const { Window } = await import("happy-dom");
  const window = new Window();
  (globalThis as any).window = window;
  (globalThis as any).document = window.document;
  Object.defineProperty(globalThis, "navigator", {
    configurable: true,
    value: window.navigator,
  });
  (globalThis as any).Node = window.Node;
  (globalThis as any).Element = window.Element;
  (globalThis as any).HTMLElement = window.HTMLElement;
  (globalThis as any).SVGElement = window.SVGElement;
  (globalThis as any).Event = window.Event;
  (globalThis as any).CustomEvent = window.CustomEvent;
  (globalThis as any).MouseEvent = window.MouseEvent;
  (globalThis as any).KeyboardEvent = window.KeyboardEvent;
  (globalThis as any).getComputedStyle = window.getComputedStyle.bind(window);
}

globalThis.requestAnimationFrame = ((cb: FrameRequestCallback) => {
  cb(0);
  return 1;
}) as typeof globalThis.requestAnimationFrame;
globalThis.cancelAnimationFrame = (() => {}) as typeof globalThis.cancelAnimationFrame;

const { defineComponent, h, nextTick, ref } = await import("vue");
const { createTerminalApp } = await import("@simon_he/vue-tui");
const { DimSdkAskUserDialog } = await import("../src/dimsdk/ui/components/AskUserDialog.ts");

afterEach(() => {
  document.body.innerHTML = "";
});

async function flushApp(app: {
  scheduler: { flush: () => void };
}): Promise<void> {
  await nextTick();
  await nextTick();
  app.scheduler.flush();
}

function snapshotText(app: {
  terminal: { snapshot: () => { lines: readonly string[] } };
}): string {
  return app.terminal.snapshot().lines.join("\n");
}

function findTextCell(lines: readonly string[], text: string): { x: number; y: number } | null {
  const wanted = text.toLowerCase();
  for (let y = 0; y < lines.length; y += 1) {
    const x = lines[y]?.toLowerCase().indexOf(wanted) ?? -1;
    if (x >= 0) {
      return { x, y };
    }
  }
  return null;
}

async function pressKey(
  app: {
    events: { dispatch: (event: any) => boolean | void };
    scheduler: { flush: () => void };
  },
  key: string,
  options: Readonly<{ shiftKey?: boolean; code?: string }> = {},
): Promise<void> {
  app.events.dispatch({ type: "keydown", key, code: options.code ?? key, ...options } as const);
  await flushApp(app);
}

async function typeText(
  app: {
    events: { dispatch: (event: any) => boolean | void };
    scheduler: { flush: () => void };
  },
  text: string,
): Promise<void> {
  for (const ch of text) {
    await pressKey(app, ch);
  }
}

async function clickText(app: {
  events: { dispatch: (event: any) => boolean | void };
  scheduler: { flush: () => void };
  terminal: { snapshot: () => { lines: readonly string[] } };
}, text: string): Promise<void> {
  const cell = findTextCell(app.terminal.snapshot().lines, text);
  if (!cell) {
    throw new Error(`Unable to find "${text}" in terminal snapshot:\n${snapshotText(app)}`);
  }
  const eventBase = { cellX: cell.x + 1, cellY: cell.y };
  app.events.dispatch({ type: "pointerdown", button: 0, ...eventBase } as const);
  app.events.dispatch({ type: "pointerup", button: 0, ...eventBase } as const);
  app.events.dispatch({ type: "click", ...eventBase } as const);
  await flushApp(app);
}

describe("dimsdk ask-user dialog", () => {
  it("submits a single-select option immediately when clicked", async () => {
    let submittedAnswers: Record<string, string | string[]> | null = null;
    const App = defineComponent({
      setup() {
        const open = ref(true);
        return () =>
          h(DimSdkAskUserDialog, {
            modelValue: open.value,
            "onUpdate:modelValue": (v: boolean) => {
              open.value = v;
            },
            toolCallId: "tool_call_click_submit",
            questions: [
              {
                id: "city",
                header: "Question",
                question: "Choose city",
                options: ["Beijing", "Shanghai"],
                multiSelect: false,
                allowCustom: false,
              },
            ],
            w: 100,
            h: 18,
            onSubmit: (answers: Record<string, string | string[]>) => {
              submittedAnswers = answers;
            },
            onCancel: () => {},
          });
      },
    });

    const app = createTerminalApp({
      cols: 120,
      rows: 28,
      component: App as never,
      defaultStyle: { bg: "black", fg: "whiteBright" },
    });

    try {
      app.mount();
      await flushApp(app);

      expect(snapshotText(app)).toContain("Beijing");
      expect(snapshotText(app)).toContain("Shanghai");

      await clickText(app, "Shanghai");

      expect(submittedAnswers).toEqual({ city: "Shanghai" });
    } finally {
      app.dispose();
    }
  });

  it("accepts a custom Other answer", async () => {
    let submittedAnswers: Record<string, string | string[]> | null = null;
    const App = defineComponent({
      setup() {
        const open = ref(true);
        return () =>
          h(DimSdkAskUserDialog, {
            modelValue: open.value,
            "onUpdate:modelValue": (v: boolean) => {
              open.value = v;
            },
            toolCallId: "tool_call_1",
            questions: [
              {
                id: "meal",
                header: "Question",
                question: "Dinner?",
                options: ["Soup", "Noodles"],
                multiSelect: false,
              },
            ],
            w: 100,
            h: 18,
            onSubmit: (answers: Record<string, string | string[]>) => {
              submittedAnswers = answers;
            },
            onCancel: () => {},
          });
      },
    });

    const app = createTerminalApp({
      cols: 120,
      rows: 28,
      component: App as never,
      defaultStyle: { bg: "black", fg: "whiteBright" },
    });

    try {
      app.mount();
      await flushApp(app);

      expect(snapshotText(app)).toContain("Soup");
      expect(snapshotText(app)).toContain("Noodles");

      await clickText(app, "Other");
      await typeText(app, "hotpot");
      await clickText(app, "Submit");

      expect(submittedAnswers).toEqual({ meal: "hotpot" });
    } finally {
      app.dispose();
    }
  });

  it("accepts a direct input question without rendering Other", async () => {
    let submittedAnswers: Record<string, string | string[]> | null = null;
    const App = defineComponent({
      setup() {
        const open = ref(true);
        return () =>
          h(DimSdkAskUserDialog, {
            modelValue: open.value,
            "onUpdate:modelValue": (v: boolean) => {
              open.value = v;
            },
            toolCallId: "tool_call_only_other",
            questions: [
              {
                id: "meal",
                header: "Question",
                kind: "input",
                question: "Dinner?",
                placeholder: "Type dinner plans",
              },
            ],
            w: 100,
            h: 18,
            onSubmit: (answers: Record<string, string | string[]>) => {
              submittedAnswers = answers;
            },
            onCancel: () => {},
          });
      },
    });

    const app = createTerminalApp({
      cols: 120,
      rows: 28,
      component: App as never,
      defaultStyle: { bg: "black", fg: "whiteBright" },
    });

    try {
      app.mount();
      await flushApp(app);

      expect(snapshotText(app)).not.toContain("Other");

      await typeText(app, "hotpot");
      await clickText(app, "Submit");

      expect(submittedAnswers).toEqual({ meal: "hotpot" });
    } finally {
      app.dispose();
    }
  });
});
