import { sleep } from '@/utils';
import { ApiError } from '@/lib/api/errors';
import type { Paginated } from '@/lib/api/types';
import {
  BOOKINGS_PAGE_SIZE,
  MOCK_EVV_REFERENCE_LAT,
  MOCK_EVV_REFERENCE_LNG,
  MOCK_EVV_TOLERANCE_METERS,
} from '../constants';
import type {
  BookingDetailDto,
  BookingListItemDto,
  BookingListParams,
  BookingSessionDto,
  BookingSessionListItemDto,
  BookingsApi,
  BookingViewerRole,
  CareInstructionsDto,
  CheckInVisitInput,
  CheckOutVisitInput,
  TodaySessionsParams,
  VisitVerificationDto,
} from '../types';

const MOCK_LATENCY_MS = 350;

/** Dispute window the mock stamps at completion — the payout-eligibility gate is server truth, mocked here. */
const DISPUTE_WINDOW_HOURS = 72;

const NURSE_ID = 1;
const NURSE_NAME = 'مریم رضایی';

/** `YYYY-MM-DD` for a day offset from today (mock seed dates — runs client-side, so `new Date()` is fine). */
function isoDate(daysFromToday: number): string {
  const d = new Date();
  d.setDate(d.getDate() + daysFromToday);
  return d.toISOString().slice(0, 10);
}

/** Haversine distance in metres — the mock stand-in for the server's address-match math. */
function distanceMeters(lat1: number, lng1: number, lat2: number, lng2: number): number {
  const R = 6_371_000;
  const toRad = (deg: number) => (deg * Math.PI) / 180;
  const dLat = toRad(lat2 - lat1);
  const dLng = toRad(lng2 - lng1);
  const a =
    Math.sin(dLat / 2) ** 2 + Math.cos(toRad(lat1)) * Math.cos(toRad(lat2)) * Math.sin(dLng / 2) ** 2;
  return 2 * R * Math.asin(Math.sqrt(a));
}

function makeSession(id: number, sessionIndex: number, daysFromToday: number, payoutIrr: string): BookingSessionDto {
  return {
    id,
    sessionIndex,
    scheduledDate: isoDate(daysFromToday),
    scheduledTimeStart: '09:00:00',
    scheduledTimeEnd: '13:00:00',
    status: 'scheduled',
    visitPayoutAmount: payoutIrr,
    payoutEligibleAt: null,
    evvStatus: 'pending',
    checkInAt: null,
    checkOutAt: null,
    checkInAddressMatch: null,
  };
}

// Shared, module-level store so both the customer view and the nurse view read the same booking, and an
// EVV check-in/out flips the timeline/session/banner for both. Seeded with one multi-session booking and
// one single-visit booking (proving the single-visit path renders one session row through the same card),
// both assigned to the seeded nurse and scheduled with a today session so check-in is demoable out of box.
let bookings: BookingDetailDto[] = [];
const verifications: Record<number, VisitVerificationDto> = {};
const care: Record<number, CareInstructionsDto> = {};

