/**
 * Comprehensive tests for Real Estate Properties API
 *
 * GET  /api/real-estate/properties       - List properties with filters
 * POST /api/real-estate/properties       - Create a new property listing
 * GET  /api/real-estate/properties/[id]  - Retrieve single property
 * PUT  /api/real-estate/properties/[id]  - Update a property
 * DELETE /api/real-estate/properties/[id] - Soft-delete (archive) a property
 */

// ── Mocks (must be declared before any imports) ─────────────────────

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: any) =>
        new Response(JSON.stringify({ success: true, ...data }), {
          status: 200,
        }),
    ),
  },
}));

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

jest.mock("validator", () => ({
  default: { escape: (s: string) => s, trim: (s: string) => s.trim() },
  escape: (s: string) => s,
  trim: (s: string) => s.trim(),
}));

jest.mock(
  "@/lib/services/real-estate/property-update-triggers.service",
  () => ({
    handleNewListing: jest.fn().mockResolvedValue(undefined),
    handlePriceChange: jest.fn().mockResolvedValue(undefined),
    handleStatusChange: jest.fn().mockResolvedValue(undefined),
  }),
);

// ── Imports ─────────────────────────────────────────────────────────

import { NextRequest } from "next/server";
import { getAuthenticatedEmail } from "@/lib/utils/auth-helpers.util";
import { prisma } from "@/lib/db";
import {
  handleNewListing,
  handlePriceChange,
  handleStatusChange,
} from "@/lib/services/real-estate/property-update-triggers.service";

// ── Type helpers ────────────────────────────────────────────────────

const mockGetAuth = getAuthenticatedEmail as jest.Mock;
const mockUsersFind = prisma.users.findUnique as jest.Mock;
const mockOrgsFind = prisma.organizations.findUnique as jest.Mock;
const mockPropertyFindMany = prisma.property.findMany as jest.Mock;
const mockPropertyFindFirst = prisma.property.findFirst as jest.Mock;
const mockPropertyCreate = prisma.property.create as jest.Mock;
const mockPropertyUpdate = prisma.property.update as jest.Mock;
const mockPropertyCount = prisma.property.count as jest.Mock;
const mockViewingCount = (prisma as any).propertyViewing.count as jest.Mock;

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

function makeReq(url: string, method = "GET", body?: object): NextRequest {
  const opts: RequestInit = { method };
  if (body) {
    opts.body = JSON.stringify(body);
    opts.headers = { "Content-Type": "application/json" };
  }
  return new NextRequest(`http://localhost:9000${url}`, opts);
}

const TEST_USER_EMAIL = "agent@realestate.qa";
const TEST_USER_ID = "user-re-001";
const TEST_ORG_ID = "org-re-001";

function authenticateUser() {
  mockGetAuth.mockResolvedValue(TEST_USER_EMAIL);
  mockUsersFind.mockResolvedValue({ id: TEST_USER_ID });
}

function unauthenticateUser() {
  mockGetAuth.mockResolvedValue(null);
}

const VALID_PROPERTY_BODY = {
  titleEn: "Luxury Apartment in The Pearl",
  propertyType: "APARTMENT",
  listingType: "SALE",
  city: "Doha",
  price: 2500000,
  bedrooms: 3,
  bathrooms: 2,
  builtUpArea: 200,
  district: "The Pearl",
  descriptionEn: "A beautiful luxury apartment with sea view.",
};

const MOCK_PROPERTY = {
  id: "mock-cuid-1",
  userId: TEST_USER_ID,
  organizationId: TEST_ORG_ID,
  titleEn: "Luxury Apartment in The Pearl",
  propertyType: "APARTMENT",
  listingType: "SALE",
  status: "AVAILABLE",
  city: "Doha",
  district: "The Pearl",
  price: 2500000,
  currency: "QAR",
  bedrooms: 3,
  bathrooms: 2,
  builtUpArea: 200,
  pricePerSqm: 12500,
  referenceNumber: "PROP-S-00001",
  images: [],
  createdAt: new Date(),
  updatedAt: new Date(),
};

// ── Route module imports (lazy, after mocks) ────────────────────────

let listRoute: typeof import("@/app/api/real-estate/properties/route");
let detailRoute: typeof import("@/app/api/real-estate/properties/[id]/route");

beforeAll(async () => {
  listRoute = await import("@/app/api/real-estate/properties/route");
  detailRoute = await import("@/app/api/real-estate/properties/[id]/route");
});

beforeEach(() => {
  jest.clearAllMocks();
});

