/**
 * Comprehensive Jest Tests for Real Estate Viewings Extended API
 *
 * Tests the following endpoints:
 * - GET/POST /api/real-estate/viewings/tours         - Tour listing & creation
 * - GET      /api/real-estate/viewings/calendar       - Calendar view data
 * - GET      /api/real-estate/viewings/availability   - Available time slots
 * - GET/POST /api/real-estate/viewings/[id]/feedback  - Viewing feedback
 */

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: callback receives a `tx` object with the same shape as prisma
const txMock = {
  viewingTour: { create: jest.fn() },
  propertyViewing: { findFirst: jest.fn(), create: jest.fn() },
  propertyImage: { updateMany: jest.fn(), create: jest.fn() },
};

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

jest.mock("@/lib/rate-limiter", () => ({
  withRateLimit: (handler: Function) => handler,
  rateLimiters: { authGeneral: {} },
}));
jest.mock("@/lib/csrf", () => ({ withCSRF: (handler: Function) => handler }));
jest.mock("@/lib/logger", () => ({
  logger: {
    info: jest.fn(),
    error: jest.fn(),
    warn: jest.fn(),
    debug: jest.fn(),
  },
}));
jest.mock("@/lib/errors", () => ({
  getErrorMessage: jest.fn((e: Error) => e?.message || "Unknown"),
}));
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 }), {
          status: 200,
        }),
    ),
  },
  paginatedResponse: jest.fn(
    (data: unknown, page: number, limit: number, total: number) =>
      new Response(
        JSON.stringify({ success: true, data, page, limit, total }),
        { status: 200 },
      ),
  ),
}));
jest.mock("@/lib/config/constants", () => ({
  clampPaginationLimit: jest.fn((v: unknown) =>
    Math.min(50, Math.max(1, parseInt(String(v) || "20", 10) || 20)),
  ),
}));
jest.mock("@paralleldrive/cuid2", () => ({
  createId: jest.fn(() => "mock-cuid-id"),
}));
jest.mock("@/lib/services/real-estate/notification.service", () => ({
  sendPostViewingFollowUp: jest.fn(() => Promise.resolve()),
}));
jest.mock("validator", () => ({
  default: {
    isURL: jest.fn(() => true),
    escape: jest.fn((s: string) => s),
    trim: jest.fn((s: string) => s.trim()),
  },
  isURL: jest.fn(() => true),
  escape: jest.fn((s: string) => s),
  trim: jest.fn((s: string) => s.trim()),
}));

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

import { prisma } from "@/lib/db";
import { getAuthenticatedEmail } from "@/lib/utils/auth-helpers.util";
import { sendPostViewingFollowUp } from "@/lib/services/real-estate/notification.service";
import {
  GET as listTours,
  POST as createTour,
} from "@/app/api/real-estate/viewings/tours/route";
import { GET as getCalendar } from "@/app/api/real-estate/viewings/calendar/route";
import { GET as getAvailability } from "@/app/api/real-estate/viewings/availability/route";
import {
  GET as getFeedback,
  POST as postFeedback,
} from "@/app/api/real-estate/viewings/[id]/feedback/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;
    groupBy: jest.Mock;
    count: jest.Mock;
  };
  viewingFeedback: { findMany: jest.Mock; create: jest.Mock };
  viewingTour: { create: jest.Mock; count: jest.Mock; findMany: jest.Mock };
  broker: { findFirst: jest.Mock; findMany: jest.Mock };
  brokerAvailability: { findMany: jest.Mock };
  organizations: { findUnique: jest.Mock };
  $transaction: jest.Mock;
};

const mockSendPostViewingFollowUp =
  sendPostViewingFollowUp as jest.MockedFunction<
    typeof sendPostViewingFollowUp
  >;

// ─── 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 (7 days ahead) for tests that need valid future dates
const futureDate = new Date();
futureDate.setDate(futureDate.getDate() + 7);
const FUTURE_DATE_STR = futureDate.toISOString().split("T")[0];

// ─── Helpers ───

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

function makeFeedbackGetRequest(): NextRequest {
  return new NextRequest(
    `http://localhost/api/real-estate/viewings/${TEST_VIEWING_ID}/feedback`,
    { method: "GET" },
  );
}

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

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

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

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

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

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

// ─── Test Suites ───

