import { describe, expect, test } from "@jest/globals"
import { act, renderHook } from "@testing-library/react"
import { type PropsWithChildren } from "react"

import { Lang } from "@/constants/languages"

import {
  bedType,
  booking,
  breakfastPackage,
  guestDetailsMember,
  guestDetailsNonMember,
  roomPrice,
  roomRate,
} from "@/__mocks__/hotelReservation"
import EnterDetailsProvider from "@/providers/EnterDetailsProvider"

import { selectRoom, selectRoomStatus } from "./helpers"
import { detailsStorageName, useEnterDetailsStore } from "."

import { StepEnum } from "@/types/enums/step"
import type { PersistedState } from "@/types/stores/enter-details"

jest.mock("react", () => ({
  ...jest.requireActual("react"),
  cache: jest.fn(),
}))

jest.mock("@/server/utils", () => ({
  toLang: () => Lang.en,
}))

jest.mock("@/lib/api", () => ({
  fetchRetry: jest.fn((fn) => fn),
}))

interface CreateWrapperParams {
  showBreakfastStep?: boolean
  breakfastIncluded?: boolean
  mustBeGuaranteed?: boolean
  onlyOneBedType?: boolean
}

function createWrapper(params: Partial<CreateWrapperParams> = {}) {
  const {
    showBreakfastStep = true,
    breakfastIncluded = false,
    mustBeGuaranteed = false,
    onlyOneBedType = false,
  } = params

  return function Wrapper({ children }: PropsWithChildren) {
    return (
      <EnterDetailsProvider
        booking={booking}
        showBreakfastStep={showBreakfastStep}
        roomsData={[
          {
            bedTypes: onlyOneBedType
              ? [bedType.king]
              : [bedType.king, bedType.queen],
            packages: null,
            mustBeGuaranteed,
            breakfastIncluded,
            cancellationText: "",
            rateDetails: [],
            roomType: "Standard",
            roomRate: roomRate,
          },
          {
            bedTypes: onlyOneBedType
              ? [bedType.king]
              : [bedType.king, bedType.queen],
            packages: null,
            mustBeGuaranteed,
            breakfastIncluded,
            cancellationText: "",
            rateDetails: [],
            roomType: "Standard",
            roomRate: roomRate,
          },
        ]}
        searchParamsStr=""
        user={null}
        vat={0}
      >
        {children}
      </EnterDetailsProvider>
    )
  }
}

