/**
 * Comprehensive Jest Tests for Real Estate Viewings API
 *
 * Tests the following endpoints:
 * - GET  /api/real-estate/viewings       - List viewings with filters
 * - POST /api/real-estate/viewings       - Create viewing with conflict detection
 * - GET  /api/real-estate/viewings/[id]  - Get viewing detail
 * - PUT  /api/real-estate/viewings/[id]  - Update / reschedule viewing
 * - DELETE /api/real-estate/viewings/[id] - Cancel viewing
 */

import { NextRequest } from "next/server";

// ─── Mocks (must be declared before any imports that use them) ───

jest.mock("@/lib/auth", () => ({ auth: jest.fn() }));

jest.mock("@/lib/utils/auth-helpers.util", () => ({
  getAuthenticatedEmail: jest.fn(),
}));

jest.mock("@/lib/utils/organization-data", () => ({
  getEffectiveOwnerId: jest.fn((id: string) => id),
}));

// Transaction mock: the callback receives a `tx` object with the same shape as prisma
const txMock = {
  propertyViewing: {
    findFirst: jest.fn(),
    create: jest.fn(),
    update: jest.fn(),
  },
};

jest.mock("@/lib/db");

jest.mock("@/lib/rate-limiter", () => ({
  withRateLimit: (handler: Function) => handler,
  rateLimiters: { authGeneral: {}, api: {} },
}));

jest.mock("@/lib/csrf", () => ({
  withCSRF: (handler: Function) => handler,
}));

jest.mock("@/lib/errors", () => ({
  getErrorMessage: (e: unknown) =>
    e instanceof Error ? e.message : "Unknown error",
}));

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

jest.mock("@/lib/utils/api-response.util", () => ({
  ApiResponses: {
    unauthorized: jest.fn(
      () =>
        new Response(JSON.stringify({ error: "Unauthorized" }), {
          status: 401,
        }),
    ),
    notFound: jest.fn(
      (msg: string) =>
        new Response(JSON.stringify({ error: msg }), { status: 404 }),
    ),
    badRequest: jest.fn(
      (msg: string) =>
        new Response(JSON.stringify({ error: msg }), { status: 400 }),
    ),
    serverError: jest.fn(
      (msg: string) =>
        new Response(JSON.stringify({ error: msg }), { status: 500 }),
    ),
    success: jest.fn(
      (data: unknown) =>
        new Response(JSON.stringify({ success: true, ...(data as object) }), {
          status: 200,
        }),
    ),
  },
  paginatedResponse: jest.fn(
    (data: unknown[], page: number, pageSize: number, totalCount: number) => {
      const totalPages = Math.ceil(totalCount / pageSize);
      return new Response(
        JSON.stringify({
          success: true,
          data,
          pagination: {
            page,
            pageSize,
            totalCount,
            totalPages,
            hasNextPage: page < totalPages,
            hasPreviousPage: page > 1,
          },
        }),
        { status: 200 },
      );
    },
  ),
}));

jest.mock("@/lib/config/constants", () => ({
  clampPaginationLimit: jest.fn((v: string | null) =>
    v ? Math.min(parseInt(v, 10), 100) : 50,
  ),
}));

jest.mock("@/lib/services/real-estate/notification.service", () => ({
  sendViewingConfirmation: jest.fn().mockResolvedValue(undefined),
}));

jest.mock("@/lib/utils/viewing-calendar-sync", () => ({
  syncViewingToCalendar: jest.fn().mockResolvedValue(undefined),
}));

// ─── Imports (after mocks) ───

import { prisma } from "@/lib/db";
import { getAuthenticatedEmail } from "@/lib/utils/auth-helpers.util";
import {
  GET as listViewings,
  POST as createViewing,
} from "@/app/api/real-estate/viewings/route";
import {
  GET as getViewing,
  PUT as updateViewing,
  DELETE as deleteViewing,
} from "@/app/api/real-estate/viewings/[id]/route";

// ─── Typed mocks ───

const mockGetAuthenticatedEmail = getAuthenticatedEmail as jest.MockedFunction<
  typeof getAuthenticatedEmail
>;
const mockPrisma = prisma as unknown as {
  users: { findUnique: jest.Mock };
  property: { findFirst: jest.Mock; findMany: jest.Mock };
  propertyViewing: {
    findMany: jest.Mock;
    findFirst: jest.Mock;
    create: jest.Mock;
    update: jest.Mock;
    count: jest.Mock;
    deleteMany: jest.Mock;
  };
  broker: { findFirst: jest.Mock; findMany: jest.Mock };
  organizations: { findUnique: jest.Mock };
  $transaction: jest.Mock;
};

// ─── Test data ───

const TEST_USER_EMAIL = "agent@realestate.test";
const TEST_USER_ID = "user-re-001";
const TEST_ORG_ID = "org-re-001";
const TEST_PROPERTY_ID = "prop-001";
const TEST_BROKER_ID = "broker-001";
const TEST_VIEWING_ID = "viewing-001";

// Future date to pass validation
const futureDate = new Date();
futureDate.setDate(futureDate.getDate() + 7);
const FUTURE_DATE_STR = futureDate.toISOString().split("T")[0];

const sampleViewing = {
  id: TEST_VIEWING_ID,
  userId: TEST_USER_ID,
  organizationId: TEST_ORG_ID,
  propertyId: TEST_PROPERTY_ID,
  brokerId: TEST_BROKER_ID,
  clientName: "Mohammed Al-Thani",
  clientPhone: "+97455512345",
  clientEmail: "client@example.com",
  viewingType: "INDIVIDUAL",
  viewingDate: futureDate,
  viewingTime: "14:00",
  duration: 30,
  status: "REQUESTED",
  meetingLink: null,
  clientTimezone: null,
  maxAttendees: null,
  currentAttendees: 0,
  source: "manual",
  conversationId: null,
  clientNotes: null,
  createdAt: new Date(),
  updatedAt: new Date(),
  property: {
    id: TEST_PROPERTY_ID,
    titleEn: "Luxury Villa in The Pearl",
    titleAr: "فيلا فاخرة في اللؤلؤة",
    referenceNumber: "RE-001",
    propertyType: "VILLA",
    city: "Doha",
    district: "The Pearl",
  },
  broker: {
    id: TEST_BROKER_ID,
    nameEn: "Ahmed Hassan",
    nameAr: "أحمد حسن",
    phone: "+97455500001",
  },
};