describe("Real Estate Viewings Extended API", () => {
  beforeEach(() => {
    jest.clearAllMocks();
    // Reset tx mocks
    txMock.viewingTour.create.mockReset();
    txMock.propertyViewing.findFirst.mockReset();
    txMock.propertyViewing.create.mockReset();
    // Reset $transaction to default behavior
    mockPrisma.$transaction.mockImplementation((cb: Function) => cb(txMock));
  });

  // ═══════════════════════════════════════════════════════
  // GET /api/real-estate/viewings/tours
  // ═══════════════════════════════════════════════════════
  describe("GET /api/real-estate/viewings/tours", () => {
    it("returns 401 when user is not authenticated", async () => {
      setupAuth(null);
      const req = makeToursGetRequest();
      const res = await listTours(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 = makeToursGetRequest();
      const res = await listTours(req);
      expect(res.status).toBe(404);
    });

    it("returns paginated tours list for authenticated user", async () => {
      setupAuth();
      const tourData = [
        {
          id: "tour-001",
          userId: TEST_USER_ID,
          tourDate: futureDate,
          startTime: "10:00",
          estimatedEnd: "12:15",
          status: "scheduled",
          viewings: [
            {
              id: "v1",
              property: { id: "p1", titleEn: "Villa 1" },
              broker: { id: "b1", nameEn: "Ahmed" },
            },
            {
              id: "v2",
              property: { id: "p2", titleEn: "Villa 2" },
              broker: { id: "b1", nameEn: "Ahmed" },
            },
          ],
        },
      ];
      mockPrisma.viewingTour.count.mockResolvedValue(1);
      mockPrisma.viewingTour.findMany.mockResolvedValue(tourData);

      const req = makeToursGetRequest();
      const res = await listTours(req);
      expect(res.status).toBe(200);
      const json = await res.json();
      expect(json.success).toBe(true);
      expect(json.data).toHaveLength(1);
    });

    it("supports pagination with page and limit params", async () => {
      setupAuth();
      mockPrisma.viewingTour.count.mockResolvedValue(100);
      mockPrisma.viewingTour.findMany.mockResolvedValue([]);

      const req = makeToursGetRequest({ page: "2", limit: "10" });
      const res = await listTours(req);
      expect(res.status).toBe(200);

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

    it("returns empty list when user has no tours", async () => {
      setupAuth();
      mockPrisma.viewingTour.count.mockResolvedValue(0);
      mockPrisma.viewingTour.findMany.mockResolvedValue([]);

      const req = makeToursGetRequest();
      const res = await listTours(req);
      expect(res.status).toBe(200);
      const json = await res.json();
      expect(json.data).toEqual([]);
      expect(json.total).toBe(0);
    });

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

      const req = makeToursGetRequest();
      const res = await listTours(req);
      expect(res.status).toBe(500);
    });
  });

  // ═══════════════════════════════════════════════════════
  // POST /api/real-estate/viewings/tours
  // ═══════════════════════════════════════════════════════
  describe("POST /api/real-estate/viewings/tours", () => {
    const validTourBody = {
      clientName: "Fatima Al-Sulaiti",
      clientPhone: "+97455599999",
      clientEmail: "fatima@example.com",
      propertyIds: ["prop-001", "prop-002", "prop-003"],
      tourDate: FUTURE_DATE_STR,
      startTime: "10:00",
      brokerId: TEST_BROKER_ID,
      notes: "Client prefers modern style",
    };

    function setupTourCreateMocks() {
      setupAuth();
      mockPrisma.property.findMany.mockResolvedValue([
        { id: "prop-001", titleEn: "Villa 1", status: "AVAILABLE" },
        { id: "prop-002", titleEn: "Villa 2", status: "AVAILABLE" },
        { id: "prop-003", titleEn: "Villa 3", status: "AVAILABLE" },
      ]);
      mockPrisma.organizations.findUnique.mockResolvedValue({
        id: TEST_ORG_ID,
      });
      mockPrisma.broker.findFirst.mockResolvedValue({
        id: TEST_BROKER_ID,
      });
      // No conflicts in transaction
      txMock.propertyViewing.findFirst.mockResolvedValue(null);
      txMock.viewingTour.create.mockResolvedValue({
        id: "tour-new",
        userId: TEST_USER_ID,
        organizationId: TEST_ORG_ID,
        brokerId: TEST_BROKER_ID,
        clientName: "Fatima Al-Sulaiti",
        clientPhone: "+97455599999",
        tourDate: new Date(FUTURE_DATE_STR),
        startTime: "10:00",
        estimatedEnd: "12:00",
        status: "scheduled",
      });
      txMock.propertyViewing.create.mockImplementation(
        ({
          data,
          include,
        }: {
          data: Record<string, unknown>;
          include?: unknown;
        }) => {
          return Promise.resolve({
            id: `viewing-${data.propertyId}`,
            ...data,
            property: {
              id: data.propertyId,
              titleEn: `Property ${data.propertyId}`,
              titleAr: "عقار",
              referenceNumber: "RE-XXX",
            },
          });
        },
      );
    }

    it("returns 401 when user is not authenticated", async () => {
      setupAuth(null);
      const req = makeToursPostRequest(validTourBody);
      const res = await createTour(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 = makeToursPostRequest(validTourBody);
      const res = await createTour(req);
      expect(res.status).toBe(404);
    });

    it("returns 400 when clientName is missing or too short", async () => {
      setupAuth();
      const body = { ...validTourBody, clientName: "A" };
      const req = makeToursPostRequest(body);
      const res = await createTour(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 = { ...validTourBody, clientPhone: "12" };
      const req = makeToursPostRequest(body);
      const res = await createTour(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 propertyIds is empty", async () => {
      setupAuth();
      const body = { ...validTourBody, propertyIds: [] };
      const req = makeToursPostRequest(body);
      const res = await createTour(req);
      expect(res.status).toBe(400);
      const json = await res.json();
      expect(json.errors).toEqual(
        expect.arrayContaining([
          expect.stringContaining("At least one property ID"),
        ]),
      );
    });

    it("returns 400 when propertyIds is not an array", async () => {
      setupAuth();
      const body = { ...validTourBody, propertyIds: "not-an-array" };
      const req = makeToursPostRequest(body);
      const res = await createTour(req);
      expect(res.status).toBe(400);
      const json = await res.json();
      expect(json.errors).toEqual(
        expect.arrayContaining([expect.stringContaining("property ID")]),
      );
    });

    it("returns 400 when propertyIds exceeds 10", async () => {
      setupAuth();
      const body = {
        ...validTourBody,
        propertyIds: Array.from({ length: 11 }, (_, i) => `prop-${i}`),
      };
      const req = makeToursPostRequest(body);
      const res = await createTour(req);
      expect(res.status).toBe(400);
      const json = await res.json();
      expect(json.errors).toEqual(
        expect.arrayContaining([
          expect.stringContaining("maximum of 10 properties"),
        ]),
      );
    });

    it("returns 400 when tourDate is missing", async () => {
      setupAuth();
      const body = { ...validTourBody, tourDate: undefined };
      const req = makeToursPostRequest(body);
      const res = await createTour(req);
      expect(res.status).toBe(400);
      const json = await res.json();
      expect(json.errors).toEqual(
        expect.arrayContaining([
          expect.stringContaining("Tour date is required"),
        ]),
      );
    });

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

    it("returns 400 when startTime is missing", async () => {
      setupAuth();
      const body = { ...validTourBody, startTime: undefined };
      const req = makeToursPostRequest(body);
      const res = await createTour(req);
      expect(res.status).toBe(400);
      const json = await res.json();
      expect(json.errors).toEqual(
        expect.arrayContaining([
          expect.stringContaining("Start time is required"),
        ]),
      );
    });

    it("returns 400 when startTime has invalid format", async () => {
      setupAuth();
      const body = { ...validTourBody, startTime: "25:00" };
      const req = makeToursPostRequest(body);
      const res = await createTour(req);
      expect(res.status).toBe(400);
      const json = await res.json();
      expect(json.errors).toEqual(
        expect.arrayContaining([
          expect.stringContaining("Invalid start time format"),
        ]),
      );
    });

    it("returns multiple validation errors at once", async () => {
      setupAuth();
      const body = {
        clientName: "",
        clientPhone: "",
        propertyIds: [],
        tourDate: undefined,
        startTime: undefined,
      };
      const req = makeToursPostRequest(body);
      const res = await createTour(req);
      expect(res.status).toBe(400);
      const json = await res.json();
      expect(json.errors.length).toBeGreaterThanOrEqual(4);
    });

    it("returns 404 when some properties not found", async () => {
      setupTourCreateMocks();
      // Only 2 of 3 properties found
      mockPrisma.property.findMany.mockResolvedValue([
        { id: "prop-001", titleEn: "Villa 1", status: "AVAILABLE" },
        { id: "prop-002", titleEn: "Villa 2", status: "AVAILABLE" },
      ]);

      const req = makeToursPostRequest(validTourBody);
      const res = await createTour(req);
      expect(res.status).toBe(404);
      const json = await res.json();
      expect(json.errorCode).toBe("PROPERTIES_NOT_FOUND");
      expect(json.error).toContain("prop-003");
    });

    it("returns 404 when broker not found or inactive", async () => {
      setupAuth();
      mockPrisma.property.findMany.mockResolvedValue([
        { id: "prop-001", titleEn: "V1", status: "AVAILABLE" },
        { id: "prop-002", titleEn: "V2", status: "AVAILABLE" },
        { id: "prop-003", titleEn: "V3", status: "AVAILABLE" },
      ]);
      mockPrisma.organizations.findUnique.mockResolvedValue({
        id: TEST_ORG_ID,
      });
      mockPrisma.broker.findFirst.mockResolvedValue(null);

      const req = makeToursPostRequest(validTourBody);
      const res = await createTour(req);
      expect(res.status).toBe(404);
      const json = await res.json();
      expect(json.error).toBe("Broker not found or inactive");
    });

    it("creates tour with individual viewings for each property", async () => {
      setupTourCreateMocks();

      const req = makeToursPostRequest(validTourBody);
      const res = await createTour(req);
      expect(res.status).toBe(201);
      const json = await res.json();
      expect(json.success).toBe(true);
      expect(json.tour).toBeDefined();
      expect(json.tour.viewings).toHaveLength(3);

      // Verify individual viewings were created with correct times
      // 3 properties: 10:00, 10:45, 11:30
      const createCalls = txMock.propertyViewing.create.mock.calls;
      expect(createCalls).toHaveLength(3);

      expect(createCalls[0][0].data.viewingTime).toBe("10:00");
      expect(createCalls[0][0].data.propertyId).toBe("prop-001");
      expect(createCalls[0][0].data.viewingType).toBe("TOUR");
      expect(createCalls[0][0].data.duration).toBe(30);

      expect(createCalls[1][0].data.viewingTime).toBe("10:45");
      expect(createCalls[1][0].data.propertyId).toBe("prop-002");

      expect(createCalls[2][0].data.viewingTime).toBe("11:30");
      expect(createCalls[2][0].data.propertyId).toBe("prop-003");
    });

    it("calculates estimatedEnd time correctly (3 properties = 120 min)", async () => {
      setupTourCreateMocks();

      const req = makeToursPostRequest(validTourBody);
      const res = await createTour(req);
      expect(res.status).toBe(201);

      // 3 props * 30 min = 90 min viewing + 2 * 15 min travel = 120 min total
      // 10:00 + 120 min = 12:00
      expect(txMock.viewingTour.create).toHaveBeenCalledWith(
        expect.objectContaining({
          data: expect.objectContaining({
            startTime: "10:00",
            estimatedEnd: "12:00",
          }),
        }),
      );
    });

    it("calculates estimatedEnd time correctly (2 properties = 75 min)", async () => {
      setupTourCreateMocks();
      mockPrisma.property.findMany.mockResolvedValue([
        { id: "prop-001", titleEn: "Villa 1", status: "AVAILABLE" },
        { id: "prop-002", titleEn: "Villa 2", status: "AVAILABLE" },
      ]);

      const body = {
        ...validTourBody,
        propertyIds: ["prop-001", "prop-002"],
      };
      const req = makeToursPostRequest(body);
      const res = await createTour(req);
      expect(res.status).toBe(201);

      // 2 props * 30 min = 60 min viewing + 1 * 15 min travel = 75 min total
      // 10:00 + 75 min = 11:15
      expect(txMock.viewingTour.create).toHaveBeenCalledWith(
        expect.objectContaining({
          data: expect.objectContaining({
            startTime: "10:00",
            estimatedEnd: "11:15",
          }),
        }),
      );
    });

    it("calculates estimatedEnd for single property (30 min only)", async () => {
      setupTourCreateMocks();
      mockPrisma.property.findMany.mockResolvedValue([
        { id: "prop-001", titleEn: "Villa 1", status: "AVAILABLE" },
      ]);

      const body = {
        ...validTourBody,
        propertyIds: ["prop-001"],
        startTime: "14:00",
      };
      const req = makeToursPostRequest(body);
      await createTour(req);

      // 1 prop * 30 min = 30 min, no travel
      // 14:00 + 30 = 14:30
      expect(txMock.viewingTour.create).toHaveBeenCalledWith(
        expect.objectContaining({
          data: expect.objectContaining({
            estimatedEnd: "14:30",
          }),
        }),
      );
    });

    it("returns 409 when broker has conflict at a time slot", async () => {
      setupTourCreateMocks();
      // First property OK, second has broker conflict
      txMock.propertyViewing.findFirst
        .mockResolvedValueOnce(null) // no conflict at 10:00
        .mockResolvedValueOnce({ id: "conflict-viewing" }); // conflict at 10:45

      const req = makeToursPostRequest(validTourBody);
      const res = await createTour(req);
      expect(res.status).toBe(409);
      const json = await res.json();
      expect(json.errorCode).toBe("BROKER_CONFLICT");
      expect(json.error).toContain("10:45");
    });

    it("auto-assigns broker when brokerId is not provided", async () => {
      setupAuth();
      mockPrisma.property.findMany.mockResolvedValue([
        { id: "prop-001", titleEn: "V1", status: "AVAILABLE" },
        { id: "prop-002", titleEn: "V2", status: "AVAILABLE" },
      ]);
      mockPrisma.organizations.findUnique.mockResolvedValue({
        id: TEST_ORG_ID,
      });

      const dayOfWeek = new Date(FUTURE_DATE_STR).getDay();
      mockPrisma.broker.findMany.mockResolvedValue([
        {
          id: "broker-auto-1",
          workingDays: [dayOfWeek],
          workStartTime: "08:00",
          workEndTime: "18:00",
          maxViewingsPerDay: 10,
          _count: { viewings: 1 },
        },
      ]);

      txMock.propertyViewing.findFirst.mockResolvedValue(null);
      txMock.viewingTour.create.mockResolvedValue({
        id: "tour-auto",
        brokerId: "broker-auto-1",
        startTime: "10:00",
        estimatedEnd: "11:15",
        status: "scheduled",
      });
      txMock.propertyViewing.create.mockResolvedValue({
        id: "v-auto",
        property: { id: "prop-001", titleEn: "V1" },
      });

      const body = {
        ...validTourBody,
        propertyIds: ["prop-001", "prop-002"],
        brokerId: undefined,
      };
      const req = makeToursPostRequest(body);
      const res = await createTour(req);
      expect(res.status).toBe(201);
      const json = await res.json();
      expect(json.autoAssignedBroker).toBe(true);
    });

    it("picks broker with fewest viewings during auto-assignment", async () => {
      setupAuth();
      mockPrisma.property.findMany.mockResolvedValue([
        { id: "prop-001", titleEn: "V1", status: "AVAILABLE" },
      ]);
      mockPrisma.organizations.findUnique.mockResolvedValue({
        id: TEST_ORG_ID,
      });

      const dayOfWeek = new Date(FUTURE_DATE_STR).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 },
        },
      ]);

      txMock.propertyViewing.findFirst.mockResolvedValue(null);
      txMock.viewingTour.create.mockResolvedValue({
        id: "tour-auto",
        brokerId: "free-broker",
        status: "scheduled",
      });
      txMock.propertyViewing.create.mockResolvedValue({
        id: "v-auto",
        property: { id: "prop-001" },
      });

      const body = {
        ...validTourBody,
        propertyIds: ["prop-001"],
        brokerId: undefined,
      };
      const req = makeToursPostRequest(body);
      await createTour(req);

      // free-broker has fewer viewings, should be picked
      expect(txMock.viewingTour.create).toHaveBeenCalledWith(
        expect.objectContaining({
          data: expect.objectContaining({
            brokerId: "free-broker",
          }),
        }),
      );
    });

    it("creates tour without broker when none eligible for auto-assignment", async () => {
      setupAuth();
      mockPrisma.property.findMany.mockResolvedValue([
        { id: "prop-001", titleEn: "V1", status: "AVAILABLE" },
      ]);
      mockPrisma.organizations.findUnique.mockResolvedValue({
        id: TEST_ORG_ID,
      });
      // No brokers available
      mockPrisma.broker.findMany.mockResolvedValue([]);

      txMock.propertyViewing.findFirst.mockResolvedValue(null);
      txMock.viewingTour.create.mockResolvedValue({
        id: "tour-no-broker",
        brokerId: null,
        status: "scheduled",
      });
      txMock.propertyViewing.create.mockResolvedValue({
        id: "v-id",
        property: { id: "prop-001" },
      });

      const body = {
        ...validTourBody,
        propertyIds: ["prop-001"],
        brokerId: undefined,
      };
      const req = makeToursPostRequest(body);
      const res = await createTour(req);
      expect(res.status).toBe(201);
      const json = await res.json();
      expect(json.autoAssignedBroker).toBe(false);
    });

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

      const req = makeToursPostRequest(validTourBody);
      const res = await createTour(req);
      expect(res.status).toBe(500);
    });
  });

  // ═══════════════════════════════════════════════════════
  // GET /api/real-estate/viewings/calendar
  // ═══════════════════════════════════════════════════════
  describe("GET /api/real-estate/viewings/calendar", () => {
    const startDate = FUTURE_DATE_STR;
    const endDateObj = new Date(futureDate);
    endDateObj.setDate(endDateObj.getDate() + 14);
    const endDate = endDateObj.toISOString().split("T")[0];

    it("returns 401 when user is not authenticated", async () => {
      setupAuth(null);
      const req = makeCalendarGetRequest({ startDate, endDate });
      const res = await getCalendar(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 = makeCalendarGetRequest({ startDate, endDate });
      const res = await getCalendar(req);
      expect(res.status).toBe(404);
    });

    it("returns 400 when startDate is missing", async () => {
      setupAuth();
      const req = makeCalendarGetRequest({ endDate });
      const res = await getCalendar(req);
      expect(res.status).toBe(400);
      const json = await res.json();
      expect(json.error).toContain("startDate");
    });

    it("returns 400 when endDate is missing", async () => {
      setupAuth();
      const req = makeCalendarGetRequest({ startDate });
      const res = await getCalendar(req);
      expect(res.status).toBe(400);
      const json = await res.json();
      expect(json.error).toContain("endDate");
    });

    it("returns 400 when both dates are missing", async () => {
      setupAuth();
      const req = makeCalendarGetRequest();
      const res = await getCalendar(req);
      expect(res.status).toBe(400);
    });

    it("returns 400 when date format is invalid", async () => {
      setupAuth();
      const req = makeCalendarGetRequest({
        startDate: "not-valid",
        endDate: "also-not-valid",
      });
      const res = await getCalendar(req);
      expect(res.status).toBe(400);
      const json = await res.json();
      expect(json.error).toContain("Invalid date format");
    });

    it("returns 400 when startDate is after endDate", async () => {
      setupAuth();
      const req = makeCalendarGetRequest({
        startDate: endDate,
        endDate: startDate,
      });
      const res = await getCalendar(req);
      expect(res.status).toBe(400);
      const json = await res.json();
      expect(json.error).toContain("startDate must be before");
    });

    it("returns 400 when date range exceeds 90 days", async () => {
      setupAuth();
      const farDate = new Date(futureDate);
      farDate.setDate(farDate.getDate() + 100);
      const req = makeCalendarGetRequest({
        startDate,
        endDate: farDate.toISOString().split("T")[0],
      });
      const res = await getCalendar(req);
      expect(res.status).toBe(400);
      const json = await res.json();
      expect(json.error).toContain("90 days");
    });

    it("returns viewings grouped by date with broker and status summaries", async () => {
      setupAuth();

      const date1 = new Date(FUTURE_DATE_STR);
      const date2 = new Date(FUTURE_DATE_STR);
      date2.setDate(date2.getDate() + 1);

      const viewingsData = [
        {
          id: "v-1",
          viewingDate: date1,
          viewingTime: "10:00",
          status: "CONFIRMED",
          property: {
            id: "p1",
            titleEn: "Villa 1",
            titleAr: "فيلا",
            referenceNumber: "RE-001",
            propertyType: "VILLA",
            city: "Doha",
            district: "The Pearl",
          },
          broker: {
            id: "b1",
            nameEn: "Ahmed",
            nameAr: "أحمد",
            phone: "+974555",
            photoUrl: null,
          },
        },
        {
          id: "v-2",
          viewingDate: date1,
          viewingTime: "14:00",
          status: "REQUESTED",
          property: {
            id: "p2",
            titleEn: "Apartment 1",
            titleAr: "شقة",
            referenceNumber: "RE-002",
            propertyType: "APARTMENT",
            city: "Doha",
            district: "Lusail",
          },
          broker: null,
        },
        {
          id: "v-3",
          viewingDate: date2,
          viewingTime: "11:00",
          status: "CONFIRMED",
          property: {
            id: "p3",
            titleEn: "Townhouse 1",
            titleAr: "تاونهاوس",
            referenceNumber: "RE-003",
            propertyType: "TOWNHOUSE",
            city: "Doha",
            district: "Al Wakrah",
          },
          broker: {
            id: "b1",
            nameEn: "Ahmed",
            nameAr: "أحمد",
            phone: "+974555",
            photoUrl: null,
          },
        },
      ];

      mockPrisma.propertyViewing.findMany.mockResolvedValue(viewingsData);
      mockPrisma.propertyViewing.groupBy
        .mockResolvedValueOnce([{ brokerId: "b1", _count: { id: 2 } }])
        .mockResolvedValueOnce([
          { status: "CONFIRMED", _count: { id: 2 } },
          { status: "REQUESTED", _count: { id: 1 } },
        ]);
      mockPrisma.broker.findMany.mockResolvedValue([
        { id: "b1", nameEn: "Ahmed", nameAr: "أحمد" },
      ]);

      const req = makeCalendarGetRequest({ startDate, endDate });
      const res = await getCalendar(req);
      expect(res.status).toBe(200);
      const json = await res.json();

      expect(json.success).toBe(true);
      expect(json.calendar).toHaveLength(2);
      expect(json.summary.totalViewings).toBe(3);
      expect(json.summary.daysWithViewings).toBe(2);
      expect(json.summary.byStatus).toEqual({
        CONFIRMED: 2,
        REQUESTED: 1,
      });
      expect(json.summary.byBroker).toHaveLength(1);
      expect(json.summary.byBroker[0].viewingCount).toBe(2);
      expect(json.dateRange).toEqual({
        from: startDate,
        to: endDate,
      });
    });

    it("filters by brokerId when provided", async () => {
      setupAuth();
      mockPrisma.propertyViewing.findMany.mockResolvedValue([]);
      mockPrisma.propertyViewing.groupBy
        .mockResolvedValueOnce([])
        .mockResolvedValueOnce([]);

      const req = makeCalendarGetRequest({
        startDate,
        endDate,
        brokerId: TEST_BROKER_ID,
      });
      await getCalendar(req);

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

    it("returns empty calendar when no viewings in range", async () => {
      setupAuth();
      mockPrisma.propertyViewing.findMany.mockResolvedValue([]);
      mockPrisma.propertyViewing.groupBy
        .mockResolvedValueOnce([])
        .mockResolvedValueOnce([]);

      const req = makeCalendarGetRequest({ startDate, endDate });
      const res = await getCalendar(req);
      expect(res.status).toBe(200);
      const json = await res.json();
      expect(json.calendar).toHaveLength(0);
      expect(json.summary.totalViewings).toBe(0);
      expect(json.summary.daysWithViewings).toBe(0);
    });

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

      const req = makeCalendarGetRequest({ startDate, endDate });
      const res = await getCalendar(req);
      expect(res.status).toBe(500);
    });
  });

  // ═══════════════════════════════════════════════════════
  // GET /api/real-estate/viewings/availability
  // ═══════════════════════════════════════════════════════
  describe("GET /api/real-estate/viewings/availability", () => {
    it("returns 401 when user is not authenticated", async () => {
      setupAuth(null);
      const req = makeAvailabilityGetRequest({
        propertyId: TEST_PROPERTY_ID,
        date: FUTURE_DATE_STR,
      });
      const res = await getAvailability(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 = makeAvailabilityGetRequest({
        propertyId: TEST_PROPERTY_ID,
        date: FUTURE_DATE_STR,
      });
      const res = await getAvailability(req);
      expect(res.status).toBe(404);
    });

    it("returns 400 when propertyId is missing", async () => {
      setupAuth();
      const req = makeAvailabilityGetRequest({ date: FUTURE_DATE_STR });
      const res = await getAvailability(req);
      expect(res.status).toBe(400);
      const json = await res.json();
      expect(json.error).toContain("propertyId");
    });

    it("returns 400 when date is missing", async () => {
      setupAuth();
      const req = makeAvailabilityGetRequest({
        propertyId: TEST_PROPERTY_ID,
      });
      const res = await getAvailability(req);
      expect(res.status).toBe(400);
      const json = await res.json();
      expect(json.error).toContain("date");
    });

    it("returns 400 when date format is invalid", async () => {
      setupAuth();
      const req = makeAvailabilityGetRequest({
        propertyId: TEST_PROPERTY_ID,
        date: "not-a-date",
      });
      const res = await getAvailability(req);
      expect(res.status).toBe(400);
      const json = await res.json();
      expect(json.error).toContain("Invalid date format");
    });

    it("returns 400 when date is in the past", async () => {
      setupAuth();
      const req = makeAvailabilityGetRequest({
        propertyId: TEST_PROPERTY_ID,
        date: "2020-01-01",
      });
      const res = await getAvailability(req);
      expect(res.status).toBe(400);
      const json = await res.json();
      expect(json.error).toContain("past");
    });

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

      const req = makeAvailabilityGetRequest({
        propertyId: TEST_PROPERTY_ID,
        date: FUTURE_DATE_STR,
      });
      const res = await getAvailability(req);
      expect(res.status).toBe(404);
      const json = await res.json();
      expect(json.error).toBe("Property not found");
    });

    it("returns available time slots with default 09:00-18:00 hours when no broker specified", async () => {
      setupAuth();
      mockPrisma.property.findFirst.mockResolvedValue({
        id: TEST_PROPERTY_ID,
        titleEn: "Luxury Villa",
      });
      // No existing viewings
      mockPrisma.propertyViewing.findMany.mockResolvedValue([]);

      const req = makeAvailabilityGetRequest({
        propertyId: TEST_PROPERTY_ID,
        date: FUTURE_DATE_STR,
      });
      const res = await getAvailability(req);
      expect(res.status).toBe(200);
      const json = await res.json();
      expect(json.success).toBe(true);
      expect(json.slotDuration).toBe(30);
      expect(json.workingHours).toEqual({ start: "09:00", end: "18:00" });
      // 09:00 to 18:00 in 30-min slots = 18 slots
      expect(json.totalSlots).toBe(18);
      expect(json.availableCount).toBe(18);
      expect(json.slots.every((s: { available: boolean }) => s.available)).toBe(
        true,
      );
    });

    it("excludes already-booked slots from availability", async () => {
      setupAuth();
      mockPrisma.property.findFirst.mockResolvedValue({
        id: TEST_PROPERTY_ID,
        titleEn: "Luxury Villa",
      });
      // One existing booking at 10:00 (30 min)
      mockPrisma.propertyViewing.findMany.mockResolvedValue([
        {
          viewingTime: "10:00",
          duration: 30,
          viewingType: "INDIVIDUAL",
        },
      ]);

      const req = makeAvailabilityGetRequest({
        propertyId: TEST_PROPERTY_ID,
        date: FUTURE_DATE_STR,
      });
      const res = await getAvailability(req);
      expect(res.status).toBe(200);
      const json = await res.json();

      const slot1000 = json.slots.find(
        (s: { time: string }) => s.time === "10:00",
      );
      expect(slot1000).toBeDefined();
      expect(slot1000.available).toBe(false);
      expect(slot1000.conflictReason).toBe("Time slot is already booked");

      // 09:00 and 10:30 should still be available
      const slot0900 = json.slots.find(
        (s: { time: string }) => s.time === "09:00",
      );
      expect(slot0900.available).toBe(true);

      const slot1030 = json.slots.find(
        (s: { time: string }) => s.time === "10:30",
      );
      expect(slot1030.available).toBe(true);

      expect(json.availableCount).toBe(17);
    });

    it("does not block slots for OPEN_HOUSE viewing type (shared)", async () => {
      setupAuth();
      mockPrisma.property.findFirst.mockResolvedValue({
        id: TEST_PROPERTY_ID,
        titleEn: "Luxury Villa",
      });
      // Existing open house at 10:00 should NOT block
      mockPrisma.propertyViewing.findMany.mockResolvedValue([
        {
          viewingTime: "10:00",
          duration: 30,
          viewingType: "OPEN_HOUSE",
        },
      ]);

      const req = makeAvailabilityGetRequest({
        propertyId: TEST_PROPERTY_ID,
        date: FUTURE_DATE_STR,
      });
      const res = await getAvailability(req);
      expect(res.status).toBe(200);
      const json = await res.json();

      const slot1000 = json.slots.find(
        (s: { time: string }) => s.time === "10:00",
      );
      expect(slot1000.available).toBe(true);
      expect(json.availableCount).toBe(18);
    });

    it("uses broker working hours when brokerId is specified", async () => {
      setupAuth();
      mockPrisma.property.findFirst.mockResolvedValue({
        id: TEST_PROPERTY_ID,
        titleEn: "Luxury Villa",
      });

      const dayOfWeek = new Date(FUTURE_DATE_STR).getDay();
      mockPrisma.broker.findFirst.mockResolvedValue({
        id: TEST_BROKER_ID,
        nameEn: "Ahmed",
        workingDays: [dayOfWeek],
        workStartTime: "10:00",
        workEndTime: "14:00",
        maxViewingsPerDay: 10,
      });

      // No existing viewings
      mockPrisma.propertyViewing.findMany.mockResolvedValue([]);
      mockPrisma.brokerAvailability.findMany.mockResolvedValue([]);

      const req = makeAvailabilityGetRequest({
        propertyId: TEST_PROPERTY_ID,
        date: FUTURE_DATE_STR,
        brokerId: TEST_BROKER_ID,
      });
      const res = await getAvailability(req);
      expect(res.status).toBe(200);
      const json = await res.json();

      expect(json.workingHours).toEqual({ start: "10:00", end: "14:00" });
      // 10:00 to 14:00 in 30-min slots = 8 slots
      expect(json.totalSlots).toBe(8);
      expect(json.brokerName).toBe("Ahmed");
    });

    it("returns no slots when broker does not work on that day", async () => {
      setupAuth();
      mockPrisma.property.findFirst.mockResolvedValue({
        id: TEST_PROPERTY_ID,
        titleEn: "Luxury Villa",
      });

      const dayOfWeek = new Date(FUTURE_DATE_STR).getDay();
      const otherDay = (dayOfWeek + 1) % 7;
      mockPrisma.broker.findFirst.mockResolvedValue({
        id: TEST_BROKER_ID,
        nameEn: "Ahmed",
        workingDays: [otherDay],
        workStartTime: "09:00",
        workEndTime: "18:00",
        maxViewingsPerDay: 10,
      });

      const req = makeAvailabilityGetRequest({
        propertyId: TEST_PROPERTY_ID,
        date: FUTURE_DATE_STR,
        brokerId: TEST_BROKER_ID,
      });
      const res = await getAvailability(req);
      expect(res.status).toBe(200);
      const json = await res.json();
      expect(json.available).toBe(false);
      expect(json.reason).toBe("Broker does not work on this day");
      expect(json.slots).toHaveLength(0);
    });

    it("returns no slots when broker has reached max viewings for the day", async () => {
      setupAuth();
      mockPrisma.property.findFirst.mockResolvedValue({
        id: TEST_PROPERTY_ID,
        titleEn: "Luxury Villa",
      });

      const dayOfWeek = new Date(FUTURE_DATE_STR).getDay();
      mockPrisma.broker.findFirst.mockResolvedValue({
        id: TEST_BROKER_ID,
        nameEn: "Ahmed",
        workingDays: [dayOfWeek],
        workStartTime: "09:00",
        workEndTime: "18:00",
        maxViewingsPerDay: 3,
      });

      mockPrisma.propertyViewing.findMany
        .mockResolvedValueOnce([]) // property viewings
        .mockResolvedValueOnce([
          // broker viewings - 3 existing (at max)
          { viewingTime: "09:00", duration: 30 },
          { viewingTime: "10:00", duration: 30 },
          { viewingTime: "11:00", duration: 30 },
        ]);

      const req = makeAvailabilityGetRequest({
        propertyId: TEST_PROPERTY_ID,
        date: FUTURE_DATE_STR,
        brokerId: TEST_BROKER_ID,
      });
      const res = await getAvailability(req);
      expect(res.status).toBe(200);
      const json = await res.json();
      expect(json.available).toBe(false);
      expect(json.reason).toContain("maximum viewings");
      expect(json.slots).toHaveLength(0);
    });

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

      const req = makeAvailabilityGetRequest({
        propertyId: TEST_PROPERTY_ID,
        date: FUTURE_DATE_STR,
        brokerId: "nonexistent-broker",
      });
      const res = await getAvailability(req);
      expect(res.status).toBe(404);
      const json = await res.json();
      expect(json.error).toBe("Broker not found or inactive");
    });

    it("excludes broker blocked time ranges from availability", async () => {
      setupAuth();
      mockPrisma.property.findFirst.mockResolvedValue({
        id: TEST_PROPERTY_ID,
        titleEn: "Luxury Villa",
      });

      const dayOfWeek = new Date(FUTURE_DATE_STR).getDay();
      mockPrisma.broker.findFirst.mockResolvedValue({
        id: TEST_BROKER_ID,
        nameEn: "Ahmed",
        workingDays: [dayOfWeek],
        workStartTime: "09:00",
        workEndTime: "18:00",
        maxViewingsPerDay: 10,
      });

      mockPrisma.propertyViewing.findMany
        .mockResolvedValueOnce([]) // property viewings
        .mockResolvedValueOnce([]); // broker viewings

      // Broker has a blocked time range (lunch break 12:00-13:00)
      mockPrisma.brokerAvailability.findMany.mockResolvedValue([
        { startTime: "12:00", endTime: "13:00" },
      ]);

      const req = makeAvailabilityGetRequest({
        propertyId: TEST_PROPERTY_ID,
        date: FUTURE_DATE_STR,
        brokerId: TEST_BROKER_ID,
      });
      const res = await getAvailability(req);
      expect(res.status).toBe(200);
      const json = await res.json();

      // 12:00 and 12:30 slots should be blocked
      const slot1200 = json.slots.find(
        (s: { time: string }) => s.time === "12:00",
      );
      expect(slot1200.available).toBe(false);

      const slot1230 = json.slots.find(
        (s: { time: string }) => s.time === "12:30",
      );
      expect(slot1230.available).toBe(false);

      // 11:30 and 13:00 should still be available
      const slot1130 = json.slots.find(
        (s: { time: string }) => s.time === "11:30",
      );
      expect(slot1130.available).toBe(true);

      const slot1300 = json.slots.find(
        (s: { time: string }) => s.time === "13:00",
      );
      expect(slot1300.available).toBe(true);

      // 18 total slots - 2 blocked = 16 available
      expect(json.availableCount).toBe(16);
    });

    it("excludes broker bookings across all properties", async () => {
      setupAuth();
      mockPrisma.property.findFirst.mockResolvedValue({
        id: TEST_PROPERTY_ID,
        titleEn: "Luxury Villa",
      });

      const dayOfWeek = new Date(FUTURE_DATE_STR).getDay();
      mockPrisma.broker.findFirst.mockResolvedValue({
        id: TEST_BROKER_ID,
        nameEn: "Ahmed",
        workingDays: [dayOfWeek],
        workStartTime: "09:00",
        workEndTime: "18:00",
        maxViewingsPerDay: 10,
      });

      mockPrisma.propertyViewing.findMany
        .mockResolvedValueOnce([]) // property viewings (this property)
        .mockResolvedValueOnce([
          // broker viewings on OTHER properties
          { viewingTime: "15:00", duration: 30 },
        ]);
      mockPrisma.brokerAvailability.findMany.mockResolvedValue([]);

      const req = makeAvailabilityGetRequest({
        propertyId: TEST_PROPERTY_ID,
        date: FUTURE_DATE_STR,
        brokerId: TEST_BROKER_ID,
      });
      const res = await getAvailability(req);
      expect(res.status).toBe(200);
      const json = await res.json();

      const slot1500 = json.slots.find(
        (s: { time: string }) => s.time === "15:00",
      );
      expect(slot1500).toBeDefined();
      expect(slot1500.available).toBe(false);
    });

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

      const req = makeAvailabilityGetRequest({
        propertyId: TEST_PROPERTY_ID,
        date: FUTURE_DATE_STR,
      });
      const res = await getAvailability(req);
      expect(res.status).toBe(500);
    });
  });

  // ═══════════════════════════════════════════════════════
  // GET /api/real-estate/viewings/[id]/feedback
  // ═══════════════════════════════════════════════════════
  describe("GET /api/real-estate/viewings/[id]/feedback", () => {
    it("returns 401 when user is not authenticated", async () => {
      setupAuth(null);
      const req = makeFeedbackGetRequest();
      const res = await getFeedback(req, routeParams());
      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 = makeFeedbackGetRequest();
      const res = await getFeedback(req, routeParams());
      expect(res.status).toBe(404);
      const json = await res.json();
      expect(json.error).toBe("User not found");
    });

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

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

    it("returns feedback list for a valid viewing", async () => {
      setupAuth();
      mockPrisma.propertyViewing.findFirst.mockResolvedValue({
        id: TEST_VIEWING_ID,
      });

      const feedbackItems = [
        {
          id: "fb-1",
          viewingId: TEST_VIEWING_ID,
          overallRating: 4,
          priceOpinion: "fair",
          interestLevel: "warm",
          createdAt: new Date(),
        },
        {
          id: "fb-2",
          viewingId: TEST_VIEWING_ID,
          overallRating: 5,
          priceOpinion: "bargain",
          interestLevel: "hot",
          createdAt: new Date(),
        },
      ];
      mockPrisma.viewingFeedback.findMany.mockResolvedValue(feedbackItems);

      const req = makeFeedbackGetRequest();
      const res = await getFeedback(req, routeParams());
      expect(res.status).toBe(200);
      const json = await res.json();
      expect(json.success).toBe(true);
      expect(json.feedback).toHaveLength(2);
      expect(json.feedback[0].id).toBe("fb-1");
    });

    it("returns empty feedback array when no feedback exists", async () => {
      setupAuth();
      mockPrisma.propertyViewing.findFirst.mockResolvedValue({
        id: TEST_VIEWING_ID,
      });
      mockPrisma.viewingFeedback.findMany.mockResolvedValue([]);

      const req = makeFeedbackGetRequest();
      const res = await getFeedback(req, routeParams());
      expect(res.status).toBe(200);
      const json = await res.json();
      expect(json.success).toBe(true);
      expect(json.feedback).toHaveLength(0);
    });

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

      const req = makeFeedbackGetRequest();
      const res = await getFeedback(req, routeParams());
      expect(res.status).toBe(500);
      const json = await res.json();
      expect(json.error).toBe("Failed to retrieve feedback");
    });
  });

  // ═══════════════════════════════════════════════════════
  // POST /api/real-estate/viewings/[id]/feedback
  // ═══════════════════════════════════════════════════════
  describe("POST /api/real-estate/viewings/[id]/feedback", () => {
    function setupFeedbackPostMocks(viewingStatus: string = "IN_PROGRESS") {
      setupAuth();
      mockPrisma.propertyViewing.findFirst.mockResolvedValue({
        id: TEST_VIEWING_ID,
        status: viewingStatus,
      });
      mockPrisma.viewingFeedback.create.mockImplementation(
        ({ data }: { data: Record<string, unknown> }) => {
          return Promise.resolve({
            id: "fb-new",
            ...data,
            createdAt: new Date(),
            updatedAt: new Date(),
          });
        },
      );
      mockPrisma.propertyViewing.update.mockResolvedValue({
        id: TEST_VIEWING_ID,
      });
    }

    it("returns 401 when user is not authenticated", async () => {
      setupAuth(null);
      const req = makeFeedbackPostRequest({ overallRating: 4 });
      const res = await postFeedback(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 = makeFeedbackPostRequest({ overallRating: 4 });
      const res = await postFeedback(req, routeParams());
      expect(res.status).toBe(404);
    });

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

      const req = makeFeedbackPostRequest({ overallRating: 4 });
      const res = await postFeedback(req, routeParams());
      expect(res.status).toBe(404);
    });

    // --- overallRating validation ---
    it("returns 400 when overallRating is 0 (below min)", async () => {
      setupFeedbackPostMocks();
      const req = makeFeedbackPostRequest({ overallRating: 0 });
      const res = await postFeedback(req, routeParams());
      expect(res.status).toBe(400);
      const json = await res.json();
      expect(json.errors).toEqual(
        expect.arrayContaining([
          expect.stringContaining(
            "Overall rating must be an integer between 1 and 5",
          ),
        ]),
      );
    });

    it("returns 400 when overallRating is 6 (above max)", async () => {
      setupFeedbackPostMocks();
      const req = makeFeedbackPostRequest({ overallRating: 6 });
      const res = await postFeedback(req, routeParams());
      expect(res.status).toBe(400);
    });

    it("returns 400 when overallRating is a float", async () => {
      setupFeedbackPostMocks();
      const req = makeFeedbackPostRequest({ overallRating: 3.5 });
      const res = await postFeedback(req, routeParams());
      expect(res.status).toBe(400);
    });

    it("accepts valid overallRating of 1 (boundary)", async () => {
      setupFeedbackPostMocks();
      const req = makeFeedbackPostRequest({ overallRating: 1 });
      const res = await postFeedback(req, routeParams());
      expect(res.status).toBe(200);
    });

    it("accepts valid overallRating of 5 (boundary)", async () => {
      setupFeedbackPostMocks();
      const req = makeFeedbackPostRequest({ overallRating: 5 });
      const res = await postFeedback(req, routeParams());
      expect(res.status).toBe(200);
    });

    // --- priceOpinion enum validation ---
    it("returns 400 for invalid priceOpinion value", async () => {
      setupFeedbackPostMocks();
      const req = makeFeedbackPostRequest({ priceOpinion: "cheap" });
      const res = await postFeedback(req, routeParams());
      expect(res.status).toBe(400);
      const json = await res.json();
      expect(json.errors).toEqual(
        expect.arrayContaining([
          expect.stringContaining(
            "Price opinion must be one of: fair, too_high, bargain",
          ),
        ]),
      );
    });

    it("accepts valid priceOpinion values", async () => {
      for (const val of ["fair", "too_high", "bargain"]) {
        setupFeedbackPostMocks();
        const req = makeFeedbackPostRequest({ priceOpinion: val });
        const res = await postFeedback(req, routeParams());
        expect(res.status).toBe(200);
      }
    });

    // --- sizeOpinion enum validation ---
    it("returns 400 for invalid sizeOpinion value", async () => {
      setupFeedbackPostMocks();
      const req = makeFeedbackPostRequest({ sizeOpinion: "huge" });
      const res = await postFeedback(req, routeParams());
      expect(res.status).toBe(400);
      const json = await res.json();
      expect(json.errors).toEqual(
        expect.arrayContaining([
          expect.stringContaining(
            "Size opinion must be one of: too_small, just_right, spacious",
          ),
        ]),
      );
    });

    it("accepts valid sizeOpinion values", async () => {
      for (const val of ["too_small", "just_right", "spacious"]) {
        setupFeedbackPostMocks();
        const req = makeFeedbackPostRequest({ sizeOpinion: val });
        const res = await postFeedback(req, routeParams());
        expect(res.status).toBe(200);
      }
    });

    // --- locationOpinion enum validation ---
    it("returns 400 for invalid locationOpinion value", async () => {
      setupFeedbackPostMocks();
      const req = makeFeedbackPostRequest({ locationOpinion: "perfect" });
      const res = await postFeedback(req, routeParams());
      expect(res.status).toBe(400);
      const json = await res.json();
      expect(json.errors).toEqual(
        expect.arrayContaining([
          expect.stringContaining(
            "Location opinion must be one of: convenient, acceptable, too_far",
          ),
        ]),
      );
    });

    it("accepts valid locationOpinion values", async () => {
      for (const val of ["convenient", "acceptable", "too_far"]) {
        setupFeedbackPostMocks();
        const req = makeFeedbackPostRequest({ locationOpinion: val });
        const res = await postFeedback(req, routeParams());
        expect(res.status).toBe(200);
      }
    });

    // --- interestLevel enum validation ---
    it("returns 400 for invalid interestLevel value", async () => {
      setupFeedbackPostMocks();
      const req = makeFeedbackPostRequest({ interestLevel: "lukewarm" });
      const res = await postFeedback(req, routeParams());
      expect(res.status).toBe(400);
      const json = await res.json();
      expect(json.errors).toEqual(
        expect.arrayContaining([
          expect.stringContaining(
            "Interest level must be one of: hot, warm, cool, cold",
          ),
        ]),
      );
    });

    it("accepts valid interestLevel values", async () => {
      for (const val of ["hot", "warm", "cool", "cold"]) {
        setupFeedbackPostMocks();
        const req = makeFeedbackPostRequest({ interestLevel: val });
        const res = await postFeedback(req, routeParams());
        expect(res.status).toBe(200);
      }
    });

    // --- suggestedAction enum validation ---
    it("returns 400 for invalid suggestedAction value", async () => {
      setupFeedbackPostMocks();
      const req = makeFeedbackPostRequest({ suggestedAction: "negotiate" });
      const res = await postFeedback(req, routeParams());
      expect(res.status).toBe(400);
      const json = await res.json();
      expect(json.errors).toEqual(
        expect.arrayContaining([
          expect.stringContaining(
            "Suggested action must be one of: make_offer, second_viewing, not_interested, show_similar",
          ),
        ]),
      );
    });

    it("accepts valid suggestedAction values", async () => {
      for (const val of [
        "make_offer",
        "second_viewing",
        "not_interested",
        "show_similar",
      ]) {
        setupFeedbackPostMocks();
        const req = makeFeedbackPostRequest({ suggestedAction: val });
        const res = await postFeedback(req, routeParams());
        expect(res.status).toBe(200);
      }
    });

    // --- Multiple validation errors ---
    it("returns multiple validation errors at once", async () => {
      setupFeedbackPostMocks();
      const req = makeFeedbackPostRequest({
        overallRating: 10,
        priceOpinion: "invalid",
        sizeOpinion: "invalid",
        locationOpinion: "invalid",
        interestLevel: "invalid",
        suggestedAction: "invalid",
      });
      const res = await postFeedback(req, routeParams());
      expect(res.status).toBe(400);
      const json = await res.json();
      expect(json.errors.length).toBe(6);
    });

    // --- Auto-complete viewing ---
    it("auto-completes viewing when interest is 'hot' and status is IN_PROGRESS", async () => {
      setupFeedbackPostMocks("IN_PROGRESS");

      const req = makeFeedbackPostRequest({
        interestLevel: "hot",
        overallRating: 5,
      });
      const res = await postFeedback(req, routeParams());
      expect(res.status).toBe(200);

      expect(mockPrisma.propertyViewing.update).toHaveBeenCalledWith(
        expect.objectContaining({
          where: { id: TEST_VIEWING_ID },
          data: expect.objectContaining({
            interestLevel: "hot",
            status: "COMPLETED",
          }),
        }),
      );
    });

    it("does NOT auto-complete viewing when interest is 'hot' but status is CONFIRMED", async () => {
      setupFeedbackPostMocks("CONFIRMED");

      const req = makeFeedbackPostRequest({
        interestLevel: "hot",
        overallRating: 5,
      });
      const res = await postFeedback(req, routeParams());
      expect(res.status).toBe(200);

      // Should update interestLevel but NOT status
      expect(mockPrisma.propertyViewing.update).toHaveBeenCalledWith(
        expect.objectContaining({
          data: expect.not.objectContaining({ status: "COMPLETED" }),
        }),
      );
    });

    it("does NOT auto-complete viewing when interest is 'warm' and status is IN_PROGRESS", async () => {
      setupFeedbackPostMocks("IN_PROGRESS");

      const req = makeFeedbackPostRequest({
        interestLevel: "warm",
        overallRating: 4,
      });
      const res = await postFeedback(req, routeParams());
      expect(res.status).toBe(200);

      expect(mockPrisma.propertyViewing.update).toHaveBeenCalledWith(
        expect.objectContaining({
          data: expect.not.objectContaining({ status: "COMPLETED" }),
        }),
      );
    });

    // --- Updates viewing.interestLevel and viewing.clientNotes ---
    it("updates viewing.interestLevel when interestLevel is provided", async () => {
      setupFeedbackPostMocks();

      const req = makeFeedbackPostRequest({ interestLevel: "warm" });
      const res = await postFeedback(req, routeParams());
      expect(res.status).toBe(200);

      expect(mockPrisma.propertyViewing.update).toHaveBeenCalledWith(
        expect.objectContaining({
          where: { id: TEST_VIEWING_ID },
          data: expect.objectContaining({ interestLevel: "warm" }),
        }),
      );
    });

    it("updates viewing.clientNotes when brokerNotes is provided", async () => {
      setupFeedbackPostMocks();

      const req = makeFeedbackPostRequest({
        brokerNotes: "  Client very interested in the view  ",
      });
      const res = await postFeedback(req, routeParams());
      expect(res.status).toBe(200);

      expect(mockPrisma.propertyViewing.update).toHaveBeenCalledWith(
        expect.objectContaining({
          where: { id: TEST_VIEWING_ID },
          data: expect.objectContaining({
            clientNotes: "Client very interested in the view",
          }),
        }),
      );
    });

    it("does not update viewing when no side-effect fields provided", async () => {
      setupFeedbackPostMocks();

      const req = makeFeedbackPostRequest({
        overallRating: 3,
        conditionNote: "Walls need repainting",
      });
      const res = await postFeedback(req, routeParams());
      expect(res.status).toBe(200);

      expect(mockPrisma.propertyViewing.update).not.toHaveBeenCalled();
    });

    // --- Non-blocking sendPostViewingFollowUp ---
    it("fires sendPostViewingFollowUp non-blocking", async () => {
      setupFeedbackPostMocks();

      const req = makeFeedbackPostRequest({
        overallRating: 4,
        interestLevel: "warm",
      });
      const res = await postFeedback(req, routeParams());
      expect(res.status).toBe(200);

      expect(mockSendPostViewingFollowUp).toHaveBeenCalledWith(TEST_VIEWING_ID);
    });

    it("does not fail when notification throws (non-blocking)", async () => {
      setupFeedbackPostMocks();
      mockSendPostViewingFollowUp.mockRejectedValue(
        new Error("Email service down"),
      );

      const req = makeFeedbackPostRequest({ overallRating: 4 });
      const res = await postFeedback(req, routeParams());
      // Response should still be 200 since the notification is fire-and-forget
      expect(res.status).toBe(200);
    });

    // --- Successful feedback creation ---
    it("creates feedback record with all fields", async () => {
      setupFeedbackPostMocks();

      const body = {
        overallRating: 4,
        priceOpinion: "fair",
        sizeOpinion: "just_right",
        locationOpinion: "convenient",
        conditionNote: "  Good condition  ",
        wouldRecommend: true,
        brokerNotes: "Client wants second viewing",
        interestLevel: "warm",
        suggestedAction: "second_viewing",
      };

      const req = makeFeedbackPostRequest(body);
      const res = await postFeedback(req, routeParams());
      expect(res.status).toBe(200);
      const json = await res.json();
      expect(json.success).toBe(true);
      expect(json.feedback).toBeDefined();

      expect(mockPrisma.viewingFeedback.create).toHaveBeenCalledWith(
        expect.objectContaining({
          data: expect.objectContaining({
            viewingId: TEST_VIEWING_ID,
            overallRating: 4,
            priceOpinion: "fair",
            sizeOpinion: "just_right",
            locationOpinion: "convenient",
            conditionNote: "Good condition",
            wouldRecommend: true,
            brokerNotes: "Client wants second viewing",
            interestLevel: "warm",
            suggestedAction: "second_viewing",
          }),
        }),
      );
    });

    it("accepts null values for optional enum fields", async () => {
      setupFeedbackPostMocks();

      const body = {
        overallRating: null,
        priceOpinion: null,
        sizeOpinion: null,
        locationOpinion: null,
        interestLevel: null,
        suggestedAction: null,
      };
      const req = makeFeedbackPostRequest(body);
      const res = await postFeedback(req, routeParams());
      expect(res.status).toBe(200);
    });

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

      const req = makeFeedbackPostRequest({ overallRating: 3 });
      const res = await postFeedback(req, routeParams());
      expect(res.status).toBe(500);
    });
  });
});
