import { DEFAULT_OPENAI_MODEL } from "@capakraken/shared";
import { beforeEach, describe, expect, it, vi } from "vitest";

vi.mock("../ai-client.js", () => ({
  loggedAiCall: vi.fn(async (_provider, _model, _promptLength, fn: () => Promise<unknown>) => fn()),
  parseAiError: vi.fn((error: unknown) => error instanceof Error ? error.message : String(error)),
}));

vi.mock("../lib/audit.js", () => ({
  createAuditEntry: vi.fn().mockResolvedValue(undefined),
}));

vi.mock("../lib/content-filter.js", () => ({
  checkAiOutput: vi.fn((content: string) => ({ clean: true, redacted: content })),
}));

vi.mock("../lib/logger.js", () => ({
  logger: {
    debug: vi.fn(),
    info: vi.fn(),
    warn: vi.fn(),
    error: vi.fn(),
  },
}));

vi.mock("../router/assistant-approvals.js", () => {
  class AssistantApprovalStorageUnavailableError extends Error {}

  return {
    AssistantApprovalStorageUnavailableError,
    createPendingAssistantApproval: vi.fn(),
    toApprovalPayload: vi.fn((approval: { id: string; toolName: string; summary: string }, status: string) => ({
      id: approval.id,
      toolName: approval.toolName,
      summary: approval.summary,
      status,
    })),
  };
});

vi.mock("../router/assistant-insights.js", () => ({
  buildAssistantInsight: vi.fn(),
}));

vi.mock("../router/assistant-tools.js", () => ({
  MUTATION_TOOLS: new Set(["create_project"]),
  executeTool: vi.fn(),
}));

import { createAuditEntry } from "../lib/audit.js";
import { logger } from "../lib/logger.js";
import { checkAiOutput } from "../lib/content-filter.js";
import {
  AssistantApprovalStorageUnavailableError,
  createPendingAssistantApproval,
} from "../router/assistant-approvals.js";
import { ASSISTANT_CONFIRMATION_PREFIX } from "../router/assistant-confirmation.js";
import { buildAssistantInsight } from "../router/assistant-insights.js";
import { runAssistantToolLoop } from "../router/assistant-chat-loop.js";
import { executeTool } from "../router/assistant-tools.js";

function createClient(...responses: unknown[]) {
  return {
    chat: {
      completions: {
        create: vi.fn()
          .mockImplementation(async () => {
            const next = responses.shift();
            if (!next) {
              throw new Error("No mock AI response configured");
            }
            return next;
          }),
      },
    },
  };
}

function createLoopInput(overrides: Partial<Parameters<typeof runAssistantToolLoop>[0]> = {}) {
  return {
    db: {} as never,
    dbUserId: "user_1",
    client: createClient({
      choices: [{ message: { content: "ok" } }],
    }),
    provider: "openai",
    model: DEFAULT_OPENAI_MODEL,
    maxTokens: 2000,
    temperature: 0.4,
    openaiMessages: [{ role: "system", content: "system" }],
    availableTools: [],
    toolCtx: {
      db: {} as never,
      userId: "user_1",
      userRole: "USER",
      permissions: new Set(),
      session: null,
      dbUser: null,
      roleDefaults: null,
    },
    userId: "user_1",
    conversationId: "conv_1",
    collectedActions: [],
    collectedInsights: [],
    maxToolIterations: 8,
    ...overrides,
  };
}

