/**
 * Real Estate Clients & Matching API Tests
 *
 * Comprehensive tests for:
 * - GET  /api/real-estate/clients           (list client requirements)
 * - POST /api/real-estate/clients           (create client requirements)
 * - GET  /api/real-estate/clients/[id]      (client detail with top matches)
 * - PUT  /api/real-estate/clients/[id]      (update requirements, re-run matching)
 * - DELETE /api/real-estate/clients/[id]    (soft-delete, remove matches)
 * - GET  /api/real-estate/matches/[clientId] (matching properties scored by algorithm)
 * - POST /api/real-estate/matches/refresh   (batch refresh all matches for org)
 */

import { NextRequest } from "next/server";

// ─── Mocks ──────────────────────────────────────────────────────────────────

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),
}));
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/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(data), { status: 200 }),
    ),
  },
}));

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

import { prisma } from "@/lib/db";
import { getAuthenticatedEmail } from "@/lib/utils/auth-helpers.util";
import { ApiResponses } from "@/lib/utils/api-response.util";

import {
  GET as clientsGET,
  POST as clientsPOST,
} from "@/app/api/real-estate/clients/route";

import {
  GET as clientDetailGET,
  PUT as clientDetailPUT,
  DELETE as clientDetailDELETE,
} from "@/app/api/real-estate/clients/[id]/route";

import { GET as matchesGET } from "@/app/api/real-estate/matches/[clientId]/route";

import { POST as matchesRefreshPOST } from "@/app/api/real-estate/matches/refresh/route";

// ─── Helpers ────────────────────────────────────────────────────────────────

const mockGetAuthenticatedEmail = getAuthenticatedEmail as jest.Mock;
const mockPrisma = prisma as unknown as {
  users: { findUnique: jest.Mock };
  clientRequirements: {
    findMany: jest.Mock;
    findFirst: jest.Mock;
    create: jest.Mock;
    update: jest.Mock;
    count: jest.Mock;
  };
  organization_members: { findFirst: jest.Mock };
  property: { findMany: jest.Mock; findFirst: jest.Mock };
  propertyMatch: {
    findMany: jest.Mock;
    findUnique: jest.Mock;
    upsert: jest.Mock;
    create: jest.Mock;
    update: jest.Mock;
    deleteMany: jest.Mock;
  };
  organizations: { findUnique: jest.Mock };
  $transaction: jest.Mock;
};

function makeRequest(
  url: string,
  options?: { method?: string; body?: unknown },
): NextRequest {
  const init: RequestInit = {
    method: options?.method || "GET",
  };
  if (options?.body) {
    init.body = JSON.stringify(options.body);
    init.headers = { "Content-Type": "application/json" };
  }
  return new NextRequest(new URL(url, "http://localhost:9000"), init);
}

async function parseResponse(response: Response) {
  const text = await response.text();
  try {
    return JSON.parse(text);
  } catch {
    return text;
  }
}

// Default test fixtures
const MOCK_USER = { id: "user-1" };
const MOCK_EMAIL = "agent@realestate.qa";

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

// ─── Test Suite ─────────────────────────────────────────────────────────────