function seed(): void {
  const addr5001 = JSON.stringify({
    title: 'منزل',
    city: 'تهران',
    district: 'سعادت‌آباد',
    line: 'خیابان نمونه، کوچه دوم، پلاک ۱۲',
    postalCode: '1998887766',
  });
  const addr5002 = JSON.stringify({
    title: 'آپارتمان',
    city: 'تهران',
    district: 'ونک',
    line: 'خیابان ملاصدرا، پلاک ۴۵، واحد ۷',
    postalCode: '1991112233',
  });

  bookings = [
    {
      id: 5001,
      bookingRequestId: 9001,
      status: 'confirmed',
      nurseId: NURSE_ID,
      nurseName: NURSE_NAME,
      patientId: 901,
      patientName: 'حاج‌آقا موسوی',
      variantId: 11,
      variantSnapshotJson: JSON.stringify({ displayName: 'مراقبت سالمند — شیفت روز', priceUnit: 'per_day' }),
      customerAddressId: 801,
      addressSnapshotJson: addr5001,
      grossPriceIrr: '45000000',
      balinyaarCommissionIrr: '5400000',
      nursePayoutAmount: '39600000',
      pspFeeAmount: '900000',
      platformFeeRate: 0.12,
      sessionCount: 3,
      scheduledDate: isoDate(0),
      scheduledTimeStart: '09:00:00',
      scheduledTimeEnd: '13:00:00',
      confirmedAt: new Date().toISOString(),
      completedAt: null,
      cancelledAt: null,
      cancelledBy: null,
      cancellationReason: null,
      cancellationPolicyCode: null,
      cancellationRefundPercentage: null,
      refundableAmountIrr: null,
      disputeWindowEndsAt: null,
      createdAt: new Date().toISOString(),
      sessions: [
        makeSession(70011, 1, 0, '13200000'),
        makeSession(70012, 2, 1, '13200000'),
        makeSession(70013, 3, 2, '13200000'),
      ],
    },
    {
      id: 5002,
      bookingRequestId: 9002,
      status: 'confirmed',
      nurseId: NURSE_ID,
      nurseName: NURSE_NAME,
      patientId: 902,
      patientName: 'خانم احمدی',
      variantId: 12,
      variantSnapshotJson: JSON.stringify({ displayName: 'مراقبت پس از جراحی', priceUnit: 'per_session' }),
      customerAddressId: 802,
      addressSnapshotJson: addr5002,
      grossPriceIrr: '18000000',
      balinyaarCommissionIrr: '2160000',
      nursePayoutAmount: '15840000',
      pspFeeAmount: '360000',
      platformFeeRate: 0.12,
      sessionCount: 1,
      scheduledDate: isoDate(0),
      scheduledTimeStart: '15:00:00',
      scheduledTimeEnd: '19:00:00',
      confirmedAt: new Date().toISOString(),
      completedAt: null,
      cancelledAt: null,
      cancelledBy: null,
      cancellationReason: null,
      cancellationPolicyCode: null,
      cancellationRefundPercentage: null,
      refundableAmountIrr: null,
      disputeWindowEndsAt: null,
      createdAt: new Date().toISOString(),
      sessions: [{ ...makeSession(70021, 1, 0, '15840000'), scheduledTimeStart: '15:00:00', scheduledTimeEnd: '19:00:00' }],
    },
    // Mid-engagement multi-session booking (f10 refund demo): session 1 is completed-and-verified (locked,
    // stays payout-eligible) while sessions 2 & 3 are un-started and > 24h out — so the cancellation flow
    // shows a mixed refundable/locked breakdown at the free-cancellation tier out of the box.
    {
      id: 5003,
      bookingRequestId: 9003,
      status: 'in_progress',
      nurseId: NURSE_ID,
      nurseName: NURSE_NAME,
      patientId: 903,
      patientName: 'آقای کریمی',
      variantId: 13,
      variantSnapshotJson: JSON.stringify({ displayName: 'مراقبت سالمند — شیفت روز', priceUnit: 'per_day' }),
      customerAddressId: 803,
      addressSnapshotJson: JSON.stringify({
        title: 'منزل',
        city: 'تهران',
        district: 'پونک',
        line: 'بلوار عدل، کوچه سوم، پلاک ۸',
        postalCode: '1477889900',
      }),
      grossPriceIrr: '30000000',
      balinyaarCommissionIrr: '3600000',
      nursePayoutAmount: '26400000',
      pspFeeAmount: '600000',
      platformFeeRate: 0.12,
      sessionCount: 3,
      scheduledDate: isoDate(3),
      scheduledTimeStart: '09:00:00',
      scheduledTimeEnd: '13:00:00',
      confirmedAt: new Date(Date.now() - 2 * 86_400_000).toISOString(),
      completedAt: null,
      cancelledAt: null,
      cancelledBy: null,
      cancellationReason: null,
      cancellationPolicyCode: null,
      cancellationRefundPercentage: null,
      refundableAmountIrr: null,
      disputeWindowEndsAt: null,
      createdAt: new Date(Date.now() - 3 * 86_400_000).toISOString(),
      sessions: [
        {
          ...makeSession(70031, 1, -1, '8800000'),
          status: 'completed',
          evvStatus: 'completed',
          checkInAt: new Date(Date.now() - 86_400_000).toISOString(),
          checkOutAt: new Date(Date.now() - 72_000_000).toISOString(),
          payoutEligibleAt: new Date(Date.now() + DISPUTE_WINDOW_HOURS * 3_600_000).toISOString(),
          checkInAddressMatch: true,
        },
        makeSession(70032, 2, 3, '8800000'),
        makeSession(70033, 3, 5, '8800000'),
      ],
    },
    // Already-cancelled booking whose refund FAILED (f10 refund-status demo): the customer sees the
    // needs-attention / contact-support state (never a retry — retry is admin-only, DEFERRED to f15). Its
    // failed refund is seeded in the refunds mock; here it just carries the cancellation snapshot b9 stamps.
    {
      id: 5004,
      bookingRequestId: 9004,
      status: 'cancelled',
      nurseId: NURSE_ID,
      nurseName: NURSE_NAME,
      patientId: 904,
      patientName: 'خانم صادقی',
      variantId: 14,
      variantSnapshotJson: JSON.stringify({ displayName: 'مراقبت پس از جراحی', priceUnit: 'per_session' }),
      customerAddressId: 804,
      addressSnapshotJson: JSON.stringify({
        title: 'منزل',
        city: 'تهران',
        district: 'جنت‌آباد',
        line: 'خیابان لاله، پلاک ۲۲، واحد ۳',
        postalCode: '1476612345',
      }),
      grossPriceIrr: '12000000',
      balinyaarCommissionIrr: '1440000',
      nursePayoutAmount: '10560000',
      pspFeeAmount: '240000',
      platformFeeRate: 0.12,
      sessionCount: 1,
      scheduledDate: isoDate(-3),
      scheduledTimeStart: '10:00:00',
      scheduledTimeEnd: '14:00:00',
      confirmedAt: new Date(Date.now() - 5 * 86_400_000).toISOString(),
      completedAt: null,
      cancelledAt: new Date(Date.now() - 2 * 86_400_000).toISOString(),
      cancelledBy: 'customer',
      cancellationReason: 'changed_mind',
      cancellationPolicyCode: 'partial_under_24h',
      cancellationRefundPercentage: 0.5,
      refundableAmountIrr: '12000000',
      disputeWindowEndsAt: null,
      createdAt: new Date(Date.now() - 6 * 86_400_000).toISOString(),
      sessions: [
        {
          ...makeSession(70041, 1, -3, '10560000'),
          status: 'cancelled',
          scheduledTimeStart: '10:00:00',
          scheduledTimeEnd: '14:00:00',
        },
      ],
    },
    // 5005 — a COMPLETED single-visit booking. There is otherwise no completed seed (checkOutVisit only
    // reaches `completed` at runtime), so f13's leave-a-review flow needs this: the customer opens 5005 →
    // review-eligible; its patient 905 + nurse 1 align with the reviews/records mocks for deep-links.
    {
      id: 5005,
      bookingRequestId: 9005,
      status: 'completed',
      nurseId: NURSE_ID,
      nurseName: NURSE_NAME,
      patientId: 905,
      patientName: 'بانو حسینی',
      variantId: 15,
      variantSnapshotJson: JSON.stringify({ displayName: 'مراقبت پس از جراحی — ویزیت', priceUnit: 'per_visit' }),
      customerAddressId: 805,
      addressSnapshotJson: JSON.stringify({ title: 'خانه', addressLine: 'تهران، خیابان ولیعصر', cityName: 'تهران', latitude: 35.72, longitude: 51.41 }),
      grossPriceIrr: '3000000',
      balinyaarCommissionIrr: '360000',
      nursePayoutAmount: '2640000',
      pspFeeAmount: '60000',
      platformFeeRate: 0.12,
      sessionCount: 1,
      scheduledDate: isoDate(-2),
      scheduledTimeStart: '09:00:00',
      scheduledTimeEnd: '13:00:00',
      confirmedAt: new Date(Date.now() - 4 * 86_400_000).toISOString(),
      completedAt: new Date(Date.now() - 1 * 86_400_000).toISOString(),
      cancelledAt: null,
      cancelledBy: null,
      cancellationReason: null,
      cancellationPolicyCode: null,
      cancellationRefundPercentage: null,
      refundableAmountIrr: null,
      disputeWindowEndsAt: new Date(Date.now() + 2 * 86_400_000).toISOString(),
      createdAt: new Date(Date.now() - 5 * 86_400_000).toISOString(),
      sessions: [
        {
          ...makeSession(70051, 1, -2, '2640000'),
          status: 'completed',
          payoutEligibleAt: new Date(Date.now() - 1 * 86_400_000).toISOString(),
          evvStatus: 'completed',
          checkInAt: new Date(Date.now() - 1 * 86_400_000 - 4 * 3_600_000).toISOString(),
          checkOutAt: new Date(Date.now() - 1 * 86_400_000).toISOString(),
          checkInAddressMatch: true,
        },
      ],
    },
  ];

  care[5001] = {
    bookingId: 5001,
    currentConditions: 'دیابت نوع ۲، فشار خون بالا',
    medications: 'متفورمین ۵۰۰ (صبح و شب) · لوزارتان ۲۵ (صبح)',
    allergies: 'حساسیت به پنی‌سیلین',
    specialInstructions: 'قند خون پیش از هر وعده اندازه‌گیری شود؛ یک پیاده‌روی کوتاه بعدازظهر توصیه شده است.',
    emergencyContactName: 'زهرا موسوی',
    emergencyContactPhone: '09121234567',
  };
  care[5002] = {
    bookingId: 5002,
    currentConditions: 'دورهٔ نقاهت پس از عمل زانو',
    medications: 'مسکن طبق دستور پزشک',
    allergies: null,
    specialInstructions: 'در جابه‌جایی و تعویض پانسمان کمک شود؛ از فشار روی زانوی عمل‌شده پرهیز شود.',
    emergencyContactName: 'علی احمدی',
    emergencyContactPhone: '09120009988',
  };
}