// =====================================================================
// GET /api/real-estate/properties
// =====================================================================
describe("GET /api/real-estate/properties", () => {
  it("returns 401 for unauthenticated requests", async () => {
    unauthenticateUser();

    const res = await listRoute.GET(makeReq("/api/real-estate/properties"));
    expect(res.status).toBe(401);
    const data = await res.json();
    expect(data.error).toBe("Unauthorized");
  });

  it("returns 404 when user record does not exist", async () => {
    mockGetAuth.mockResolvedValue(TEST_USER_EMAIL);
    mockUsersFind.mockResolvedValue(null);

    const res = await listRoute.GET(makeReq("/api/real-estate/properties"));
    expect(res.status).toBe(404);
    const data = await res.json();
    expect(data.error).toBe("User not found");
  });

  it("returns properties list for authenticated user", async () => {
    authenticateUser();
    const properties = [
      MOCK_PROPERTY,
      { ...MOCK_PROPERTY, id: "prop-2", titleEn: "Villa in Al Dafna" },
    ];
    mockPropertyFindMany.mockResolvedValue(properties);
    mockPropertyCount.mockResolvedValue(2);

    const res = await listRoute.GET(makeReq("/api/real-estate/properties"));
    expect(res.status).toBe(200);

    const data = await res.json();
    expect(data.success).toBe(true);
    expect(data.properties).toHaveLength(2);
    expect(data.total).toBe(2);
    expect(data.limit).toBe(20);
    expect(data.offset).toBe(0);
  });

  it("filters by status", async () => {
    authenticateUser();
    mockPropertyFindMany.mockResolvedValue([]);
    mockPropertyCount.mockResolvedValue(0);

    await listRoute.GET(
      makeReq("/api/real-estate/properties?status=AVAILABLE"),
    );

    const whereArg = mockPropertyFindMany.mock.calls[0][0].where;
    expect(whereArg.status).toBe("AVAILABLE");
  });

  it("filters by propertyType", async () => {
    authenticateUser();
    mockPropertyFindMany.mockResolvedValue([]);
    mockPropertyCount.mockResolvedValue(0);

    await listRoute.GET(
      makeReq("/api/real-estate/properties?propertyType=VILLA"),
    );

    const whereArg = mockPropertyFindMany.mock.calls[0][0].where;
    expect(whereArg.propertyType).toBe("VILLA");
  });

  it("filters by listingType", async () => {
    authenticateUser();
    mockPropertyFindMany.mockResolvedValue([]);
    mockPropertyCount.mockResolvedValue(0);

    await listRoute.GET(
      makeReq("/api/real-estate/properties?listingType=RENT"),
    );

    const whereArg = mockPropertyFindMany.mock.calls[0][0].where;
    expect(whereArg.listingType).toBe("RENT");
  });

  it("filters by city (case-insensitive contains)", async () => {
    authenticateUser();
    mockPropertyFindMany.mockResolvedValue([]);
    mockPropertyCount.mockResolvedValue(0);

    await listRoute.GET(makeReq("/api/real-estate/properties?city=Doha"));

    const whereArg = mockPropertyFindMany.mock.calls[0][0].where;
    expect(whereArg.city).toEqual({
      contains: "Doha",
      mode: "insensitive",
    });
  });

  it("filters by bedrooms (exact match)", async () => {
    authenticateUser();
    mockPropertyFindMany.mockResolvedValue([]);
    mockPropertyCount.mockResolvedValue(0);

    await listRoute.GET(makeReq("/api/real-estate/properties?bedrooms=3"));

    const whereArg = mockPropertyFindMany.mock.calls[0][0].where;
    expect(whereArg.bedrooms).toBe(3);
  });

  it("filters by price range (minPrice and maxPrice)", async () => {
    authenticateUser();
    mockPropertyFindMany.mockResolvedValue([]);
    mockPropertyCount.mockResolvedValue(0);

    await listRoute.GET(
      makeReq("/api/real-estate/properties?minPrice=100000&maxPrice=500000"),
    );

    const whereArg = mockPropertyFindMany.mock.calls[0][0].where;
    expect(whereArg.price).toEqual({ gte: 100000, lte: 500000 });
  });

  it("filters by minPrice only", async () => {
    authenticateUser();
    mockPropertyFindMany.mockResolvedValue([]);
    mockPropertyCount.mockResolvedValue(0);

    await listRoute.GET(makeReq("/api/real-estate/properties?minPrice=200000"));

    const whereArg = mockPropertyFindMany.mock.calls[0][0].where;
    expect(whereArg.price).toEqual({ gte: 200000 });
  });

  it("pagination works (limit and offset)", async () => {
    authenticateUser();
    mockPropertyFindMany.mockResolvedValue([]);
    mockPropertyCount.mockResolvedValue(100);

    await listRoute.GET(
      makeReq("/api/real-estate/properties?limit=10&offset=20"),
    );

    const call = mockPropertyFindMany.mock.calls[0][0];
    expect(call.take).toBe(10);
    expect(call.skip).toBe(20);
  });

  it("sorts by price ascending", async () => {
    authenticateUser();
    mockPropertyFindMany.mockResolvedValue([]);
    mockPropertyCount.mockResolvedValue(0);

    await listRoute.GET(
      makeReq("/api/real-estate/properties?sort=price&order=asc"),
    );

    const call = mockPropertyFindMany.mock.calls[0][0];
    expect(call.orderBy).toEqual({ price: "asc" });
  });

  it("defaults to sort by createdAt desc", async () => {
    authenticateUser();
    mockPropertyFindMany.mockResolvedValue([]);
    mockPropertyCount.mockResolvedValue(0);

    await listRoute.GET(makeReq("/api/real-estate/properties"));

    const call = mockPropertyFindMany.mock.calls[0][0];
    expect(call.orderBy).toEqual({ createdAt: "desc" });
  });

  it("ignores invalid sort field and defaults to createdAt", async () => {
    authenticateUser();
    mockPropertyFindMany.mockResolvedValue([]);
    mockPropertyCount.mockResolvedValue(0);

    await listRoute.GET(
      makeReq("/api/real-estate/properties?sort=hackerField"),
    );

    const call = mockPropertyFindMany.mock.calls[0][0];
    expect(call.orderBy).toEqual({ createdAt: "desc" });
  });

  it("ignores invalid enum values for status filter", async () => {
    authenticateUser();
    mockPropertyFindMany.mockResolvedValue([]);
    mockPropertyCount.mockResolvedValue(0);

    await listRoute.GET(
      makeReq("/api/real-estate/properties?status=INVALID_STATUS"),
    );

    const whereArg = mockPropertyFindMany.mock.calls[0][0].where;
    expect(whereArg.status).toBeUndefined();
  });

  it("filters by district", async () => {
    authenticateUser();
    mockPropertyFindMany.mockResolvedValue([]);
    mockPropertyCount.mockResolvedValue(0);

    await listRoute.GET(
      makeReq("/api/real-estate/properties?district=The%20Pearl"),
    );

    const whereArg = mockPropertyFindMany.mock.calls[0][0].where;
    expect(whereArg.district).toEqual({
      contains: "The Pearl",
      mode: "insensitive",
    });
  });
});

