import type { ImageContent, TextContent } from "@earendil-works/pi-ai";
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
import { describe, expect, it, vi } from "vitest";
import { createAskUserToolDefinition, type AskUserInvocation } from "./askUserTool.js";
import { PendingAskStore, PendingAskValidationError } from "./pendingAskStore.js";

function ctxFor(sessionId: string): ExtensionContext {
  const sessionManager = { getSessionId: () => sessionId, getSessionFile: () => undefined };
  // eslint-disable-next-line @typescript-eslint/consistent-type-assertions -- test stub with the minimal surface the tool uses.
  return { sessionManager } as unknown as ExtensionContext;
}

function firstText(content: readonly (TextContent | ImageContent)[]): string {
  const first = content[0];
  return first?.type === "text" ? first.text : "";
}

/** The tool over a real store, so the schema and the store's contract stay aligned. */
function toolOverStore(askIds: string[] = ["ask-1", "ask-2"]) {
  const remaining = [...askIds];
  const store = new PendingAskStore({
    now: () => new Date("2026-02-01T10:00:00.000Z"),
    createAskId: () => remaining.shift() ?? "ask-exhausted",
  });
  const open = vi.fn((input: AskUserInvocation) => Promise.resolve(store.open(input)));
  return { store, open, tool: createAskUserToolDefinition({ open }) };
}

const twoQuestions = {
  questions: [
    { id: "db", question: "Which database?", options: [{ value: "pg", label: "Postgres" }, { value: "sqlite", label: "SQLite" }] },
    { id: "why", question: "Why?", options: [], allowOther: true },
  ],
};

describe("createAskUserToolDefinition", () => {
  it("advertises a non-blocking question set whose answers arrive as a follow-up", () => {
    const { tool } = toolOverStore();

    expect(tool.name).toBe("ask_user");
    expect(tool.description).toBe("Post a set of questions to the user as a browser form and end this run. Answers arrive later as a follow-up message; the user may leave any question unanswered.");
    expect(tool.promptSnippet).toBe("ask_user: post a question set to the user; ends the run, answers return as a follow-up");
    expect(tool.promptGuidelines).toEqual([
      "When you need decisions from the user, post them together with ask_user instead of asking in prose one at a time. It ends the run and the answers, including the questions the user left unanswered, come back as a follow-up message that wakes you. Call it alone and last, and do not repost the same questions or poll for answers.",
    ]);
  });

  it("bounds the question set in its parameter schema", () => {
    const { tool } = toolOverStore();

    expect(tool.parameters).toMatchObject({
      type: "object",
      required: ["questions"],
      properties: { questions: { type: "array", minItems: 1, maxItems: 20 } },
    });
  });

  it("opens the ask for the calling session and terminates the run instead of awaiting the user", async () => {
    const { open, tool } = toolOverStore();

    const result = await tool.execute("call-1", twoQuestions, undefined, undefined, ctxFor("session-1"));

    expect(open).toHaveBeenCalledWith({
      sessionId: "session-1",
      questions: [
        { id: "db", question: "Which database?", options: [{ value: "pg", label: "Postgres" }, { value: "sqlite", label: "SQLite" }] },
        { id: "why", question: "Why?", options: [], allowOther: true },
      ],
    });
    expect(result.terminate).toBe(true);
    expect(result.details).toMatchObject({ ask: { askId: "ask-1", questions: [{ id: "db" }, { id: "why" }] } });
    expect(firstText(result.content)).toBe("Posted 2 questions to the user as ask ask-1. Ending this run; the answers arrive as a follow-up message that wakes you, naming every question the user left unanswered. Do not repost these questions.");
  });

  it("defaults a question without options to an empty option list so free text alone is expressible", async () => {
    const { open, tool } = toolOverStore();

    await tool.execute("call-free", { questions: [{ id: "note", question: "Anything else?", allowOther: true }] }, undefined, undefined, ctxFor("session-1"));

    expect(open).toHaveBeenCalledWith({
      sessionId: "session-1",
      questions: [{ id: "note", question: "Anything else?", options: [], allowOther: true }],
    });
  });

  it("forwards per-question detail, option detail, and multi-select without inventing defaults", async () => {
    const { open, tool } = toolOverStore();

    await tool.execute("call-rich", {
      questions: [{
        id: "targets",
        question: "Which targets?",
        detail: "Pick every platform we should build for.",
        options: [{ value: "web", label: "Web", detail: "Chromium and Firefox" }, { value: "cli", label: "CLI" }],
        multiple: true,
      }],
    }, undefined, undefined, ctxFor("session-1"));

    expect(open).toHaveBeenCalledWith({
      sessionId: "session-1",
      questions: [{
        id: "targets",
        question: "Which targets?",
        detail: "Pick every platform we should build for.",
        options: [{ value: "web", label: "Web", detail: "Chromium and Firefox" }, { value: "cli", label: "CLI" }],
        multiple: true,
      }],
    });
  });

  it("tells the model which questions the superseded ask left unanswered", async () => {
    const { tool } = toolOverStore();
    await tool.execute("call-first", twoQuestions, undefined, undefined, ctxFor("session-1"));

    const result = await tool.execute("call-second", { questions: [{ id: "again", question: "Still there?", options: [{ value: "yes", label: "Yes" }] }] }, undefined, undefined, ctxFor("session-1"));

    expect(result.terminate).toBe(true);
    expect(firstText(result.content)).toContain("Posted 1 question to the user as ask ask-2.");
    expect(firstText(result.content)).toContain("This replaced an earlier question set (ask-1) that the user never submitted.");
    expect(firstText(result.content)).toContain("Left unanswered: db, why.");
    expect(result.details).toMatchObject({ superseded: { askId: "ask-1", reason: "superseded", unansweredIds: ["db", "why"] } });
  });

  it("says nothing about superseding when no earlier ask was open", async () => {
    const { tool } = toolOverStore();

    const result = await tool.execute("call-only", twoQuestions, undefined, undefined, ctxFor("session-1"));

    expect(firstText(result.content)).not.toContain("replaced an earlier question set");
    expect(result.details).not.toHaveProperty("superseded");
  });

  it("propagates a rejected question set so the agent loop reports it to the model", async () => {
    const { tool } = toolOverStore();

    await expect(tool.execute("call-dup", {
      questions: [
        { id: "same", question: "First?", options: [{ value: "a", label: "A" }] },
        { id: "same", question: "Second?", options: [{ value: "b", label: "B" }] },
      ],
    }, undefined, undefined, ctxFor("session-1"))).rejects.toThrow(PendingAskValidationError);
  });
});
