import {
  hasFreeBusyConflict,
  useAttendeesFreeBusy
} from '@/components/Attendees/useFreeBusy'
import * as CalendarApi from '@/features/Calendars/CalendarApi'
import * as FreeBusyDao from '@/features/Events/FreeBusyDao'
import * as UserDao from '@/features/User/UserDao'
import { renderHook, waitFor } from '@testing-library/react'

jest.mock('moment-timezone', () => {
  const actual = jest.requireActual('moment-timezone')
  return actual
})

jest.mock('@/features/Events/FreeBusyDao')
jest.mock('@/features/User/UserDao')
jest.mock('@/features/Calendars/CalendarApi')

const mockGetUserData = UserDao.fetchUserByEmail as jest.MockedFunction<
  typeof UserDao.fetchUserByEmail
>
const mockREPORT = FreeBusyDao.fetchFreeBusyReports as jest.MockedFunction<
  typeof FreeBusyDao.fetchFreeBusyReports
>
const mockPOST = FreeBusyDao.fetchFreeBusyPost as jest.MockedFunction<
  typeof FreeBusyDao.fetchFreeBusyPost
>
const mockGetCalendars = CalendarApi.getCalendars as jest.MockedFunction<
  typeof CalendarApi.getCalendars
>

const mockCalendarsResponse = {
  _embedded: {
    'dav:calendar': [
      {
        _links: {
          self: {
            href: '/calendars/user-carol/user-carol'
          }
        }
      }
    ]
  }
}

const freeIcal = [
  {
    _links: {
      self: {
        href: '\/calendars\/user-carol\/e38d1ea7-6d2c-46c6-8933-cdb325d4458e'
      }
    },
    data: [
      'vcalendar',
      [],
      [
        [
          'vfreebusy',
          [
            ['dtstart', {}, 'date-time', '2026-03-14T14:00:00Z'],
            ['dtend', {}, 'date-time', '2026-03-14T15:00:00Z'],
            ['dtstamp', {}, 'date-time', '2026-03-14T13:00:00Z']
          ],
          []
        ]
      ]
    ]
  }
]

const busyIcal = [
  {
    _links: {
      self: {
        href: '\/calendars\/user-carol\/e38d1ea7-6d2c-46c6-8933-cdb325d4458e'
      }
    },
    data: [
      'vcalendar',
      [],
      [
        [
          'vfreebusy',
          [
            ['dtstart', {}, 'date-time', '2026-03-14T14:00:00Z'],
            ['dtend', {}, 'date-time', '2026-03-14T15:00:00Z'],
            ['dtstamp', {}, 'date-time', '2026-03-14T13:00:00Z'],
            [
              'freebusy',
              {},
              'period',
              ['2026-03-14T14:00:00Z', '2026-03-14T15:00:00Z']
            ]
          ],
          []
        ]
      ]
    ]
  }
]

const START = '2026-03-14T14:00:00'
const END = '2026-03-14T15:00:00'
const TZ = 'Europe/Paris'

const freePostResponse = {
  start: '20260314T130000',
  end: '20260314T140000',
  users: [
    {
      id: 'user-carol',
      calendars: [{ id: 'user-carol', busy: [] }]
    }
  ]
}

const busyPostResponse = {
  start: '20260314T130000',
  end: '20260314T140000',
  users: [
    {
      id: 'user-carol',
      calendars: [
        {
          id: 'user-carol',
          busy: [
            {
              uid: 'fa507111-07d5-44b4-a611-c27845a66ccf',
              start: START,
              end: END
            }
          ]
        }
      ]
    }
  ]
}

describe('hasFreeBusyConflict', () => {
  it('returns false for non-vcalendar data', () => {
    expect(hasFreeBusyConflict({ data: ['not-vcalendar', [], []] })).toBe(false)
  })

  it('returns false when vfreebusy has no freebusy property', () => {
    const data = {
      data: [
        'vcalendar',
        [],
        [
          [
            'vfreebusy',
            [['dtstart', {}, 'date-time', '2026-03-14T14:00:00Z']],
            []
          ]
        ]
      ]
    }
    expect(hasFreeBusyConflict(data)).toBe(false)
  })

  it('returns true when vfreebusy contains a freebusy property', () => {
    const data = {
      data: [
        'vcalendar',
        [],
        [
          [
            'vfreebusy',
            [
              ['dtstart', {}, 'date-time', '2026-03-14T14:00:00Z'],
              ['freebusy', {}, 'period', '2026-03-14T14:00:00Z/PT1H']
            ],
            []
          ]
        ]
      ]
    }
    expect(hasFreeBusyConflict(data)).toBe(true)
  })

  it('returns false for malformed data without throwing', () => {
    expect(hasFreeBusyConflict(null)).toBe(false)
    expect(hasFreeBusyConflict('garbage')).toBe(false)
    expect(hasFreeBusyConflict({ data: null })).toBe(false)
  })
})