// =====================================================================
// POST /api/real-estate/properties
// =====================================================================
describe("POST /api/real-estate/properties", () => {
  it("returns 401 for unauthenticated requests", async () => {
    unauthenticateUser();

    const res = await listRoute.POST(
      makeReq("/api/real-estate/properties", "POST", VALID_PROPERTY_BODY),
    );
    expect(res.status).toBe(401);
    const data = await res.json();
    expect(data.error).toBe("Unauthorized");
  });

  it("returns 404 when user record does not exist", async () => {
    mockGetAuth.mockResolvedValue(TEST_USER_EMAIL);
    mockUsersFind.mockResolvedValue(null);

    const res = await listRoute.POST(
      makeReq("/api/real-estate/properties", "POST", VALID_PROPERTY_BODY),
    );
    expect(res.status).toBe(404);
    const data = await res.json();
    expect(data.error).toBe("User not found");
  });

  it("validates required field: titleEn", async () => {
    authenticateUser();

    const body = { ...VALID_PROPERTY_BODY, titleEn: "" };
    const res = await listRoute.POST(
      makeReq("/api/real-estate/properties", "POST", body),
    );
    expect(res.status).toBe(400);
    const data = await res.json();
    expect(data.errors).toEqual(
      expect.arrayContaining([
        expect.stringContaining("Property title (English)"),
      ]),
    );
  });

  it("validates required field: propertyType", async () => {
    authenticateUser();

    const body = { ...VALID_PROPERTY_BODY, propertyType: "SPACESHIP" };
    const res = await listRoute.POST(
      makeReq("/api/real-estate/properties", "POST", body),
    );
    expect(res.status).toBe(400);
    const data = await res.json();
    expect(data.errors).toEqual(
      expect.arrayContaining([
        expect.stringContaining("Invalid property type"),
      ]),
    );
  });

  it("validates required field: listingType", async () => {
    authenticateUser();

    const body = { ...VALID_PROPERTY_BODY, listingType: "BARTER" };
    const res = await listRoute.POST(
      makeReq("/api/real-estate/properties", "POST", body),
    );
    expect(res.status).toBe(400);
    const data = await res.json();
    expect(data.errors).toEqual(
      expect.arrayContaining([expect.stringContaining("Invalid listing type")]),
    );
  });

  it("validates required field: city", async () => {
    authenticateUser();

    const body = { ...VALID_PROPERTY_BODY, city: "" };
    const res = await listRoute.POST(
      makeReq("/api/real-estate/properties", "POST", body),
    );
    expect(res.status).toBe(400);
    const data = await res.json();
    expect(data.errors).toEqual(
      expect.arrayContaining([expect.stringContaining("City is required")]),
    );
  });

  it("validates required field: price", async () => {
    authenticateUser();

    const body = { ...VALID_PROPERTY_BODY, price: "not-a-number" };
    const res = await listRoute.POST(
      makeReq("/api/real-estate/properties", "POST", body),
    );
    expect(res.status).toBe(400);
    const data = await res.json();
    expect(data.errors).toEqual(
      expect.arrayContaining([expect.stringContaining("valid price")]),
    );
  });

  it("rejects invalid optional enums (status)", async () => {
    authenticateUser();

    const body = { ...VALID_PROPERTY_BODY, status: "BANANAS" };
    const res = await listRoute.POST(
      makeReq("/api/real-estate/properties", "POST", body),
    );
    expect(res.status).toBe(400);
    const data = await res.json();
    expect(data.errors).toEqual(
      expect.arrayContaining([expect.stringContaining("Invalid status")]),
    );
  });

  it("rejects invalid optional enums (furnishing)", async () => {
    authenticateUser();

    const body = { ...VALID_PROPERTY_BODY, furnishing: "MAGIC_CARPET" };
    const res = await listRoute.POST(
      makeReq("/api/real-estate/properties", "POST", body),
    );
    expect(res.status).toBe(400);
    const data = await res.json();
    expect(data.errors).toEqual(
      expect.arrayContaining([
        expect.stringContaining("Invalid furnishing type"),
      ]),
    );
  });

  it("rejects invalid optional enums (currency)", async () => {
    authenticateUser();

    const body = { ...VALID_PROPERTY_BODY, currency: "BITCOIN" };
    const res = await listRoute.POST(
      makeReq("/api/real-estate/properties", "POST", body),
    );
    expect(res.status).toBe(400);
    const data = await res.json();
    expect(data.errors).toEqual(
      expect.arrayContaining([expect.stringContaining("Invalid currency")]),
    );
  });

  it("creates property with correct data and auto-generates reference number", async () => {
    authenticateUser();
    mockOrgsFind.mockResolvedValue({ id: TEST_ORG_ID });
    mockPropertyCount.mockResolvedValue(5); // 5 existing properties
    mockPropertyCreate.mockResolvedValue({
      ...MOCK_PROPERTY,
      referenceNumber: "PROP-S-00006",
      images: [],
    });

    const res = await listRoute.POST(
      makeReq("/api/real-estate/properties", "POST", VALID_PROPERTY_BODY),
    );
    expect(res.status).toBe(201);

    const data = await res.json();
    expect(data.success).toBe(true);
    expect(data.property).toBeDefined();

    // Verify prisma.property.create was called with correct structure
    const createCall = mockPropertyCreate.mock.calls[0][0];
    expect(createCall.data.titleEn).toBe("Luxury Apartment in The Pearl");
    expect(createCall.data.propertyType).toBe("APARTMENT");
    expect(createCall.data.listingType).toBe("SALE");
    expect(createCall.data.city).toBe("Doha");
    expect(createCall.data.price).toBe(2500000);
    expect(createCall.data.userId).toBe(TEST_USER_ID);
    expect(createCall.data.organizationId).toBe(TEST_ORG_ID);
    // Auto-generated reference: S for SALE, 5 existing = 6th
    expect(createCall.data.referenceNumber).toBe("PROP-S-00006");
  });

  it("auto-generates R prefix for RENT listings", async () => {
    authenticateUser();
    mockOrgsFind.mockResolvedValue({ id: TEST_ORG_ID });
    mockPropertyCount.mockResolvedValue(0);
    mockPropertyCreate.mockResolvedValue({
      ...MOCK_PROPERTY,
      listingType: "RENT",
      referenceNumber: "PROP-R-00001",
      images: [],
    });

    const body = { ...VALID_PROPERTY_BODY, listingType: "RENT" };
    await listRoute.POST(makeReq("/api/real-estate/properties", "POST", body));

    const createCall = mockPropertyCreate.mock.calls[0][0];
    expect(createCall.data.referenceNumber).toBe("PROP-R-00001");
  });

  it("auto-calculates pricePerSqm when builtUpArea and price are provided", async () => {
    authenticateUser();
    mockOrgsFind.mockResolvedValue({ id: TEST_ORG_ID });
    mockPropertyCount.mockResolvedValue(0);
    mockPropertyCreate.mockResolvedValue({ ...MOCK_PROPERTY, images: [] });

    const body = {
      ...VALID_PROPERTY_BODY,
      price: 1000000,
      builtUpArea: 200,
    };
    await listRoute.POST(makeReq("/api/real-estate/properties", "POST", body));

    const createCall = mockPropertyCreate.mock.calls[0][0];
    // 1000000 / 200 = 5000
    expect(createCall.data.pricePerSqm).toBe(5000);
  });

  it("does not override explicit pricePerSqm", async () => {
    authenticateUser();
    mockOrgsFind.mockResolvedValue({ id: TEST_ORG_ID });
    mockPropertyCount.mockResolvedValue(0);
    mockPropertyCreate.mockResolvedValue({ ...MOCK_PROPERTY, images: [] });

    const body = {
      ...VALID_PROPERTY_BODY,
      price: 1000000,
      builtUpArea: 200,
      pricePerSqm: 9999,
    };
    await listRoute.POST(makeReq("/api/real-estate/properties", "POST", body));

    const createCall = mockPropertyCreate.mock.calls[0][0];
    expect(createCall.data.pricePerSqm).toBe(9999);
  });

  it("detects duplicate building+unit and returns 409", async () => {
    authenticateUser();
    mockOrgsFind.mockResolvedValue({ id: TEST_ORG_ID });

    // Simulate duplicate found
    mockPropertyFindFirst.mockResolvedValue({
      id: "existing-prop-1",
      referenceNumber: "PROP-S-00001",
    });

    const body = {
      ...VALID_PROPERTY_BODY,
      buildingName: "Pearl Tower 1",
      unitNumber: "A301",
    };
    const res = await listRoute.POST(
      makeReq("/api/real-estate/properties", "POST", body),
    );
    expect(res.status).toBe(409);

    const data = await res.json();
    expect(data.success).toBe(false);
    expect(data.errorCode).toBe("DUPLICATE_PROPERTY");
    expect(data.existingPropertyId).toBe("existing-prop-1");
  });

  it("allows creation when building+unit is unique", async () => {
    authenticateUser();
    mockOrgsFind.mockResolvedValue({ id: TEST_ORG_ID });
    mockPropertyFindFirst.mockResolvedValue(null); // no duplicate
    mockPropertyCount.mockResolvedValue(0);
    mockPropertyCreate.mockResolvedValue({ ...MOCK_PROPERTY, images: [] });

    const body = {
      ...VALID_PROPERTY_BODY,
      buildingName: "Pearl Tower 1",
      unitNumber: "A301",
    };
    const res = await listRoute.POST(
      makeReq("/api/real-estate/properties", "POST", body),
    );
    expect(res.status).toBe(201);
  });

  it("calls handleNewListing after successful creation", async () => {
    authenticateUser();
    mockOrgsFind.mockResolvedValue({ id: TEST_ORG_ID });
    mockPropertyCount.mockResolvedValue(0);
    mockPropertyCreate.mockResolvedValue({
      ...MOCK_PROPERTY,
      id: "new-prop-id",
      images: [],
    });

    await listRoute.POST(
      makeReq("/api/real-estate/properties", "POST", VALID_PROPERTY_BODY),
    );

    // handleNewListing is called as fire-and-forget; give the microtask queue a tick
    await new Promise((r) => setTimeout(r, 10));
    expect(handleNewListing).toHaveBeenCalledWith("new-prop-id", TEST_USER_ID);
  });

  it("validates bedrooms range (rejects > 50)", async () => {
    authenticateUser();

    const body = { ...VALID_PROPERTY_BODY, bedrooms: 51 };
    const res = await listRoute.POST(
      makeReq("/api/real-estate/properties", "POST", body),
    );
    expect(res.status).toBe(400);
    const data = await res.json();
    expect(data.errors).toEqual(
      expect.arrayContaining([
        expect.stringContaining("Bedrooms must be between 0 and 50"),
      ]),
    );
  });

  it("validates builtUpArea (rejects negative)", async () => {
    authenticateUser();

    const body = { ...VALID_PROPERTY_BODY, builtUpArea: -10 };
    const res = await listRoute.POST(
      makeReq("/api/real-estate/properties", "POST", body),
    );
    expect(res.status).toBe(400);
    const data = await res.json();
    expect(data.errors).toEqual(
      expect.arrayContaining([
        expect.stringContaining("Built-up area must be a positive number"),
      ]),
    );
  });

  it("collects multiple validation errors at once", async () => {
    authenticateUser();

    const body = {
      titleEn: "",
      propertyType: "INVALID",
      listingType: "INVALID",
      city: "",
      price: "NaN",
    };
    const res = await listRoute.POST(
      makeReq("/api/real-estate/properties", "POST", body),
    );
    expect(res.status).toBe(400);
    const data = await res.json();
    expect(data.errors.length).toBeGreaterThanOrEqual(4);
  });
});

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

  it("returns 401 for unauthenticated requests", async () => {
    unauthenticateUser();

    const res = await detailRoute.GET(
      makeReq("/api/real-estate/properties/test-property-id"),
      routeCtx("test-property-id"),
    );
    expect(res.status).toBe(401);
    const data = await res.json();
    expect(data.error).toBe("Unauthorized");
  });

  it("returns 404 when user record does not exist", async () => {
    mockGetAuth.mockResolvedValue(TEST_USER_EMAIL);
    mockUsersFind.mockResolvedValue(null);

    const res = await detailRoute.GET(
      makeReq("/api/real-estate/properties/test-property-id"),
      routeCtx("test-property-id"),
    );
    expect(res.status).toBe(404);
  });

  it("returns property detail with images", async () => {
    authenticateUser();
    const propertyWithImages = {
      ...MOCK_PROPERTY,
      images: [
        {
          id: "img-1",
          url: "https://cdn.example.com/1.jpg",
          isPrimary: true,
          sortOrder: 0,
        },
        {
          id: "img-2",
          url: "https://cdn.example.com/2.jpg",
          isPrimary: false,
          sortOrder: 1,
        },
      ],
      listingBroker: null,
    };
    mockPropertyFindFirst.mockResolvedValue(propertyWithImages);
    // Mock the viewCount increment (fire-and-forget)
    mockPropertyUpdate.mockResolvedValue({});

    const res = await detailRoute.GET(
      makeReq("/api/real-estate/properties/test-property-id"),
      routeCtx("test-property-id"),
    );
    expect(res.status).toBe(200);

    const data = await res.json();
    expect(data.success).toBe(true);
    expect(data.property.images).toHaveLength(2);
    expect(data.property.titleEn).toBe("Luxury Apartment in The Pearl");
  });

  it("returns 404 for property belonging to a different org", async () => {
    authenticateUser();
    mockPropertyFindFirst.mockResolvedValue(null); // scoped query returns null

    const res = await detailRoute.GET(
      makeReq("/api/real-estate/properties/other-org-prop"),
      routeCtx("other-org-prop"),
    );
    expect(res.status).toBe(404);
    const data = await res.json();
    expect(data.error).toBe("Property not found");
  });

  it("returns 400 for invalid property ID (too short)", async () => {
    authenticateUser();

    const res = await detailRoute.GET(
      makeReq("/api/real-estate/properties/ab"),
      routeCtx("ab"),
    );
    expect(res.status).toBe(400);
    const data = await res.json();
    expect(data.error).toBe("Invalid property ID");
  });

  it("increments view count as fire-and-forget", async () => {
    authenticateUser();
    mockPropertyFindFirst.mockResolvedValue({
      ...MOCK_PROPERTY,
      listingBroker: null,
    });
    mockPropertyUpdate.mockResolvedValue({});

    await detailRoute.GET(
      makeReq("/api/real-estate/properties/test-property-id"),
      routeCtx("test-property-id"),
    );

    // Give microtask queue a tick for the fire-and-forget to execute
    await new Promise((r) => setTimeout(r, 10));
    expect(mockPropertyUpdate).toHaveBeenCalledWith(
      expect.objectContaining({
        where: { id: "test-property-id" },
        data: { viewCount: { increment: 1 } },
      }),
    );
  });
});

