/* eslint-disable @typescript-eslint/no-explicit-any */
import { describe, it, expect, vi, beforeEach } from "vitest";
import { AppointmentService } from "../appointment-service";
import { NotFoundError, ConflictError } from "../../utils/errors";

// Mock dependencies
vi.mock("../../db", () => ({
  getTenantDb: vi.fn(),
  centralDb: {
    select: vi.fn(),
  },
}));

vi.mock("../challenge-store", () => ({
  challengeStore: {
    consume: vi.fn(),
    store: vi.fn(),
  },
}));

vi.mock("../challenge-throttle", () => ({
  challengeThrottleService: {
    recordFailedAttempt: vi.fn(),
    clearThrottle: vi.fn(),
  },
}));

vi.mock("../notification-service", () => ({
  NotificationService: {
    forTenant: vi.fn().mockResolvedValue({
      sendAppointmentConfirmationEmail: vi.fn().mockResolvedValue(undefined),
      sendAppointmentCancellationEmail: vi.fn().mockResolvedValue(undefined),
      createNotification: vi.fn().mockResolvedValue(undefined),
    }),
  },
}));

const mockAppointment = {
  id: "appointment-123",
  tunnelId: "tunnel-123",
  channelId: "channel-123",
  appointmentDate: new Date("2024-01-15T10:00:00Z"),
  duration: 10,
  status: "NEW" as const,
  encryptedPayload: "encrypted-data",
  iv: "iv-data",
  authTag: "auth-tag",
  createdAt: new Date(),
  updatedAt: new Date(),
};

const mockClientTunnel = {
  id: "tunnel-123",
  emailHash: "email-hash-123",
  clientPublicKey: "client-public-key",
  createdAt: new Date(),
  updatedAt: new Date(),
};

const mockClientTunnelData = {
  tunnelId: "tunnel-123",
  channelId: "channel-123",
  agentId: "agent-123",
  appointmentDate: "2024-01-15T10:00:00Z",
  appointmentTimeZone: "Europe/Berlin",
  duration: 10,
  emailHash: "email-hash-123",
  clientEmail: "test@example.com",
  clientLanguage: "de",
  clientPublicKey: "client-public-key",
  privateKeyShare: "private-key-share",
  encryptedAppointment: {
    encryptedPayload: "encrypted-data",
    iv: "iv-data",
    authTag: "auth-tag",
  },
  staffKeyShares: [
    {
      userId: "staff-123",
      encryptedTunnelKey: "encrypted-tunnel-key",
    },
  ],
  clientEncryptedTunnelKey: "client-encrypted-tunnel-key",
};