describe("assistant chat loop", () => {
  beforeEach(() => {
    vi.clearAllMocks();
    vi.mocked(checkAiOutput).mockImplementation((content: string) => ({ clean: true, redacted: content }));
    vi.mocked(buildAssistantInsight).mockReturnValue(undefined);
  });

  it("returns a confirmation response instead of executing mutation tools immediately", async () => {
    vi.mocked(createPendingAssistantApproval).mockResolvedValue({
      id: "approval_1",
      toolName: "create_project",
      summary: "create project (name=Apollo)",
    } as never);

    const result = await runAssistantToolLoop(createLoopInput({
      client: createClient({
        choices: [{
          message: {
            tool_calls: [{
              id: "call_1",
              function: {
                name: "create_project",
                arguments: "{\"name\":\"Apollo\"}",
              },
            }],
          },
        }],
      }),
    }));

    expect(result).toMatchObject({
      role: "assistant",
      content: `${ASSISTANT_CONFIRMATION_PREFIX} create project (name=Apollo). Bitte bestätigen.`,
      approval: {
        id: "approval_1",
        toolName: "create_project",
        status: "pending",
      },
    });
    expect(executeTool).not.toHaveBeenCalled();
    expect(createAuditEntry).toHaveBeenCalledWith(expect.objectContaining({
      entityName: "create_project",
      summary: "AI tool blocked pending confirmation: create_project",
    }));
  });

  it("continues after read-only tool calls and returns collected actions and insights", async () => {
    vi.mocked(executeTool).mockResolvedValue({
      content: "{\"resources\":1}",
      data: { resources: 1 },
      action: { type: "navigate", href: "/resources" },
    } as never);
    vi.mocked(buildAssistantInsight).mockReturnValue({
      kind: "holiday_region",
      title: "Berlin",
      subtitle: "Resolved public holiday set",
      metrics: [{ label: "Resolved holidays", value: "1" }],
    });

    const result = await runAssistantToolLoop(createLoopInput({
      client: createClient(
        {
          choices: [{
            message: {
              tool_calls: [{
                id: "call_1",
                function: {
                  name: "search_resources",
                  arguments: "{\"query\":\"Alice\"}",
                },
              }],
            },
          }],
        },
        {
          choices: [{ message: { content: "Hier ist die passende Resource." } }],
        },
      ),
    }));

    expect(result).toMatchObject({
      content: "Hier ist die passende Resource.",
      actions: [{ type: "navigate", href: "/resources" }],
      insights: [{
        kind: "holiday_region",
        title: "Berlin",
      }],
    });
    expect(executeTool).toHaveBeenCalledWith(
      "search_resources",
      "{\"query\":\"Alice\"}",
      expect.objectContaining({ userId: "user_1" }),
    );
    expect(createAuditEntry).toHaveBeenCalledWith(expect.objectContaining({
      entityName: "search_resources",
      summary: "AI executed tool: search_resources",
    }));
  });

  it("redacts unsafe AI output before returning it", async () => {
    vi.mocked(checkAiOutput).mockReturnValue({
      clean: false,
      redacted: "[redacted]",
    });

    const result = await runAssistantToolLoop(createLoopInput({
      client: createClient({
        choices: [{ message: { content: "API key is sk-secret" } }],
      }),
    }));

    expect(result.content).toBe("[redacted]");
    expect(logger.warn).toHaveBeenCalledWith(
      { userId: "user_1" },
      "AI output contained sensitive content — redacted before delivery",
    );
    expect(createAuditEntry).toHaveBeenCalledWith(expect.objectContaining({
      entityName: "AiOutputRedacted",
    }));
  });

  it("returns a stable fallback after too many tool-call iterations", async () => {
    vi.mocked(executeTool).mockResolvedValue({
      content: "{\"ok\":true}",
      data: { ok: true },
    } as never);

    const result = await runAssistantToolLoop(createLoopInput({
      client: createClient(
        {
          choices: [{
            message: {
              tool_calls: [{
                id: "call_1",
                function: {
                  name: "search_resources",
                  arguments: "{\"query\":\"A\"}",
                },
              }],
            },
          }],
        },
        {
          choices: [{
            message: {
              tool_calls: [{
                id: "call_2",
                function: {
                  name: "search_resources",
                  arguments: "{\"query\":\"B\"}",
                },
              }],
            },
          }],
        },
      ),
      maxToolIterations: 2,
    }));

    expect(result.content).toBe("I had to stop after too many tool calls. Please try a simpler question.");
  });

  it("degrades mutation confirmations when approval storage is unavailable", async () => {
    vi.mocked(createPendingAssistantApproval).mockRejectedValue(
      new AssistantApprovalStorageUnavailableError("missing table"),
    );

    const result = await runAssistantToolLoop(createLoopInput({
      client: createClient({
        choices: [{
          message: {
            tool_calls: [{
              id: "call_1",
              function: {
                name: "create_project",
                arguments: "{\"name\":\"Apollo\"}",
              },
            }],
          },
        }],
      }),
    }));

    expect(result.content).toContain("Schreibende Assistant-Aktionen sind gerade nicht verfuegbar");
    expect(executeTool).not.toHaveBeenCalled();
  });
});