// =====================================================================
// PUT /api/real-estate/properties/[id]
// =====================================================================
describe("PUT /api/real-estate/properties/[id]", () => {
  const routeCtx = (id: string) => ({ params: Promise.resolve({ id }) });
  const PROP_ID = "test-property-12345";

  it("returns 401 for unauthenticated requests", async () => {
    unauthenticateUser();

    const res = await detailRoute.PUT(
      makeReq(`/api/real-estate/properties/${PROP_ID}`, "PUT", {
        titleEn: "New Title",
      }),
      routeCtx(PROP_ID),
    );
    expect(res.status).toBe(401);
  });

  it("returns 404 for non-existent property", async () => {
    authenticateUser();
    mockPropertyFindFirst.mockResolvedValue(null);

    const res = await detailRoute.PUT(
      makeReq(`/api/real-estate/properties/${PROP_ID}`, "PUT", {
        titleEn: "New Title",
      }),
      routeCtx(PROP_ID),
    );
    expect(res.status).toBe(404);
    const data = await res.json();
    expect(data.error).toBe("Property not found");
  });

  it("updates property successfully", async () => {
    authenticateUser();
    const existing = {
      id: PROP_ID,
      status: "AVAILABLE",
      price: 2500000,
      builtUpArea: 200,
    };
    mockPropertyFindFirst.mockResolvedValue(existing);
    const updated = {
      ...MOCK_PROPERTY,
      id: PROP_ID,
      titleEn: "Updated Title",
      images: [],
    };
    mockPropertyUpdate.mockResolvedValue(updated);

    const res = await detailRoute.PUT(
      makeReq(`/api/real-estate/properties/${PROP_ID}`, "PUT", {
        titleEn: "Updated Title",
      }),
      routeCtx(PROP_ID),
    );
    expect(res.status).toBe(200);
    const data = await res.json();
    expect(data.success).toBe(true);
    expect(data.property.titleEn).toBe("Updated Title");
  });

  it("triggers price change notification when price changes", async () => {
    authenticateUser();
    const existing = {
      id: PROP_ID,
      status: "AVAILABLE",
      price: 2500000,
      builtUpArea: 200,
    };
    mockPropertyFindFirst.mockResolvedValue(existing);
    mockPropertyUpdate.mockResolvedValue({
      ...MOCK_PROPERTY,
      id: PROP_ID,
      price: 2200000,
      images: [],
    });

    await detailRoute.PUT(
      makeReq(`/api/real-estate/properties/${PROP_ID}`, "PUT", {
        price: 2200000,
      }),
      routeCtx(PROP_ID),
    );

    await new Promise((r) => setTimeout(r, 10));
    expect(handlePriceChange).toHaveBeenCalledWith(
      PROP_ID,
      2500000, // old price
      2200000, // new price
    );
  });

  it("does NOT trigger price change when price stays the same", async () => {
    authenticateUser();
    const existing = {
      id: PROP_ID,
      status: "AVAILABLE",
      price: 2500000,
      builtUpArea: 200,
    };
    mockPropertyFindFirst.mockResolvedValue(existing);
    mockPropertyUpdate.mockResolvedValue({
      ...MOCK_PROPERTY,
      id: PROP_ID,
      images: [],
    });

    await detailRoute.PUT(
      makeReq(`/api/real-estate/properties/${PROP_ID}`, "PUT", {
        price: 2500000, // same price
      }),
      routeCtx(PROP_ID),
    );

    await new Promise((r) => setTimeout(r, 10));
    expect(handlePriceChange).not.toHaveBeenCalled();
  });

  it("triggers status change notification when status changes", async () => {
    authenticateUser();
    const existing = {
      id: PROP_ID,
      status: "AVAILABLE",
      price: 2500000,
      builtUpArea: 200,
    };
    mockPropertyFindFirst.mockResolvedValue(existing);
    mockPropertyUpdate.mockResolvedValue({
      ...MOCK_PROPERTY,
      id: PROP_ID,
      status: "UNDER_OFFER",
      images: [],
    });

    await detailRoute.PUT(
      makeReq(`/api/real-estate/properties/${PROP_ID}`, "PUT", {
        status: "UNDER_OFFER",
      }),
      routeCtx(PROP_ID),
    );

    await new Promise((r) => setTimeout(r, 10));
    expect(handleStatusChange).toHaveBeenCalledWith(
      PROP_ID,
      "AVAILABLE", // old status
      "UNDER_OFFER", // new status
      TEST_USER_ID,
    );
  });

  it("validates status enum", async () => {
    authenticateUser();
    mockPropertyFindFirst.mockResolvedValue({
      id: PROP_ID,
      status: "AVAILABLE",
      price: 2500000,
      builtUpArea: 200,
    });

    const res = await detailRoute.PUT(
      makeReq(`/api/real-estate/properties/${PROP_ID}`, "PUT", {
        status: "FLYING",
      }),
      routeCtx(PROP_ID),
    );
    expect(res.status).toBe(400);
    const data = await res.json();
    expect(data.errors).toEqual(
      expect.arrayContaining([expect.stringContaining("Invalid status")]),
    );
  });

  it("validates status transition rules", async () => {
    authenticateUser();
    mockPropertyFindFirst.mockResolvedValue({
      id: PROP_ID,
      status: "SOLD",
      price: 2500000,
      builtUpArea: 200,
    });

    // SOLD can only go to OFF_MARKET, not AVAILABLE
    const res = await detailRoute.PUT(
      makeReq(`/api/real-estate/properties/${PROP_ID}`, "PUT", {
        status: "AVAILABLE",
      }),
      routeCtx(PROP_ID),
    );
    expect(res.status).toBe(400);
    const data = await res.json();
    expect(data.errors).toEqual(
      expect.arrayContaining([
        expect.stringContaining("Invalid status transition"),
      ]),
    );
  });

  it("allows valid status transition (AVAILABLE -> UNDER_OFFER)", async () => {
    authenticateUser();
    mockPropertyFindFirst.mockResolvedValue({
      id: PROP_ID,
      status: "AVAILABLE",
      price: 2500000,
      builtUpArea: 200,
    });
    mockPropertyUpdate.mockResolvedValue({
      ...MOCK_PROPERTY,
      id: PROP_ID,
      status: "UNDER_OFFER",
      images: [],
    });

    const res = await detailRoute.PUT(
      makeReq(`/api/real-estate/properties/${PROP_ID}`, "PUT", {
        status: "UNDER_OFFER",
      }),
      routeCtx(PROP_ID),
    );
    expect(res.status).toBe(200);
    const data = await res.json();
    expect(data.success).toBe(true);
  });

  it("recalculates pricePerSqm when price changes", async () => {
    authenticateUser();
    mockPropertyFindFirst.mockResolvedValue({
      id: PROP_ID,
      status: "AVAILABLE",
      price: 2500000,
      builtUpArea: 250,
    });
    mockPropertyUpdate.mockResolvedValue({
      ...MOCK_PROPERTY,
      id: PROP_ID,
      images: [],
    });

    await detailRoute.PUT(
      makeReq(`/api/real-estate/properties/${PROP_ID}`, "PUT", {
        price: 3000000,
      }),
      routeCtx(PROP_ID),
    );

    const updateCall = mockPropertyUpdate.mock.calls[0][0];
    // 3000000 / 250 = 12000
    expect(updateCall.data.pricePerSqm).toBe(12000);
  });

  it("sets soldRentedAt when status changes to SOLD", async () => {
    authenticateUser();
    mockPropertyFindFirst.mockResolvedValue({
      id: PROP_ID,
      status: "UNDER_OFFER",
      price: 2500000,
      builtUpArea: 200,
    });
    mockPropertyUpdate.mockResolvedValue({
      ...MOCK_PROPERTY,
      id: PROP_ID,
      status: "SOLD",
      images: [],
    });

    await detailRoute.PUT(
      makeReq(`/api/real-estate/properties/${PROP_ID}`, "PUT", {
        status: "SOLD",
      }),
      routeCtx(PROP_ID),
    );

    const updateCall = mockPropertyUpdate.mock.calls[0][0];
    expect(updateCall.data.soldRentedAt).toBeInstanceOf(Date);
  });

  it("validates price must be >= 0 on update", async () => {
    authenticateUser();
    mockPropertyFindFirst.mockResolvedValue({
      id: PROP_ID,
      status: "AVAILABLE",
      price: 2500000,
      builtUpArea: 200,
    });

    const res = await detailRoute.PUT(
      makeReq(`/api/real-estate/properties/${PROP_ID}`, "PUT", {
        price: -100,
      }),
      routeCtx(PROP_ID),
    );
    expect(res.status).toBe(400);
    const data = await res.json();
    expect(data.errors).toEqual(
      expect.arrayContaining([expect.stringContaining("Price must be >= 0")]),
    );
  });

  it("returns 400 for invalid property ID on update", async () => {
    authenticateUser();

    const res = await detailRoute.PUT(
      makeReq("/api/real-estate/properties/ab", "PUT", { titleEn: "X" }),
      routeCtx("ab"),
    );
    expect(res.status).toBe(400);
    const data = await res.json();
    expect(data.error).toBe("Invalid property ID");
  });
});