seed();

function cloneBooking(b: BookingDetailDto): BookingDetailDto {
  return { ...b, sessions: b.sessions.map((s) => ({ ...s })) };
}

function findBooking(id: number): BookingDetailDto {
  const b = bookings.find((row) => row.id === id);
  // Tenancy is not modelled in the single-session mock; a missing booking 404s (no leak either way).
  if (!b) throw new ApiError(404, 'Booking not found', 'not_found');
  return b;
}

function findSession(sessionId: number): { booking: BookingDetailDto; session: BookingSessionDto } {
  for (const booking of bookings) {
    const session = booking.sessions.find((s) => s.id === sessionId);
    if (session) return { booking, session };
  }
  throw new ApiError(404, 'Session not found', 'not_found');
}

/** The nurse view omits the full address snapshot (two-stage disclosure — coarse context only). */
function forViewer(b: BookingDetailDto, viewerRole: BookingViewerRole | undefined): BookingDetailDto {
  const clone = cloneBooking(b);
  if (viewerRole === 'nurse') clone.addressSnapshotJson = null;
  return clone;
}

function toListItem(b: BookingDetailDto, role: BookingListParams['role']): BookingListItemDto {
  return {
    id: b.id,
    status: b.status,
    counterpartyName: role === 'nurse' ? b.patientName : b.nurseName,
    scheduledDate: b.scheduledDate,
    sessionCount: b.sessionCount,
    // Contract: gross for the customer, the nurse's payout for the nurse.
    amountIrr: role === 'nurse' ? b.nursePayoutAmount : b.grossPriceIrr,
    disputeWindowEndsAt: b.disputeWindowEndsAt,
    createdAt: b.createdAt,
  };
}

