import {
  BookingApiClient,
  cancelBookingNowViaApi,
  createBookingPriceChangeViaApi,
  createBookingViaApi,
  resolveBookingApiClient,
} from '@/manager/modules/booking/booking-api';
import {
  BookingCreateDataOptions,
  getBookingCreateData,
  getBookingDiscountCreateData,
  getBookingInsuranceCreateData,
  getBookingOneTimeProductCreateData,
  getBookingRecurringProductCreateData,
} from '@/manager/modules/booking/booking-factories';
import { BookingCreateData, BookingPriceChange } from '@/manager/modules/booking/booking-types';
import { BookingCreateDialog } from '@/manager/modules/booking/views/BookingCreateDialog';
import { BookingDetailsPage } from '@/manager/modules/booking/views/BookingDetailsPage';
import { BookingListPage } from '@/manager/modules/booking/views/BookingListPage';
import { createCustomerViaApi, deleteCustomerViaApi } from '@/manager/modules/customer/customer-api';
import { getCustomerCreateData } from '@/manager/modules/customer/customer-factories';
import { CustomerCreateData } from '@/manager/modules/customer/customer-types';
import { createDepositViaApi, deleteDepositViaApi } from '@/manager/modules/deposit/deposit-api';
import { DepositCreateDataOptions, getDepositCreateData } from '@/manager/modules/deposit/deposit-factories';
import { DepositCreateData } from '@/manager/modules/deposit/deposit-types';
import { createDiscountViaApi, deleteDiscountViaApi } from '@/manager/modules/discount/discount-api';
import { DiscountCreateDataOptions } from '@/manager/modules/discount/discount-factories';
import { DiscountCreateData } from '@/manager/modules/discount/discount-types';
import { createInsuranceViaApi, deleteInsuranceViaApi } from '@/manager/modules/insurance/insurance-api';
import { InsuranceCreateDataOptions } from '@/manager/modules/insurance/insurance-factories';
import { InsuranceCreateData } from '@/manager/modules/insurance/insurance-types';
import {
  createProductOneTimeViaApi,
  createProductRecurringViaApi,
  deleteProductOneTimeViaApi,
  deleteProductRecurringViaApi,
} from '@/manager/modules/product/product-api';
import {
  ProductOneTimeCreateDataOptions,
  ProductRecurringCreateDataOptions,
} from '@/manager/modules/product/product-factories';
import { ProductOneTimeCreateData, ProductRecurringCreateData } from '@/manager/modules/product/product-types';
import { createUnitViaApi, deleteUnitViaApi, updateUnitStatusViaApi } from '@/manager/modules/unit/unit-api';
import { getUnitCreateData } from '@/manager/modules/unit/unit-factories';
import { UnitCreateData } from '@/manager/modules/unit/unit-types';
import { createUnitTypeViaApi, deleteUnitTypeViaApi } from '@/manager/modules/unit-type/unit-type-api';
import {
  getDefaultUnitTypeCreateMonthlyBookingPlans,
  getDefaultUnitTypeCreateWeeklyBookingPlans,
  getUnitTypeCreateData,
} from '@/manager/modules/unit-type/unit-type-factories';
import { UnitTypeCreateData } from '@/manager/modules/unit-type/unit-type-types';
import { test as apiTest } from '@/manager/shared/api-fixtures';
import { APIRequestContext } from '@playwright/test';
import { Booking } from '@/shared/types/booking-types';
import { Customer, CustomerType } from '@/shared/types/customer-types';
import { Deposit } from '@/shared/types/deposit-types';
import { Discount } from '@/shared/types/discount-types';
import { Insurance } from '@/shared/types/insurance-types';
import { ProductOneTime, ProductRecurring } from '@/shared/types/product-types';
import { Unit } from '@/shared/types/unit-types';
import { UnitType } from '@/shared/types/unit-type-types';

// Directives describing WHICH dependency entities setupBookingCreateData stands up over the API before a
// booking is built — every field here triggers a create* call. This is the complement to
// BookingCreateDataOptions, which describes HOW to assemble those entities (no creation). `insurance: true`
// stands one up with the billing-type default plans; pass an options object to vary it. `discount` carries
// its own type/value straight into the discount factory. `recurringProducts`/`oneTimeProducts` create one
// entity per array element with the given quantity (default 1). `deposit: true` stands a standalone deposit
// up and adds it to the booking; whether it bills upfront is an assembly choice carried by
// options.chargeDepositUpfront.
export interface BookingCreateDataSetup {
  customerType?: CustomerType;
  insurance?: true | InsuranceCreateDataOptions;
  discount?: DiscountCreateDataOptions;
  deposit?: true | DepositCreateDataOptions;
  recurringProducts?: BookingCreateDataRecurringProductSetup[];
  oneTimeProducts?: BookingCreateDataOneTimeProductSetup[];
}

