import { updateSeries } from '@/features/Events/api/updateSeries'
import {
  deleteEvent,
  importEventFromFile,
  moveEvent,
  putEvent,
  searchEvent
} from '@/features/Events/EventApi'
import { CalendarEvent } from '@/features/Events/EventsTypes'
import { calendarEventToJCal } from '@/features/Events/utils'
import { clientConfig } from '@/features/User/oidcAuth'
import { api } from '@/utils/apiUtils'

clientConfig.url = 'https://example.com'

jest.mock('@/utils/apiUtils')

const day = new Date()

const mockEvent = {
  uid: 'event1',
  title: 'Test Event',
  timezone: 'UTC',
  calId: '667037022b752d0026472254/cal1',
  URL: '/calendars/667037022b752d0026472254/667037022b752d0026472254/cal1.ics',
  start: day.toISOString(),
  end: day.toISOString(),
  status: 'PUBLIC',
  organizer: { cn: 'test', cal_address: 'test@test.com' },
  attendee: [
    {
      cn: 'test',
      cal_address: 'test@test.com',
      partstat: 'NEEDS-ACTION',
      rsvp: 'TRUE',
      role: 'REQ-PARTICIPANT',
      cutype: 'INDIVIDUAL'
    },
    {
      cn: 'John',
      cal_address: 'john@test.com',
      partstat: 'NEEDS-ACTION',
      rsvp: 'TRUE',
      role: 'REQ-PARTICIPANT',
      cutype: 'INDIVIDUAL'
    }
  ]
} as CalendarEvent