function pendingVerification(session: BookingSessionDto): VisitVerificationDto {
  return {
    id: session.id,
    bookingSessionId: session.id,
    status: session.evvStatus,
    checkInAt: session.checkInAt,
    checkInLat: null,
    checkInLng: null,
    checkOutAt: session.checkOutAt,
    checkOutLat: null,
    checkOutLng: null,
    checkInAddressMatch: session.checkInAddressMatch,
    checkInDistanceMeters: null,
  };
}

/**
 * In-memory mock behind the `BookingsApi` seam. Seeds confirmed bookings + sessions + care + EVV and
 * drives the check-in/out state machine so the timeline, session chips, and EVV banner all transition
 * without a live b9 backend. Mirrors the real shapes + status/EVV/masking/gating semantics for a one-line
 * swap once conversion (b10) is live client-side (`USE_BOOKINGS_MOCK = false`).
 */
export const bookingsMockApi: BookingsApi = {
  getBookingDetail: async (id, viewerRole) => {
    await sleep(MOCK_LATENCY_MS);
    return forViewer(findBooking(id), viewerRole);
  },

  listBookings: async (params: BookingListParams): Promise<Paginated<BookingListItemDto>> => {
    await sleep(MOCK_LATENCY_MS);
    const matched = bookings
      .filter((b) => (params.status ? b.status === params.status : true))
      .sort((a, b) => Date.parse(b.createdAt) - Date.parse(a.createdAt))
      .map((b) => toListItem(b, params.role));
    const page = params.page ?? 1;
    const pageSize = params.pageSize ?? BOOKINGS_PAGE_SIZE;
    const start = (page - 1) * pageSize;
    return { items: matched.slice(start, start + pageSize), total: matched.length, page, pageSize };
  },

  listTodaySessions: async (params: TodaySessionsParams): Promise<Paginated<BookingSessionListItemDto>> => {
    await sleep(MOCK_LATENCY_MS);
    const day = params.date ?? isoDate(0);
    const items: BookingSessionListItemDto[] = [];
    for (const booking of bookings) {
      for (const session of booking.sessions) {
        if (session.scheduledDate !== day) continue;
        items.push({
          sessionId: session.id,
          bookingId: booking.id,
          sessionIndex: session.sessionIndex,
          patientName: booking.patientName,
          scheduledDate: session.scheduledDate,
          scheduledTimeStart: session.scheduledTimeStart,
          scheduledTimeEnd: session.scheduledTimeEnd,
          status: session.status,
          evvStatus: session.evvStatus,
        });
      }
    }
    items.sort((a, b) => a.scheduledTimeStart.localeCompare(b.scheduledTimeStart));
    const page = params.page ?? 1;
    const pageSize = params.pageSize ?? BOOKINGS_PAGE_SIZE;
    const start = (page - 1) * pageSize;
    return { items: items.slice(start, start + pageSize), total: items.length, page, pageSize };
  },

  getSessionEvv: async (sessionId: number) => {
    await sleep(MOCK_LATENCY_MS);
    const existing = verifications[sessionId];
    if (existing) return { ...existing };
    const { session } = findSession(sessionId);
    return pendingVerification(session);
  },

  getCareInstructions: async (bookingId: number, viewerRole) => {
    await sleep(MOCK_LATENCY_MS);
    const booking = findBooking(bookingId);
    // The gated boundary — anyone but the assigned nurse/admin 404s (never leaks). The client UI gate
    // means this is a defence-in-depth path the customer should never even reach.
    if (viewerRole !== 'nurse') throw new ApiError(404, 'Not found', 'not_found');
    const record = care[booking.id];
    if (!record) throw new ApiError(404, 'Not found', 'not_found');
    return { ...record };
  },

  checkInVisit: async (input: CheckInVisitInput) => {
    await sleep(MOCK_LATENCY_MS);
    const { booking, session } = findSession(input.bookingSessionId);
    if (session.status !== 'scheduled') throw new ApiError(409, 'Session is not startable', 'not_startable');

    const now = new Date().toISOString();
    const hasCoords = input.latitude != null && input.longitude != null;
    const meters = hasCoords
      ? distanceMeters(input.latitude as number, input.longitude as number, MOCK_EVV_REFERENCE_LAT, MOCK_EVV_REFERENCE_LNG)
      : null;
    // Advisory: true in range · false out-of-range (under review) · null when GPS was unavailable.
    const match = meters == null ? null : meters <= MOCK_EVV_TOLERANCE_METERS;

    session.status = 'in_progress';
    session.evvStatus = 'checked_in';
    session.checkInAt = now;
    session.checkInAddressMatch = match;
    if (booking.status === 'confirmed') booking.status = 'in_progress';

    const verification: VisitVerificationDto = {
      id: session.id,
      bookingSessionId: session.id,
      status: 'checked_in',
      checkInAt: now,
      checkInLat: input.latitude,
      checkInLng: input.longitude,
      checkOutAt: null,
      checkOutLat: null,
      checkOutLng: null,
      checkInAddressMatch: match,
      checkInDistanceMeters: meters == null ? null : Math.round(meters),
    };
    verifications[session.id] = verification;
    return { ...verification };
  },

  checkOutVisit: async (input: CheckOutVisitInput) => {
    await sleep(MOCK_LATENCY_MS);
    const { booking, session } = findSession(input.bookingSessionId);
    if (session.evvStatus !== 'checked_in') throw new ApiError(400, 'No open check-in to close', 'no_open_check_in');

    const now = new Date().toISOString();
    session.status = 'completed';
    session.evvStatus = 'completed';
    session.checkOutAt = now;

    const disputeEnd = new Date(Date.now() + DISPUTE_WINDOW_HOURS * 3_600_000).toISOString();
    // Payout eligibility is server truth (gated by the dispute window). The mock stamps it; the client
    // renders it and never recomputes it.
    session.payoutEligibleAt = disputeEnd;

    const allSettled = booking.sessions.every((s) => s.status === 'completed' || s.status === 'cancelled');
    if (allSettled) {
      booking.status = 'completed';
      booking.completedAt = now;
      booking.disputeWindowEndsAt = disputeEnd;
    }

    const verification = verifications[session.id] ?? pendingVerification(session);
    const updated: VisitVerificationDto = {
      ...verification,
      status: 'completed',
      checkOutAt: now,
      checkOutLat: input.latitude,
      checkOutLng: input.longitude,
    };
    verifications[session.id] = updated;
    return { ...updated };
  },
};