export interface BookingCreateDataRecurringProductSetup {
  quantity?: number; // defaults to 1
  options?: ProductRecurringCreateDataOptions;
}

export interface BookingCreateDataOneTimeProductSetup {
  quantity?: number; // defaults to 1
  options?: ProductOneTimeCreateDataOptions;
}

export interface BookingFixtures {
  bookingListPage: BookingListPage;
  bookingDetailsPage: BookingDetailsPage;
  bookingCreateDialog: BookingCreateDialog;
  setupBookingCreateData: (
    setup?: BookingCreateDataSetup,
    options?: BookingCreateDataOptions
  ) => Promise<BookingCreateData>;
  createBooking: (data: BookingCreateData) => Promise<Booking>;
  createBookingPriceChange: (bookingIds: number[], priceChange: BookingPriceChange) => Promise<void>;
  createCustomer: (data: CustomerCreateData) => Promise<Customer>;
  createUnitType: (data: UnitTypeCreateData) => Promise<UnitType>;
  createUnit: (data: UnitCreateData) => Promise<Unit>;
  createDiscount: (data: DiscountCreateData) => Promise<Discount>;
  createDeposit: (data: DepositCreateData) => Promise<Deposit>;
  createInsurance: (data: InsuranceCreateData) => Promise<Insurance>;
  createProductOneTime: (data: ProductOneTimeCreateData) => Promise<ProductOneTime>;
  createProductRecurring: (data: ProductRecurringCreateData) => Promise<ProductRecurring>;
  trackBookingForTeardown: (id: number) => void;
}

interface BookingInternalFixtures {
  bookingApiClient: BookingApiClient;
  bookingTeardown: {
    bookingIds: number[];
    customerIds: number[];
    unitIds: number[];
    unitTypeIds: number[];
    insuranceIds: number[];
    discountIds: number[];
    depositIds: number[];
    productOneTimeIds: number[];
    productRecurringIds: number[];
  };
}

// Booking teardown is best-effort: a booking can't be deleted, and cancelling it kicks off async
// backend work (unit release, customer→former transition, Stripe insurance-subscription teardown).
// We give those a short bounded poll so the common case cleans up fully and fast, but never block the
// suite — anything still stuck is swallowed and left for the environment's periodic reseed to reclaim.
const TEARDOWN_MAX_ATTEMPTS = 5;
const TEARDOWN_RETRY_DELAY_MS = 2000;

async function retryUntilSuccess(action: () => Promise<void>): Promise<void> {
  for (let attempt = 1; attempt <= TEARDOWN_MAX_ATTEMPTS; attempt++) {
    try {
      await action();
      return;
    } catch (error) {
      if (attempt === TEARDOWN_MAX_ATTEMPTS) {
        throw error;
      }

      await new Promise((resolve) => setTimeout(resolve, TEARDOWN_RETRY_DELAY_MS));
    }
  }
}

// Cancelling a booking releases its unit asynchronously, and the cancellation job can flip the unit
// to 'to check' after we reset it. So we re-assert 'vacant' and retry the delete until the unit is
// both vacant and free of active bookings (the delete 422s with unit_cannot_be_deleted until then).
function deleteUnitOnceDeletableViaApi(request: APIRequestContext, id: number): Promise<void> {
  return retryUntilSuccess(async () => {
    await updateUnitStatusViaApi(request, id, 'vacant');
    await deleteUnitViaApi(request, id);
  });
}

// Cancelling all of a customer's bookings flips it to `former` asynchronously (an event/job runs
// after the cancel response returns). A customer is only deletable while `lead`/`former`, so we poll
// the delete — it 422s with customer_status_invalid until the transition lands, then succeeds.
function deleteCustomerOnceFormerViaApi(request: APIRequestContext, id: number): Promise<void> {
  return retryUntilSuccess(() => deleteCustomerViaApi(request, id));
}