// ─── Helpers ───

function setupAuth(email: string | null = TEST_USER_EMAIL) {
  mockGetAuthenticatedEmail.mockResolvedValue(email);
  if (email) {
    mockPrisma.users.findUnique.mockResolvedValue({ id: TEST_USER_ID });
  }
}

function makeGetRequest(params: Record<string, string> = {}): NextRequest {
  const url = new URL("http://localhost/api/real-estate/viewings");
  for (const [k, v] of Object.entries(params)) {
    url.searchParams.set(k, v);
  }
  return new NextRequest(url.toString(), { method: "GET" });
}

function makePostRequest(body: Record<string, unknown>): NextRequest {
  return new NextRequest("http://localhost/api/real-estate/viewings", {
    method: "POST",
    body: JSON.stringify(body),
    headers: { "Content-Type": "application/json" },
  });
}

function makeDetailRequest(
  method: string,
  body?: Record<string, unknown>,
): NextRequest {
  const init: RequestInit = {
    method,
    headers: { "Content-Type": "application/json" },
  };
  if (body) init.body = JSON.stringify(body);
  return new NextRequest(
    `http://localhost/api/real-estate/viewings/${TEST_VIEWING_ID}`,
    init,
  );
}

function routeParams(id: string = TEST_VIEWING_ID) {
  return { params: Promise.resolve({ id }) };
}

// ─── Test Suites ───