// Converted bookings get ids above the seeded 5001/5002 range; their sessions above the seeded 700xx range.
let nextConvertedBookingId = 6001;
let nextConvertedSessionId = 80001;

/** What the payment mock knows about a captured request — enough to snapshot a faithful booking. */
export interface ConvertedBookingSeed {
  bookingRequestId: number;
  nurseId: number;
  nurseName: string;
  patientId: number;
  patientName: string;
  variantId: number;
  variantLabel: string;
  variantPriceUnit: string;
  customerAddressId: number;
  /** Pre-serialized address snapshot (customer view; the nurse view masks it), or `null` if unknown. */
  addressSnapshotJson: string | null;
  grossPriceIrr: string;
  balinyaarCommissionIrr: string;
  nursePayoutAmount: string;
  pspFeeAmount: string | null;
  platformFeeRate: number;
  scheduledDate: string;
  scheduledTimeStart: string;
  scheduledTimeEnd: string;
}

/**
 * Mock-only capture bridge (f9): the client stand-in for the server's webhook-confirm → `BookingFactory`
 * step (b10) that creates & confirms the booking. Inserts a **confirmed** single-session booking built
 * from the paid request's snapshot, so the f8 booking detail/list show the conversion immediately. NOT
 * part of the `BookingsApi` seam — no real endpoint does this from the client; only `services/payment`'s
 * mock imports it.
 */