describe('useAttendeesFreeBusy — Flow B (new attendees)', () => {
  beforeEach(() => {
    jest.clearAllMocks()
    mockGetCalendars.mockResolvedValue(mockCalendarsResponse as never)
  })

  const newAttendee = { email: 'alice@example.com', userId: 'user-alice' }

  it('shows loading then free for a new attendee', async () => {
    mockGetCalendars.mockResolvedValue(mockCalendarsResponse as never)
    mockREPORT.mockResolvedValue(freeIcal)
    const { result } = renderHook(() =>
      useAttendeesFreeBusy({
        existingAttendees: [],
        newAttendees: [newAttendee],
        start: START,
        end: END,
        timezone: TZ
      })
    )

    expect(result.current[newAttendee.email]).toBe('loading')

    await waitFor(() => expect(result.current[newAttendee.email]).toBe('free'))
  })

  it('shows busy when REPORT returns true', async () => {
    mockGetCalendars.mockResolvedValue(mockCalendarsResponse as never)
    mockREPORT.mockResolvedValue(busyIcal)
    const { result } = renderHook(() =>
      useAttendeesFreeBusy({
        existingAttendees: [],
        newAttendees: [newAttendee],
        start: START,
        end: END,
        timezone: TZ
      })
    )

    await waitFor(() => expect(result.current[newAttendee.email]).toBe('busy'))
  })

  it('shows unknown when REPORT throws', async () => {
    mockREPORT.mockRejectedValue(new Error('network error'))

    const { result } = renderHook(() =>
      useAttendeesFreeBusy({
        existingAttendees: [],
        newAttendees: [newAttendee],
        start: START,
        end: END,
        timezone: TZ
      })
    )

    await waitFor(() =>
      expect(result.current[newAttendee.email]).toBe('unknown')
    )
  })

  it('resolves userId via getUserDataFromEmail when not provided', async () => {
    mockGetUserData.mockResolvedValue([{ _id: 'resolved-id' }] as never)
    mockGetCalendars.mockResolvedValue({
      _embedded: {
        'dav:calendar': [
          {
            _links: {
              self: {
                href: '/calendars/resolved-id/resolved-id'
              }
            }
          }
        ]
      }
    } as never)
    mockREPORT.mockResolvedValue(freeIcal)
    const attendeeWithoutId = { email: 'bob@example.com' }

    const { result } = renderHook(() =>
      useAttendeesFreeBusy({
        existingAttendees: [],
        newAttendees: [attendeeWithoutId],
        start: START,
        end: END,
        timezone: TZ
      })
    )

    await waitFor(() =>
      expect(result.current[attendeeWithoutId.email]).toBe('free')
    )
    expect(mockGetUserData).toHaveBeenCalledWith('bob@example.com')
    expect(mockREPORT).toHaveBeenCalledWith(
      expect.objectContaining({
        hrefs: ['/calendars/resolved-id/resolved-id'],
        start: expect.any(String),
        end: expect.any(String)
      })
    )
  })

  it('shows unknown when userId cannot be resolved', async () => {
    mockGetUserData.mockResolvedValue([])

    const { result } = renderHook(() =>
      useAttendeesFreeBusy({
        existingAttendees: [],
        newAttendees: [{ email: 'ghost@example.com' }],
        start: START,
        end: END,
        timezone: TZ
      })
    )

    await waitFor(() =>
      expect(result.current['ghost@example.com']).toBe('unknown')
    )
    expect(mockREPORT).not.toHaveBeenCalled()
  })

  it('does not re-fetch an attendee already fetched', async () => {
    mockGetCalendars.mockResolvedValue(mockCalendarsResponse as never)
    mockREPORT.mockResolvedValue(freeIcal)
    const { result, rerender } = renderHook(
      ({ attendees }) =>
        useAttendeesFreeBusy({
          existingAttendees: [],
          newAttendees: attendees,
          start: START,
          end: END,
          timezone: TZ
        }),
      { initialProps: { attendees: [newAttendee] } }
    )

    await waitFor(() => expect(result.current[newAttendee.email]).toBe('free'))

    rerender({ attendees: [newAttendee] })
    expect(mockREPORT).toHaveBeenCalledTimes(1)
  })

  it('removes departed attendee from the map', async () => {
    mockGetCalendars.mockResolvedValue(mockCalendarsResponse as never)
    mockREPORT.mockResolvedValue(freeIcal)
    const { result, rerender } = renderHook(
      ({ attendees }) =>
        useAttendeesFreeBusy({
          existingAttendees: [],
          newAttendees: attendees,
          start: START,
          end: END,
          timezone: TZ
        }),
      { initialProps: { attendees: [newAttendee] } }
    )

    await waitFor(() => expect(result.current[newAttendee.email]).toBe('free'))

    rerender({ attendees: [] })

    await waitFor(() =>
      expect(result.current[newAttendee.email]).toBeUndefined()
    )
  })

  it('invalidates cache and re-fetches when time window changes', async () => {
    mockGetCalendars.mockResolvedValue(mockCalendarsResponse as never)
    mockREPORT.mockResolvedValue(freeIcal)
    const { result, rerender } = renderHook(
      ({ start, end }) =>
        useAttendeesFreeBusy({
          existingAttendees: [],
          newAttendees: [newAttendee],
          start,
          end,
          timezone: TZ
        }),
      { initialProps: { start: START, end: END } }
    )

    await waitFor(() => expect(result.current[newAttendee.email]).toBe('free'))

    rerender({ start: '2026-03-15T14:00:00', end: '2026-03-15T15:00:00' })

    await waitFor(() => expect(result.current[newAttendee.email]).toBe('free'))

    expect(mockREPORT).toHaveBeenCalledTimes(2)
  })

  it('passes UTC-converted iCal times to the API', async () => {
    mockGetCalendars.mockResolvedValue(mockCalendarsResponse as never)
    mockREPORT.mockResolvedValue(freeIcal)
    renderHook(() =>
      useAttendeesFreeBusy({
        existingAttendees: [],
        newAttendees: [newAttendee],
        start: '2026-03-14T16:00:00', // 16:00 Paris = 15:00 UTC in winter
        end: '2026-03-14T17:00:00',
        timezone: 'Europe/Paris'
      })
    )

    await waitFor(() => expect(mockREPORT).toHaveBeenCalled())

    // Paris is UTC+1 in March (before DST), so 16:00 → 15:00 UTC
    expect(mockREPORT).toHaveBeenCalledWith(
      expect.objectContaining({
        start: '20260314T150000',
        end: '20260314T160000'
      })
    )
  })
})

