import { beforeEach, describe, expect, it, vi } from "vitest";
import {
  AssistantApprovalStorageUnavailableError,
  clearPendingAssistantApproval,
  consumePendingAssistantApproval,
  createPendingAssistantApproval,
  listPendingAssistantApprovals,
  peekPendingAssistantApproval,
  resetAssistantApprovalStorageWarningStateForTests,
} from "../router/assistant-approvals.js";
import { logger } from "../lib/logger.js";
import {
  createApprovalStoreMock,
  createMissingApprovalTableError,
  TEST_CONVERSATION_ID,
  TEST_USER_ID,
} from "./assistant-approval-test-helpers.js";

describe("assistant approvals", () => {
  let approvalStore = createApprovalStoreMock();

  beforeEach(() => {
    approvalStore = createApprovalStoreMock();
    resetAssistantApprovalStorageWarningStateForTests();
  });

  it("stores and consumes pending approvals independently from chat text", async () => {
    const approval = await createPendingAssistantApproval(
      approvalStore,
      TEST_USER_ID,
      TEST_CONVERSATION_ID,
      "create_project",
      JSON.stringify({ name: "Gelddruckmaschine", status: "DRAFT" }),
    );

    await expect(peekPendingAssistantApproval(approvalStore, TEST_USER_ID, TEST_CONVERSATION_ID)).resolves.toMatchObject({
      id: approval.id,
      toolName: "create_project",
      summary: expect.stringContaining("create project"),
    });

    await expect(consumePendingAssistantApproval(approvalStore, TEST_USER_ID, TEST_CONVERSATION_ID)).resolves.toMatchObject({
      id: approval.id,
      toolName: "create_project",
    });
    await expect(peekPendingAssistantApproval(approvalStore, TEST_USER_ID, TEST_CONVERSATION_ID)).resolves.toBeNull();
  });

  it("expires stale pending approvals", async () => {
    await createPendingAssistantApproval(
      approvalStore,
      TEST_USER_ID,
      TEST_CONVERSATION_ID,
      "create_project",
      JSON.stringify({ name: "Apollo" }),
      { ttlMs: -1 },
    );

    await expect(peekPendingAssistantApproval(approvalStore, TEST_USER_ID, TEST_CONVERSATION_ID)).resolves.toBeNull();
  });

  it("clears pending approvals for cancellation semantics", async () => {
    await createPendingAssistantApproval(
      approvalStore,
      TEST_USER_ID,
      TEST_CONVERSATION_ID,
      "create_project",
      JSON.stringify({ name: "Apollo" }),
    );

    await clearPendingAssistantApproval(approvalStore, TEST_USER_ID, TEST_CONVERSATION_ID);

    await expect(peekPendingAssistantApproval(approvalStore, TEST_USER_ID, TEST_CONVERSATION_ID)).resolves.toBeNull();
  });

  it("isolates pending approvals by conversation", async () => {
    const otherConversationId = `${TEST_CONVERSATION_ID}-other`;

    await createPendingAssistantApproval(
      approvalStore,
      TEST_USER_ID,
      TEST_CONVERSATION_ID,
      "create_project",
      JSON.stringify({ name: "Apollo" }),
    );
    await createPendingAssistantApproval(
      approvalStore,
      TEST_USER_ID,
      otherConversationId,
      "create_project",
      JSON.stringify({ name: "Hermes" }),
    );

    await clearPendingAssistantApproval(approvalStore, TEST_USER_ID, TEST_CONVERSATION_ID);

    await expect(peekPendingAssistantApproval(approvalStore, TEST_USER_ID, TEST_CONVERSATION_ID)).resolves.toBeNull();
    await expect(peekPendingAssistantApproval(approvalStore, TEST_USER_ID, otherConversationId)).resolves.toMatchObject({
      toolName: "create_project",
      summary: expect.stringContaining("Hermes"),
    });
  });

  it("lists only still-pending approvals for the current user across conversations", async () => {
    const otherConversationId = `${TEST_CONVERSATION_ID}-other`;

    await createPendingAssistantApproval(
      approvalStore,
      TEST_USER_ID,
      TEST_CONVERSATION_ID,
      "create_project",
      JSON.stringify({ name: "Apollo" }),
    );
    await createPendingAssistantApproval(
      approvalStore,
      TEST_USER_ID,
      otherConversationId,
      "create_project",
      JSON.stringify({ name: "Hermes" }),
    );

    const cancelled = await createPendingAssistantApproval(
      approvalStore,
      TEST_USER_ID,
      `${TEST_CONVERSATION_ID}-cancelled`,
      "create_project",
      JSON.stringify({ name: "Cancelled" }),
    );
    await approvalStore.assistantApproval.updateMany({
      where: { id: cancelled.id, userId: TEST_USER_ID, status: "PENDING" },
      data: { status: "CANCELLED", cancelledAt: new Date() },
    });

    await createPendingAssistantApproval(
      approvalStore,
      "other-user",
      `${TEST_CONVERSATION_ID}-foreign`,
      "create_project",
      JSON.stringify({ name: "Foreign" }),
    );
    await createPendingAssistantApproval(
      approvalStore,
      TEST_USER_ID,
      `${TEST_CONVERSATION_ID}-expired`,
      "create_project",
      JSON.stringify({ name: "Expired" }),
      { ttlMs: -1 },
    );

    const approvals = await listPendingAssistantApprovals(approvalStore, TEST_USER_ID);
    const approvalSummaries = approvals.map((approval) => approval.summary).join(" ");

    expect(approvals).toHaveLength(2);
    expect([...approvals.map((approval) => approval.conversationId)].sort()).toEqual([
      otherConversationId,
      TEST_CONVERSATION_ID,
    ].sort());
    expect(approvals.every((approval) => approval.userId === TEST_USER_ID)).toBe(true);
    expect(approvalSummaries).toContain("Apollo");
    expect(approvalSummaries).toContain("Hermes");
    expect(approvalSummaries).not.toContain("Cancelled");
    expect(approvalSummaries).not.toContain("Expired");
    expect(approvalSummaries).not.toContain("Foreign");
  });

  it("degrades approval reads gracefully when approval storage is missing", async () => {
    const warnSpy = vi.spyOn(logger, "warn").mockImplementation(() => logger);
    const missingTableError = createMissingApprovalTableError();
    const missingStore = {
      assistantApproval: {
        findFirst: vi.fn(async () => {
          throw missingTableError;
        }),
        findMany: vi.fn(async () => {
          throw missingTableError;
        }),
        create: vi.fn(async () => {
          throw missingTableError;
        }),
        updateMany: vi.fn(async () => {
          throw missingTableError;
        }),
      },
    };

    await expect(listPendingAssistantApprovals(missingStore, TEST_USER_ID)).resolves.toEqual([]);
    await expect(peekPendingAssistantApproval(missingStore, TEST_USER_ID, TEST_CONVERSATION_ID)).resolves.toBeNull();
    await expect(consumePendingAssistantApproval(missingStore, TEST_USER_ID, TEST_CONVERSATION_ID)).resolves.toBeNull();
    await expect(clearPendingAssistantApproval(missingStore, TEST_USER_ID, TEST_CONVERSATION_ID)).resolves.toBeUndefined();
    expect(warnSpy).toHaveBeenCalledTimes(1);
  });

  it("returns an explicit error when approval storage is missing for mutation confirmation", async () => {
    const warnSpy = vi.spyOn(logger, "warn").mockImplementation(() => logger);
    const missingTableError = createMissingApprovalTableError();
    const missingStore = {
      assistantApproval: {
        findFirst: vi.fn(async () => {
          throw missingTableError;
        }),
        findMany: vi.fn(async () => {
          throw missingTableError;
        }),
        create: vi.fn(async () => {
          throw missingTableError;
        }),
        updateMany: vi.fn(async () => {
          throw missingTableError;
        }),
      },
    };

    await expect(createPendingAssistantApproval(
      missingStore,
      TEST_USER_ID,
      TEST_CONVERSATION_ID,
      "create_project",
      JSON.stringify({ name: "Apollo" }),
    )).rejects.toBeInstanceOf(AssistantApprovalStorageUnavailableError);
    expect(warnSpy).toHaveBeenCalledTimes(1);
  });
});