export function mockInsertConvertedBooking(seed: ConvertedBookingSeed): BookingDetailDto {
  const now = new Date().toISOString();
  const bookingId = nextConvertedBookingId++;
  const booking: BookingDetailDto = {
    id: bookingId,
    bookingRequestId: seed.bookingRequestId,
    status: 'confirmed',
    nurseId: seed.nurseId,
    nurseName: seed.nurseName,
    patientId: seed.patientId,
    patientName: seed.patientName,
    variantId: seed.variantId,
    variantSnapshotJson: JSON.stringify({ displayName: seed.variantLabel, priceUnit: seed.variantPriceUnit }),
    customerAddressId: seed.customerAddressId,
    addressSnapshotJson: seed.addressSnapshotJson,
    grossPriceIrr: seed.grossPriceIrr,
    balinyaarCommissionIrr: seed.balinyaarCommissionIrr,
    nursePayoutAmount: seed.nursePayoutAmount,
    pspFeeAmount: seed.pspFeeAmount,
    platformFeeRate: seed.platformFeeRate,
    sessionCount: 1,
    scheduledDate: seed.scheduledDate,
    scheduledTimeStart: seed.scheduledTimeStart,
    scheduledTimeEnd: seed.scheduledTimeEnd,
    confirmedAt: now,
    completedAt: null,
    cancelledAt: null,
    cancelledBy: null,
    cancellationReason: null,
    cancellationPolicyCode: null,
    cancellationRefundPercentage: null,
    refundableAmountIrr: null,
    disputeWindowEndsAt: null,
    createdAt: now,
    sessions: [
      {
        ...makeSession(nextConvertedSessionId++, 1, 0, seed.nursePayoutAmount),
        scheduledDate: seed.scheduledDate,
        scheduledTimeStart: seed.scheduledTimeStart,
        scheduledTimeEnd: seed.scheduledTimeEnd,
      },
    ],
  };
  bookings = [booking, ...bookings];
  return cloneBooking(booking);
}