// =====================================================================
// DELETE /api/real-estate/properties/[id]
// =====================================================================
describe("DELETE /api/real-estate/properties/[id]", () => {
  const routeCtx = (id: string) => ({ params: Promise.resolve({ id }) });
  const PROP_ID = "test-property-12345";

  it("returns 401 for unauthenticated requests", async () => {
    unauthenticateUser();

    const res = await detailRoute.DELETE(
      makeReq(`/api/real-estate/properties/${PROP_ID}`, "DELETE"),
      routeCtx(PROP_ID),
    );
    expect(res.status).toBe(401);
  });

  it("returns 404 for non-existent property", async () => {
    authenticateUser();
    mockPropertyFindFirst.mockResolvedValue(null);

    const res = await detailRoute.DELETE(
      makeReq(`/api/real-estate/properties/${PROP_ID}`, "DELETE"),
      routeCtx(PROP_ID),
    );
    expect(res.status).toBe(404);
    const data = await res.json();
    expect(data.error).toBe("Property not found");
  });

  it("soft deletes property by setting status to OFF_MARKET", async () => {
    authenticateUser();
    mockPropertyFindFirst.mockResolvedValue({
      id: PROP_ID,
      status: "AVAILABLE",
    });
    mockViewingCount.mockResolvedValue(0);
    mockPropertyUpdate.mockResolvedValue({
      id: PROP_ID,
      status: "OFF_MARKET",
    });

    const res = await detailRoute.DELETE(
      makeReq(`/api/real-estate/properties/${PROP_ID}`, "DELETE"),
      routeCtx(PROP_ID),
    );
    expect(res.status).toBe(200);

    const data = await res.json();
    expect(data.success).toBe(true);
    expect(data.message).toBe("Property archived successfully");
    expect(data.property.status).toBe("OFF_MARKET");

    // Verify the update was called with OFF_MARKET
    expect(mockPropertyUpdate).toHaveBeenCalledWith(
      expect.objectContaining({
        where: { id: PROP_ID },
        data: expect.objectContaining({ status: "OFF_MARKET" }),
      }),
    );
  });

  it("blocks deletion when active viewings exist (409)", async () => {
    authenticateUser();
    mockPropertyFindFirst.mockResolvedValue({
      id: PROP_ID,
      status: "AVAILABLE",
    });
    mockViewingCount.mockResolvedValue(3); // 3 active viewings

    const res = await detailRoute.DELETE(
      makeReq(`/api/real-estate/properties/${PROP_ID}`, "DELETE"),
      routeCtx(PROP_ID),
    );
    expect(res.status).toBe(409);

    const data = await res.json();
    expect(data.success).toBe(false);
    expect(data.errorCode).toBe("ACTIVE_VIEWINGS_EXIST");
    expect(data.activeViewings).toBe(3);
    expect(data.error).toContain("3 active viewing(s)");
  });

  it("returns 400 for invalid property ID on delete", async () => {
    authenticateUser();

    const res = await detailRoute.DELETE(
      makeReq("/api/real-estate/properties/ab", "DELETE"),
      routeCtx("ab"),
    );
    expect(res.status).toBe(400);
    const data = await res.json();
    expect(data.error).toBe("Invalid property ID");
  });

  it("allows deletion when no active viewings exist", async () => {
    authenticateUser();
    mockPropertyFindFirst.mockResolvedValue({
      id: PROP_ID,
      status: "UNDER_OFFER",
    });
    mockViewingCount.mockResolvedValue(0);
    mockPropertyUpdate.mockResolvedValue({
      id: PROP_ID,
      status: "OFF_MARKET",
    });

    const res = await detailRoute.DELETE(
      makeReq(`/api/real-estate/properties/${PROP_ID}`, "DELETE"),
      routeCtx(PROP_ID),
    );
    expect(res.status).toBe(200);
    const data = await res.json();
    expect(data.success).toBe(true);
  });
});