describe("Real Estate Viewings API", () => {
  beforeEach(() => {
    jest.clearAllMocks();
    // Reset tx mocks
    txMock.propertyViewing.findFirst.mockReset();
    txMock.propertyViewing.create.mockReset();
    txMock.propertyViewing.update.mockReset();
    // Reset $transaction to default behavior (call through)
    mockPrisma.$transaction.mockImplementation(
      (cb: (tx: typeof txMock) => Promise<unknown>) => cb(txMock),
    );
  });

  // ═══════════════════════════════════════════════════════
  // GET /api/real-estate/viewings
  // ═══════════════════════════════════════════════════════
  describe("GET /api/real-estate/viewings", () => {
    it("returns 401 when user is not authenticated", async () => {
      setupAuth(null);
      const req = makeGetRequest();
      const res = await listViewings(req);
      expect(res.status).toBe(401);
      const json = await res.json();
      expect(json.error).toBe("Unauthorized");
    });

    it("returns 404 when user record not found", async () => {
      mockGetAuthenticatedEmail.mockResolvedValue(TEST_USER_EMAIL);
      mockPrisma.users.findUnique.mockResolvedValue(null);
      const req = makeGetRequest();
      const res = await listViewings(req);
      expect(res.status).toBe(404);
    });

    it("returns paginated viewings list for authenticated user", async () => {
      setupAuth();
      mockPrisma.propertyViewing.count.mockResolvedValue(1);
      mockPrisma.propertyViewing.findMany.mockResolvedValue([sampleViewing]);

      const req = makeGetRequest();
      const res = await listViewings(req);
      expect(res.status).toBe(200);
      const json = await res.json();
      expect(json.success).toBe(true);
      expect(json.data).toHaveLength(1);
      expect(json.pagination).toBeDefined();
      expect(json.pagination.totalCount).toBe(1);
    });

    it("filters viewings by status query param", async () => {
      setupAuth();
      mockPrisma.propertyViewing.count.mockResolvedValue(0);
      mockPrisma.propertyViewing.findMany.mockResolvedValue([]);

      const req = makeGetRequest({ status: "CONFIRMED" });
      await listViewings(req);

      expect(mockPrisma.propertyViewing.findMany).toHaveBeenCalledWith(
        expect.objectContaining({
          where: expect.objectContaining({ status: "CONFIRMED" }),
        }),
      );
    });

    it("filters viewings by brokerId query param", async () => {
      setupAuth();
      mockPrisma.propertyViewing.count.mockResolvedValue(0);
      mockPrisma.propertyViewing.findMany.mockResolvedValue([]);

      const req = makeGetRequest({ brokerId: TEST_BROKER_ID });
      await listViewings(req);

      expect(mockPrisma.propertyViewing.findMany).toHaveBeenCalledWith(
        expect.objectContaining({
          where: expect.objectContaining({ brokerId: TEST_BROKER_ID }),
        }),
      );
    });

    it("filters viewings by propertyId query param", async () => {
      setupAuth();
      mockPrisma.propertyViewing.count.mockResolvedValue(0);
      mockPrisma.propertyViewing.findMany.mockResolvedValue([]);

      const req = makeGetRequest({ propertyId: TEST_PROPERTY_ID });
      await listViewings(req);

      expect(mockPrisma.propertyViewing.findMany).toHaveBeenCalledWith(
        expect.objectContaining({
          where: expect.objectContaining({ propertyId: TEST_PROPERTY_ID }),
        }),
      );
    });

    it("filters viewings by date range (dateFrom and dateTo)", async () => {
      setupAuth();
      mockPrisma.propertyViewing.count.mockResolvedValue(0);
      mockPrisma.propertyViewing.findMany.mockResolvedValue([]);

      const req = makeGetRequest({
        dateFrom: "2026-03-01",
        dateTo: "2026-03-31",
      });
      await listViewings(req);

      const call = mockPrisma.propertyViewing.findMany.mock.calls[0][0];
      expect(call.where.viewingDate).toBeDefined();
      expect(call.where.viewingDate.gte).toEqual(new Date("2026-03-01"));
      expect(call.where.viewingDate.lte).toEqual(new Date("2026-03-31"));
    });

    it("does not add status filter when status is 'all'", async () => {
      setupAuth();
      mockPrisma.propertyViewing.count.mockResolvedValue(0);
      mockPrisma.propertyViewing.findMany.mockResolvedValue([]);

      const req = makeGetRequest({ status: "all" });
      await listViewings(req);

      const call = mockPrisma.propertyViewing.findMany.mock.calls[0][0];
      expect(call.where.status).toBeUndefined();
    });

    it("paginates with custom page and limit", async () => {
      setupAuth();
      mockPrisma.propertyViewing.count.mockResolvedValue(100);
      mockPrisma.propertyViewing.findMany.mockResolvedValue([]);

      const req = makeGetRequest({ page: "3", limit: "10" });
      await listViewings(req);

      expect(mockPrisma.propertyViewing.findMany).toHaveBeenCalledWith(
        expect.objectContaining({
          skip: 20, // (3-1) * 10
          take: 10,
        }),
      );
    });

    it("returns 500 on unexpected error", async () => {
      setupAuth();
      mockPrisma.propertyViewing.count.mockRejectedValue(
        new Error("DB connection lost"),
      );

      const req = makeGetRequest();
      const res = await listViewings(req);
      expect(res.status).toBe(500);
    });
  });

  // ═══════════════════════════════════════════════════════
  // POST /api/real-estate/viewings
  // ═══════════════════════════════════════════════════════
  describe("POST /api/real-estate/viewings", () => {
    const validBody = {
      propertyId: TEST_PROPERTY_ID,
      brokerId: TEST_BROKER_ID,
      clientName: "Mohammed Al-Thani",
      clientPhone: "+97455512345",
      clientEmail: "client@example.com",
      viewingDate: FUTURE_DATE_STR,
      viewingTime: "14:00",
      duration: 30,
    };

    function setupCreateMocks() {
      setupAuth();
      mockPrisma.property.findFirst.mockResolvedValue({
        id: TEST_PROPERTY_ID,
        titleEn: "Luxury Villa",
        status: "AVAILABLE",
      });
      mockPrisma.organizations.findUnique.mockResolvedValue({
        id: TEST_ORG_ID,
      });
      mockPrisma.broker.findFirst.mockResolvedValue({
        id: TEST_BROKER_ID,
      });
      // No conflicts by default
      txMock.propertyViewing.findFirst.mockResolvedValue(null);
      txMock.propertyViewing.create.mockResolvedValue(sampleViewing);
    }

    it("returns 401 when user is not authenticated", async () => {
      setupAuth(null);
      const req = makePostRequest(validBody);
      const res = await createViewing(req);
      expect(res.status).toBe(401);
    });

    it("returns 404 when user record not found", async () => {
      mockGetAuthenticatedEmail.mockResolvedValue(TEST_USER_EMAIL);
      mockPrisma.users.findUnique.mockResolvedValue(null);

      const req = makePostRequest(validBody);
      const res = await createViewing(req);
      expect(res.status).toBe(404);
    });

    it("returns 400 when propertyId is missing", async () => {
      setupAuth();
      const body = { ...validBody, propertyId: undefined };
      const req = makePostRequest(body);
      const res = await createViewing(req);
      expect(res.status).toBe(400);
      const json = await res.json();
      expect(json.errors).toContain("Property ID is required");
    });

    it("returns 400 when clientName is missing or too short", async () => {
      setupAuth();
      const body = { ...validBody, clientName: "A" };
      const req = makePostRequest(body);
      const res = await createViewing(req);
      expect(res.status).toBe(400);
      const json = await res.json();
      expect(json.errors).toEqual(
        expect.arrayContaining([expect.stringContaining("Client name")]),
      );
    });

    it("returns 400 when clientPhone is missing or too short", async () => {
      setupAuth();
      const body = { ...validBody, clientPhone: "123" };
      const req = makePostRequest(body);
      const res = await createViewing(req);
      expect(res.status).toBe(400);
      const json = await res.json();
      expect(json.errors).toEqual(
        expect.arrayContaining([expect.stringContaining("Client phone")]),
      );
    });

    it("returns 400 when viewingDate is missing", async () => {
      setupAuth();
      const body = { ...validBody, viewingDate: undefined };
      const req = makePostRequest(body);
      const res = await createViewing(req);
      expect(res.status).toBe(400);
      const json = await res.json();
      expect(json.errors).toContain("Viewing date is required");
    });

    it("returns 400 when viewingTime is missing", async () => {
      setupAuth();
      const body = { ...validBody, viewingTime: undefined };
      const req = makePostRequest(body);
      const res = await createViewing(req);
      expect(res.status).toBe(400);
      const json = await res.json();
      expect(json.errors).toContain("Viewing time is required");
    });

    it("returns 400 when viewingTime format is invalid", async () => {
      setupAuth();
      const body = { ...validBody, viewingTime: "25:99" };
      const req = makePostRequest(body);
      const res = await createViewing(req);
      expect(res.status).toBe(400);
      const json = await res.json();
      expect(json.errors).toEqual(
        expect.arrayContaining([
          expect.stringContaining("Invalid time format"),
        ]),
      );
    });

    it("returns 400 when viewingDate is in the past", async () => {
      setupAuth();
      const body = { ...validBody, viewingDate: "2020-01-01" };
      const req = makePostRequest(body);
      const res = await createViewing(req);
      expect(res.status).toBe(400);
      const json = await res.json();
      expect(json.errors).toEqual(
        expect.arrayContaining([expect.stringContaining("past")]),
      );
    });

    it("returns 400 when clientEmail is invalid format", async () => {
      setupAuth();
      const body = { ...validBody, clientEmail: "not-an-email" };
      const req = makePostRequest(body);
      const res = await createViewing(req);
      expect(res.status).toBe(400);
      const json = await res.json();
      expect(json.errors).toEqual(
        expect.arrayContaining([expect.stringContaining("Invalid email")]),
      );
    });

    it("returns 400 when duration is out of range", async () => {
      setupAuth();
      const body = { ...validBody, duration: 5 };
      const req = makePostRequest(body);
      const res = await createViewing(req);
      expect(res.status).toBe(400);
      const json = await res.json();
      expect(json.errors).toEqual(
        expect.arrayContaining([
          expect.stringContaining("Duration must be between"),
        ]),
      );
    });

    it("returns 400 when OPEN_HOUSE is missing maxAttendees", async () => {
      setupAuth();
      const body = {
        ...validBody,
        viewingType: "OPEN_HOUSE",
        maxAttendees: undefined,
      };
      const req = makePostRequest(body);
      const res = await createViewing(req);
      expect(res.status).toBe(400);
      const json = await res.json();
      expect(json.errors).toEqual(
        expect.arrayContaining([expect.stringContaining("Max attendees")]),
      );
    });

    it("returns 400 when VIRTUAL is missing meetingLink", async () => {
      setupAuth();
      const body = {
        ...validBody,
        viewingType: "VIRTUAL",
        meetingLink: undefined,
      };
      const req = makePostRequest(body);
      const res = await createViewing(req);
      expect(res.status).toBe(400);
      const json = await res.json();
      expect(json.errors).toEqual(
        expect.arrayContaining([expect.stringContaining("Meeting link")]),
      );
    });

    it("returns 404 when property not found", async () => {
      setupAuth();
      mockPrisma.property.findFirst.mockResolvedValue(null);

      const req = makePostRequest(validBody);
      const res = await createViewing(req);
      expect(res.status).toBe(404);
      const json = await res.json();
      expect(json.error).toBe("Property not found");
    });

    it("returns 409 when property status is SOLD (not AVAILABLE/UNDER_OFFER)", async () => {
      setupAuth();
      mockPrisma.property.findFirst.mockResolvedValue({
        id: TEST_PROPERTY_ID,
        titleEn: "Sold Villa",
        status: "SOLD",
      });

      const req = makePostRequest(validBody);
      const res = await createViewing(req);
      expect(res.status).toBe(409);
      const json = await res.json();
      expect(json.errorCode).toBe("PROPERTY_UNAVAILABLE");
    });

    it("allows viewings for UNDER_OFFER properties", async () => {
      setupCreateMocks();
      mockPrisma.property.findFirst.mockResolvedValue({
        id: TEST_PROPERTY_ID,
        titleEn: "Under Offer Villa",
        status: "UNDER_OFFER",
      });

      const req = makePostRequest(validBody);
      const res = await createViewing(req);
      expect(res.status).toBe(201);
    });

    it("returns 404 when broker not found or inactive", async () => {
      setupAuth();
      mockPrisma.property.findFirst.mockResolvedValue({
        id: TEST_PROPERTY_ID,
        titleEn: "Luxury Villa",
        status: "AVAILABLE",
      });
      mockPrisma.organizations.findUnique.mockResolvedValue({
        id: TEST_ORG_ID,
      });
      mockPrisma.broker.findFirst.mockResolvedValue(null);

      const req = makePostRequest(validBody);
      const res = await createViewing(req);
      expect(res.status).toBe(404);
      const json = await res.json();
      expect(json.error).toBe("Broker not found or inactive");
    });

    it("creates viewing successfully with correct data", async () => {
      setupCreateMocks();

      const req = makePostRequest(validBody);
      const res = await createViewing(req);
      expect(res.status).toBe(201);
      const json = await res.json();
      expect(json.success).toBe(true);
      expect(json.viewing).toBeDefined();
      expect(json.autoAssignedBroker).toBe(false);
    });

    it("creates viewing with correct data fields passed to prisma", async () => {
      setupCreateMocks();

      const req = makePostRequest(validBody);
      await createViewing(req);

      expect(txMock.propertyViewing.create).toHaveBeenCalledWith(
        expect.objectContaining({
          data: expect.objectContaining({
            userId: TEST_USER_ID,
            propertyId: TEST_PROPERTY_ID,
            brokerId: TEST_BROKER_ID,
            clientName: "Mohammed Al-Thani",
            clientPhone: "+97455512345",
            clientEmail: "client@example.com",
            viewingTime: "14:00",
            duration: 30,
            status: "REQUESTED",
            viewingType: "INDIVIDUAL",
          }),
        }),
      );
    });

    it("detects property double-booking (same property + time) and returns 409", async () => {
      setupCreateMocks();
      // First call: broker conflict check returns null
      // Second call: property conflict check returns existing viewing
      txMock.propertyViewing.findFirst
        .mockResolvedValueOnce(null) // broker check passes
        .mockResolvedValueOnce({ id: "existing-viewing" }); // property conflict

      const req = makePostRequest(validBody);
      const res = await createViewing(req);
      expect(res.status).toBe(409);
      const json = await res.json();
      expect(json.errorCode).toBe("PROPERTY_CONFLICT");
      expect(json.error).toContain("already has a viewing");
    });

    it("detects broker conflict (same broker + time) and returns 409", async () => {
      setupCreateMocks();
      // Broker conflict found on first check
      txMock.propertyViewing.findFirst.mockResolvedValueOnce({
        id: "broker-busy-viewing",
      });

      const req = makePostRequest(validBody);
      const res = await createViewing(req);
      expect(res.status).toBe(409);
      const json = await res.json();
      expect(json.errorCode).toBe("BROKER_CONFLICT");
      expect(json.error).toContain("broker already has a viewing");
    });

    it("auto-assigns broker when brokerId is not provided", async () => {
      setupAuth();
      mockPrisma.property.findFirst.mockResolvedValue({
        id: TEST_PROPERTY_ID,
        titleEn: "Luxury Villa",
        status: "AVAILABLE",
      });
      mockPrisma.organizations.findUnique.mockResolvedValue({
        id: TEST_ORG_ID,
      });

      // Return available brokers for auto-assignment
      const dayOfWeek = futureDate.getDay();
      mockPrisma.broker.findMany.mockResolvedValue([
        {
          id: "broker-auto-1",
          workingDays: [dayOfWeek],
          workStartTime: "08:00",
          workEndTime: "18:00",
          maxViewingsPerDay: 10,
          _count: { viewings: 2 },
        },
        {
          id: "broker-auto-2",
          workingDays: [dayOfWeek],
          workStartTime: "08:00",
          workEndTime: "18:00",
          maxViewingsPerDay: 10,
          _count: { viewings: 5 },
        },
      ]);

      // Verify the auto-assigned broker exists
      mockPrisma.broker.findFirst.mockResolvedValue({ id: "broker-auto-1" });

      // No conflicts in transaction
      txMock.propertyViewing.findFirst.mockResolvedValue(null);
      txMock.propertyViewing.create.mockResolvedValue({
        ...sampleViewing,
        brokerId: "broker-auto-1",
      });

      const body = { ...validBody, brokerId: undefined };
      const req = makePostRequest(body);
      const res = await createViewing(req);
      expect(res.status).toBe(201);
      const json = await res.json();
      expect(json.success).toBe(true);
      expect(json.autoAssignedBroker).toBe(true);
    });

    it("picks broker with fewest viewings for auto-assignment", async () => {
      setupAuth();
      mockPrisma.property.findFirst.mockResolvedValue({
        id: TEST_PROPERTY_ID,
        titleEn: "Luxury Villa",
        status: "AVAILABLE",
      });
      mockPrisma.organizations.findUnique.mockResolvedValue({
        id: TEST_ORG_ID,
      });

      const dayOfWeek = futureDate.getDay();
      mockPrisma.broker.findMany.mockResolvedValue([
        {
          id: "busy-broker",
          workingDays: [dayOfWeek],
          workStartTime: "08:00",
          workEndTime: "18:00",
          maxViewingsPerDay: 10,
          _count: { viewings: 8 },
        },
        {
          id: "free-broker",
          workingDays: [dayOfWeek],
          workStartTime: "08:00",
          workEndTime: "18:00",
          maxViewingsPerDay: 10,
          _count: { viewings: 1 },
        },
      ]);
      mockPrisma.broker.findFirst.mockResolvedValue({ id: "free-broker" });
      txMock.propertyViewing.findFirst.mockResolvedValue(null);
      txMock.propertyViewing.create.mockResolvedValue({
        ...sampleViewing,
        brokerId: "free-broker",
      });

      const body = { ...validBody, brokerId: undefined };
      const req = makePostRequest(body);
      await createViewing(req);

      // Verify broker.findFirst was called to verify the free-broker
      expect(mockPrisma.broker.findFirst).toHaveBeenCalledWith(
        expect.objectContaining({
          where: expect.objectContaining({ id: "free-broker" }),
        }),
      );
    });

    it("creates viewing without broker when none are available for auto-assignment", async () => {
      setupAuth();
      mockPrisma.property.findFirst.mockResolvedValue({
        id: TEST_PROPERTY_ID,
        titleEn: "Luxury Villa",
        status: "AVAILABLE",
      });
      mockPrisma.organizations.findUnique.mockResolvedValue({
        id: TEST_ORG_ID,
      });

      // No available brokers
      mockPrisma.broker.findMany.mockResolvedValue([]);

      // No conflicts in transaction
      txMock.propertyViewing.findFirst.mockResolvedValue(null);
      txMock.propertyViewing.create.mockResolvedValue({
        ...sampleViewing,
        brokerId: null,
      });

      const body = { ...validBody, brokerId: undefined };
      const req = makePostRequest(body);
      const res = await createViewing(req);
      expect(res.status).toBe(201);
      const json = await res.json();
      expect(json.success).toBe(true);
      // autoAssignedBroker is false because no broker was assigned
      expect(json.autoAssignedBroker).toBe(false);
    });

    it("returns 409 when OPEN_HOUSE is full", async () => {
      setupAuth();
      mockPrisma.property.findFirst.mockResolvedValue({
        id: TEST_PROPERTY_ID,
        titleEn: "Luxury Villa",
        status: "AVAILABLE",
      });
      mockPrisma.organizations.findUnique.mockResolvedValue({
        id: TEST_ORG_ID,
      });
      mockPrisma.broker.findFirst.mockResolvedValue({
        id: TEST_BROKER_ID,
      });

      // Simulate OPEN_HOUSE capacity exceeded
      // For OPEN_HOUSE: no broker conflict, then the open house findFirst returns a full event
      txMock.propertyViewing.findFirst
        .mockResolvedValueOnce(null) // broker conflict check
        .mockResolvedValueOnce({
          // existing open house at capacity
          id: "open-house-master",
          maxAttendees: 20,
          currentAttendees: 20,
        });

      const body = {
        ...validBody,
        viewingType: "OPEN_HOUSE",
        maxAttendees: 20,
      };
      const req = makePostRequest(body);
      const res = await createViewing(req);
      expect(res.status).toBe(409);
      const json = await res.json();
      expect(json.errorCode).toBe("OPEN_HOUSE_FULL");
    });

    it("returns 500 on unexpected transaction error", async () => {
      setupCreateMocks();
      mockPrisma.$transaction.mockRejectedValue(
        new Error("Connection timeout"),
      );

      const req = makePostRequest(validBody);
      const res = await createViewing(req);
      expect(res.status).toBe(500);
    });

    it("returns multiple validation errors at once", async () => {
      setupAuth();
      const body = {
        // missing propertyId, clientName, clientPhone, viewingDate, viewingTime
      };
      const req = makePostRequest(body);
      const res = await createViewing(req);
      expect(res.status).toBe(400);
      const json = await res.json();
      expect(json.errors.length).toBeGreaterThanOrEqual(4);
    });
  });

  // ═══════════════════════════════════════════════════════
  // GET /api/real-estate/viewings/[id]
  // ═══════════════════════════════════════════════════════
  describe("GET /api/real-estate/viewings/[id]", () => {
    it("returns 401 when user is not authenticated", async () => {
      setupAuth(null);
      const req = makeDetailRequest("GET");
      const res = await getViewing(req, routeParams());
      expect(res.status).toBe(401);
    });

    it("returns 404 when user record not found", async () => {
      mockGetAuthenticatedEmail.mockResolvedValue(TEST_USER_EMAIL);
      mockPrisma.users.findUnique.mockResolvedValue(null);

      const req = makeDetailRequest("GET");
      const res = await getViewing(req, routeParams());
      expect(res.status).toBe(404);
    });

    it("returns viewing detail for authenticated user", async () => {
      setupAuth();
      const viewingWithDetails = {
        ...sampleViewing,
        feedback: null,
        tour: null,
      };
      mockPrisma.propertyViewing.findFirst.mockResolvedValue(
        viewingWithDetails,
      );

      const req = makeDetailRequest("GET");
      const res = await getViewing(req, routeParams());
      expect(res.status).toBe(200);
      const json = await res.json();
      expect(json.success).toBe(true);
      expect(json.viewing.id).toBe(TEST_VIEWING_ID);
      expect(json.viewing.property).toBeDefined();
      expect(json.viewing.broker).toBeDefined();
    });

    it("returns 404 when viewing not found (wrong org)", async () => {
      setupAuth();
      mockPrisma.propertyViewing.findFirst.mockResolvedValue(null);

      const req = makeDetailRequest("GET");
      const res = await getViewing(req, routeParams());
      expect(res.status).toBe(404);
      const json = await res.json();
      expect(json.error).toBe("Viewing not found");
    });

    it("returns 404 for non-existent viewing ID", async () => {
      setupAuth();
      mockPrisma.propertyViewing.findFirst.mockResolvedValue(null);

      const req = makeDetailRequest("GET");
      const res = await getViewing(req, routeParams("non-existent-id"));
      expect(res.status).toBe(404);
    });

    it("returns 500 on database error", async () => {
      setupAuth();
      mockPrisma.propertyViewing.findFirst.mockRejectedValue(
        new Error("DB error"),
      );

      const req = makeDetailRequest("GET");
      const res = await getViewing(req, routeParams());
      expect(res.status).toBe(500);
    });
  });

  // ═══════════════════════════════════════════════════════
  // PUT /api/real-estate/viewings/[id]
  // ═══════════════════════════════════════════════════════
  describe("PUT /api/real-estate/viewings/[id]", () => {
    const existingViewing = {
      id: TEST_VIEWING_ID,
      status: "REQUESTED",
      brokerId: TEST_BROKER_ID,
      propertyId: TEST_PROPERTY_ID,
      viewingDate: futureDate,
      viewingTime: "14:00",
      viewingType: "INDIVIDUAL",
    };

    function setupUpdateMocks() {
      setupAuth();
      mockPrisma.propertyViewing.findFirst.mockResolvedValue(existingViewing);
      txMock.propertyViewing.findFirst.mockResolvedValue(null); // No conflicts
      txMock.propertyViewing.update.mockResolvedValue({
        ...sampleViewing,
        status: "CONFIRMED",
      });
    }

    it("returns 401 when user is not authenticated", async () => {
      setupAuth(null);
      const req = makeDetailRequest("PUT", { status: "CONFIRMED" });
      const res = await updateViewing(req, routeParams());
      expect(res.status).toBe(401);
    });

    it("returns 404 when user record not found", async () => {
      mockGetAuthenticatedEmail.mockResolvedValue(TEST_USER_EMAIL);
      mockPrisma.users.findUnique.mockResolvedValue(null);

      const req = makeDetailRequest("PUT", { status: "CONFIRMED" });
      const res = await updateViewing(req, routeParams());
      expect(res.status).toBe(404);
    });

    it("returns 404 when viewing not found", async () => {
      setupAuth();
      mockPrisma.propertyViewing.findFirst.mockResolvedValue(null);

      const req = makeDetailRequest("PUT", { status: "CONFIRMED" });
      const res = await updateViewing(req, routeParams());
      expect(res.status).toBe(404);
      const json = await res.json();
      expect(json.error).toBe("Viewing not found");
    });

    it("updates viewing status successfully", async () => {
      setupUpdateMocks();

      const req = makeDetailRequest("PUT", { status: "CONFIRMED" });
      const res = await updateViewing(req, routeParams());
      expect(res.status).toBe(200);
      const json = await res.json();
      expect(json.success).toBe(true);
      expect(json.viewing).toBeDefined();
    });

    it("marks reschedule when viewingDate changes", async () => {
      setupUpdateMocks();
      const newDate = new Date();
      newDate.setDate(newDate.getDate() + 14);
      const newDateStr = newDate.toISOString().split("T")[0];

      txMock.propertyViewing.update.mockResolvedValue({
        ...sampleViewing,
        status: "RESCHEDULED",
        viewingDate: newDate,
      });

      const req = makeDetailRequest("PUT", { viewingDate: newDateStr });
      const res = await updateViewing(req, routeParams());
      expect(res.status).toBe(200);
      const json = await res.json();
      expect(json.rescheduled).toBe(true);
    });

    it("marks reschedule when viewingTime changes", async () => {
      setupUpdateMocks();
      txMock.propertyViewing.update.mockResolvedValue({
        ...sampleViewing,
        status: "RESCHEDULED",
        viewingTime: "16:00",
      });

      const req = makeDetailRequest("PUT", { viewingTime: "16:00" });
      const res = await updateViewing(req, routeParams());
      expect(res.status).toBe(200);
      const json = await res.json();
      expect(json.rescheduled).toBe(true);
    });

    it("sets status to RESCHEDULED automatically when date/time changes without explicit status", async () => {
      setupUpdateMocks();
      const newDate = new Date();
      newDate.setDate(newDate.getDate() + 14);
      const newDateStr = newDate.toISOString().split("T")[0];

      txMock.propertyViewing.update.mockResolvedValue({
        ...sampleViewing,
        status: "RESCHEDULED",
      });

      const req = makeDetailRequest("PUT", { viewingDate: newDateStr });
      await updateViewing(req, routeParams());

      // Verify updateData includes RESCHEDULED status
      expect(txMock.propertyViewing.update).toHaveBeenCalledWith(
        expect.objectContaining({
          data: expect.objectContaining({ status: "RESCHEDULED" }),
        }),
      );
    });

    it("returns 400 for invalid status value", async () => {
      setupAuth();
      mockPrisma.propertyViewing.findFirst.mockResolvedValue(existingViewing);

      const req = makeDetailRequest("PUT", { status: "INVALID_STATUS" });
      const res = await updateViewing(req, routeParams());
      expect(res.status).toBe(400);
      const json = await res.json();
      expect(json.errors).toEqual(
        expect.arrayContaining([expect.stringContaining("Invalid status")]),
      );
    });

    it("returns 400 for invalid time format on update", async () => {
      setupAuth();
      mockPrisma.propertyViewing.findFirst.mockResolvedValue(existingViewing);

      const req = makeDetailRequest("PUT", { viewingTime: "99:00" });
      const res = await updateViewing(req, routeParams());
      expect(res.status).toBe(400);
      const json = await res.json();
      expect(json.errors).toEqual(
        expect.arrayContaining([
          expect.stringContaining("Invalid time format"),
        ]),
      );
    });

    it("returns 400 for past date on reschedule", async () => {
      setupAuth();
      mockPrisma.propertyViewing.findFirst.mockResolvedValue(existingViewing);

      const req = makeDetailRequest("PUT", { viewingDate: "2020-01-01" });
      const res = await updateViewing(req, routeParams());
      expect(res.status).toBe(400);
      const json = await res.json();
      expect(json.errors).toEqual(
        expect.arrayContaining([expect.stringContaining("past")]),
      );
    });

    it("returns 400 for invalid duration on update", async () => {
      setupAuth();
      mockPrisma.propertyViewing.findFirst.mockResolvedValue(existingViewing);

      const req = makeDetailRequest("PUT", { duration: 300 });
      const res = await updateViewing(req, routeParams());
      expect(res.status).toBe(400);
    });

    it("detects broker conflict on reschedule and returns 409", async () => {
      setupUpdateMocks();
      // Broker conflict during reschedule transaction
      txMock.propertyViewing.findFirst.mockResolvedValueOnce({
        id: "conflicting-viewing",
      });

      const newDate = new Date();
      newDate.setDate(newDate.getDate() + 14);
      const newDateStr = newDate.toISOString().split("T")[0];

      const req = makeDetailRequest("PUT", { viewingDate: newDateStr });
      const res = await updateViewing(req, routeParams());
      expect(res.status).toBe(409);
      const json = await res.json();
      expect(json.errorCode).toBe("BROKER_CONFLICT");
    });

    it("detects property conflict on reschedule and returns 409", async () => {
      setupUpdateMocks();
      // First findFirst: broker check passes, second: property conflict
      txMock.propertyViewing.findFirst
        .mockResolvedValueOnce(null) // broker ok
        .mockResolvedValueOnce({ id: "conflicting-property-viewing" }); // property conflict

      const newDate = new Date();
      newDate.setDate(newDate.getDate() + 14);
      const newDateStr = newDate.toISOString().split("T")[0];

      const req = makeDetailRequest("PUT", { viewingDate: newDateStr });
      const res = await updateViewing(req, routeParams());
      expect(res.status).toBe(409);
      const json = await res.json();
      expect(json.errorCode).toBe("PROPERTY_CONFLICT");
    });

    it("updates notes and other fields without triggering reschedule", async () => {
      setupUpdateMocks();
      txMock.propertyViewing.update.mockResolvedValue({
        ...sampleViewing,
        clientNotes: "Bring keys",
      });

      const req = makeDetailRequest("PUT", {
        clientNotes: "Bring keys",
        accessConfirmed: true,
      });
      const res = await updateViewing(req, routeParams());
      expect(res.status).toBe(200);
      const json = await res.json();
      expect(json.rescheduled).toBeFalsy();
    });

    it("returns 500 on unexpected error during update", async () => {
      setupAuth();
      mockPrisma.propertyViewing.findFirst.mockResolvedValue(existingViewing);
      mockPrisma.$transaction.mockRejectedValue(
        new Error("Unexpected DB failure"),
      );

      const req = makeDetailRequest("PUT", { status: "CONFIRMED" });
      const res = await updateViewing(req, routeParams());
      expect(res.status).toBe(500);
    });
  });

  // ═══════════════════════════════════════════════════════
  // DELETE /api/real-estate/viewings/[id]
  // ═══════════════════════════════════════════════════════
  describe("DELETE /api/real-estate/viewings/[id]", () => {
    function setupDeleteMocks(overrides: Partial<typeof sampleViewing> = {}) {
      setupAuth();
      mockPrisma.propertyViewing.findFirst.mockResolvedValue({
        id: TEST_VIEWING_ID,
        status: "REQUESTED",
        viewingType: "INDIVIDUAL",
        propertyId: TEST_PROPERTY_ID,
        viewingDate: futureDate,
        viewingTime: "14:00",
        ...overrides,
      });
      txMock.propertyViewing.findFirst.mockResolvedValue(null);
      txMock.propertyViewing.update.mockResolvedValue({
        id: TEST_VIEWING_ID,
        status: "CANCELLED",
        viewingDate: futureDate,
        viewingTime: "14:00",
        viewingType: "INDIVIDUAL",
        propertyId: TEST_PROPERTY_ID,
        brokerId: TEST_BROKER_ID,
        ...overrides,
      });
    }

    it("returns 401 when user is not authenticated", async () => {
      setupAuth(null);
      const req = makeDetailRequest("DELETE");
      const res = await deleteViewing(req, routeParams());
      expect(res.status).toBe(401);
    });

    it("returns 404 when user record not found", async () => {
      mockGetAuthenticatedEmail.mockResolvedValue(TEST_USER_EMAIL);
      mockPrisma.users.findUnique.mockResolvedValue(null);

      const req = makeDetailRequest("DELETE");
      const res = await deleteViewing(req, routeParams());
      expect(res.status).toBe(404);
    });

    it("returns 404 when viewing not found", async () => {
      setupAuth();
      mockPrisma.propertyViewing.findFirst.mockResolvedValue(null);

      const req = makeDetailRequest("DELETE");
      const res = await deleteViewing(req, routeParams());
      expect(res.status).toBe(404);
      const json = await res.json();
      expect(json.error).toBe("Viewing not found");
    });

    it("cancels a viewing successfully (soft delete)", async () => {
      setupDeleteMocks();

      const req = makeDetailRequest("DELETE");
      const res = await deleteViewing(req, routeParams());
      expect(res.status).toBe(200);
      const json = await res.json();
      expect(json.success).toBe(true);
      expect(json.viewing.status).toBe("CANCELLED");
    });

    it("sets status to CANCELLED via transaction update", async () => {
      setupDeleteMocks();

      const req = makeDetailRequest("DELETE");
      await deleteViewing(req, routeParams());

      expect(txMock.propertyViewing.update).toHaveBeenCalledWith(
        expect.objectContaining({
          data: expect.objectContaining({ status: "CANCELLED" }),
        }),
      );
    });

    it("returns 400 when viewing is already cancelled", async () => {
      setupAuth();
      mockPrisma.propertyViewing.findFirst.mockResolvedValue({
        id: TEST_VIEWING_ID,
        status: "CANCELLED",
        viewingType: "INDIVIDUAL",
        propertyId: TEST_PROPERTY_ID,
        viewingDate: futureDate,
        viewingTime: "14:00",
      });

      const req = makeDetailRequest("DELETE");
      const res = await deleteViewing(req, routeParams());
      expect(res.status).toBe(400);
      const json = await res.json();
      expect(json.error).toBe("Viewing is already cancelled");
    });

    it("returns 400 when trying to cancel a completed viewing", async () => {
      setupAuth();
      mockPrisma.propertyViewing.findFirst.mockResolvedValue({
        id: TEST_VIEWING_ID,
        status: "VIEW_COMPLETED",
        viewingType: "INDIVIDUAL",
        propertyId: TEST_PROPERTY_ID,
        viewingDate: futureDate,
        viewingTime: "14:00",
      });

      const req = makeDetailRequest("DELETE");
      const res = await deleteViewing(req, routeParams());
      expect(res.status).toBe(400);
      const json = await res.json();
      expect(json.error).toBe("Cannot cancel a completed viewing");
    });

    it("includes cancellation reason when provided in body", async () => {
      setupDeleteMocks();

      const req = makeDetailRequest("DELETE", {
        reason: "Client changed mind",
      });
      await deleteViewing(req, routeParams());

      expect(txMock.propertyViewing.update).toHaveBeenCalledWith(
        expect.objectContaining({
          data: expect.objectContaining({
            status: "CANCELLED",
            clientNotes: "[CANCELLED] Client changed mind",
          }),
        }),
      );
    });

    it("decrements OPEN_HOUSE currentAttendees on sibling when cancelling", async () => {
      setupDeleteMocks({ viewingType: "OPEN_HOUSE" });

      // Sibling open house exists with attendees
      txMock.propertyViewing.findFirst.mockResolvedValue({
        id: "sibling-open-house",
      });
      txMock.propertyViewing.update
        .mockResolvedValueOnce({}) // decrement update
        .mockResolvedValueOnce({
          // cancellation update
          id: TEST_VIEWING_ID,
          status: "CANCELLED",
          viewingType: "OPEN_HOUSE",
          viewingDate: futureDate,
          viewingTime: "14:00",
          propertyId: TEST_PROPERTY_ID,
          brokerId: TEST_BROKER_ID,
        });

      const req = makeDetailRequest("DELETE");
      const res = await deleteViewing(req, routeParams());
      expect(res.status).toBe(200);

      // First update call: decrement currentAttendees
      expect(txMock.propertyViewing.update).toHaveBeenCalledWith(
        expect.objectContaining({
          where: { id: "sibling-open-house" },
          data: { currentAttendees: { decrement: 1 } },
        }),
      );

      // Second update call: cancel the viewing itself
      expect(txMock.propertyViewing.update).toHaveBeenCalledWith(
        expect.objectContaining({
          where: { id: TEST_VIEWING_ID },
          data: expect.objectContaining({ status: "CANCELLED" }),
        }),
      );
    });

    it("does not decrement when no sibling open house found", async () => {
      setupDeleteMocks({ viewingType: "OPEN_HOUSE" });

      // No sibling found
      txMock.propertyViewing.findFirst.mockResolvedValue(null);
      txMock.propertyViewing.update.mockResolvedValue({
        id: TEST_VIEWING_ID,
        status: "CANCELLED",
        viewingType: "OPEN_HOUSE",
        viewingDate: futureDate,
        viewingTime: "14:00",
        propertyId: TEST_PROPERTY_ID,
        brokerId: TEST_BROKER_ID,
      });

      const req = makeDetailRequest("DELETE");
      await deleteViewing(req, routeParams());

      // Only one update call (cancellation), no decrement
      expect(txMock.propertyViewing.update).toHaveBeenCalledTimes(1);
    });

    it("handles missing body gracefully (no reason)", async () => {
      setupDeleteMocks();

      // Request with no body / empty body
      const req = new NextRequest(
        `http://localhost/api/real-estate/viewings/${TEST_VIEWING_ID}`,
        { method: "DELETE" },
      );
      const res = await deleteViewing(req, routeParams());
      expect(res.status).toBe(200);

      expect(txMock.propertyViewing.update).toHaveBeenCalledWith(
        expect.objectContaining({
          data: expect.objectContaining({
            status: "CANCELLED",
            clientNotes: undefined,
          }),
        }),
      );
    });

    it("returns 500 on unexpected transaction error", async () => {
      setupDeleteMocks();
      mockPrisma.$transaction.mockRejectedValue(new Error("TX failure"));

      const req = makeDetailRequest("DELETE");
      const res = await deleteViewing(req, routeParams());
      expect(res.status).toBe(500);
    });
  });
});