/**
 * Mock-only read for the refunds domain (f10): the booking + its sessions (a safe clone), so the refunds
 * mock can resolve the cancellation tier by lead time and per-session refundability without the
 * viewer-masking `getBookingDetail`. Throws `404` if the booking is unknown. NOT part of the `BookingsApi`
 * seam — only `services/refunds`' mock imports it.
 */
export function mockGetBookingForRefund(bookingId: number): BookingDetailDto {
  return cloneBooking(findBooking(bookingId));
}

/**
 * Mock-only read for the reviews domain (f13): the booking (a safe clone) so the reviews mock can gate
 * review-eligibility on a completed/closed booking and read `nurseId`/`patientId` for a submission. Throws
 * `404` if unknown. One-way edge INTO bookings (the bookings mock never imports f13 back → no cycle). NOT
 * part of the `BookingsApi` seam — only `services/reviews`' mock imports it.
 */
export function mockGetBookingForReview(bookingId: number): BookingDetailDto {
  return cloneBooking(findBooking(bookingId));
}

/** The cancellation snapshot the refunds mock writes onto a booking when a customer cancels (f10). */
export interface CancelBookingSnapshot {
  cancelledBy: string;
  cancellationReason: string | null;
  cancellationPolicyCode: string;
  cancellationRefundPercentage: number;
  refundableAmountIrr: string;
  /** The un-started sessions being cancelled; completed-and-verified sessions stay payout-eligible. */
  cancelledSessionIds: number[];
}

/**
 * Mock-only cancellation bridge (f10): flip a booking to `cancelled` and stamp the cancellation snapshot
 * the b9 `BookingDetailDto` already declares (currently only ever read, never written). Mirrors the
 * in-place mutation pattern of `checkOutVisit` — it mutates the live store object (a reference into
 * `bookings`), so the next `getBookingDetail`/`listBookings` reflects it once the cancel mutation
 * invalidates the caches. Only still-`scheduled` sessions in `cancelledSessionIds` are marked `cancelled`
 * (per-remaining-session cancellation). NOT part of the `BookingsApi` seam.
 */
export function mockMarkBookingCancelled(
  bookingId: number,
  snapshot: CancelBookingSnapshot,
): BookingDetailDto {
  const booking = findBooking(bookingId);
  booking.status = 'cancelled';
  booking.cancelledAt = new Date().toISOString();
  booking.cancelledBy = snapshot.cancelledBy;
  booking.cancellationReason = snapshot.cancellationReason;
  booking.cancellationPolicyCode = snapshot.cancellationPolicyCode;
  booking.cancellationRefundPercentage = snapshot.cancellationRefundPercentage;
  booking.refundableAmountIrr = snapshot.refundableAmountIrr;
  for (const session of booking.sessions) {
    if (snapshot.cancelledSessionIds.includes(session.id) && session.status === 'scheduled') {
      session.status = 'cancelled';
    }
  }
  return cloneBooking(booking);
}