describe("AppointmentService", () => {
  beforeEach(() => {
    vi.clearAllMocks();
  });

  describe("forTenant", () => {
    it("should create service for valid tenant", async () => {
      const { getTenantDb } = await import("../../db");
      const mockDb = { select: vi.fn() };
      vi.mocked(getTenantDb).mockResolvedValue(mockDb as any);

      const service = await AppointmentService.forTenant("tenant-123");

      expect(service.tenantId).toBe("tenant-123");
      expect(getTenantDb).toHaveBeenCalledWith("tenant-123");
    });

    it("should handle database connection errors", async () => {
      const { getTenantDb } = await import("../../db");
      vi.mocked(getTenantDb).mockRejectedValue(new Error("Database connection failed"));

      await expect(AppointmentService.forTenant("tenant-123")).rejects.toThrow();
    });
  });

  describe("getClientTunnels", () => {
    it("should return client tunnels", async () => {
      const { getTenantDb } = await import("../../db");
      const mockDb = {
        select: vi.fn().mockReturnValue({
          from: vi.fn().mockReturnValue({
            orderBy: vi.fn().mockResolvedValue([mockClientTunnel]),
          }),
        }),
      };
      vi.mocked(getTenantDb).mockResolvedValue(mockDb as any);

      const service = await AppointmentService.forTenant("tenant-123");
      const result = await service.getClientTunnels();

      expect(result).toHaveLength(1);
      expect(result[0].id).toBe("tunnel-123");
      expect(result[0].emailHash).toBe("email-hash-123");
      expect(result[0].clientPublicKey).toBe("client-public-key");
      expect(result[0].createdAt).toBeDefined();
    });

    it("should return empty array when no tunnels exist", async () => {
      const { getTenantDb } = await import("../../db");
      const mockDb = {
        select: vi.fn().mockReturnValue({
          from: vi.fn().mockReturnValue({
            orderBy: vi.fn().mockResolvedValue([]),
          }),
        }),
      };
      vi.mocked(getTenantDb).mockResolvedValue(mockDb as any);

      const service = await AppointmentService.forTenant("tenant-123");
      const result = await service.getClientTunnels();

      expect(result).toEqual([]);
    });

    it("should include current staff encrypted tunnel key when staffUserId is provided", async () => {
      const { getTenantDb } = await import("../../db");

      const mockSelect = vi
        .fn()
        .mockReturnValueOnce({
          from: vi.fn().mockReturnValue({
            orderBy: vi.fn().mockResolvedValue([mockClientTunnel]),
          }),
        })
        .mockReturnValueOnce({
          from: vi.fn().mockReturnValue({
            where: vi.fn().mockResolvedValue([
              {
                tunnelId: "tunnel-123",
                encryptedTunnelKey: "staff-encrypted-tunnel-key",
              },
            ]),
          }),
        });

      const mockDb = {
        select: mockSelect,
      };

      vi.mocked(getTenantDb).mockResolvedValue(mockDb as any);

      const service = await AppointmentService.forTenant("tenant-123");
      const result = await service.getClientTunnels("staff-123");

      expect(result).toHaveLength(1);
      expect(result[0].currentStaffEncryptedTunnelKey).toBe("staff-encrypted-tunnel-key");
      expect(result[0]).not.toHaveProperty("clientEncryptedTunnelKey");
    });

    it("should set current staff encrypted tunnel key to undefined when no share exists", async () => {
      const { getTenantDb } = await import("../../db");

      const mockSelect = vi
        .fn()
        .mockReturnValueOnce({
          from: vi.fn().mockReturnValue({
            orderBy: vi.fn().mockResolvedValue([mockClientTunnel]),
          }),
        })
        .mockReturnValueOnce({
          from: vi.fn().mockReturnValue({
            where: vi.fn().mockResolvedValue([]),
          }),
        });

      const mockDb = {
        select: mockSelect,
      };

      vi.mocked(getTenantDb).mockResolvedValue(mockDb as any);

      const service = await AppointmentService.forTenant("tenant-123");
      const result = await service.getClientTunnels("staff-123");

      expect(result).toHaveLength(1);
      expect(result[0].currentStaffEncryptedTunnelKey).toBeUndefined();
    });
  });

  describe("createNewClientWithAppointment", () => {
    it("should create client tunnel and appointment successfully", async () => {
      const { getTenantDb, centralDb } = await import("../../db");

      // Mock authorization check - users exist
      const mockAuthBuilder = {
        from: vi.fn().mockReturnThis(),
        where: vi.fn().mockReturnThis(),
        limit: vi.fn().mockResolvedValue([{ count: "1" }]),
      };
      vi.mocked(centralDb.select).mockReturnValue(mockAuthBuilder as any);

      // Mock existing tunnel check (client doesn't exist yet)
      const mockExistingTunnelBuilder = {
        from: vi.fn().mockReturnThis(),
        where: vi.fn().mockReturnThis(),
        limit: vi.fn().mockResolvedValue([]), // No existing tunnel
      };

      // Mock tenant database transaction
      const mockTransaction = vi.fn().mockImplementation(async (callback) => {
        const tx = {
          insert: vi
            .fn()
            .mockReturnValueOnce({
              values: vi.fn().mockReturnValue({
                returning: vi.fn().mockResolvedValue([{ id: "tunnel-123" }]),
              }),
            })
            .mockReturnValueOnce({
              values: vi.fn().mockResolvedValue(undefined),
            })
            .mockReturnValueOnce({
              values: vi.fn().mockReturnValue({
                returning: vi.fn().mockResolvedValue([
                  {
                    id: "appointment-123",
                    appointmentDate: new Date("2024-01-15T10:00:00Z"),
                    status: "NEW",
                  },
                ]),
              }),
            }),
          select: vi.fn().mockReturnValue({
            from: vi.fn().mockReturnValue({
              where: vi.fn().mockReturnValue({
                limit: vi.fn().mockResolvedValue([{ requiresConfirmation: true }]),
              }),
            }),
          }),
        };
        return await callback(tx);
      });

      const mockDb = {
        select: vi.fn().mockReturnValue(mockExistingTunnelBuilder as any),
        transaction: mockTransaction,
      };
      vi.mocked(getTenantDb).mockResolvedValue(mockDb as any);

      const service = await AppointmentService.forTenant("tenant-123");
      const result = await service.createNewClientWithAppointment(mockClientTunnelData);

      expect(result.id).toBe("appointment-123");
      expect(result.status).toBe("NEW");
      expect(result.appointmentDate).toBe("2024-01-15T10:00:00.000Z");
    });

    it("should block creation when no authorized users exist", async () => {
      const { centralDb } = await import("../../db");

      // Mock authorization check - no users exist
      const mockAuthBuilder = {
        from: vi.fn().mockReturnThis(),
        where: vi.fn().mockReturnThis(),
        limit: vi.fn().mockResolvedValue([]),
      };
      vi.mocked(centralDb.select).mockReturnValue(mockAuthBuilder as any);

      const service = await AppointmentService.forTenant("tenant-123");

      await expect(service.createNewClientWithAppointment(mockClientTunnelData)).rejects.toThrow(
        ConflictError,
      );
    });

    it("should throw ConflictError when client already exists", async () => {
      const { getTenantDb, centralDb } = await import("../../db");

      // Mock authorization check - users exist
      const mockAuthBuilder = {
        from: vi.fn().mockReturnThis(),
        where: vi.fn().mockReturnThis(),
        limit: vi.fn().mockResolvedValue([{ count: "1" }]),
      };
      vi.mocked(centralDb.select).mockReturnValue(mockAuthBuilder as any);

      // Mock existing tunnel check (client already exists)
      const mockExistingTunnelBuilder = {
        from: vi.fn().mockReturnThis(),
        where: vi.fn().mockReturnThis(),
        limit: vi.fn().mockResolvedValue([{ id: "existing-tunnel-123" }]), // Existing tunnel found
      };

      const mockDb = {
        select: vi.fn().mockReturnValue(mockExistingTunnelBuilder as any),
      };
      vi.mocked(getTenantDb).mockResolvedValue(mockDb as any);

      const service = await AppointmentService.forTenant("tenant-123");

      await expect(service.createNewClientWithAppointment(mockClientTunnelData)).rejects.toThrow(
        ConflictError,
      );
      await expect(service.createNewClientWithAppointment(mockClientTunnelData)).rejects.toThrow(
        "This email address is already registered",
      );
    });

    it("should throw NotFoundError when channel not found", async () => {
      const { getTenantDb, centralDb } = await import("../../db");

      // Mock authorization check - users exist
      const mockAuthBuilder = {
        from: vi.fn().mockReturnThis(),
        where: vi.fn().mockReturnThis(),
        limit: vi.fn().mockResolvedValue([{ count: "1" }]),
      };
      vi.mocked(centralDb.select).mockReturnValue(mockAuthBuilder as any);

      // Mock existing tunnel check (client doesn't exist yet)
      const mockExistingTunnelBuilder = {
        from: vi.fn().mockReturnThis(),
        where: vi.fn().mockReturnThis(),
        limit: vi.fn().mockResolvedValue([]), // No existing tunnel
      };

      // Mock tenant database transaction with channel not found
      const mockTransaction = vi.fn().mockImplementation(async (callback) => {
        const tx = {
          insert: vi
            .fn()
            .mockReturnValueOnce({
              values: vi.fn().mockReturnValue({
                returning: vi.fn().mockResolvedValue([{ id: "tunnel-123" }]),
              }),
            })
            .mockReturnValueOnce({
              values: vi.fn().mockResolvedValue(undefined),
            }),
          select: vi.fn().mockReturnValue({
            from: vi.fn().mockReturnValue({
              where: vi.fn().mockReturnValue({
                limit: vi.fn().mockResolvedValue([]), // Channel not found
              }),
            }),
          }),
        };
        return await callback(tx);
      });

      const mockDb = {
        select: vi.fn().mockReturnValue(mockExistingTunnelBuilder as any),
        transaction: mockTransaction,
      };
      vi.mocked(getTenantDb).mockResolvedValue(mockDb as any);

      const service = await AppointmentService.forTenant("tenant-123");

      await expect(service.createNewClientWithAppointment(mockClientTunnelData)).rejects.toThrow(
        NotFoundError,
      );
    });

    it("should create appointment with CONFIRMED status when staff user creates it", async () => {
      const { getTenantDb, centralDb } = await import("../../db");

      // Mock authorization check - users exist
      const mockAuthBuilder = {
        from: vi.fn().mockReturnThis(),
        where: vi.fn().mockReturnThis(),
        limit: vi.fn().mockResolvedValue([{ count: "1" }]),
      };
      vi.mocked(centralDb.select).mockReturnValue(mockAuthBuilder as any);

      // Mock existing tunnel check (client doesn't exist yet)
      const mockExistingTunnelBuilder = {
        from: vi.fn().mockReturnThis(),
        where: vi.fn().mockReturnThis(),
        limit: vi.fn().mockResolvedValue([]), // No existing tunnel
      };

      // Mock tenant database transaction with successful creation
      const mockTransaction = vi.fn().mockImplementation(async (callback) => {
        const tx = {
          insert: vi
            .fn()
            .mockReturnValueOnce({
              values: vi.fn().mockReturnValue({
                returning: vi.fn().mockResolvedValue([{ id: "tunnel-123" }]),
              }),
            })
            .mockReturnValueOnce({
              values: vi.fn().mockResolvedValue(undefined),
            })
            .mockReturnValueOnce({
              values: vi.fn().mockReturnValue({
                returning: vi.fn().mockResolvedValue([
                  {
                    id: "apt-123",
                    appointmentDate: new Date("2024-01-01T10:00:00Z"),
                    status: "CONFIRMED",
                  },
                ]),
              }),
            }),
          select: vi.fn().mockReturnValue({
            from: vi.fn().mockReturnValue({
              where: vi.fn().mockReturnValue({
                limit: vi.fn().mockResolvedValue([{ id: "channel-456" }]), // Channel found
              }),
            }),
          }),
        };
        return await callback(tx);
      });

      const mockDb = {
        select: vi.fn().mockReturnValue(mockExistingTunnelBuilder as any),
        transaction: mockTransaction,
      };
      vi.mocked(getTenantDb).mockResolvedValue(mockDb as any);

      const service = await AppointmentService.forTenant("tenant-123");
      const result = await service.createNewClientWithAppointment(mockClientTunnelData);

      expect(result.status).toBe("CONFIRMED");
      expect(mockTransaction).toHaveBeenCalled();
    });
  });

  describe("getAppointmentsByTimeRange", () => {
    it("should return appointments within time range", async () => {
      const { getTenantDb } = await import("../../db");
      const mockDb = {
        select: vi.fn().mockReturnValue({
          from: vi.fn().mockReturnValue({
            where: vi.fn().mockReturnValue({
              orderBy: vi.fn().mockResolvedValue([mockAppointment]),
            }),
          }),
        }),
      };
      vi.mocked(getTenantDb).mockResolvedValue(mockDb as any);

      const service = await AppointmentService.forTenant("tenant-123");
      const startDate = new Date("2024-01-01T00:00:00Z");
      const endDate = new Date("2024-01-31T23:59:59Z");

      const result = await service.getAppointmentsByTimeRange(startDate, endDate);

      expect(result).toHaveLength(1);
      expect(result[0].id).toBe("appointment-123");
    });

    it("should return empty array when no appointments in range", async () => {
      const { getTenantDb } = await import("../../db");
      const mockDb = {
        select: vi.fn().mockReturnValue({
          from: vi.fn().mockReturnValue({
            where: vi.fn().mockReturnValue({
              orderBy: vi.fn().mockResolvedValue([]),
            }),
          }),
        }),
      };
      vi.mocked(getTenantDb).mockResolvedValue(mockDb as any);

      const service = await AppointmentService.forTenant("tenant-123");
      const startDate = new Date("2024-01-01T00:00:00Z");
      const endDate = new Date("2024-01-31T23:59:59Z");

      const result = await service.getAppointmentsByTimeRange(startDate, endDate);

      expect(result).toEqual([]);
    });
  });

  describe("deleteAppointment", () => {
    it("should delete appointment successfully", async () => {
      const { getTenantDb } = await import("../../db");
      const mockDb = {
        delete: vi.fn().mockReturnValue({
          where: vi.fn().mockReturnValue({
            returning: vi.fn().mockResolvedValue([mockAppointment]),
          }),
        }),
      };
      vi.mocked(getTenantDb).mockResolvedValue(mockDb as any);

      const service = await AppointmentService.forTenant("tenant-123");
      const result = await service.deleteAppointment("appointment-123");

      expect(result).toBe(true);
    });

    it("should return false when appointment not found", async () => {
      const { getTenantDb } = await import("../../db");
      const mockDb = {
        delete: vi.fn().mockReturnValue({
          where: vi.fn().mockReturnValue({
            returning: vi.fn().mockResolvedValue([]),
          }),
        }),
      };
      vi.mocked(getTenantDb).mockResolvedValue(mockDb as any);

      const service = await AppointmentService.forTenant("tenant-123");
      const result = await service.deleteAppointment("appointment-123");

      expect(result).toBe(false);
    });
  });

  describe("deleteAppointmentByStaff", () => {
    it("should delete appointment and send email/notifications", async () => {
      const { getTenantDb } = await import("../../db");

      // Mock email service
      const emailModule = await import("../../email/email-service");
      const mockSendEmail = vi.fn().mockResolvedValue(undefined);
      const mockGetChannelTitle = vi.fn().mockResolvedValue("Test Channel");
      vi.spyOn(emailModule, "sendAppointmentCancelledEmail").mockImplementation(mockSendEmail);
      vi.spyOn(emailModule, "getChannelTitle").mockImplementation(mockGetChannelTitle);

      // Mock TenantAdminService
      const tenantModule = await import("../tenant-admin-service");
      const mockTenant = {
        id: "tenant-123",
        shortName: "test-clinic",
        longName: "Test Clinic",
        languages: ["de", "en"],
      };
      vi.spyOn(tenantModule.TenantAdminService, "getTenantById").mockResolvedValue({
        tenantData: mockTenant,
      } as any);

      // Mock NotificationService
      const notificationModule = await import("../notification-service");
      const mockCreateNotification = vi.fn().mockResolvedValue(["notification-1"]);
      vi.spyOn(notificationModule.NotificationService, "forTenant").mockResolvedValue({
        createNotification: mockCreateNotification,
      } as any);

      const mockDb = {
        select: vi.fn().mockReturnValue({
          from: vi.fn().mockReturnValue({
            where: vi.fn().mockReturnValue({
              limit: vi.fn().mockResolvedValue([mockAppointment]),
            }),
          }),
        }),
        delete: vi.fn().mockReturnValue({
          where: vi.fn().mockResolvedValue(undefined),
        }),
      };
      vi.mocked(getTenantDb).mockResolvedValue(mockDb as any);

      const service = await AppointmentService.forTenant("tenant-123");

      // Use a promise to track async operations
      const deletePromise = service.deleteAppointmentByStaff(
        "appointment-123",
        "client@example.com",
        "de",
      );

      await deletePromise;

      expect(mockDb.delete).toHaveBeenCalled();
      expect(mockGetChannelTitle).toHaveBeenCalledWith("tenant-123", "channel-123", "de");
    });

    it("should throw NotFoundError when appointment does not exist", async () => {
      const { getTenantDb } = await import("../../db");
      const mockDb = {
        select: vi.fn().mockReturnValue({
          from: vi.fn().mockReturnValue({
            where: vi.fn().mockReturnValue({
              limit: vi.fn().mockResolvedValue([]),
            }),
          }),
        }),
      };
      vi.mocked(getTenantDb).mockResolvedValue(mockDb as any);

      const service = await AppointmentService.forTenant("tenant-123");

      await expect(
        service.deleteAppointmentByStaff("appointment-123", "client@example.com", "de"),
      ).rejects.toThrow(NotFoundError);
    });

    it("should use default language when not provided", async () => {
      const { getTenantDb } = await import("../../db");

      // Mock email service
      const emailModule = await import("../../email/email-service");
      const mockSendEmail = vi.fn().mockResolvedValue(undefined);
      const mockGetChannelTitle = vi.fn().mockResolvedValue("Test Channel");
      vi.spyOn(emailModule, "sendAppointmentCancelledEmail").mockImplementation(mockSendEmail);
      vi.spyOn(emailModule, "getChannelTitle").mockImplementation(mockGetChannelTitle);

      // Mock TenantAdminService
      const tenantModule = await import("../tenant-admin-service");
      const mockTenant = {
        id: "tenant-123",
        shortName: "test-clinic",
        longName: "Test Clinic",
        languages: ["de", "en"],
      };
      vi.spyOn(tenantModule.TenantAdminService, "getTenantById").mockResolvedValue({
        tenantData: mockTenant,
      } as any);

      // Mock NotificationService
      const notificationModule = await import("../notification-service");
      const mockCreateNotification = vi.fn().mockResolvedValue(["notification-1"]);
      vi.spyOn(notificationModule.NotificationService, "forTenant").mockResolvedValue({
        createNotification: mockCreateNotification,
      } as any);

      const mockDb = {
        select: vi.fn().mockReturnValue({
          from: vi.fn().mockReturnValue({
            where: vi.fn().mockReturnValue({
              limit: vi.fn().mockResolvedValue([mockAppointment]),
            }),
          }),
        }),
        delete: vi.fn().mockReturnValue({
          where: vi.fn().mockResolvedValue(undefined),
        }),
      };
      vi.mocked(getTenantDb).mockResolvedValue(mockDb as any);

      const service = await AppointmentService.forTenant("tenant-123");

      // Use a promise to track async operations
      const deletePromise = service.deleteAppointmentByStaff(
        "appointment-123",
        "client@example.com",
      );

      await deletePromise;

      expect(mockGetChannelTitle).toHaveBeenCalledWith("tenant-123", "channel-123", "de");
    });
  });

  describe("getFutureAppointmentsByTunnelId", () => {
    it("should return future appointments for a client tunnel", async () => {
      const { getTenantDb } = await import("../../db");

      const futureDate1 = new Date("2025-02-15T10:00:00Z");
      const futureDate2 = new Date("2025-03-20T14:30:00Z");

      const mockFutureAppointments = [
        {
          id: "appointment-1",
          appointmentDate: futureDate1,
          status: "CONFIRMED",
          channelId: "channel-1",
          encryptedPayload: "encrypted-1",
          iv: "iv-1",
          authTag: "auth-tag-1",
        },
        {
          id: "appointment-2",
          appointmentDate: futureDate2,
          status: "NEW",
          channelId: "channel-2",
          encryptedPayload: "encrypted-2",
          iv: "iv-2",
          authTag: "auth-tag-2",
        },
      ];

      const mockDb = {
        select: vi.fn().mockReturnValue({
          from: vi.fn().mockReturnValue({
            where: vi.fn().mockReturnValue({
              orderBy: vi.fn().mockResolvedValue(mockFutureAppointments),
            }),
          }),
        }),
      };
      vi.mocked(getTenantDb).mockResolvedValue(mockDb as any);

      const service = await AppointmentService.forTenant("tenant-123");
      const result = await service.getFutureAppointmentsByTunnelId("tunnel-123");

      expect(result).toHaveLength(2);
      expect(result[0]).toEqual({
        id: "appointment-1",
        appointmentDate: futureDate1.toISOString(),
        status: "CONFIRMED",
        channelId: "channel-1",
        encryptedPayload: "encrypted-1",
        iv: "iv-1",
        authTag: "auth-tag-1",
      });
      expect(result[1]).toEqual({
        id: "appointment-2",
        appointmentDate: futureDate2.toISOString(),
        status: "NEW",
        channelId: "channel-2",
        encryptedPayload: "encrypted-2",
        iv: "iv-2",
        authTag: "auth-tag-2",
      });
    });

    it("should return empty array when no future appointments exist", async () => {
      const { getTenantDb } = await import("../../db");

      const mockDb = {
        select: vi.fn().mockReturnValue({
          from: vi.fn().mockReturnValue({
            where: vi.fn().mockReturnValue({
              orderBy: vi.fn().mockResolvedValue([]),
            }),
          }),
        }),
      };
      vi.mocked(getTenantDb).mockResolvedValue(mockDb as any);

      const service = await AppointmentService.forTenant("tenant-123");
      const result = await service.getFutureAppointmentsByTunnelId("tunnel-123");

      expect(result).toEqual([]);
    });

    it("should handle null encrypted fields gracefully", async () => {
      const { getTenantDb } = await import("../../db");

      const futureDate = new Date("2025-02-15T10:00:00Z");
      const mockAppointmentsWithNulls = [
        {
          id: "appointment-1",
          appointmentDate: futureDate,
          status: "NEW",
          channelId: "channel-1",
          encryptedPayload: null,
          iv: null,
          authTag: null,
        },
      ];

      const mockDb = {
        select: vi.fn().mockReturnValue({
          from: vi.fn().mockReturnValue({
            where: vi.fn().mockReturnValue({
              orderBy: vi.fn().mockResolvedValue(mockAppointmentsWithNulls),
            }),
          }),
        }),
      };
      vi.mocked(getTenantDb).mockResolvedValue(mockDb as any);

      const service = await AppointmentService.forTenant("tenant-123");
      const result = await service.getFutureAppointmentsByTunnelId("tunnel-123");

      expect(result).toHaveLength(1);
      expect(result[0].encryptedPayload).toBe("");
      expect(result[0].iv).toBe("");
      expect(result[0].authTag).toBe("");
    });
  });

  describe("deleteAppointmentByClient", () => {
    it("should delete appointment after verifying challenge and ownership", async () => {
      const { getTenantDb } = await import("../../db");
      const { challengeStore } = await import("../challenge-store");
      const { challengeThrottleService } = await import("../challenge-throttle");

      const mockAppointmentDate = new Date("2025-02-15T10:00:00Z");

      // Mock challenge verification
      vi.mocked(challengeStore.consume).mockResolvedValue({
        challenge: Buffer.from("test-challenge").toString("base64"),
        emailHash: "email-hash-123",
        createdAt: new Date(),
        expiresAt: new Date(Date.now() + 5 * 60 * 1000),
      });

      vi.mocked(challengeThrottleService.clearThrottle).mockResolvedValue();

      // Mock database queries - use mockReturnValueOnce for sequential calls
      const mockLimit = vi
        .fn()
        // First call: appointment query
        .mockResolvedValueOnce([
          {
            id: "appointment-123",
            tunnelId: "tunnel-123",
            appointmentDate: mockAppointmentDate,
            channelId: "channel-123",
          },
        ])
        // Second call: tunnel query
        .mockResolvedValueOnce([
          {
            id: "tunnel-123",
            emailHash: "email-hash-123",
          },
        ]);

      const mockDb = {
        select: vi.fn().mockReturnValue({
          from: vi.fn().mockReturnThis(),
          where: vi.fn().mockReturnThis(),
          limit: mockLimit,
        }),
        delete: vi.fn().mockReturnValue({
          where: vi.fn().mockResolvedValue(undefined),
        }),
      };

      vi.mocked(getTenantDb).mockResolvedValue(mockDb as any);

      const service = await AppointmentService.forTenant("tenant-123");
      await service.deleteAppointmentByClient(
        "appointment-123",
        "email-hash-123",
        "challenge-123",
        Buffer.from("test-challenge").toString("base64"),
      );

      expect(challengeStore.consume).toHaveBeenCalledWith("challenge-123", "tenant-123");
      expect(challengeThrottleService.clearThrottle).toHaveBeenCalledWith(
        "email-hash-123",
        "pin",
        "tenant-123",
      );
    });

    it("should throw NotFoundError when challenge is not found", async () => {
      const { challengeStore } = await import("../challenge-store");

      vi.mocked(challengeStore.consume).mockResolvedValue(null);

      const service = await AppointmentService.forTenant("tenant-123");

      await expect(
        service.deleteAppointmentByClient(
          "appointment-123",
          "email-hash-123",
          "invalid-challenge",
          "challenge-response",
        ),
      ).rejects.toThrow(NotFoundError);
    });

    it("should throw ValidationError when challenge doesn't belong to client", async () => {
      const { challengeStore } = await import("../challenge-store");

      vi.mocked(challengeStore.consume).mockResolvedValue({
        challenge: Buffer.from("test-challenge").toString("base64"),
        emailHash: "different-email-hash",
        createdAt: new Date(),
        expiresAt: new Date(Date.now() + 5 * 60 * 1000),
      });

      const service = await AppointmentService.forTenant("tenant-123");

      await expect(
        service.deleteAppointmentByClient(
          "appointment-123",
          "email-hash-123",
          "challenge-123",
          Buffer.from("test-challenge").toString("base64"),
        ),
      ).rejects.toThrow("Invalid authentication");
    });

    it("should throw ValidationError when challenge response is incorrect", async () => {
      const { challengeStore } = await import("../challenge-store");
      const { challengeThrottleService } = await import("../challenge-throttle");

      vi.mocked(challengeStore.consume).mockResolvedValue({
        challenge: Buffer.from("correct-challenge").toString("base64"),
        emailHash: "email-hash-123",
        createdAt: new Date(),
        expiresAt: new Date(Date.now() + 5 * 60 * 1000),
      });

      vi.mocked(challengeThrottleService.recordFailedAttempt).mockResolvedValue();

      const service = await AppointmentService.forTenant("tenant-123");

      await expect(
        service.deleteAppointmentByClient(
          "appointment-123",
          "email-hash-123",
          "challenge-123",
          Buffer.from("wrong-challenge").toString("base64"),
        ),
      ).rejects.toThrow("Invalid challenge response");

      expect(challengeThrottleService.recordFailedAttempt).toHaveBeenCalledWith(
        "email-hash-123",
        "pin",
        "tenant-123",
      );
    });

    it("should throw ValidationError when appointment doesn't belong to client", async () => {
      const { getTenantDb } = await import("../../db");
      const { challengeStore } = await import("../challenge-store");
      const { challengeThrottleService } = await import("../challenge-throttle");

      const mockAppointmentDate = new Date("2025-02-15T10:00:00Z");

      vi.mocked(challengeStore.consume).mockResolvedValue({
        challenge: Buffer.from("test-challenge").toString("base64"),
        emailHash: "email-hash-123",
        createdAt: new Date(),
        expiresAt: new Date(Date.now() + 5 * 60 * 1000),
      });

      vi.mocked(challengeThrottleService.clearThrottle).mockResolvedValue();

      const mockDb = {
        select: vi.fn().mockImplementation(() => ({
          from: vi.fn().mockReturnThis(),
          where: vi.fn().mockReturnThis(),
          limit: vi.fn().mockImplementation(() => {
            const callStack = new Error().stack || "";
            if (callStack.includes("appointment")) {
              return Promise.resolve([
                {
                  id: "appointment-123",
                  tunnelId: "tunnel-123",
                  appointmentDate: mockAppointmentDate,
                  channelId: "channel-123",
                },
              ]);
            } else {
              // Tunnel belongs to different email
              return Promise.resolve([
                {
                  id: "tunnel-123",
                  emailHash: "different-email-hash",
                },
              ]);
            }
          }),
        })),
      };

      vi.mocked(getTenantDb).mockResolvedValue(mockDb as any);

      const service = await AppointmentService.forTenant("tenant-123");

      await expect(
        service.deleteAppointmentByClient(
          "appointment-123",
          "email-hash-123",
          "challenge-123",
          Buffer.from("test-challenge").toString("base64"),
        ),
      ).rejects.toThrow("Appointment does not belong to this client");
    });
  });
});
