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

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

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

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

function countStyleCells(
  app: {
    terminal: {
      getCell: (x: number, y: number) => { style: { fg?: string; bg?: string } };
      snapshot: () => { lines: string[] };
    };
  },
  predicate: (style: { fg?: string; bg?: string }) => boolean,
): number {
  const lines = app.terminal.snapshot().lines;
  let count = 0;
  for (let y = 0; y < lines.length; y += 1) {
    const width = lines[y]?.length ?? 0;
    for (let x = 0; x < width; x += 1) {
      if (predicate(app.terminal.getCell(x, y).style)) {
        count += 1;
      }
    }
  }
  return count;
}

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 clickText(app: {
  events: { dispatch: (event: unknown) => boolean | void };
  scheduler: { flush: () => void };
  terminal: { snapshot: () => { lines: 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 command palette", () => {
  it("renders when modelValue is true", async () => {
    const App = defineComponent({
      render() {
        return h(DimSdkCommandPalette, {
          modelValue: true,
          title: "Connect a provider",
          options: [
            { label: "OpenAI", detail: "openai" },
            { label: "Anthropic", detail: "anthropic" },
          ],
          selectedIndex: 0,
          "onUpdate:modelValue": () => {},
          "onUpdate:selectedIndex": () => {},
          onSelect: () => {},
          onClose: () => {},
          showRowDetails: true,
        });
      },
    });

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

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

      const snapshot = snapshotText(app);
      expect(snapshot).toContain("Connect a provider");
      expect(snapshot).toContain("OpenAI");
      expect(snapshot).toContain("Anthropic");
    } finally {
      app.dispose();
    }
  });

  it("centers the dialog within the terminal viewport", async () => {
    const App = defineComponent({
      render() {
        return h(DimSdkCommandPalette, {
          modelValue: true,
          title: "Select a model",
          options: [
            { label: "gpt-5", detail: "gpt-5" },
            { label: "gpt-4.1", detail: "gpt-4.1" },
          ],
          selectedIndex: 0,
          "onUpdate:modelValue": () => {},
          "onUpdate:selectedIndex": () => {},
          onSelect: () => {},
          onClose: () => {},
          showRowDetails: true,
        });
      },
    });

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

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

      const cell = findTextCell(app.terminal.snapshot().lines, "Select a model");
      expect(cell).not.toBeNull();
      expect((cell as { x: number }).x).toBeGreaterThan(0);
    } finally {
      app.dispose();
    }
  });

  it("selects an option when clicked", async () => {
    const selections: Array<string | null> = [];
    const App = defineComponent({
      render() {
        return h(DimSdkCommandPalette, {
          modelValue: true,
          title: "Connect a provider",
          options: [
            { label: "OpenAI", detail: "openai" },
            { label: "Anthropic", detail: "anthropic" },
          ],
          selectedIndex: 0,
          "onUpdate:modelValue": () => {},
          "onUpdate:selectedIndex": () => {},
          onSelect: (label: string | null) => selections.push(label),
          onClose: () => {},
          showRowDetails: true,
        });
      },
    });

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

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

      await clickText(app, "Anthropic");

      expect(selections).toContain("Anthropic");
    } finally {
      app.dispose();
    }
  });

  it("moves the active option with arrow keys before confirming", async () => {
    const selections: Array<string | null> = [];
    let selectedState = 0;
    const App = defineComponent({
      setup() {
        const selectedIndex = ref(0);
        return () =>
          h(DimSdkCommandPalette, {
            modelValue: true,
            title: "Connect a provider",
            options: [
              { label: "OpenAI", detail: "openai" },
              { label: "Anthropic", detail: "anthropic" },
              { label: "DeepSeek", detail: "deepseek" },
            ],
            selectedIndex: selectedIndex.value,
            "onUpdate:modelValue": () => {},
            "onUpdate:selectedIndex": (value: number) => {
              selectedIndex.value = value;
              selectedState = value;
            },
            onSelect: (label: string | null) => selections.push(label),
            onClose: () => {},
            showRowDetails: true,
          });
      },
    });

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

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

      app.events.dispatch({
        type: "keydown",
        key: "ArrowDown",
        code: "ArrowDown",
      } as const);
      await flushApp(app);

      app.events.dispatch({
        type: "keydown",
        key: "Enter",
        code: "Enter",
      } as const);
      await flushApp(app);

      expect(selectedState).toBe(1);
      expect(selections.at(-1)).toBe("Anthropic");
    } finally {
      app.dispose();
    }
  });

  it("confirms a selected option without row details", async () => {
    const selections: Array<string | null> = [];
    let selectedState = 0;
    const App = defineComponent({
      setup() {
        const selectedIndex = ref(0);
        return () =>
          h(DimSdkCommandPalette, {
            modelValue: true,
            title: "Add custom provider",
            options: [
              { label: "OpenAI", detail: "openai" },
              { label: "OpenAI Responses", detail: "openai-responses" },
              { label: "Gemini", detail: "gemini" },
            ],
            selectedIndex: selectedIndex.value,
            "onUpdate:modelValue": () => {},
            "onUpdate:selectedIndex": (value: number) => {
              selectedIndex.value = value;
              selectedState = value;
            },
            onSelect: (label: string | null) => selections.push(label),
            onClose: () => {},
          });
      },
    });

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

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

      app.events.dispatch({
        type: "keydown",
        key: "ArrowDown",
        code: "ArrowDown",
      } as const);
      await flushApp(app);

      app.events.dispatch({
        type: "keydown",
        key: "Enter",
        code: "Enter",
      } as const);
      await flushApp(app);

      expect(selectedState).toBe(1);
      expect(selections.at(-1)).toBe("OpenAI Responses");

      await clickText(app, "Gemini");
      expect(selections.at(-1)).toBe("Gemini");
    } finally {
      app.dispose();
    }
  });

  it("resets the query and filtered results after closing and reopening", async () => {
    const selections: Array<string | null> = [];
    const open = ref(true);
    const selectedIndex = ref(0);
    const App = defineComponent({
      setup() {
        return () =>
          h(DimSdkCommandPalette, {
            modelValue: open.value,
            title: "Connect a provider",
            options: [
              { label: "OpenAI", detail: "openai" },
              { label: "Anthropic", detail: "anthropic" },
              { label: "DeepSeek", detail: "deepseek" },
            ],
            selectedIndex: selectedIndex.value,
            "onUpdate:modelValue": (value: boolean) => {
              open.value = value;
            },
            "onUpdate:selectedIndex": (value: number) => {
              selectedIndex.value = value;
            },
            onSelect: (label: string | null) => selections.push(label),
            onClose: () => {
              open.value = false;
            },
            showRowDetails: true,
          });
      },
    });

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

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

      for (const character of "anth") {
        app.events.dispatch({
          type: "keydown",
          key: character,
          code: `Key${character.toUpperCase()}`,
        } as const);
        await flushApp(app);
      }

      const filteredSnapshot = snapshotText(app);
      expect(filteredSnapshot).toContain("Anthropic");
      expect(filteredSnapshot).not.toContain("DeepSeek");

      app.events.dispatch({ type: "keydown", key: "Escape", code: "Escape" } as const);
      await flushApp(app);

      open.value = true;
      await flushApp(app);

      const reopenedSnapshot = snapshotText(app);
      expect(reopenedSnapshot).toContain("OpenAI");
      expect(reopenedSnapshot).toContain("Anthropic");
      expect(reopenedSnapshot).toContain("DeepSeek");

      app.events.dispatch({ type: "keydown", key: "Enter", code: "Enter" } as const);
      await flushApp(app);

      expect(selections.at(-1)).toBe("OpenAI");
    } finally {
      app.dispose();
    }
  });

  it("highlights fuzzy matches when filtering provider options", async () => {
    const App = defineComponent({
      render() {
        return h(DimSdkCommandPalette, {
          modelValue: true,
          title: "Connect a provider",
          options: [
            { label: "OpenAI", detail: "openai" },
            { label: "Anthropic", detail: "anthropic" },
            { label: "DeepSeek", detail: "deepseek" },
          ],
          selectedIndex: 0,
          "onUpdate:modelValue": () => {},
          "onUpdate:selectedIndex": () => {},
          onSelect: () => {},
          onClose: () => {},
          showRowDetails: true,
        });
      },
    });

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

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

      for (const character of "anth") {
        app.events.dispatch({
          type: "keydown",
          key: character,
          code: `Key${character.toUpperCase()}`,
        } as const);
        await flushApp(app);
      }

      expect(snapshotText(app)).toContain("Anthropic");
      expect(countStyleCells(app, style => style.fg === "yellow")).toBeGreaterThan(0);
    } finally {
      app.dispose();
    }
  });
});