describe("Real Estate Clients & Matching API", () => {
  beforeEach(() => {
    jest.clearAllMocks();
  });

  // ========================================================================
  // GET /api/real-estate/clients
  // ========================================================================
  describe("GET /api/real-estate/clients", () => {
    it("returns 401 when unauthenticated", async () => {
      setupAuth(null);
      const req = makeRequest("http://localhost:9000/api/real-estate/clients");
      const res = await clientsGET(req);
      expect(res.status).toBe(401);
      expect(ApiResponses.unauthorized).toHaveBeenCalled();
    });

    it("returns 404 when user not found", async () => {
      mockGetAuthenticatedEmail.mockResolvedValue(MOCK_EMAIL);
      mockPrisma.users.findUnique.mockResolvedValue(null);
      const req = makeRequest("http://localhost:9000/api/real-estate/clients");
      const res = await clientsGET(req);
      expect(res.status).toBe(404);
      expect(ApiResponses.notFound).toHaveBeenCalledWith("User not found");
    });

    it("returns active client requirements with match counts", async () => {
      setupAuth();
      mockPrisma.clientRequirements.findMany.mockResolvedValue([
        {
          id: "cr-1",
          clientName: "Ali Ahmad",
          clientPhone: "+97412345678",
          _count: { matches: 5 },
        },
        {
          id: "cr-2",
          clientName: "Sara Khalid",
          clientPhone: "+97487654321",
          _count: { matches: 2 },
        },
      ]);

      const req = makeRequest("http://localhost:9000/api/real-estate/clients");
      const res = await clientsGET(req);
      expect(res.status).toBe(200);

      const data = await parseResponse(res);
      expect(data.clients).toHaveLength(2);
      expect(data.clients[0].matchCount).toBe(5);
      expect(data.clients[0]._count).toBeUndefined();
      expect(data.clients[1].matchCount).toBe(2);

      // Verify query was called with isActive = true by default
      expect(mockPrisma.clientRequirements.findMany).toHaveBeenCalledWith(
        expect.objectContaining({
          where: expect.objectContaining({ isActive: true }),
        }),
      );
    });

    it("filters by isActive=false when query param provided", async () => {
      setupAuth();
      mockPrisma.clientRequirements.findMany.mockResolvedValue([]);

      const req = makeRequest(
        "http://localhost:9000/api/real-estate/clients?isActive=false",
      );
      const res = await clientsGET(req);
      expect(res.status).toBe(200);

      expect(mockPrisma.clientRequirements.findMany).toHaveBeenCalledWith(
        expect.objectContaining({
          where: expect.objectContaining({ isActive: false }),
        }),
      );
    });

    it("defaults isActive to true when no query param", async () => {
      setupAuth();
      mockPrisma.clientRequirements.findMany.mockResolvedValue([]);

      const req = makeRequest("http://localhost:9000/api/real-estate/clients");
      await clientsGET(req);

      expect(mockPrisma.clientRequirements.findMany).toHaveBeenCalledWith(
        expect.objectContaining({
          where: expect.objectContaining({ isActive: true }),
        }),
      );
    });

    it("returns 500 on database error", async () => {
      setupAuth();
      mockPrisma.clientRequirements.findMany.mockRejectedValue(
        new Error("DB connection failed"),
      );

      const req = makeRequest("http://localhost:9000/api/real-estate/clients");
      const res = await clientsGET(req);
      expect(res.status).toBe(500);
      expect(ApiResponses.serverError).toHaveBeenCalledWith(
        "Failed to fetch client requirements",
      );
    });
  });

  // ========================================================================
  // POST /api/real-estate/clients
  // ========================================================================
  describe("POST /api/real-estate/clients", () => {
    it("returns 401 when unauthenticated", async () => {
      setupAuth(null);
      const req = makeRequest("http://localhost:9000/api/real-estate/clients", {
        method: "POST",
        body: { clientName: "Test", clientPhone: "+97411111111" },
      });
      const res = await clientsPOST(req);
      expect(res.status).toBe(401);
    });

    it("returns 400 when clientName is missing", async () => {
      setupAuth();
      const req = makeRequest("http://localhost:9000/api/real-estate/clients", {
        method: "POST",
        body: { clientPhone: "+97411111111" },
      });
      const res = await clientsPOST(req);
      expect(res.status).toBe(400);
      expect(ApiResponses.badRequest).toHaveBeenCalledWith(
        "clientName is required",
      );
    });

    it("returns 400 when clientName is empty string", async () => {
      setupAuth();
      const req = makeRequest("http://localhost:9000/api/real-estate/clients", {
        method: "POST",
        body: { clientName: "   ", clientPhone: "+97411111111" },
      });
      const res = await clientsPOST(req);
      expect(res.status).toBe(400);
      expect(ApiResponses.badRequest).toHaveBeenCalledWith(
        "clientName is required",
      );
    });

    it("returns 400 when clientPhone is missing", async () => {
      setupAuth();
      const req = makeRequest("http://localhost:9000/api/real-estate/clients", {
        method: "POST",
        body: { clientName: "Ali" },
      });
      const res = await clientsPOST(req);
      expect(res.status).toBe(400);
      expect(ApiResponses.badRequest).toHaveBeenCalledWith(
        "clientPhone is required",
      );
    });

    it("returns 400 when clientPhone is empty string", async () => {
      setupAuth();
      const req = makeRequest("http://localhost:9000/api/real-estate/clients", {
        method: "POST",
        body: { clientName: "Ali", clientPhone: "" },
      });
      const res = await clientsPOST(req);
      expect(res.status).toBe(400);
      expect(ApiResponses.badRequest).toHaveBeenCalledWith(
        "clientPhone is required",
      );
    });

    it("returns 400 when minBudget exceeds maxBudget", async () => {
      setupAuth();
      const req = makeRequest("http://localhost:9000/api/real-estate/clients", {
        method: "POST",
        body: {
          clientName: "Ali Ahmad",
          clientPhone: "+97412345678",
          minBudget: 500000,
          maxBudget: 100000,
        },
      });
      const res = await clientsPOST(req);
      expect(res.status).toBe(400);
      expect(ApiResponses.badRequest).toHaveBeenCalledWith(
        "minBudget cannot exceed maxBudget",
      );
    });

    it("returns 400 when minBedrooms exceeds maxBedrooms", async () => {
      setupAuth();
      const req = makeRequest("http://localhost:9000/api/real-estate/clients", {
        method: "POST",
        body: {
          clientName: "Ali Ahmad",
          clientPhone: "+97412345678",
          minBedrooms: 5,
          maxBedrooms: 2,
        },
      });
      const res = await clientsPOST(req);
      expect(res.status).toBe(400);
      expect(ApiResponses.badRequest).toHaveBeenCalledWith(
        "minBedrooms cannot exceed maxBedrooms",
      );
    });

    it("creates client requirements with all fields", async () => {
      setupAuth();
      mockPrisma.organization_members.findFirst.mockResolvedValue({
        organizationId: "org-1",
      });
      const createdClient = {
        id: "cr-new",
        clientName: "Ali Ahmad",
        clientPhone: "+97412345678",
        clientEmail: "ali@test.qa",
        listingType: "SALE",
        propertyTypes: ["VILLA", "APARTMENT"],
        minBedrooms: 2,
        maxBedrooms: 5,
        minBudget: 100000,
        maxBudget: 500000,
        currency: "QAR",
        preferredAreas: ["The Pearl", "West Bay"],
        mustHaves: ["pool", "gym"],
        timeline: "Within 3 months",
        purpose: "Investment",
        financingType: "Cash",
      };
      mockPrisma.clientRequirements.create.mockResolvedValue(createdClient);

      const req = makeRequest("http://localhost:9000/api/real-estate/clients", {
        method: "POST",
        body: {
          clientName: "  Ali Ahmad  ",
          clientPhone: " +97412345678 ",
          clientEmail: " ali@test.qa ",
          listingType: "SALE",
          propertyTypes: ["VILLA", "APARTMENT"],
          minBedrooms: 2,
          maxBedrooms: 5,
          minBudget: 100000,
          maxBudget: 500000,
          currency: "QAR",
          preferredAreas: ["The Pearl", "West Bay"],
          mustHaves: ["pool", "gym"],
          timeline: " Within 3 months ",
          purpose: " Investment ",
          financingType: " Cash ",
        },
      });

      const res = await clientsPOST(req);
      expect(res.status).toBe(200);

      const data = await parseResponse(res);
      expect(data.client).toBeDefined();
      expect(data.client.clientName).toBe("Ali Ahmad");

      // Verify create was called with trimmed values
      expect(mockPrisma.clientRequirements.create).toHaveBeenCalledWith({
        data: expect.objectContaining({
          clientName: "Ali Ahmad",
          clientPhone: "+97412345678",
          clientEmail: "ali@test.qa",
          userId: "user-1",
          organizationId: "org-1",
          currency: "QAR",
          propertyTypes: ["VILLA", "APARTMENT"],
          preferredAreas: ["The Pearl", "West Bay"],
          mustHaves: ["pool", "gym"],
        }),
      });
    });

    it("creates client with null organizationId if no membership", async () => {
      setupAuth();
      mockPrisma.organization_members.findFirst.mockResolvedValue(null);
      mockPrisma.clientRequirements.create.mockResolvedValue({
        id: "cr-new",
        clientName: "Ali",
        clientPhone: "+97412345678",
        organizationId: null,
      });

      const req = makeRequest("http://localhost:9000/api/real-estate/clients", {
        method: "POST",
        body: { clientName: "Ali", clientPhone: "+97412345678" },
      });

      const res = await clientsPOST(req);
      expect(res.status).toBe(200);

      expect(mockPrisma.clientRequirements.create).toHaveBeenCalledWith({
        data: expect.objectContaining({
          organizationId: null,
        }),
      });
    });

    it("allows valid budget range (minBudget <= maxBudget)", async () => {
      setupAuth();
      mockPrisma.organization_members.findFirst.mockResolvedValue(null);
      mockPrisma.clientRequirements.create.mockResolvedValue({
        id: "cr-new",
        clientName: "Ali",
        clientPhone: "+97412345678",
        minBudget: 100000,
        maxBudget: 500000,
      });

      const req = makeRequest("http://localhost:9000/api/real-estate/clients", {
        method: "POST",
        body: {
          clientName: "Ali",
          clientPhone: "+97412345678",
          minBudget: 100000,
          maxBudget: 500000,
        },
      });

      const res = await clientsPOST(req);
      expect(res.status).toBe(200);
    });

    it("uses default currency QAR if not provided", async () => {
      setupAuth();
      mockPrisma.organization_members.findFirst.mockResolvedValue(null);
      mockPrisma.clientRequirements.create.mockResolvedValue({
        id: "cr-new",
        clientName: "Ali",
        clientPhone: "+97412345678",
        currency: "QAR",
      });

      const req = makeRequest("http://localhost:9000/api/real-estate/clients", {
        method: "POST",
        body: { clientName: "Ali", clientPhone: "+97412345678" },
      });

      await clientsPOST(req);

      expect(mockPrisma.clientRequirements.create).toHaveBeenCalledWith({
        data: expect.objectContaining({
          currency: "QAR",
        }),
      });
    });

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

      const req = makeRequest("http://localhost:9000/api/real-estate/clients", {
        method: "POST",
        body: { clientName: "Ali", clientPhone: "+97412345678" },
      });

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

  // ========================================================================
  // GET /api/real-estate/clients/[id]
  // ========================================================================
  describe("GET /api/real-estate/clients/[id]", () => {
    const params = { params: Promise.resolve({ id: "cr-1" }) };

    it("returns 401 when unauthenticated", async () => {
      setupAuth(null);
      const req = makeRequest(
        "http://localhost:9000/api/real-estate/clients/cr-1",
      );
      const res = await clientDetailGET(req, params);
      expect(res.status).toBe(401);
    });

    it("returns 404 when user not found", async () => {
      mockGetAuthenticatedEmail.mockResolvedValue(MOCK_EMAIL);
      mockPrisma.users.findUnique.mockResolvedValue(null);
      const req = makeRequest(
        "http://localhost:9000/api/real-estate/clients/cr-1",
      );
      const res = await clientDetailGET(req, params);
      expect(res.status).toBe(404);
      expect(ApiResponses.notFound).toHaveBeenCalledWith("User not found");
    });

    it("returns 404 when client not found or belongs to different user", async () => {
      setupAuth();
      mockPrisma.clientRequirements.findFirst.mockResolvedValue(null);

      const req = makeRequest(
        "http://localhost:9000/api/real-estate/clients/cr-999",
      );
      const res = await clientDetailGET(req, {
        params: Promise.resolve({ id: "cr-999" }),
      });
      expect(res.status).toBe(404);
      expect(ApiResponses.notFound).toHaveBeenCalledWith(
        "Client requirements not found",
      );
    });

    it("returns client detail with top 10 matches", async () => {
      setupAuth();
      const mockClient = {
        id: "cr-1",
        clientName: "Ali Ahmad",
        clientPhone: "+97412345678",
        matches: [
          {
            id: "match-1",
            matchScore: 85,
            property: {
              id: "prop-1",
              titleEn: "Luxury Villa",
              titleAr: null,
              propertyType: "VILLA",
              listingType: "SALE",
              status: "AVAILABLE",
              price: 2000000,
              currency: "QAR",
              bedrooms: 5,
              bathrooms: 4,
              builtUpArea: 500,
              city: "Doha",
              district: "The Pearl",
              images: [{ url: "https://img.example.com/villa.jpg" }],
            },
          },
        ],
      };
      mockPrisma.clientRequirements.findFirst.mockResolvedValue(mockClient);

      const req = makeRequest(
        "http://localhost:9000/api/real-estate/clients/cr-1",
      );
      const res = await clientDetailGET(req, params);
      expect(res.status).toBe(200);

      const data = await parseResponse(res);
      expect(data.client.id).toBe("cr-1");
      expect(data.client.matches).toHaveLength(1);
      expect(data.client.matches[0].matchScore).toBe(85);

      // Verify findFirst includes matches ordered by score
      expect(mockPrisma.clientRequirements.findFirst).toHaveBeenCalledWith({
        where: { id: "cr-1", userId: "user-1" },
        include: expect.objectContaining({
          matches: expect.objectContaining({
            orderBy: { matchScore: "desc" },
            take: 10,
          }),
        }),
      });
    });

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

      const req = makeRequest(
        "http://localhost:9000/api/real-estate/clients/cr-1",
      );
      const res = await clientDetailGET(req, params);
      expect(res.status).toBe(500);
      expect(ApiResponses.serverError).toHaveBeenCalledWith(
        "Failed to fetch client details",
      );
    });
  });

  // ========================================================================
  // PUT /api/real-estate/clients/[id]
  // ========================================================================
  describe("PUT /api/real-estate/clients/[id]", () => {
    const params = { params: Promise.resolve({ id: "cr-1" }) };

    const existingClient = {
      id: "cr-1",
      userId: "user-1",
      clientName: "Ali Ahmad",
      clientPhone: "+97412345678",
      minBudget: 100000,
      maxBudget: 500000,
      minBedrooms: 2,
      maxBedrooms: 5,
      listingType: "SALE",
      propertyTypes: ["VILLA"],
      preferredAreas: ["The Pearl"],
      mustHaves: ["pool"],
    };

    it("returns 401 when unauthenticated", async () => {
      setupAuth(null);
      const req = makeRequest(
        "http://localhost:9000/api/real-estate/clients/cr-1",
        { method: "PUT", body: { clientName: "Updated" } },
      );
      const res = await clientDetailPUT(req, params);
      expect(res.status).toBe(401);
    });

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

      const req = makeRequest(
        "http://localhost:9000/api/real-estate/clients/cr-1",
        { method: "PUT", body: { clientName: "Updated" } },
      );
      const res = await clientDetailPUT(req, params);
      expect(res.status).toBe(404);
      expect(ApiResponses.notFound).toHaveBeenCalledWith(
        "Client requirements not found",
      );
    });

    it("returns 400 when updated minBudget exceeds maxBudget", async () => {
      setupAuth();
      mockPrisma.clientRequirements.findFirst.mockResolvedValue(existingClient);

      const req = makeRequest(
        "http://localhost:9000/api/real-estate/clients/cr-1",
        {
          method: "PUT",
          body: { minBudget: 600000 },
        },
      );
      const res = await clientDetailPUT(req, params);
      expect(res.status).toBe(400);
      expect(ApiResponses.badRequest).toHaveBeenCalledWith(
        "minBudget cannot exceed maxBudget",
      );
    });

    it("returns 400 when new maxBudget is below existing minBudget", async () => {
      setupAuth();
      mockPrisma.clientRequirements.findFirst.mockResolvedValue(existingClient);

      const req = makeRequest(
        "http://localhost:9000/api/real-estate/clients/cr-1",
        {
          method: "PUT",
          body: { maxBudget: 50000 },
        },
      );
      const res = await clientDetailPUT(req, params);
      expect(res.status).toBe(400);
      expect(ApiResponses.badRequest).toHaveBeenCalledWith(
        "minBudget cannot exceed maxBudget",
      );
    });

    it("updates allowed fields and re-runs matching", async () => {
      setupAuth();
      mockPrisma.clientRequirements.findFirst.mockResolvedValue(existingClient);

      const updatedClient = {
        ...existingClient,
        clientName: "Ali Ahmad Updated",
        maxBudget: 700000,
        id: "cr-1",
        listingType: "SALE",
        propertyTypes: ["VILLA"],
        preferredAreas: ["The Pearl"],
        mustHaves: ["pool"],
        minBudget: 100000,
        minBedrooms: null,
        maxBedrooms: null,
      };
      mockPrisma.clientRequirements.update.mockResolvedValue(updatedClient);

      // Mock properties for matching (returns empty so matching is simple)
      mockPrisma.property.findMany.mockResolvedValue([]);

      const req = makeRequest(
        "http://localhost:9000/api/real-estate/clients/cr-1",
        {
          method: "PUT",
          body: { clientName: "  Ali Ahmad Updated  ", maxBudget: 700000 },
        },
      );
      const res = await clientDetailPUT(req, params);
      expect(res.status).toBe(200);

      const data = await parseResponse(res);
      expect(data.client).toBeDefined();
      expect(data.matchesRefreshed).toBe(0);

      // Verify update was called with trimmed string and lastContactAt
      expect(mockPrisma.clientRequirements.update).toHaveBeenCalledWith({
        where: { id: "cr-1" },
        data: expect.objectContaining({
          clientName: "Ali Ahmad Updated",
          maxBudget: 700000,
          lastContactAt: expect.any(Date),
        }),
      });
    });

    it("runs matching algorithm after update and returns top matches", async () => {
      setupAuth();
      mockPrisma.clientRequirements.findFirst.mockResolvedValue(existingClient);

      const updatedClient = {
        ...existingClient,
        id: "cr-1",
        minBudget: 100000,
        maxBudget: 500000,
        minBedrooms: null,
        maxBedrooms: null,
        listingType: "SALE",
        propertyTypes: ["VILLA"],
        preferredAreas: ["The Pearl"],
        mustHaves: ["pool"],
      };
      mockPrisma.clientRequirements.update.mockResolvedValue(updatedClient);

      // Mock property that matches
      mockPrisma.property.findMany.mockResolvedValue([
        {
          id: "prop-1",
          propertyType: "VILLA",
          listingType: "SALE",
          price: 300000,
          currency: "QAR",
          bedrooms: 3,
          builtUpArea: 300,
          city: "Doha",
          district: "The Pearl",
          amenities: ["Swimming Pool", "Gym"],
        },
      ]);
      mockPrisma.propertyMatch.upsert.mockResolvedValue({});

      const req = makeRequest(
        "http://localhost:9000/api/real-estate/clients/cr-1",
        { method: "PUT", body: { leadScore: 80 } },
      );
      const res = await clientDetailPUT(req, params);
      expect(res.status).toBe(200);

      const data = await parseResponse(res);
      expect(data.matchesRefreshed).toBeGreaterThan(0);
      expect(data.topMatches).toBeDefined();

      // Upsert should have been called for the matching property
      expect(mockPrisma.propertyMatch.upsert).toHaveBeenCalled();
    });

    it("ignores disallowed fields in update payload", async () => {
      setupAuth();
      mockPrisma.clientRequirements.findFirst.mockResolvedValue({
        ...existingClient,
        minBudget: null,
        maxBudget: null,
      });
      mockPrisma.clientRequirements.update.mockResolvedValue({
        ...existingClient,
        minBudget: null,
        maxBudget: null,
      });
      mockPrisma.property.findMany.mockResolvedValue([]);

      const req = makeRequest(
        "http://localhost:9000/api/real-estate/clients/cr-1",
        {
          method: "PUT",
          body: {
            id: "hacked-id",
            userId: "hacked-user",
            createdAt: "2020-01-01",
            clientName: "Valid Name",
          },
        },
      );
      const res = await clientDetailPUT(req, params);
      expect(res.status).toBe(200);

      const updateCall = mockPrisma.clientRequirements.update.mock.calls[0][0];
      expect(updateCall.data.id).toBeUndefined();
      expect(updateCall.data.userId).toBeUndefined();
      expect(updateCall.data.createdAt).toBeUndefined();
      expect(updateCall.data.clientName).toBe("Valid Name");
    });

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

      const req = makeRequest(
        "http://localhost:9000/api/real-estate/clients/cr-1",
        { method: "PUT", body: { clientName: "X" } },
      );
      const res = await clientDetailPUT(req, params);
      expect(res.status).toBe(500);
    });
  });

  // ========================================================================
  // DELETE /api/real-estate/clients/[id]
  // ========================================================================
  describe("DELETE /api/real-estate/clients/[id]", () => {
    const params = { params: Promise.resolve({ id: "cr-1" }) };

    it("returns 401 when unauthenticated", async () => {
      setupAuth(null);
      const req = makeRequest(
        "http://localhost:9000/api/real-estate/clients/cr-1",
        { method: "DELETE" },
      );
      const res = await clientDetailDELETE(req, params);
      expect(res.status).toBe(401);
    });

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

      const req = makeRequest(
        "http://localhost:9000/api/real-estate/clients/cr-1",
        { method: "DELETE" },
      );
      const res = await clientDetailDELETE(req, params);
      expect(res.status).toBe(404);
      expect(ApiResponses.notFound).toHaveBeenCalledWith(
        "Client requirements not found",
      );
    });

    it("soft-deletes client and removes matches via transaction", async () => {
      setupAuth();
      mockPrisma.clientRequirements.findFirst.mockResolvedValue({
        id: "cr-1",
        userId: "user-1",
        clientName: "Ali Ahmad",
      });

      // Mock $transaction to execute the callback
      mockPrisma.$transaction.mockImplementation(async (fn: Function) => {
        const tx = {
          clientRequirements: { update: jest.fn() },
          propertyMatch: { deleteMany: jest.fn() },
        };
        await fn(tx);
        return tx;
      });

      const req = makeRequest(
        "http://localhost:9000/api/real-estate/clients/cr-1",
        { method: "DELETE" },
      );
      const res = await clientDetailDELETE(req, params);
      expect(res.status).toBe(200);

      const data = await parseResponse(res);
      expect(data.deleted).toBe(true);
      expect(data.message).toContain("deactivated");

      // Verify $transaction was called
      expect(mockPrisma.$transaction).toHaveBeenCalled();
    });

    it("passes correct data to transaction operations", async () => {
      setupAuth();
      mockPrisma.clientRequirements.findFirst.mockResolvedValue({
        id: "cr-1",
        userId: "user-1",
        clientName: "Ali Ahmad",
      });

      const txUpdate = jest.fn();
      const txDeleteMany = jest.fn();
      mockPrisma.$transaction.mockImplementation(async (fn: Function) => {
        const tx = {
          clientRequirements: { update: txUpdate },
          propertyMatch: { deleteMany: txDeleteMany },
        };
        await fn(tx);
      });

      const req = makeRequest(
        "http://localhost:9000/api/real-estate/clients/cr-1",
        { method: "DELETE" },
      );
      await clientDetailDELETE(req, params);

      expect(txUpdate).toHaveBeenCalledWith({
        where: { id: "cr-1" },
        data: { isActive: false },
      });
      expect(txDeleteMany).toHaveBeenCalledWith({
        where: { requirementId: "cr-1" },
      });
    });

    it("returns 500 on transaction error", async () => {
      setupAuth();
      mockPrisma.clientRequirements.findFirst.mockResolvedValue({
        id: "cr-1",
        userId: "user-1",
        clientName: "Ali",
      });
      mockPrisma.$transaction.mockRejectedValue(
        new Error("Transaction failed"),
      );

      const req = makeRequest(
        "http://localhost:9000/api/real-estate/clients/cr-1",
        { method: "DELETE" },
      );
      const res = await clientDetailDELETE(req, params);
      expect(res.status).toBe(500);
      expect(ApiResponses.serverError).toHaveBeenCalledWith(
        "Failed to delete client requirements",
      );
    });
  });

  // ========================================================================
  // GET /api/real-estate/matches/[clientId]
  // ========================================================================
  describe("GET /api/real-estate/matches/[clientId]", () => {
    const params = { params: Promise.resolve({ clientId: "cr-1" }) };

    const requirements = {
      id: "cr-1",
      userId: "user-1",
      clientName: "Ali Ahmad",
      listingType: "SALE",
      propertyTypes: ["VILLA", "APARTMENT"],
      minBedrooms: 2,
      maxBedrooms: 5,
      minBudget: 200000,
      maxBudget: 500000,
      currency: "QAR",
      preferredAreas: ["The Pearl", "West Bay"],
      mustHaves: ["pool", "gym", "parking"],
    };

    it("returns 401 when unauthenticated", async () => {
      setupAuth(null);
      const req = makeRequest(
        "http://localhost:9000/api/real-estate/matches/cr-1",
      );
      const res = await matchesGET(req, params);
      expect(res.status).toBe(401);
    });

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

      const req = makeRequest(
        "http://localhost:9000/api/real-estate/matches/cr-1",
      );
      const res = await matchesGET(req, params);
      expect(res.status).toBe(404);
    });

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

      const req = makeRequest(
        "http://localhost:9000/api/real-estate/matches/cr-1",
      );
      const res = await matchesGET(req, params);
      expect(res.status).toBe(404);
      expect(ApiResponses.notFound).toHaveBeenCalledWith(
        "Client requirements not found",
      );
    });

    it("returns empty matches when no properties available", async () => {
      setupAuth();
      mockPrisma.clientRequirements.findFirst.mockResolvedValue(requirements);
      mockPrisma.property.findMany.mockResolvedValue([]);

      const req = makeRequest(
        "http://localhost:9000/api/real-estate/matches/cr-1",
      );
      const res = await matchesGET(req, params);
      expect(res.status).toBe(200);

      const data = await parseResponse(res);
      expect(data.matches).toHaveLength(0);
      expect(data.totalAvailable).toBe(0);
      expect(data.clientId).toBe("cr-1");
    });

    // ── Matching Algorithm Tests ──────────────────────────────────────

    describe("Matching Algorithm Scoring", () => {
      beforeEach(() => {
        setupAuth();
        mockPrisma.clientRequirements.findFirst.mockResolvedValue(requirements);
        mockPrisma.propertyMatch.upsert.mockResolvedValue({});
      });

      it("awards 30 points for price within budget", async () => {
        mockPrisma.property.findMany.mockResolvedValue([
          {
            id: "prop-1",
            propertyType: "OTHER",
            listingType: "SALE",
            price: 300000,
            currency: "QAR",
            bedrooms: null,
            builtUpArea: null,
            city: null,
            district: null,
            amenities: [],
            images: [],
          },
        ]);

        const req = makeRequest(
          "http://localhost:9000/api/real-estate/matches/cr-1",
        );
        const res = await matchesGET(req, params);
        const data = await parseResponse(res);

        expect(data.matches[0].breakdown.price).toBe(30);
        expect(data.matches[0].reasons).toContain("Price within budget");
      });

      it("awards 20 points for price slightly over budget (within 10%)", async () => {
        // maxBudget=500000, so 10% over = 550000
        mockPrisma.property.findMany.mockResolvedValue([
          {
            id: "prop-1",
            propertyType: "OTHER",
            listingType: "SALE",
            price: 540000, // within 10%
            currency: "QAR",
            bedrooms: null,
            builtUpArea: null,
            city: null,
            district: null,
            amenities: [],
            images: [],
          },
        ]);

        const req = makeRequest(
          "http://localhost:9000/api/real-estate/matches/cr-1",
        );
        const res = await matchesGET(req, params);
        const data = await parseResponse(res);

        expect(data.matches[0].breakdown.price).toBe(20);
        expect(data.matches[0].reasons).toContain(
          "Price slightly over budget (within 10%)",
        );
      });

      it("awards 10 points for price moderately over budget (within 25%)", async () => {
        // maxBudget=500000, 25% over = 625000
        mockPrisma.property.findMany.mockResolvedValue([
          {
            id: "prop-1",
            propertyType: "OTHER",
            listingType: "SALE",
            price: 600000, // between 10-25% over
            currency: "QAR",
            bedrooms: null,
            builtUpArea: null,
            city: null,
            district: null,
            amenities: [],
            images: [],
          },
        ]);

        const req = makeRequest(
          "http://localhost:9000/api/real-estate/matches/cr-1",
        );
        const res = await matchesGET(req, params);
        const data = await parseResponse(res);

        expect(data.matches[0].breakdown.price).toBe(10);
        expect(data.matches[0].reasons).toContain(
          "Price moderately over budget (within 25%)",
        );
      });

      it("awards 0 points for price way over budget", async () => {
        mockPrisma.property.findMany.mockResolvedValue([
          {
            id: "prop-1",
            propertyType: "OTHER",
            listingType: "SALE",
            price: 1000000, // way over budget
            currency: "QAR",
            bedrooms: null,
            builtUpArea: null,
            city: null,
            district: null,
            amenities: [],
            images: [],
          },
        ]);

        const req = makeRequest(
          "http://localhost:9000/api/real-estate/matches/cr-1",
        );
        const res = await matchesGET(req, params);
        const data = await parseResponse(res);

        expect(data.matches[0].breakdown.price).toBe(0);
      });

      it("awards 15 points for price when no budget set", async () => {
        mockPrisma.clientRequirements.findFirst.mockResolvedValue({
          ...requirements,
          minBudget: null,
          maxBudget: null,
        });
        mockPrisma.property.findMany.mockResolvedValue([
          {
            id: "prop-1",
            propertyType: "OTHER",
            listingType: "SALE",
            price: 999999,
            currency: "QAR",
            bedrooms: null,
            builtUpArea: null,
            city: null,
            district: null,
            amenities: [],
            images: [],
          },
        ]);

        const req = makeRequest(
          "http://localhost:9000/api/real-estate/matches/cr-1",
        );
        const res = await matchesGET(req, params);
        const data = await parseResponse(res);

        expect(data.matches[0].breakdown.price).toBe(15);
      });

      it("awards 20 points for bedroom match", async () => {
        mockPrisma.property.findMany.mockResolvedValue([
          {
            id: "prop-1",
            propertyType: "OTHER",
            listingType: "SALE",
            price: 300000,
            currency: "QAR",
            bedrooms: 3,
            builtUpArea: null,
            city: null,
            district: null,
            amenities: [],
            images: [],
          },
        ]);

        const req = makeRequest(
          "http://localhost:9000/api/real-estate/matches/cr-1",
        );
        const res = await matchesGET(req, params);
        const data = await parseResponse(res);

        expect(data.matches[0].breakdown.bedrooms).toBe(20);
      });

      it("awards 10 points when bedrooms close to range", async () => {
        // minBedrooms=2, so bedrooms=1 is close
        mockPrisma.property.findMany.mockResolvedValue([
          {
            id: "prop-1",
            propertyType: "OTHER",
            listingType: "SALE",
            price: 300000,
            currency: "QAR",
            bedrooms: 1,
            builtUpArea: null,
            city: null,
            district: null,
            amenities: [],
            images: [],
          },
        ]);

        const req = makeRequest(
          "http://localhost:9000/api/real-estate/matches/cr-1",
        );
        const res = await matchesGET(req, params);
        const data = await parseResponse(res);

        expect(data.matches[0].breakdown.bedrooms).toBe(10);
        expect(data.matches[0].reasons).toContain("Bedrooms close to range");
      });

      it("awards 10 points when no bedroom requirement set", async () => {
        mockPrisma.clientRequirements.findFirst.mockResolvedValue({
          ...requirements,
          minBedrooms: null,
          maxBedrooms: null,
        });
        mockPrisma.property.findMany.mockResolvedValue([
          {
            id: "prop-1",
            propertyType: "OTHER",
            listingType: "SALE",
            price: 300000,
            currency: "QAR",
            bedrooms: 3,
            builtUpArea: null,
            city: null,
            district: null,
            amenities: [],
            images: [],
          },
        ]);

        const req = makeRequest(
          "http://localhost:9000/api/real-estate/matches/cr-1",
        );
        const res = await matchesGET(req, params);
        const data = await parseResponse(res);

        expect(data.matches[0].breakdown.bedrooms).toBe(10);
      });

      it("awards 25 points for district match", async () => {
        mockPrisma.property.findMany.mockResolvedValue([
          {
            id: "prop-1",
            propertyType: "OTHER",
            listingType: "SALE",
            price: 300000,
            currency: "QAR",
            bedrooms: 3,
            builtUpArea: null,
            city: "Doha",
            district: "The Pearl",
            amenities: [],
            images: [],
          },
        ]);

        const req = makeRequest(
          "http://localhost:9000/api/real-estate/matches/cr-1",
        );
        const res = await matchesGET(req, params);
        const data = await parseResponse(res);

        expect(data.matches[0].breakdown.area).toBe(25);
        expect(data.matches[0].reasons).toContain(
          "Preferred district: The Pearl",
        );
      });

      it("awards 15 points for city match (when district not in preferredAreas)", async () => {
        mockPrisma.property.findMany.mockResolvedValue([
          {
            id: "prop-1",
            propertyType: "OTHER",
            listingType: "SALE",
            price: 300000,
            currency: "QAR",
            bedrooms: 3,
            builtUpArea: null,
            city: "West Bay",
            district: "Al Dafna",
            amenities: [],
            images: [],
          },
        ]);

        const req = makeRequest(
          "http://localhost:9000/api/real-estate/matches/cr-1",
        );
        const res = await matchesGET(req, params);
        const data = await parseResponse(res);

        // District 'Al Dafna' is not in preferredAreas, but city 'West Bay' is
        expect(data.matches[0].breakdown.area).toBe(15);
        expect(data.matches[0].reasons).toContain("Preferred city: West Bay");
      });

      it("awards 0 area points when neither city nor district match", async () => {
        mockPrisma.property.findMany.mockResolvedValue([
          {
            id: "prop-1",
            propertyType: "OTHER",
            listingType: "SALE",
            price: 300000,
            currency: "QAR",
            bedrooms: 3,
            builtUpArea: null,
            city: "Al Khor",
            district: "Industrial Area",
            amenities: [],
            images: [],
          },
        ]);

        const req = makeRequest(
          "http://localhost:9000/api/real-estate/matches/cr-1",
        );
        const res = await matchesGET(req, params);
        const data = await parseResponse(res);

        expect(data.matches[0].breakdown.area).toBe(0);
      });

      it("awards 12 area points when no preferred areas set", async () => {
        mockPrisma.clientRequirements.findFirst.mockResolvedValue({
          ...requirements,
          preferredAreas: [],
        });
        mockPrisma.property.findMany.mockResolvedValue([
          {
            id: "prop-1",
            propertyType: "OTHER",
            listingType: "SALE",
            price: 300000,
            currency: "QAR",
            bedrooms: 3,
            builtUpArea: null,
            city: "Anywhere",
            district: "Anywhere District",
            amenities: [],
            images: [],
          },
        ]);

        const req = makeRequest(
          "http://localhost:9000/api/real-estate/matches/cr-1",
        );
        const res = await matchesGET(req, params);
        const data = await parseResponse(res);

        expect(data.matches[0].breakdown.area).toBe(12);
      });

      it("awards 10 points for property type match", async () => {
        mockPrisma.property.findMany.mockResolvedValue([
          {
            id: "prop-1",
            propertyType: "VILLA",
            listingType: "SALE",
            price: 300000,
            currency: "QAR",
            bedrooms: 3,
            builtUpArea: null,
            city: null,
            district: null,
            amenities: [],
            images: [],
          },
        ]);

        const req = makeRequest(
          "http://localhost:9000/api/real-estate/matches/cr-1",
        );
        const res = await matchesGET(req, params);
        const data = await parseResponse(res);

        expect(data.matches[0].breakdown.type).toBe(10);
        expect(data.matches[0].reasons).toContain("Type: VILLA");
      });

      it("awards 0 type points when type does not match", async () => {
        mockPrisma.property.findMany.mockResolvedValue([
          {
            id: "prop-1",
            propertyType: "LAND",
            listingType: "SALE",
            price: 300000,
            currency: "QAR",
            bedrooms: 3,
            builtUpArea: null,
            city: null,
            district: null,
            amenities: [],
            images: [],
          },
        ]);

        const req = makeRequest(
          "http://localhost:9000/api/real-estate/matches/cr-1",
        );
        const res = await matchesGET(req, params);
        const data = await parseResponse(res);

        expect(data.matches[0].breakdown.type).toBe(0);
      });

      it("awards 5 type points when no property types required", async () => {
        mockPrisma.clientRequirements.findFirst.mockResolvedValue({
          ...requirements,
          propertyTypes: [],
        });
        mockPrisma.property.findMany.mockResolvedValue([
          {
            id: "prop-1",
            propertyType: "VILLA",
            listingType: "SALE",
            price: 300000,
            currency: "QAR",
            bedrooms: 3,
            builtUpArea: null,
            city: null,
            district: null,
            amenities: [],
            images: [],
          },
        ]);

        const req = makeRequest(
          "http://localhost:9000/api/real-estate/matches/cr-1",
        );
        const res = await matchesGET(req, params);
        const data = await parseResponse(res);

        expect(data.matches[0].breakdown.type).toBe(5);
      });

      it("scores amenities proportionally (partial match)", async () => {
        // mustHaves: ['pool', 'gym', 'parking'] -> 3 items
        mockPrisma.property.findMany.mockResolvedValue([
          {
            id: "prop-1",
            propertyType: "OTHER",
            listingType: "SALE",
            price: 300000,
            currency: "QAR",
            bedrooms: 3,
            builtUpArea: null,
            city: null,
            district: null,
            amenities: ["Swimming Pool", "Gym"], // matches 'pool' and 'gym' (case insensitive includes)
            images: [],
          },
        ]);

        const req = makeRequest(
          "http://localhost:9000/api/real-estate/matches/cr-1",
        );
        const res = await matchesGET(req, params);
        const data = await parseResponse(res);

        // 2/3 must-haves matched => round(2/3 * 15) = 10
        expect(data.matches[0].breakdown.amenities).toBe(10);
        expect(data.matches[0].reasons).toContain("2/3 must-haves found");
      });

      it("scores amenities full match at 15 points", async () => {
        mockPrisma.property.findMany.mockResolvedValue([
          {
            id: "prop-1",
            propertyType: "OTHER",
            listingType: "SALE",
            price: 300000,
            currency: "QAR",
            bedrooms: 3,
            builtUpArea: null,
            city: null,
            district: null,
            amenities: ["Swimming Pool", "Gym", "Covered Parking"],
            images: [],
          },
        ]);

        const req = makeRequest(
          "http://localhost:9000/api/real-estate/matches/cr-1",
        );
        const res = await matchesGET(req, params);
        const data = await parseResponse(res);

        // All 3/3 matched => round(3/3 * 15) = 15
        expect(data.matches[0].breakdown.amenities).toBe(15);
      });

      it("awards 7 amenity points when no must-haves required", async () => {
        mockPrisma.clientRequirements.findFirst.mockResolvedValue({
          ...requirements,
          mustHaves: [],
        });
        mockPrisma.property.findMany.mockResolvedValue([
          {
            id: "prop-1",
            propertyType: "OTHER",
            listingType: "SALE",
            price: 300000,
            currency: "QAR",
            bedrooms: 3,
            builtUpArea: null,
            city: null,
            district: null,
            amenities: ["Pool"],
            images: [],
          },
        ]);

        const req = makeRequest(
          "http://localhost:9000/api/real-estate/matches/cr-1",
        );
        const res = await matchesGET(req, params);
        const data = await parseResponse(res);

        expect(data.matches[0].breakdown.amenities).toBe(7);
      });

      it("computes perfect score (100) for ideal match", async () => {
        mockPrisma.property.findMany.mockResolvedValue([
          {
            id: "prop-perfect",
            propertyType: "VILLA",
            listingType: "SALE",
            price: 400000,
            currency: "QAR",
            bedrooms: 4,
            builtUpArea: 350,
            city: "Doha",
            district: "The Pearl",
            amenities: ["Swimming Pool", "Gym Equipment", "Parking"],
            images: [{ url: "https://img.example.com/perfect.jpg" }],
          },
        ]);

        const req = makeRequest(
          "http://localhost:9000/api/real-estate/matches/cr-1",
        );
        const res = await matchesGET(req, params);
        const data = await parseResponse(res);

        // Price: 30 (in range) + Bedrooms: 20 (in range) + Area: 25 (district) + Amenities: 15 (all) + Type: 10 = 100
        expect(data.matches[0].score).toBe(100);
        expect(data.matches[0].maxScore).toBe(100);
        expect(data.matches[0].breakdown).toEqual({
          price: 30,
          bedrooms: 20,
          area: 25,
          amenities: 15,
          type: 10,
        });
      });

      it("sorts matches by score descending", async () => {
        mockPrisma.property.findMany.mockResolvedValue([
          {
            id: "prop-low",
            propertyType: "LAND",
            listingType: "SALE",
            price: 900000,
            currency: "QAR",
            bedrooms: null,
            builtUpArea: null,
            city: null,
            district: null,
            amenities: [],
            images: [],
          },
          {
            id: "prop-high",
            propertyType: "VILLA",
            listingType: "SALE",
            price: 350000,
            currency: "QAR",
            bedrooms: 3,
            builtUpArea: 300,
            city: "Doha",
            district: "The Pearl",
            amenities: ["Swimming Pool", "Gym", "Parking"],
            images: [],
          },
        ]);

        const req = makeRequest(
          "http://localhost:9000/api/real-estate/matches/cr-1",
        );
        const res = await matchesGET(req, params);
        const data = await parseResponse(res);

        expect(data.matches.length).toBe(2);
        expect(data.matches[0].score).toBeGreaterThan(data.matches[1].score);
        expect(data.matches[0].propertyId).toBe("prop-high");
      });

      it("returns top 20 matches maximum", async () => {
        // Create 25 properties
        const properties = Array.from({ length: 25 }, (_, i) => ({
          id: `prop-${i}`,
          propertyType: "VILLA",
          listingType: "SALE",
          price: 300000 + i * 1000,
          currency: "QAR",
          bedrooms: 3,
          builtUpArea: 200,
          city: "Doha",
          district: "The Pearl",
          amenities: ["Swimming Pool", "Gym", "Parking"],
          images: [],
        }));
        mockPrisma.property.findMany.mockResolvedValue(properties);

        const req = makeRequest(
          "http://localhost:9000/api/real-estate/matches/cr-1",
        );
        const res = await matchesGET(req, params);
        const data = await parseResponse(res);

        expect(data.matches.length).toBeLessThanOrEqual(20);
        expect(data.totalAvailable).toBe(25);
      });

      it("upserts PropertyMatch records for top 50", async () => {
        const properties = Array.from({ length: 3 }, (_, i) => ({
          id: `prop-${i}`,
          propertyType: "VILLA",
          listingType: "SALE",
          price: 300000,
          currency: "QAR",
          bedrooms: 3,
          builtUpArea: 200,
          city: "Doha",
          district: "The Pearl",
          amenities: [],
          images: [],
        }));
        mockPrisma.property.findMany.mockResolvedValue(properties);

        const req = makeRequest(
          "http://localhost:9000/api/real-estate/matches/cr-1",
        );
        await matchesGET(req, params);

        expect(mockPrisma.propertyMatch.upsert).toHaveBeenCalledTimes(3);
        expect(mockPrisma.propertyMatch.upsert).toHaveBeenCalledWith(
          expect.objectContaining({
            where: {
              propertyId_requirementId: expect.objectContaining({
                requirementId: "cr-1",
              }),
            },
          }),
        );
      });

      it("includes image URL in match response", async () => {
        mockPrisma.property.findMany.mockResolvedValue([
          {
            id: "prop-1",
            propertyType: "VILLA",
            listingType: "SALE",
            price: 300000,
            currency: "QAR",
            bedrooms: 3,
            builtUpArea: 200,
            city: null,
            district: null,
            amenities: [],
            images: [{ url: "https://img.example.com/prop1.jpg" }],
          },
        ]);

        const req = makeRequest(
          "http://localhost:9000/api/real-estate/matches/cr-1",
        );
        const res = await matchesGET(req, params);
        const data = await parseResponse(res);

        expect(data.matches[0].imageUrl).toBe(
          "https://img.example.com/prop1.jpg",
        );
      });

      it("returns null imageUrl when no images", async () => {
        mockPrisma.property.findMany.mockResolvedValue([
          {
            id: "prop-1",
            propertyType: "VILLA",
            listingType: "SALE",
            price: 300000,
            currency: "QAR",
            bedrooms: 3,
            builtUpArea: 200,
            city: null,
            district: null,
            amenities: [],
            images: [],
          },
        ]);

        const req = makeRequest(
          "http://localhost:9000/api/real-estate/matches/cr-1",
        );
        const res = await matchesGET(req, params);
        const data = await parseResponse(res);

        expect(data.matches[0].imageUrl).toBeNull();
      });
    });

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

      const req = makeRequest(
        "http://localhost:9000/api/real-estate/matches/cr-1",
      );
      const res = await matchesGET(req, params);
      expect(res.status).toBe(500);
      expect(ApiResponses.serverError).toHaveBeenCalledWith(
        "Failed to compute matches",
      );
    });
  });

  // ========================================================================
  // POST /api/real-estate/matches/refresh
  // ========================================================================
  describe("POST /api/real-estate/matches/refresh", () => {
    it("returns 401 when unauthenticated", async () => {
      setupAuth(null);
      const req = makeRequest(
        "http://localhost:9000/api/real-estate/matches/refresh",
        { method: "POST", body: {} },
      );
      const res = await matchesRefreshPOST(req);
      expect(res.status).toBe(401);
    });

    it("returns success with refreshed=0 when no active clients", async () => {
      setupAuth();
      mockPrisma.clientRequirements.findMany.mockResolvedValue([]);

      const req = makeRequest(
        "http://localhost:9000/api/real-estate/matches/refresh",
        { method: "POST", body: {} },
      );
      const res = await matchesRefreshPOST(req);
      expect(res.status).toBe(200);

      const data = await parseResponse(res);
      expect(data.refreshed).toBe(0);
      expect(data.message).toContain("No active client requirements");
    });

    it("refreshes matches for all active clients", async () => {
      setupAuth();
      const activeClients = [
        {
          id: "cr-1",
          clientName: "Client A",
          listingType: null,
          propertyTypes: ["VILLA"],
          preferredAreas: [],
          mustHaves: [],
          minBudget: null,
          maxBudget: null,
          minBedrooms: null,
          maxBedrooms: null,
        },
        {
          id: "cr-2",
          clientName: "Client B",
          listingType: "RENT",
          propertyTypes: [],
          preferredAreas: ["West Bay"],
          mustHaves: ["parking"],
          minBudget: 5000,
          maxBudget: 10000,
          minBedrooms: 1,
          maxBedrooms: 3,
        },
      ];
      mockPrisma.clientRequirements.findMany.mockResolvedValue(activeClients);

      const properties = [
        {
          id: "prop-1",
          propertyType: "VILLA",
          listingType: "SALE",
          price: 400000,
          currency: "QAR",
          bedrooms: 4,
          builtUpArea: 350,
          city: "Doha",
          district: "The Pearl",
          amenities: ["Pool"],
        },
        {
          id: "prop-2",
          propertyType: "APARTMENT",
          listingType: "RENT",
          price: 7000,
          currency: "QAR",
          bedrooms: 2,
          builtUpArea: 120,
          city: "Doha",
          district: "West Bay",
          amenities: ["Parking"],
        },
      ];
      mockPrisma.property.findMany.mockResolvedValue(properties);

      // Mock find for upsert pattern
      mockPrisma.propertyMatch.findUnique.mockResolvedValue(null);
      mockPrisma.propertyMatch.create.mockResolvedValue({});

      const req = makeRequest(
        "http://localhost:9000/api/real-estate/matches/refresh",
        { method: "POST", body: {} },
      );
      const res = await matchesRefreshPOST(req);
      expect(res.status).toBe(200);

      const data = await parseResponse(res);
      expect(data.refreshed).toBe(2);
      expect(data.propertiesEvaluated).toBe(2);
      expect(data.summary).toHaveLength(2);
      expect(data.summary[0].clientId).toBe("cr-1");
      expect(data.summary[1].clientId).toBe("cr-2");
    });

    it("filters properties by listingType per client", async () => {
      setupAuth();
      const activeClients = [
        {
          id: "cr-rent",
          clientName: "Renter",
          listingType: "RENT",
          propertyTypes: [],
          preferredAreas: [],
          mustHaves: [],
          minBudget: null,
          maxBudget: null,
          minBedrooms: null,
          maxBedrooms: null,
        },
      ];
      mockPrisma.clientRequirements.findMany.mockResolvedValue(activeClients);

      const properties = [
        {
          id: "prop-sale",
          propertyType: "VILLA",
          listingType: "SALE",
          price: 400000,
          currency: "QAR",
          bedrooms: 4,
          builtUpArea: 350,
          city: "Doha",
          district: null,
          amenities: [],
        },
        {
          id: "prop-rent",
          propertyType: "APARTMENT",
          listingType: "RENT",
          price: 7000,
          currency: "QAR",
          bedrooms: 2,
          builtUpArea: 120,
          city: "Doha",
          district: null,
          amenities: [],
        },
      ];
      mockPrisma.property.findMany.mockResolvedValue(properties);
      mockPrisma.propertyMatch.findUnique.mockResolvedValue(null);
      mockPrisma.propertyMatch.create.mockResolvedValue({});

      const req = makeRequest(
        "http://localhost:9000/api/real-estate/matches/refresh",
        { method: "POST", body: {} },
      );
      const res = await matchesRefreshPOST(req);
      const data = await parseResponse(res);

      // Only RENT property should match for this client
      expect(data.summary[0].matchCount).toBe(1);
    });

    it("updates existing matches instead of creating new ones", async () => {
      setupAuth();
      mockPrisma.clientRequirements.findMany.mockResolvedValue([
        {
          id: "cr-1",
          clientName: "Client A",
          listingType: null,
          propertyTypes: [],
          preferredAreas: [],
          mustHaves: [],
          minBudget: null,
          maxBudget: null,
          minBedrooms: null,
          maxBedrooms: null,
        },
      ]);
      mockPrisma.property.findMany.mockResolvedValue([
        {
          id: "prop-1",
          propertyType: "VILLA",
          listingType: "SALE",
          price: 100000,
          currency: "QAR",
          bedrooms: 2,
          builtUpArea: 150,
          city: "Doha",
          district: null,
          amenities: [],
        },
      ]);

      // Simulate existing match
      mockPrisma.propertyMatch.findUnique.mockResolvedValue({
        id: "existing-match-1",
        propertyId: "prop-1",
        requirementId: "cr-1",
        matchScore: 50,
      });
      mockPrisma.propertyMatch.update.mockResolvedValue({});

      const req = makeRequest(
        "http://localhost:9000/api/real-estate/matches/refresh",
        { method: "POST", body: {} },
      );
      const res = await matchesRefreshPOST(req);
      const data = await parseResponse(res);

      expect(data.totalMatchesUpdated).toBe(1);
      expect(data.totalMatchesCreated).toBe(0);
      expect(mockPrisma.propertyMatch.update).toHaveBeenCalledWith({
        where: { id: "existing-match-1" },
        data: expect.objectContaining({
          matchScore: expect.any(Number),
          matchReasons: expect.any(Array),
        }),
      });
    });

    it("creates new matches when not existing", async () => {
      setupAuth();
      mockPrisma.clientRequirements.findMany.mockResolvedValue([
        {
          id: "cr-1",
          clientName: "Client A",
          listingType: null,
          propertyTypes: [],
          preferredAreas: [],
          mustHaves: [],
          minBudget: null,
          maxBudget: null,
          minBedrooms: null,
          maxBedrooms: null,
        },
      ]);
      mockPrisma.property.findMany.mockResolvedValue([
        {
          id: "prop-1",
          propertyType: "VILLA",
          listingType: "SALE",
          price: 100000,
          currency: "QAR",
          bedrooms: 2,
          builtUpArea: 150,
          city: "Doha",
          district: null,
          amenities: [],
        },
      ]);
      mockPrisma.propertyMatch.findUnique.mockResolvedValue(null);
      mockPrisma.propertyMatch.create.mockResolvedValue({});

      const req = makeRequest(
        "http://localhost:9000/api/real-estate/matches/refresh",
        { method: "POST", body: {} },
      );
      const res = await matchesRefreshPOST(req);
      const data = await parseResponse(res);

      expect(data.totalMatchesCreated).toBe(1);
      expect(data.totalMatchesUpdated).toBe(0);
      expect(mockPrisma.propertyMatch.create).toHaveBeenCalledWith({
        data: expect.objectContaining({
          propertyId: "prop-1",
          requirementId: "cr-1",
          matchScore: expect.any(Number),
          matchReasons: expect.any(Array),
        }),
      });
    });

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

      const req = makeRequest(
        "http://localhost:9000/api/real-estate/matches/refresh",
        { method: "POST", body: {} },
      );
      const res = await matchesRefreshPOST(req);
      expect(res.status).toBe(500);
      expect(ApiResponses.serverError).toHaveBeenCalledWith(
        "Failed to refresh matches",
      );
    });

    it("continues processing even if one upsert fails", async () => {
      setupAuth();
      mockPrisma.clientRequirements.findMany.mockResolvedValue([
        {
          id: "cr-1",
          clientName: "Client A",
          listingType: null,
          propertyTypes: [],
          preferredAreas: [],
          mustHaves: [],
          minBudget: null,
          maxBudget: null,
          minBedrooms: null,
          maxBedrooms: null,
        },
      ]);
      mockPrisma.property.findMany.mockResolvedValue([
        {
          id: "prop-1",
          propertyType: "VILLA",
          listingType: "SALE",
          price: 100000,
          currency: "QAR",
          bedrooms: 2,
          builtUpArea: 150,
          city: "Doha",
          district: null,
          amenities: [],
        },
        {
          id: "prop-2",
          propertyType: "APARTMENT",
          listingType: "SALE",
          price: 200000,
          currency: "QAR",
          bedrooms: 3,
          builtUpArea: 200,
          city: "Doha",
          district: null,
          amenities: [],
        },
      ]);

      // First find succeeds, second fails
      mockPrisma.propertyMatch.findUnique
        .mockResolvedValueOnce(null)
        .mockRejectedValueOnce(new Error("Conflict"));
      mockPrisma.propertyMatch.create.mockResolvedValue({});

      const req = makeRequest(
        "http://localhost:9000/api/real-estate/matches/refresh",
        { method: "POST", body: {} },
      );
      const res = await matchesRefreshPOST(req);
      // Should still succeed overall (non-fatal errors)
      expect(res.status).toBe(200);
    });
  });

  // ========================================================================
  // Matching Algorithm (detailed tests via PUT re-run)
  // ========================================================================
  describe("Matching Algorithm via PUT re-run", () => {
    const params = { params: Promise.resolve({ id: "cr-1" }) };

    it("awards 15 price points for slightly above budget in PUT matching", async () => {
      setupAuth();
      const existing = {
        id: "cr-1",
        userId: "user-1",
        clientName: "Ali",
        minBudget: 100000,
        maxBudget: 200000,
        minBedrooms: null,
        maxBedrooms: null,
        listingType: null,
        propertyTypes: [],
        preferredAreas: [],
        mustHaves: [],
      };
      mockPrisma.clientRequirements.findFirst.mockResolvedValue(existing);
      mockPrisma.clientRequirements.update.mockResolvedValue(existing);

      // Property at 210000, which is 5% over maxBudget (200000) -> within 10%
      mockPrisma.property.findMany.mockResolvedValue([
        {
          id: "prop-1",
          propertyType: "OTHER",
          listingType: "SALE",
          price: 210000,
          currency: "QAR",
          bedrooms: null,
          builtUpArea: null,
          city: null,
          district: null,
          amenities: [],
        },
      ]);
      mockPrisma.propertyMatch.upsert.mockResolvedValue({});

      const req = makeRequest(
        "http://localhost:9000/api/real-estate/clients/cr-1",
        { method: "PUT", body: { leadScore: 90 } },
      );
      const res = await clientDetailPUT(req, params);
      expect(res.status).toBe(200);

      const data = await parseResponse(res);
      expect(data.matchesRefreshed).toBe(1);
      // The PUT matching algorithm awards 15 for slightly above
      expect(data.topMatches[0].reasons).toContain(
        "Price slightly above budget",
      );
    });

    it("awards 25 points for district match in PUT matching", async () => {
      setupAuth();
      const existing = {
        id: "cr-1",
        userId: "user-1",
        clientName: "Ali",
        minBudget: null,
        maxBudget: null,
        minBedrooms: null,
        maxBedrooms: null,
        listingType: null,
        propertyTypes: [],
        preferredAreas: ["Lusail"],
        mustHaves: [],
      };
      mockPrisma.clientRequirements.findFirst.mockResolvedValue(existing);
      mockPrisma.clientRequirements.update.mockResolvedValue(existing);

      mockPrisma.property.findMany.mockResolvedValue([
        {
          id: "prop-1",
          propertyType: "APARTMENT",
          listingType: "SALE",
          price: 500000,
          currency: "QAR",
          bedrooms: 2,
          builtUpArea: 150,
          city: "Doha",
          district: "Lusail",
          amenities: [],
        },
      ]);
      mockPrisma.propertyMatch.upsert.mockResolvedValue({});

      const req = makeRequest(
        "http://localhost:9000/api/real-estate/clients/cr-1",
        { method: "PUT", body: { leadScore: 80 } },
      );
      const res = await clientDetailPUT(req, params);
      const data = await parseResponse(res);

      expect(data.topMatches[0].reasons).toContain("District match: Lusail");
    });

    it("awards 20 points for bedroom match in PUT matching", async () => {
      setupAuth();
      const existing = {
        id: "cr-1",
        userId: "user-1",
        clientName: "Ali",
        minBudget: null,
        maxBudget: null,
        minBedrooms: 2,
        maxBedrooms: 4,
        listingType: null,
        propertyTypes: [],
        preferredAreas: [],
        mustHaves: [],
      };
      mockPrisma.clientRequirements.findFirst.mockResolvedValue(existing);
      mockPrisma.clientRequirements.update.mockResolvedValue(existing);

      mockPrisma.property.findMany.mockResolvedValue([
        {
          id: "prop-1",
          propertyType: "APARTMENT",
          listingType: "SALE",
          price: 300000,
          currency: "QAR",
          bedrooms: 3,
          builtUpArea: 150,
          city: null,
          district: null,
          amenities: [],
        },
      ]);
      mockPrisma.propertyMatch.upsert.mockResolvedValue({});

      const req = makeRequest(
        "http://localhost:9000/api/real-estate/clients/cr-1",
        { method: "PUT", body: { leadScore: 80 } },
      );
      const res = await clientDetailPUT(req, params);
      const data = await parseResponse(res);

      expect(data.topMatches[0].reasons).toContain("Bedrooms match");
    });
  });
});
