import { describe, it, expect, vi, afterAll } from "vitest";
import prisma from "../config/database";
import {
  toolDefinitionsFor,
  executeTool,
  ctxCan,
  TOOL_LABELS,
  type AiAuthCtx,
} from "../services/ai-tools";

// The agentic loop talks to a true external (Anthropic API) — mock the SDK
// module; everything below it (tools, services, DB) runs for real.
const { createMock } = vi.hoisted(() => ({ createMock: vi.fn() }));
vi.mock("@anthropic-ai/sdk", () => ({
  default: class MockAnthropic {
    messages = { create: createMock };
  },
}));

// Imported AFTER the mock so ai.service's `new Anthropic()` gets the mock.
import { agenticChat } from "../services/ai.service";

const ADMIN: AiAuthCtx = {
  userId: 999999993,
  roleName: "admin",
  permissions: [],
};
const VIEWER_INVOICES: AiAuthCtx = {
  userId: 999999994,
  roleName: "viewer",
  permissions: ["invoices.view"],
};
const NOBODY: AiAuthCtx = {
  userId: 999999995,
  roleName: "viewer",
  permissions: [],
};

afterAll(async () => {
  await prisma.ai_usage.deleteMany({
    where: { user_id: { in: [ADMIN.userId] }, kind: "agent" },
  });
});

describe("ai-tools — permission delegation", () => {
  it("ctxCan: admin bypasses, others need the exact permission", () => {
    expect(ctxCan(ADMIN, "invoices.view")).toBe(true);
    expect(ctxCan(VIEWER_INVOICES, "invoices.view")).toBe(true);
    expect(ctxCan(VIEWER_INVOICES, "orders.view")).toBe(false);
    expect(ctxCan(NOBODY, "invoices.view")).toBe(false);
  });

  it("toolDefinitionsFor filters the tool list by the user's permissions", () => {
    const adminTools = toolDefinitionsFor(ADMIN).map((t) => t.name);
    // All registered tools (the labels map is the authoritative name list).
    expect(adminTools.sort()).toEqual(Object.keys(TOOL_LABELS).sort());

    const viewerTools = toolDefinitionsFor(VIEWER_INVOICES).map((t) => t.name);
    expect(viewerTools).toContain("list_invoices");
    expect(viewerTools).toContain("get_invoice_stats");
    expect(viewerTools).not.toContain("list_orders");
    expect(viewerTools).not.toContain("get_stock_overview");

    expect(toolDefinitionsFor(NOBODY)).toHaveLength(0);
  });

  it("executeTool DENIES a tool the user lacks the permission for", async () => {
    const res = await executeTool("list_orders", {}, VIEWER_INVOICES);
    expect(res.ok).toBe(false);
    expect(JSON.stringify(res.result)).toContain("oprávnění");
  });

  it("executeTool rejects an unknown (hallucinated) tool name", async () => {
    const res = await executeTool("drop_all_tables", {}, ADMIN);
    expect(res.ok).toBe(false);
    expect(JSON.stringify(res.result)).toContain("Neznámý nástroj");
  });

  it("get_attendance_summary: another user's data needs attendance.manage", async () => {
    const self: AiAuthCtx = {
      userId: 999999996,
      roleName: "viewer",
      permissions: ["attendance.record"],
    };
    // Own data — allowed (empty month is fine, shape matters).
    const own = await executeTool("get_attendance_summary", {}, self);
    expect(own.ok).toBe(true);
    expect(own.result).toMatchObject({ user_id: self.userId });

    // Someone else's data without attendance.manage — denied in-result.
    const other = await executeTool(
      "get_attendance_summary",
      { user_id: 1 },
      self,
    );
    expect(other.ok).toBe(true); // handler-level denial, not a thrown error
    expect(JSON.stringify(other.result)).toContain("oprávnění");
  });
});

describe("ai-tools — real reads", () => {
  it("list_invoices returns the compact shape from the real DB", async () => {
    const res = await executeTool("list_invoices", {}, ADMIN);
    expect(res.ok).toBe(true);
    const r = res.result as {
      total_matching: number;
      shown: number;
      invoices: unknown[];
    };
    expect(typeof r.total_matching).toBe("number");
    expect(Array.isArray(r.invoices)).toBe(true);
    expect(r.shown).toBe(r.invoices.length);
    expect(r.shown).toBeLessThanOrEqual(20);
  });

  it("list_projects + get_stock_overview return their shapes", async () => {
    const projects = await executeTool("list_projects", {}, ADMIN);
    expect(projects.ok).toBe(true);
    expect(
      Array.isArray((projects.result as { projects: unknown[] }).projects),
    ).toBe(true);

    const stock = await executeTool("get_stock_overview", {}, ADMIN);
    expect(stock.ok).toBe(true);
    expect(
      typeof (stock.result as { below_minimum_count: number })
        .below_minimum_count,
    ).toBe("number");
  });
});

describe("agenticChat loop (SDK mocked)", () => {
  it("executes a requested tool, feeds the result back, returns final text + trace", async () => {
    createMock.mockReset();
    createMock
      .mockResolvedValueOnce({
        stop_reason: "tool_use",
        content: [
          { type: "text", text: "Podívám se." },
          {
            type: "tool_use",
            id: "toolu_1",
            name: "list_projects",
            input: {},
          },
        ],
        usage: { input_tokens: 100, output_tokens: 20 },
      })
      .mockResolvedValueOnce({
        stop_reason: "end_turn",
        content: [{ type: "text", text: "Máte 2 projekty." }],
        usage: { input_tokens: 200, output_tokens: 30 },
      });

    const res = await agenticChat(
      [{ role: "user", content: "Projekty?" }],
      ADMIN,
    );
    expect(res.reply).toBe("Máte 2 projekty.");
    expect(res.toolTrace).toEqual([
      { name: "list_projects", label: "Projekty", ok: true },
    ]);
    expect(createMock).toHaveBeenCalledTimes(2);

    // Second call must carry the tool_result back to the model.
    const secondCall = createMock.mock.calls[1][0];
    const lastMsg = secondCall.messages[secondCall.messages.length - 1];
    expect(lastMsg.role).toBe("user");
    expect(lastMsg.content[0]).toMatchObject({
      type: "tool_result",
      tool_use_id: "toolu_1",
    });

    // Usage was recorded per round-trip.
    const usageRows = await prisma.ai_usage.findMany({
      where: { user_id: ADMIN.userId, kind: "agent" },
    });
    expect(usageRows.length).toBe(2);
  });

  it("a user without permissions gets NO tools passed to the model", async () => {
    createMock.mockReset();
    createMock.mockResolvedValueOnce({
      stop_reason: "end_turn",
      content: [{ type: "text", text: "Ahoj." }],
      usage: { input_tokens: 10, output_tokens: 5 },
    });
    await agenticChat([{ role: "user", content: "Ahoj" }], NOBODY);
    expect(createMock.mock.calls[0][0].tools).toBeUndefined();
  });

  it("stops at the iteration cap with an explanatory note", async () => {
    createMock.mockReset();
    createMock.mockResolvedValue({
      stop_reason: "tool_use",
      content: [
        { type: "tool_use", id: "toolu_x", name: "list_projects", input: {} },
      ],
      usage: { input_tokens: 10, output_tokens: 5 },
    });
    const res = await agenticChat([{ role: "user", content: "smyčka" }], ADMIN);
    expect(createMock.mock.calls.length).toBe(6); // MAX_AGENT_ITERATIONS
    expect(res.reply).toContain("příliš složitý");
  });
});