describe('useAttendeesFreeBusy — Flow A (existing attendees)', () => {
  beforeEach(() => jest.clearAllMocks())

  const existingAttendee = { email: 'carol@example.com', userId: 'user-carol' }

  it('does not fetch when eventUid is missing', () => {
    renderHook(() =>
      useAttendeesFreeBusy({
        existingAttendees: [existingAttendee],
        newAttendees: [],
        start: START,
        end: END,
        timezone: TZ
      })
    )

    expect(mockPOST).not.toHaveBeenCalled()
  })

  it('shows loading then free for existing attendees', async () => {
    mockPOST.mockResolvedValue(freePostResponse)

    const { result } = renderHook(() =>
      useAttendeesFreeBusy({
        existingAttendees: [existingAttendee],
        newAttendees: [],
        start: START,
        end: END,
        timezone: TZ,
        eventUid: 'event-123'
      })
    )

    expect(result.current[existingAttendee.email]).toBe('loading')

    await waitFor(() =>
      expect(result.current[existingAttendee.email]).toBe('free')
    )
  })

  it('shows busy when POST returns busy for userId', async () => {
    mockPOST.mockResolvedValue(busyPostResponse)

    const { result } = renderHook(() =>
      useAttendeesFreeBusy({
        existingAttendees: [existingAttendee],
        newAttendees: [],
        start: START,
        end: END,
        timezone: TZ,
        eventUid: 'event-123'
      })
    )

    await waitFor(() => {
      expect(result.current[existingAttendee.email]).toBe('busy')
    })
  })

  it('shows unknown when POST throws', async () => {
    mockPOST.mockRejectedValue(new Error('server error'))

    const { result } = renderHook(() =>
      useAttendeesFreeBusy({
        existingAttendees: [existingAttendee],
        newAttendees: [],
        start: START,
        end: END,
        timezone: TZ,
        eventUid: 'event-123'
      })
    )

    await waitFor(() =>
      expect(result.current[existingAttendee.email]).toBe('unknown')
    )
  })

  it('passes the eventUid to the POST API', async () => {
    mockPOST.mockResolvedValue(freePostResponse)

    renderHook(() =>
      useAttendeesFreeBusy({
        existingAttendees: [existingAttendee],
        newAttendees: [],
        start: START,
        end: END,
        timezone: TZ,
        eventUid: 'event-abc'
      })
    )

    await waitFor(() => expect(mockPOST).toHaveBeenCalled())
    expect(mockPOST).toHaveBeenCalledWith({
      end: '20260314T140000',
      eventUid: 'event-abc',
      start: '20260314T130000',
      userIds: ['user-carol']
    })
  })
})

describe('useAttendeesFreeBusy — enabled flag', () => {
  it('does not fetch when disabled', () => {
    renderHook(() =>
      useAttendeesFreeBusy({
        existingAttendees: [{ email: 'x@x.com', userId: 'uid' }],
        newAttendees: [{ email: 'y@y.com', userId: 'uid2' }],
        start: START,
        end: END,
        timezone: TZ,
        eventUid: 'event-123',
        enabled: false
      })
    )

    expect(mockPOST).not.toHaveBeenCalled()
    expect(mockREPORT).not.toHaveBeenCalled()
  })
})