export const test = apiTest.extend<BookingFixtures & BookingInternalFixtures>({
  bookingListPage: async ({ page }, use) => {
    await use(new BookingListPage(page));
  },

  bookingDetailsPage: async ({ page }, use) => {
    await use(new BookingDetailsPage(page));
  },

  bookingCreateDialog: async ({ page }, use) => {
    await use(new BookingCreateDialog(page));
  },

  // Routes booking create/cancel to the manager or billing API depending on the run's billing mode; every
  // other entity below always uses the manager API.
  bookingApiClient: async ({ managerApiRequest, billingApiRequest }, use) => {
    await use(resolveBookingApiClient(managerApiRequest, billingApiRequest));
  },

  createBooking: async ({ bookingApiClient, bookingTeardown }, use) => {
    await use(async (data) => {
      const booking = await apiTest.step('setup: create booking via API', () =>
        createBookingViaApi(bookingApiClient, data)
      );
      bookingTeardown.bookingIds.push(booking.id);
      return booking;
    });
  },

  // The scheduled change dies with its booking, so there is nothing separate to tear down.
  createBookingPriceChange: async ({ bookingApiClient }, use) => {
    await use(async (bookingIds, priceChange) => {
      await apiTest.step('setup: create booking price change via API', () =>
        createBookingPriceChangeViaApi(bookingApiClient, bookingIds, priceChange)
      );
    });
  },

  createCustomer: async ({ managerApiRequest, bookingTeardown }, use) => {
    await use(async (data) => {
      const customer = await apiTest.step('setup: create customer via API', () =>
        createCustomerViaApi(managerApiRequest, data)
      );
      bookingTeardown.customerIds.push(customer.id);
      return customer;
    });
  },

  createUnitType: async ({ managerApiRequest, bookingTeardown }, use) => {
    await use(async (data) => {
      const unitType = await apiTest.step('setup: create unit type via API', () =>
        createUnitTypeViaApi(managerApiRequest, data)
      );
      bookingTeardown.unitTypeIds.push(unitType.id);
      return unitType;
    });
  },

  createUnit: async ({ managerApiRequest, bookingTeardown }, use) => {
    await use(async (data) => {
      const unit = await apiTest.step('setup: create unit via API', () => createUnitViaApi(managerApiRequest, data));
      bookingTeardown.unitIds.push(unit.id);
      return unit;
    });
  },

  createInsurance: async ({ managerApiRequest, bookingTeardown }, use) => {
    await use(async (data) => {
      const insurance = await apiTest.step('setup: create insurance via API', () =>
        createInsuranceViaApi(managerApiRequest, data)
      );
      bookingTeardown.insuranceIds.push(insurance.id);
      return insurance;
    });
  },

  createDiscount: async ({ managerApiRequest, bookingTeardown }, use) => {
    await use(async (data) => {
      const discount = await apiTest.step('setup: create discount via API', () =>
        createDiscountViaApi(managerApiRequest, data)
      );
      bookingTeardown.discountIds.push(discount.id);
      return discount;
    });
  },

  createDeposit: async ({ managerApiRequest, bookingTeardown }, use) => {
    await use(async (data) => {
      const deposit = await apiTest.step('setup: create deposit via API', () =>
        createDepositViaApi(managerApiRequest, data)
      );
      bookingTeardown.depositIds.push(deposit.id);
      return deposit;
    });
  },

  createProductOneTime: async ({ managerApiRequest, bookingTeardown }, use) => {
    await use(async (data) => {
      const product = await apiTest.step('setup: create one-time product via API', () =>
        createProductOneTimeViaApi(managerApiRequest, data)
      );
      bookingTeardown.productOneTimeIds.push(product.id);
      return product;
    });
  },

  createProductRecurring: async ({ managerApiRequest, bookingTeardown }, use) => {
    await use(async (data) => {
      const product = await apiTest.step('setup: create recurring product via API', () =>
        createProductRecurringViaApi(managerApiRequest, data)
      );
      bookingTeardown.productRecurringIds.push(product.id);
      return product;
    });
  },

  // Stands a booking's dependencies up over the API from a directive (`setup`), then assembles them into a
  // BookingCreateData via getBookingCreateData — the procure-then-assemble layer over the pure factory
  // (which assumes the entities already exist). `options.billingType` is read here to seed plan presets
  // matching the booked cadence (unit type, insurance, recurring products); the rest of `options` passes
  // straight through. Both args default to {} so a case can omit either.
  setupBookingCreateData: async (
    {
      createCustomer,
      createUnitType,
      createUnit,
      createInsurance,
      createDiscount,
      createDeposit,
      createProductOneTime,
      createProductRecurring,
    },
    use
  ) => {
    await use(async (setup = {}, options = {}) => {
      const billingType = options.billingType ?? 'weekly';

      // A deposit is a standalone entity picked on the booking — it isn't tied to the unit type.
      const [customer, unitType, deposit] = await Promise.all([
        createCustomer(getCustomerCreateData({ type: setup.customerType })),
        createUnitType(
          getUnitTypeCreateData({
            bookingPlans:
              billingType === 'monthly'
                ? getDefaultUnitTypeCreateMonthlyBookingPlans()
                : getDefaultUnitTypeCreateWeeklyBookingPlans(),
          })
        ),
        setup.deposit
          ? createDeposit(getDepositCreateData(setup.deposit === true ? undefined : setup.deposit))
          : undefined,
      ]);

      const [unit, insurance, recurringProducts, oneTimeProducts] = await Promise.all([
        createUnit(getUnitCreateData(unitType)),
        setup.insurance
          ? createInsurance(
              getBookingInsuranceCreateData(
                billingType,
                unitType.location,
                setup.insurance === true ? undefined : setup.insurance
              )
            )
          : undefined,
        setup.recurringProducts
          ? Promise.all(
              setup.recurringProducts.map(async (item) => ({
                product: await createProductRecurring(
                  getBookingRecurringProductCreateData(billingType, unitType, item.options)
                ),
                quantity: item.quantity ?? 1,
              }))
            )
          : undefined,
        setup.oneTimeProducts
          ? Promise.all(
              setup.oneTimeProducts.map(async (item) => ({
                product: await createProductOneTime(getBookingOneTimeProductCreateData(unitType, item.options)),
                quantity: item.quantity ?? 1,
              }))
            )
          : undefined,
      ]);

      const data = getBookingCreateData(
        { customer, unitType, unit, insurance, deposit, recurringProducts, oneTimeProducts },
        options
      );

      if (setup.discount) {
        data.discount = await createDiscount(getBookingDiscountCreateData(data.unitType, setup.discount));
      }

      return data;
    });
  },

  trackBookingForTeardown: async ({ bookingTeardown }, use) => {
    await use((id) => bookingTeardown.bookingIds.push(id));
  },

  bookingTeardown: async ({ bookingApiClient, managerApiRequest }, use) => {
    const bookingIds: number[] = [];
    const customerIds: number[] = [];
    const unitIds: number[] = [];
    const unitTypeIds: number[] = [];
    const insuranceIds: number[] = [];
    const discountIds: number[] = [];
    const depositIds: number[] = [];
    const productOneTimeIds: number[] = [];
    const productRecurringIds: number[] = [];

    await use({
      bookingIds,
      customerIds,
      unitIds,
      unitTypeIds,
      insuranceIds,
      discountIds,
      depositIds,
      productOneTimeIds,
      productRecurringIds,
    });

    // TODO(KIN-4692): Teardown is disabled for now. Cancelling bookings kicks off a flood of async
    // backend jobs, and when the job queue backs up, invoice-generation jobs don't run in time — which
    // breaks booking e2e tests that rely on invoices being created. Until the queue pressure is sorted
    // out, we leave all created entities for the env reseed to clean up rather than tearing down here.
    return;

    // Bookings can't be deleted, and an active booking pins its unit/unit-type/customer (deletion
    // guards count only non-cancelled bookings). So we cancel each booking with immediate effect,
    // then delete the customers — cancelling all of a customer's bookings flips it to the deletable
    // 'former' status asynchronously, and deleting it soft-deletes its bookings, which frees the units
    // without waiting on the (slower) unit-release job. Units then just need flipping back to 'vacant'
    // before deletion. Every step is best-effort: the no-insurance path cleans up fully in seconds,
    // while slower cases (insurance bookings trigger async Stripe teardown) are left for the env reseed
    // rather than blocking the suite. Teardown failures never fail a passing test.
    const settle = (promise: Promise<unknown>): Promise<unknown> => promise.catch(() => undefined);

    await apiTest.step('teardown: cancel created bookings via API', () =>
      Promise.all(bookingIds.map((id) => settle(cancelBookingNowViaApi(bookingApiClient, id))))
    );
    await apiTest.step('teardown: delete created customers via API', () =>
      Promise.all(customerIds.map((id) => settle(deleteCustomerOnceFormerViaApi(managerApiRequest, id))))
    );
    await apiTest.step('teardown: delete created units via API', () =>
      Promise.all(unitIds.map((id) => settle(deleteUnitOnceDeletableViaApi(managerApiRequest, id))))
    );
    await apiTest.step('teardown: delete created unit types via API', () =>
      Promise.all(unitTypeIds.map((id) => settle(deleteUnitTypeViaApi(managerApiRequest, id))))
    );
    await apiTest.step('teardown: delete created insurances via API', () =>
      Promise.all(insuranceIds.map((id) => settle(deleteInsuranceViaApi(managerApiRequest, id))))
    );
    await apiTest.step('teardown: delete created discounts via API', () =>
      Promise.all(discountIds.map((id) => settle(deleteDiscountViaApi(managerApiRequest, id))))
    );
    await apiTest.step('teardown: delete created deposits via API', () =>
      Promise.all(depositIds.map((id) => settle(deleteDepositViaApi(managerApiRequest, id))))
    );
    await apiTest.step('teardown: delete created one-time products via API', () =>
      Promise.all(productOneTimeIds.map((id) => settle(deleteProductOneTimeViaApi(managerApiRequest, id))))
    );
    await apiTest.step('teardown: delete created recurring products via API', () =>
      Promise.all(productRecurringIds.map((id) => settle(deleteProductRecurringViaApi(managerApiRequest, id))))
    );
  },
});

export { expect } from '@playwright/test';