describe('eventApi', () => {
  beforeEach(() => {
    jest.clearAllMocks()
  })

  it('putEvent sends PUT request with JCal body', async () => {
    const mockResponse = { status: 201, url: '/dav/cals/test.ics' }
    ;(api as unknown as jest.Mock).mockReturnValue(mockResponse)
    const result = await putEvent(mockEvent)
    const expectedResult = calendarEventToJCal(mockEvent)
    expect(api).toHaveBeenCalledWith(
      'dav/calendars/667037022b752d0026472254/667037022b752d0026472254/cal1.ics',
      expect.objectContaining({
        method: 'PUT',
        headers: { 'content-type': 'text/calendar; charset=utf-8' },
        body: JSON.stringify(expectedResult)
      })
    )
    expect(result).toBe(mockResponse)
  })

  it('putEvent logs when status is 201', async () => {
    const consoleInfoSpy = jest.spyOn(console, 'info').mockImplementation()
    const mockResponse = { status: 201, url: '/dav/cals/test.ics' }
    ;(api as unknown as jest.Mock).mockReturnValue(mockResponse)

    await putEvent(mockEvent)

    expect(consoleInfoSpy).toHaveBeenCalledWith(
      'Event created successfully:',
      '/dav/cals/test.ics'
    )

    consoleInfoSpy.mockRestore()
  })

  test('moveEvent sends MOVE request with destination header', async () => {
    const mockResponse = { status: 204 }
    ;(api as unknown as jest.Mock).mockReturnValue({
      json: jest.fn().mockResolvedValue(mockResponse)
    })
    const result = await moveEvent(mockEvent, 'newurl.ics')

    expect(api).toHaveBeenCalledWith(
      'dav/calendars/667037022b752d0026472254/667037022b752d0026472254/cal1.ics',
      expect.objectContaining({
        method: 'MOVE',
        headers: {
          destination: 'newurl.ics'
        }
      })
    )
  })

  it('deleteEvent sends DELETE request and returns json response', async () => {
    const mockResponse = { ok: true }
    ;(api as unknown as jest.Mock).mockReturnValue({
      json: jest.fn().mockResolvedValue(mockResponse)
    })

    const result = await deleteEvent('/calendars/test.ics')

    expect(api).toHaveBeenCalledWith('dav/calendars/test.ics', {
      method: 'DELETE'
    })
  })

  it('import event file', async () => {
    const mockResponse = { status: 202 }
    ;(api.post as unknown as jest.Mock).mockReturnValue({
      json: jest.fn().mockResolvedValue(mockResponse)
    })

    await importEventFromFile('123456789', '/calendar/calLink.json')

    expect(api.post).toHaveBeenCalledWith('api/import', {
      body: JSON.stringify({
        fileId: '123456789',
        target: '/calendar/calLink.json'
      })
    })
  })

  test('putEvent handles byday field correctly', async () => {
    const mockResponse = { status: 201, url: '/dav/cals/test.ics' }
    ;(api as unknown as jest.Mock).mockReturnValue(mockResponse)

    const eventWithByday = {
      ...mockEvent,
      repetition: {
        freq: 'weekly',
        interval: 1,
        byday: ['MO', 'WE', 'FR']
      }
    }

    await putEvent(eventWithByday)
    const expectedResult = calendarEventToJCal(eventWithByday)

    expect(api).toHaveBeenCalledWith(
      'dav/calendars/667037022b752d0026472254/667037022b752d0026472254/cal1.ics',
      expect.objectContaining({
        method: 'PUT',
        headers: { 'content-type': 'text/calendar; charset=utf-8' },
        body: JSON.stringify(expectedResult)
      })
    )
  })

  test('putEvent handles null byday field correctly', async () => {
    const mockResponse = { status: 201, url: '/dav/cals/test.ics' }
    ;(api as unknown as jest.Mock).mockReturnValue(mockResponse)

    const eventWithNullByday = {
      ...mockEvent,
      repetition: {
        freq: 'daily',
        interval: 1,
        byday: null
      }
    }

    await putEvent(eventWithNullByday)
    const expectedResult = calendarEventToJCal(eventWithNullByday)

    expect(api).toHaveBeenCalledWith(
      'dav/calendars/667037022b752d0026472254/667037022b752d0026472254/cal1.ics',
      expect.objectContaining({
        method: 'PUT',
        headers: { 'content-type': 'text/calendar; charset=utf-8' },
        body: JSON.stringify(expectedResult)
      })
    )
  })

  describe('searchEvent', () => {
    const mockFilters = {
      searchIn: ['user1/calendar1', 'user2/calendar2'],
      keywords: 'meeting',
      organizers: ['org@example.com'],
      attendees: ['part@example.com']
    }

    it('should call API with correct parameters', async () => {
      const mockResponse = {
        _total_hits: 5,
        _embedded: { events: [] }
      }

      ;(api.post as jest.Mock).mockReturnValue({
        json: jest.fn().mockResolvedValue(mockResponse)
      })

      await searchEvent('test', mockFilters)

      expect(api.post).toHaveBeenCalledWith(
        'calendar/api/events/search?limit=30&offset=0',
        {
          body: JSON.stringify({
            query: 'meeting',
            calendars: [
              { calendarId: 'calendar1', userId: 'user1' },
              { calendarId: 'calendar2', userId: 'user2' }
            ],
            organizers: ['org@example.com'],
            attendees: ['part@example.com']
          })
        }
      )
    })

    it('should use query param when keywords is empty', async () => {
      const mockResponse = { _total_hits: 0, _embedded: { events: [] } }

      ;(api.post as jest.Mock).mockReturnValue({
        json: jest.fn().mockResolvedValue(mockResponse)
      })

      await searchEvent('fallback query', {
        ...mockFilters,
        keywords: ''
      })

      expect(api.post).toHaveBeenCalledWith(
        expect.any(String),
        expect.objectContaining({
          body: expect.stringContaining('"query":"fallback query"')
        })
      )
    })

    it('should omit organizers when empty', async () => {
      const mockResponse = { _total_hits: 0, _embedded: { events: [] } }

      ;(api.post as jest.Mock).mockReturnValue({
        json: jest.fn().mockResolvedValue(mockResponse)
      })

      await searchEvent('test', {
        ...mockFilters,
        organizers: []
      })

      const callArgs = (api.post as jest.Mock).mock.calls[0][1]
      const body = JSON.parse(callArgs.body)
      expect(body.organizers).toBeUndefined()
    })

    it('should omit participants when empty', async () => {
      const mockResponse = { _total_hits: 0, _embedded: { events: [] } }

      ;(api.post as jest.Mock).mockReturnValue({
        json: jest.fn().mockResolvedValue(mockResponse)
      })

      await searchEvent('test', {
        ...mockFilters,
        attendees: []
      })

      const callArgs = (api.post as jest.Mock).mock.calls[0][1]
      const body = JSON.parse(callArgs.body)
      expect(body.participants).toBeUndefined()
    })
  })

  describe('updateSeries', () => {
    const recurringEventICS = `BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//Test//EN
BEGIN:VEVENT
UID:event1
DTSTART:20240201T100000Z
DTEND:20240201T110000Z
RRULE:FREQ=DAILY
SUMMARY:Old title
SEQUENCE:1
END:VEVENT
BEGIN:VEVENT
UID:event1
RECURRENCE-ID:20240202T100000Z
DTSTART:20240202T100000Z
DTEND:20240202T110000Z
SUMMARY:Old title
SEQUENCE:1
END:VEVENT
END:VCALENDAR
`

    beforeEach(() => {
      jest.clearAllMocks()
      ;(api.get as jest.Mock).mockResolvedValue({
        text: jest.fn().mockResolvedValue(recurringEventICS)
      })
      ;(api as jest.Mock).mockResolvedValue({ ok: true })
    })

    it('propagates summary changes to overrides', async () => {
      await updateSeries(
        { ...mockEvent, title: 'New title' } as any,
        undefined,
        false
      )

      const body = JSON.parse((api as jest.Mock).mock.calls[0][1].body)
      const vevents = body[2].filter(([n]: any) => n === 'vevent')

      const override = vevents.find(([, props]: any) =>
        props.some(([k]: any) => k === 'recurrence-id')
      )

      const summary = override[1].find(([k]: any) => k === 'summary')
      expect(summary[3]).toBe('New title')
    })

    it('does not remove recurrence-id from overrides', async () => {
      await updateSeries(mockEvent as any, undefined, false)

      const body = JSON.parse((api as jest.Mock).mock.calls[0][1].body)
      const vevents = body[2].filter(([n]: any) => n === 'vevent')

      const override = vevents.find(([, props]: any) =>
        props.some(([k]: any) => k === 'recurrence-id')
      )

      expect(override).toBeDefined()
    })

    it('increments SEQUENCE on overrides when metadata changes', async () => {
      await updateSeries(
        { ...mockEvent, title: 'Changed title' } as any,
        undefined,
        false
      )

      const body = JSON.parse((api as jest.Mock).mock.calls[0][1].body)
      const vevents = body[2].filter(([n]: any) => n === 'vevent')

      const override = vevents.find(([, props]: any) =>
        props.some(([k]: any) => k === 'recurrence-id')
      )

      const sequence = override[1].find(([k]: any) => k === 'sequence')
      expect(sequence[3]).toBe(2)
    })

    it('propagates description, location, class, and transp changes', async () => {
      const icsWithMultipleFields = `BEGIN:VCALENDAR
VERSION:2.0
BEGIN:VEVENT
UID:event1
DTSTART:20240201T100000Z
DTEND:20240201T110000Z
RRULE:FREQ=DAILY
SUMMARY:Title
DESCRIPTION:Old description
LOCATION:Old location
CLASS:PUBLIC
TRANSP:OPAQUE
SEQUENCE:1
END:VEVENT
BEGIN:VEVENT
UID:event1
RECURRENCE-ID:20240202T100000Z
DTSTART:20240202T100000Z
DTEND:20240202T110000Z
SUMMARY:Title
DESCRIPTION:Old description
LOCATION:Old location
CLASS:PUBLIC
TRANSP:OPAQUE
SEQUENCE:1
END:VEVENT
END:VCALENDAR
`

      ;(api.get as jest.Mock).mockResolvedValueOnce({
        text: jest.fn().mockResolvedValue(icsWithMultipleFields)
      })

      await updateSeries(
        {
          ...mockEvent,
          description: 'New description',
          location: 'New location',
          class: 'PRIVATE',
          transp: 'TRANSPARENT'
        } as any,
        undefined,
        false
      )

      const body = JSON.parse((api as jest.Mock).mock.calls[0][1].body)
      const vevents = body[2].filter(([n]: any) => n === 'vevent')
      const override = vevents.find(([, props]: any) =>
        props.some(([k]: any) => k === 'recurrence-id')
      )

      const description = override[1].find(([k]: any) => k === 'description')
      const location = override[1].find(([k]: any) => k === 'location')
      const classField = override[1].find(([k]: any) => k === 'class')
      const transp = override[1].find(([k]: any) => k === 'transp')

      expect(description[3]).toBe('New description')
      expect(location[3]).toBe('New location')
      expect(classField[3]).toBe('PRIVATE')
      expect(transp[3]).toBe('TRANSPARENT')
    })

    it('propagates organizer changes', async () => {
      const icsWithOrganizer = `BEGIN:VCALENDAR
VERSION:2.0
BEGIN:VEVENT
UID:event1
DTSTART:20240201T100000Z
RRULE:FREQ=DAILY
SUMMARY:Title
ORGANIZER;CN=Alice:mailto:alice@example.com
SEQUENCE:1
END:VEVENT
BEGIN:VEVENT
UID:event1
RECURRENCE-ID:20240202T100000Z
DTSTART:20240202T100000Z
SUMMARY:Title
ORGANIZER;CN=Alice:mailto:alice@example.com
SEQUENCE:1
END:VEVENT
END:VCALENDAR
`

      ;(api.get as jest.Mock).mockResolvedValueOnce({
        text: jest.fn().mockResolvedValue(icsWithOrganizer)
      })

      await updateSeries(
        {
          ...mockEvent,
          organizer: { cn: 'Bob', cal_address: 'bob@example.com' }
        } as any,
        undefined,
        false
      )

      const body = JSON.parse((api as jest.Mock).mock.calls[0][1].body)
      const vevents = body[2].filter(([n]: any) => n === 'vevent')
      const override = vevents.find(([, props]: any) =>
        props.some(([k]: any) => k === 'recurrence-id')
      )

      const organizer = override[1].find(([k]: any) => k === 'organizer')
      expect(organizer[3]).toBe('mailto:bob@example.com')
      expect(organizer[1].cn).toBe('Bob')
    })

    it('propagates attendee changes', async () => {
      const icsWithAttendees = `BEGIN:VCALENDAR
VERSION:2.0
BEGIN:VEVENT
UID:event1
DTSTART:20240201T100000Z
RRULE:FREQ=DAILY
SUMMARY:Title
ATTENDEE;CN=Alice:mailto:alice@example.com
SEQUENCE:1
END:VEVENT
BEGIN:VEVENT
UID:event1
RECURRENCE-ID:20240202T100000Z
DTSTART:20240202T100000Z
SUMMARY:Title
ATTENDEE;CN=Alice:mailto:alice@example.com
SEQUENCE:1
END:VEVENT
END:VCALENDAR
`

      ;(api.get as jest.Mock).mockResolvedValueOnce({
        text: jest.fn().mockResolvedValue(icsWithAttendees)
      })

      await updateSeries(
        {
          ...mockEvent,
          attendee: [
            {
              cn: 'Bob',
              cal_address: 'bob@example.com',
              partstat: 'NEEDS-ACTION',
              cutype: 'INDIVIDUAL',
              role: 'REQ-PARTICIPANT',
              rsvp: 'TRUE'
            },
            {
              cn: 'Charlie',
              cal_address: 'charlie@example.com',
              partstat: 'NEEDS-ACTION',
              cutype: 'INDIVIDUAL',
              role: 'REQ-PARTICIPANT',
              rsvp: 'TRUE'
            }
          ]
        } as any,
        undefined,
        false
      )

      const body = JSON.parse((api as jest.Mock).mock.calls[0][1].body)
      const vevents = body[2].filter(([n]: any) => n === 'vevent')
      const override = vevents.find(([, props]: any) =>
        props.some(([k]: any) => k === 'recurrence-id')
      )

      const attendees = override[1].filter(([k]: any) => k === 'attendee')
      expect(attendees).toHaveLength(2)
      expect(attendees[0][3]).toBe('mailto:bob@example.com')
      expect(attendees[1][3]).toBe('mailto:charlie@example.com')
    })

    it('propagates x-openpaas-videoconference changes', async () => {
      const icsWithVideo = `BEGIN:VCALENDAR
VERSION:2.0
BEGIN:VEVENT
UID:event1
DTSTART:20240201T100000Z
RRULE:FREQ=DAILY
SUMMARY:Title
X-OPENPAAS-VIDEOCONFERENCE:https://meet.old.com
SEQUENCE:1
END:VEVENT
BEGIN:VEVENT
UID:event1
RECURRENCE-ID:20240202T100000Z
DTSTART:20240202T100000Z
SUMMARY:Title
X-OPENPAAS-VIDEOCONFERENCE:https://meet.old.com
SEQUENCE:1
END:VEVENT
END:VCALENDAR
`

      ;(api.get as jest.Mock).mockResolvedValueOnce({
        text: jest.fn().mockResolvedValue(icsWithVideo)
      })

      await updateSeries(
        {
          ...mockEvent,
          x_openpass_videoconference: 'https://meet.new.com'
        } as any,
        undefined,
        false
      )

      const body = JSON.parse((api as jest.Mock).mock.calls[0][1].body)
      const vevents = body[2].filter(([n]: any) => n === 'vevent')
      const override = vevents.find(([, props]: any) =>
        props.some(([k]: any) => k === 'recurrence-id')
      )

      const videoconf = override[1].find(
        ([k]: any) => k === 'x-openpaas-videoconference'
      )
      expect(videoconf[3]).toBe('https://meet.new.com')
    })

    it('works with multiple override instances', async () => {
      const icsMultipleOverrides = `BEGIN:VCALENDAR
VERSION:2.0
BEGIN:VEVENT
UID:event1
DTSTART:20240201T100000Z
DTEND:20240201T110000Z
RRULE:FREQ=DAILY
SUMMARY:Old
SEQUENCE:1
END:VEVENT
BEGIN:VEVENT
UID:event1
RECURRENCE-ID:20240202T100000Z
DTSTART:20240202T100000Z
DTEND:20240202T110000Z
SUMMARY:Old
SEQUENCE:1
END:VEVENT
BEGIN:VEVENT
UID:event1
RECURRENCE-ID:20240203T100000Z
DTSTART:20240203T100000Z
DTEND:20240203T110000Z
SUMMARY:Old
SEQUENCE:1
END:VEVENT
END:VCALENDAR
`

      ;(api.get as jest.Mock).mockResolvedValueOnce({
        text: jest.fn().mockResolvedValue(icsMultipleOverrides)
      })

      await updateSeries(
        { ...mockEvent, title: 'New title' } as any,
        undefined,
        false
      )

      const body = JSON.parse((api as jest.Mock).mock.calls[0][1].body)
      const vevents = body[2].filter(([n]: any) => n === 'vevent')
      const overrides = vevents.filter(([, props]: any) =>
        props.some(([k]: any) => k === 'recurrence-id')
      )

      expect(overrides).toHaveLength(2)
      overrides.forEach(override => {
        const summary = override[1].find(([k]: any) => k === 'summary')
        const sequence = override[1].find(([k]: any) => k === 'sequence')
        expect(summary[3]).toBe('New title')
        expect(sequence[3]).toBe(2)
      })
    })

    it('adds sequence field when missing and metadata changes', async () => {
      const icsNoSequence = `BEGIN:VCALENDAR
VERSION:2.0
BEGIN:VEVENT
UID:event1
DTSTART:20240201T100000Z
DTEND:20240201T110000Z
RRULE:FREQ=DAILY
SUMMARY:Old
END:VEVENT
BEGIN:VEVENT
UID:event1
RECURRENCE-ID:20240202T100000Z
DTSTART:20240202T100000Z
DTEND:20240202T110000Z
SUMMARY:Old
END:VEVENT
END:VCALENDAR
`

      ;(api.get as jest.Mock).mockResolvedValueOnce({
        text: jest.fn().mockResolvedValue(icsNoSequence)
      })

      await updateSeries(
        { ...mockEvent, title: 'New title' } as any,
        undefined,
        false
      )

      const body = JSON.parse((api as jest.Mock).mock.calls[0][1].body)
      const vevents = body[2].filter(([n]: any) => n === 'vevent')
      const override = vevents.find(([, props]: any) =>
        props.some(([k]: any) => k === 'recurrence-id')
      )

      const sequence = override[1].find(([k]: any) => k === 'sequence')
      expect(sequence[3]).toBe(1)
    })

    it('preserves override-specific fields when propagating metadata', async () => {
      const icsCustomFields = `BEGIN:VCALENDAR
VERSION:2.0
BEGIN:VEVENT
UID:event1
DTSTART:20240201T100000Z
DTEND:20240201T110000Z
RRULE:FREQ=DAILY
SUMMARY:Old
SEQUENCE:1
END:VEVENT
BEGIN:VEVENT
UID:event1
RECURRENCE-ID:20240202T100000Z
DTSTART:20240202T140000Z
DTEND:20240202T150000Z
SUMMARY:Old
DESCRIPTION:custom-value
SEQUENCE:1
END:VEVENT
END:VCALENDAR
`

      ;(api.get as jest.Mock).mockResolvedValueOnce({
        text: jest.fn().mockResolvedValue(icsCustomFields)
      })

      await updateSeries(
        { ...mockEvent, title: 'New title' } as any,
        undefined,
        false
      )

      const body = JSON.parse((api as jest.Mock).mock.calls[0][1].body)
      const vevents = body[2].filter(([n]: any) => n === 'vevent')
      const override = vevents.find(([, props]: any) =>
        props.some(([k]: any) => k === 'recurrence-id')
      )

      const summary = override[1].find(([k]: any) => k === 'summary')
      const dtstart = override[1].find(([k]: any) => k === 'dtstart')
      const dtend = override[1].find(([k]: any) => k === 'dtend')
      const description = override[1].find(([k]: any) => k === 'description')
      const recurrenceId = override[1].find(([k]: any) => k === 'recurrence-id')
      // Summary should be updated
      expect(summary[3]).toBe('New title')
      // Override-specific time should be preserved
      expect(dtstart[3]).toBe('2024-02-02T14:00:00Z')
      expect(dtend[3]).toBe('2024-02-02T15:00:00Z')
      // Custom field should be preserved
      expect(description[3]).toBe('custom-value')
      // Recurrence-id should be preserved
      expect(recurrenceId[3]).toBe('2024-02-02T10:00:00Z')
    })

    const recurringWithAlarmICS = `BEGIN:VCALENDAR
VERSION:2.0
BEGIN:VEVENT
UID:event1
DTSTART:20240201T100000Z
RRULE:FREQ=DAILY
SUMMARY:Old
BEGIN:VALARM
ACTION:DISPLAY
TRIGGER:-PT15M
END:VALARM
END:VEVENT
BEGIN:VEVENT
UID:event1
RECURRENCE-ID:20240202T100000Z
DTSTART:20240202T100000Z
SUMMARY:Old
BEGIN:VALARM
ACTION:DISPLAY
TRIGGER:-PT15M
END:VALARM
END:VEVENT
END:VCALENDAR
`

    it('replaces VALARM on overrides when alarm changes', async () => {
      ;(api.get as jest.Mock).mockResolvedValueOnce({
        text: jest.fn().mockResolvedValue(recurringWithAlarmICS)
      })

      await updateSeries(
        { ...mockEvent, alarm: { action: 'EMAIL', trigger: '-PT30M' } } as any,
        undefined,
        false
      )

      const body = JSON.parse((api as jest.Mock).mock.calls[0][1].body)
      const vevents = body[2].filter(([n]: any) => n === 'vevent')

      const override = vevents.find(([, props]: any) =>
        props.some(([k]: any) => k === 'recurrence-id')
      )

      const alarms = override[2].filter(([n]: any) => n === 'valarm')

      expect(alarms).toHaveLength(1)
      const action = alarms[0][1].find(([k]: any) => k === 'action')
      const trigger = alarms[0][1].find(([k]: any) => k === 'trigger')
      expect(action[3]).toBe('EMAIL')
      expect(trigger[3]).toBe('-PT30M')
    })
  })
})