describe("Enter Details Store", () => {
  beforeEach(() => {
    window.sessionStorage.clear()
  })

  test("initialize with correct default values", () => {
    const { result } = renderHook(
      () => useEnterDetailsStore((state) => state),
      {
        wrapper: createWrapper(),
      }
    )
    const state = result.current

    expect(state.booking).toEqual(booking)
    expect(state.breakfast).toEqual(undefined)

    // room 1
    const room1Status = selectRoomStatus(result.current, 0)
    const room1 = selectRoom(result.current, 0)

    expect(room1Status.currentStep).toBe(StepEnum.selectBed)

    expect(room1.roomPrice.perNight.local.price).toEqual(
      roomRate.publicRate.localPrice.pricePerNight
    )
    expect(room1.bedType).toEqual(undefined)
    expect(Object.values(room1.guest).every((value) => value === ""))

    // room 2
    const room2Status = selectRoomStatus(result.current, 1)
    const room2 = selectRoom(result.current, 1)

    expect(room2Status.currentStep).toBe(null)
    expect(room2.roomPrice.perNight.local.price).toEqual(
      room2.roomRate.publicRate.localPrice.pricePerNight
    )
    expect(room2.bedType).toEqual(undefined)
    expect(Object.values(room2.guest).every((value) => value === ""))
  })

  test("initialize with correct values from session storage", () => {
    const storage: PersistedState = {
      booking: booking,
      bookingProgress: {
        currentRoomIndex: 0,
        canProceedToPayment: true,
        roomStatuses: [
          {
            isComplete: false,
            currentStep: StepEnum.selectBed,
            lastCompletedStep: undefined,
            steps: {
              [StepEnum.selectBed]: {
                step: StepEnum.selectBed,
                isValid: true,
              },
              [StepEnum.breakfast]: {
                step: StepEnum.breakfast,
                isValid: true,
              },
              [StepEnum.details]: {
                step: StepEnum.details,
                isValid: true,
              },
            },
          },
        ],
      },
      rooms: [
        {
          roomFeatures: null,
          roomRate: roomRate,
          roomType: "Classic Double",
          cancellationText: "Non-refundable",
          rateDetails: [],
          bedType: {
            roomTypeCode: bedType.king.value,
            description: bedType.king.description,
          },
          adults: 1,
          childrenInRoom: [],
          breakfast: breakfastPackage,
          guest: guestDetailsNonMember,
          roomPrice: roomPrice,
        },
      ],
    }

    window.sessionStorage.setItem(detailsStorageName, JSON.stringify(storage))

    const { result } = renderHook(
      () => useEnterDetailsStore((state) => state),
      {
        wrapper: createWrapper(),
      }
    )

    expect(result.current.booking).toEqual(storage.booking)
    expect(result.current.rooms[0]).toEqual(storage.rooms[0])
    expect(result.current.bookingProgress).toEqual(storage.bookingProgress)
  })

  test("add bedtype and proceed to next step", async () => {
    const { result } = renderHook(
      () => useEnterDetailsStore((state) => state),
      {
        wrapper: createWrapper(),
      }
    )

    let roomStatus = selectRoomStatus(result.current)
    expect(roomStatus.currentStep).toEqual(StepEnum.selectBed)

    const selectedBedType = {
      roomTypeCode: bedType.king.value,
      description: bedType.king.description,
    }

    await act(async () => {
      result.current.actions.updateBedType(selectedBedType)
    })

    roomStatus = selectRoomStatus(result.current)
    const room = selectRoom(result.current)

    expect(roomStatus.steps[StepEnum.selectBed].isValid).toEqual(true)
    expect(room.bedType).toEqual(selectedBedType)

    expect(roomStatus.currentStep).toEqual(StepEnum.breakfast)
  })

  test("complete step and navigate to next step", async () => {
    const { result } = renderHook(
      () => useEnterDetailsStore((state) => state),
      {
        wrapper: createWrapper(),
      }
    )

    // Room 1
    expect(result.current.bookingProgress.currentRoomIndex).toEqual(0)

    let roomStatus = selectRoomStatus(result.current)
    expect(roomStatus.currentStep).toEqual(StepEnum.selectBed)

    await act(async () => {
      result.current.actions.updateBedType({
        roomTypeCode: bedType.king.value,
        description: bedType.king.description,
      })
    })

    roomStatus = selectRoomStatus(result.current)
    expect(roomStatus.steps[StepEnum.selectBed].isValid).toEqual(true)
    expect(roomStatus.currentStep).toEqual(StepEnum.breakfast)

    await act(async () => {
      result.current.actions.updateBreakfast(breakfastPackage)
    })

    roomStatus = selectRoomStatus(result.current)
    expect(roomStatus.steps[StepEnum.breakfast]?.isValid).toEqual(true)
    expect(roomStatus.currentStep).toEqual(StepEnum.details)

    await act(async () => {
      result.current.actions.updateDetails(guestDetailsNonMember)
    })

    expect(result.current.bookingProgress.canProceedToPayment).toBe(false)

    // Room 2
    expect(result.current.bookingProgress.currentRoomIndex).toEqual(1)

    roomStatus = selectRoomStatus(result.current)
    expect(roomStatus.currentStep).toEqual(StepEnum.selectBed)

    await act(async () => {
      const selectedBedType = {
        roomTypeCode: bedType.king.value,
        description: bedType.king.description,
      }
      result.current.actions.updateBedType(selectedBedType)
    })

    roomStatus = selectRoomStatus(result.current)
    expect(roomStatus.steps[StepEnum.selectBed].isValid).toEqual(true)
    expect(roomStatus.currentStep).toEqual(StepEnum.breakfast)

    await act(async () => {
      result.current.actions.updateBreakfast(breakfastPackage)
    })

    roomStatus = selectRoomStatus(result.current)
    expect(roomStatus.steps[StepEnum.breakfast]?.isValid).toEqual(true)
    expect(roomStatus.currentStep).toEqual(StepEnum.details)

    await act(async () => {
      result.current.actions.updateDetails(guestDetailsNonMember)
    })

    expect(result.current.bookingProgress.canProceedToPayment).toBe(true)
  })

  test("all steps needs to be completed before going to next room", async () => {
    const { result } = renderHook(
      () => useEnterDetailsStore((state) => state),
      {
        wrapper: createWrapper(),
      }
    )

    await act(async () => {
      result.current.actions.updateDetails(guestDetailsNonMember)
    })

    expect(result.current.bookingProgress.currentRoomIndex).toEqual(0)

    await act(async () => {
      result.current.actions.setStep(StepEnum.breakfast, 1)
    })

    expect(result.current.bookingProgress.currentRoomIndex).toEqual(0)
  })

  test("can go back and modify room 1 after completion", async () => {
    const { result } = renderHook(
      () => useEnterDetailsStore((state) => state),
      {
        wrapper: createWrapper(),
      }
    )

    await act(async () => {
      result.current.actions.updateBedType({
        roomTypeCode: bedType.king.value,
        description: bedType.king.description,
      })
      result.current.actions.updateBreakfast(breakfastPackage)
      result.current.actions.updateDetails(guestDetailsNonMember)
    })

    // now we are at room 2
    expect(result.current.bookingProgress.currentRoomIndex).toEqual(1)

    await act(async () => {
      result.current.actions.setStep(StepEnum.breakfast, 0) // click "modify"
    })

    expect(result.current.bookingProgress.currentRoomIndex).toEqual(0)

    await act(async () => {
      result.current.actions.updateBreakfast(breakfastPackage)
    })

    // going back to room 2
    expect(result.current.bookingProgress.currentRoomIndex).toEqual(1)
  })

  test("total price should be set properly", async () => {
    const { result } = renderHook(
      () => useEnterDetailsStore((state) => state),
      {
        wrapper: createWrapper(),
      }
    )

    const publicRate = roomRate.publicRate.localPrice.pricePerStay
    const memberRate = roomRate.memberRate?.localPrice.pricePerStay ?? 0

    const initialTotalPrice = publicRate * result.current.rooms.length
    expect(result.current.totalPrice.local.price).toEqual(initialTotalPrice)

    // room 1
    await act(async () => {
      result.current.actions.updateBedType({
        roomTypeCode: bedType.king.value,
        description: bedType.king.description,
      })
      result.current.actions.updateBreakfast(breakfastPackage)
    })

    let expectedTotalPrice =
      initialTotalPrice + Number(breakfastPackage.localPrice.price)
    expect(result.current.totalPrice.local.price).toEqual(expectedTotalPrice)

    await act(async () => {
      result.current.actions.updateDetails(guestDetailsMember)
    })

    expectedTotalPrice =
      memberRate + publicRate + Number(breakfastPackage.localPrice.price)
    expect(result.current.totalPrice.local.price).toEqual(expectedTotalPrice)

    // room 2
    await act(async () => {
      result.current.actions.updateBedType({
        roomTypeCode: bedType.king.value,
        description: bedType.king.description,
      })
      result.current.actions.updateBreakfast(breakfastPackage)
    })

    expectedTotalPrice =
      memberRate + publicRate + Number(breakfastPackage.localPrice.price) * 2
    expect(result.current.totalPrice.local.price).toEqual(expectedTotalPrice)

    await act(async () => {
      result.current.actions.updateDetails(guestDetailsNonMember)
    })

    expect(result.current.totalPrice.local.price).toEqual(expectedTotalPrice)
  })

  test("room price should be set properly", async () => {
    const { result } = renderHook(
      () => useEnterDetailsStore((state) => state),
      {
        wrapper: createWrapper(),
      }
    )

    const publicRate = roomRate.publicRate.localPrice.pricePerStay
    const memberRate = roomRate.memberRate?.localPrice.pricePerStay ?? 0

    let room1 = selectRoom(result.current, 0)
    expect(room1.roomPrice.perStay.local.price).toEqual(publicRate)

    let room2 = selectRoom(result.current, 0)
    expect(room2.roomPrice.perStay.local.price).toEqual(publicRate)

    await act(async () => {
      result.current.actions.updateDetails(guestDetailsMember)
    })

    room1 = selectRoom(result.current, 0)
    expect(room1.roomPrice.perStay.local.price).toEqual(memberRate)
  })

  test("breakfast step should be hidden when breakfast is included", async () => {
    const { result } = renderHook(
      () => useEnterDetailsStore((state) => state),
      {
        wrapper: createWrapper({ showBreakfastStep: false }),
      }
    )

    const room1Status = selectRoomStatus(result.current, 0)
    expect(Object.keys(room1Status.steps)).not.toContain(StepEnum.breakfast)

    const room2Status = selectRoomStatus(result.current, 1)
    expect(Object.keys(room2Status.steps)).not.toContain(StepEnum.breakfast)
  })

  test("select bed step should be skipped when there is only one bedtype", async () => {
    const { result } = renderHook(
      () => useEnterDetailsStore((state) => state),
      {
        wrapper: createWrapper({ onlyOneBedType: true }),
      }
    )

    const room1Status = selectRoomStatus(result.current, 0)
    expect(room1Status.steps[StepEnum.selectBed].isValid).toEqual(true)
    expect(room1Status.currentStep).toEqual(StepEnum.breakfast)

    const room2Status = selectRoomStatus(result.current, 1)
    expect(room2Status.steps[StepEnum.selectBed].isValid).toEqual(true)
    expect(room2Status.currentStep).toEqual(null)
  })
})
