import * as fetchSyncTokenChanges from '@/features/Calendars/api/fetchSyncTokenChanges'
import reducer from '@/features/Calendars/CalendarSlice'
import { Calendar } from '@/features/Calendars/CalendarTypes'
import {
  refreshCalendarWithSyncToken,
  SyncTokenUpdates
} from '@/features/Calendars/services/refreshCalendar'
import * as EventApi from '@/features/Events/EventApi'
import { CalendarEvent } from '@/features/Events/EventsTypes'
import { configureStore } from '@reduxjs/toolkit'

jest.mock('@/features/Calendars/api/fetchSyncTokenChanges')
jest.mock('@/features/Events/EventApi')

describe('refreshCalendarWithSyncToken', () => {
  const mockCalendar: Calendar = {
    id: 'user1/cal1',
    name: 'Test Calendar',
    link: '/calendars/user1/cal1.json',
    owner: { firstname: 'Test User', emails: ['test@example.com'] },
    description: 'Test calendar description',
    delegated: false,
    color: { light: '#006BD8', dark: '#FFF' },
    visibility: 'private',
    events: {
      event1: {
        uid: 'event1',
        title: 'Existing Event',
        start: '2024-01-01T10:00:00',
        end: '2024-01-01T11:00:00',
        calId: 'user1/cal1',
        URL: '/calendars/user1/cal1/event1.ics'
      } as CalendarEvent
    },
    syncToken: 'sync-token-123'
  }

  const calendarRange = {
    start: new Date('2024-01-01'),
    end: new Date('2024-01-31')
  }

  const storeFactory = () =>
    configureStore({
      reducer: { calendars: reducer },
      preloadedState: {
        calendars: {
          list: { [mockCalendar.id]: mockCalendar },
          templist: {},
          pending: false,
          error: null
        }
      }
    })

  beforeEach(() => {
    jest.resetAllMocks()
  })

  it('should return early if calendar has no syncToken', async () => {
    const calendarWithoutToken = { ...mockCalendar, syncToken: undefined }
    const store = storeFactory()

    const result = await store.dispatch(
      refreshCalendarWithSyncToken({
        calendar: calendarWithoutToken,
        calendarRange
      })
    )

    expect(result.type).toBe('calendars/refreshWithSyncToken/fulfilled')
    expect(result.payload).toEqual({
      calId: calendarWithoutToken.id,
      deletedEvents: [],
      createdOrUpdatedEvents: [],
      calType: undefined,
      syncStatus: 'NO_SYNC_TOKEN'
    })
    expect(fetchSyncTokenChanges.fetchSyncTokenChanges).not.toHaveBeenCalled()
  })

  it('should handle deleted events (status 404)', async () => {
    const mockSyncResponse = {
      'sync-token': 'new-sync-token-456',
      _embedded: {
        'dav:item': [
          {
            status: 404,
            _links: {
              self: { href: '/calendars/user1/cal1/deleted-event.ics' }
            }
          }
        ]
      }
    }

    ;(
      fetchSyncTokenChanges.fetchSyncTokenChanges as jest.Mock
    ).mockResolvedValue(mockSyncResponse)

    const store = storeFactory()
    const result = await store.dispatch(
      refreshCalendarWithSyncToken({
        calendar: mockCalendar,
        calendarRange
      })
    )

    expect(result.type).toBe('calendars/refreshWithSyncToken/fulfilled')
    expect(result.payload).toMatchObject({
      calId: mockCalendar.id,
      deletedEvents: ['/calendars/user1/cal1/deleted-event.ics'],
      createdOrUpdatedEvents: [],
      syncToken: 'new-sync-token-456'
    })
  })

  it('should handle created/updated events (status 200)', async () => {
    const mockSyncResponse = {
      'sync-token': 'new-sync-token-789',
      _embedded: {
        'dav:item': [
          {
            status: 200,
            _links: {
              self: { href: '/calendars/user1/cal1/new-event.ics' }
            }
          }
        ]
      }
    }

    const mockEventData = {
      data: [
        null,
        null,
        [
          [
            'vevent',
            [
              ['uid', {}, 'text', 'new-event'],
              ['summary', {}, 'text', 'New Event Title'],
              ['dtstart', {}, 'date-time', '2024-01-15T10:00:00Z'],
              ['dtend', {}, 'date-time', '2024-01-15T11:00:00Z']
            ],
            []
          ]
        ]
      ],
      _links: {
        self: { href: '/calendars/user1/cal1/new-event.ics' }
      }
    }

    ;(
      fetchSyncTokenChanges.fetchSyncTokenChanges as jest.Mock
    ).mockResolvedValue(mockSyncResponse)
    ;(EventApi.reportEvent as jest.Mock).mockResolvedValue(mockEventData)

    const store = storeFactory()
    const result = await store.dispatch(
      refreshCalendarWithSyncToken({
        calendar: mockCalendar,
        calendarRange
      })
    )

    expect(result.type).toBe('calendars/refreshWithSyncToken/fulfilled')
    if (result.type === 'calendars/refreshWithSyncToken/fulfilled') {
      const payload = result.payload as SyncTokenUpdates
      expect(payload?.calId).toBe(mockCalendar.id)
      expect(payload?.createdOrUpdatedEvents).toHaveLength(1)
      expect(payload?.createdOrUpdatedEvents[0].uid).toBe('new-event')
      expect(payload?.syncToken).toBe('new-sync-token-789')
    }
  })

  it('should handle both deletions and updates in same response', async () => {
    const mockSyncResponse = {
      'sync-token': 'new-sync-token-mixed',
      _embedded: {
        'dav:item': [
          {
            status: 404,
            _links: {
              self: { href: '/calendars/user1/cal1/deleted-event.ics' }
            }
          },
          {
            status: 200,
            _links: {
              self: { href: '/calendars/user1/cal1/updated-event.ics' }
            }
          }
        ]
      }
    }

    const mockEventData = {
      data: [
        null,
        null,
        [
          [
            'vevent',
            [
              ['uid', {}, 'text', 'updated-event'],
              ['summary', {}, 'text', 'Updated Event'],
              ['dtstart', {}, 'date-time', '2024-01-20T10:00:00Z'],
              ['dtend', {}, 'date-time', '2024-01-20T11:00:00Z']
            ],
            []
          ]
        ]
      ],
      _links: {
        self: { href: '/calendars/user1/cal1/updated-event.ics' }
      }
    }

    ;(
      fetchSyncTokenChanges.fetchSyncTokenChanges as jest.Mock
    ).mockResolvedValue(mockSyncResponse)
    ;(EventApi.reportEvent as jest.Mock).mockResolvedValue(mockEventData)

    const store = storeFactory()
    const result = await store.dispatch(
      refreshCalendarWithSyncToken({
        calendar: mockCalendar,
        calendarRange
      })
    )

    expect(result.type).toBe('calendars/refreshWithSyncToken/fulfilled')
    if (result.type === 'calendars/refreshWithSyncToken/fulfilled') {
      const payload = result.payload as SyncTokenUpdates
      expect(payload?.deletedEvents).toEqual([
        '/calendars/user1/cal1/deleted-event.ics',
        '/calendars/user1/cal1/updated-event.ics'
      ])
      expect(payload?.createdOrUpdatedEvents).toHaveLength(1)
      expect(payload?.createdOrUpdatedEvents[0].uid).toBe('updated-event')
    }
  })

  it('should reject with SYNC_TOKEN_INVALID error on 410 status', async () => {
    const mockSyncResponse = {
      _embedded: {
        'dav:item': [
          {
            status: 410,
            _links: {
              self: { href: '/calendars/user1/cal1/some-event.ics' }
            }
          }
        ]
      }
    }

    ;(
      fetchSyncTokenChanges.fetchSyncTokenChanges as jest.Mock
    ).mockResolvedValue(mockSyncResponse)

    const store = storeFactory()
    const result = await store.dispatch(
      refreshCalendarWithSyncToken({
        calendar: mockCalendar,
        calendarRange
      })
    )

    expect(result.type).toBe('calendars/refreshWithSyncToken/rejected')
    expect(result.payload).toMatchObject({
      message: expect.stringContaining('SYNC_TOKEN_INVALID')
    })
  })

  it('should handle API errors gracefully', async () => {
    const mockError = new Error('Network error')
    ;(
      fetchSyncTokenChanges.fetchSyncTokenChanges as jest.Mock
    ).mockRejectedValue(mockError)

    const store = storeFactory()
    const result = await store.dispatch(
      refreshCalendarWithSyncToken({
        calendar: mockCalendar,
        calendarRange
      })
    )

    expect(result.type).toBe('calendars/refreshWithSyncToken/rejected')
    expect(result.payload).toMatchObject({
      message: expect.stringContaining('Network error')
    })
  })

  it('should handle malformed sync response', async () => {
    const mockSyncResponse = {
      'sync-token': 'new-token'
      // Missing _embedded field
    }

    ;(
      fetchSyncTokenChanges.fetchSyncTokenChanges as jest.Mock
    ).mockResolvedValue(mockSyncResponse)

    const store = storeFactory()
    const result = await store.dispatch(
      refreshCalendarWithSyncToken({
        calendar: mockCalendar,
        calendarRange
      })
    )

    expect(result.type).toBe('calendars/refreshWithSyncToken/fulfilled')
    expect(result.payload).toMatchObject({
      calId: mockCalendar.id,
      deletedEvents: [],
      createdOrUpdatedEvents: [],
      syncToken: 'new-token'
    })
  })

  it('should handle events without href in _links', async () => {
    const mockSyncResponse = {
      'sync-token': 'new-token',
      _embedded: {
        'dav:item': [
          {
            status: 200,
            _links: {
              self: {} // Missing href
            }
          }
        ]
      }
    }

    ;(
      fetchSyncTokenChanges.fetchSyncTokenChanges as jest.Mock
    ).mockResolvedValue(mockSyncResponse)

    const store = storeFactory()
    const result = await store.dispatch(
      refreshCalendarWithSyncToken({
        calendar: mockCalendar,
        calendarRange
      })
    )

    expect(result.type).toBe('calendars/refreshWithSyncToken/fulfilled')
    if (result.type === 'calendars/refreshWithSyncToken/fulfilled') {
      const payload = result.payload as SyncTokenUpdates
      expect(payload?.createdOrUpdatedEvents).toHaveLength(0)
    }
  })

  it('should continue processing other events if one fails to fetch', async () => {
    const mockSyncResponse = {
      'sync-token': 'new-token',
      _embedded: {
        'dav:item': [
          {
            status: 200,
            _links: {
              self: { href: '/calendars/user1/cal1/failing-event.ics' }
            }
          },
          {
            status: 200,
            _links: {
              self: { href: '/calendars/user1/cal1/success-event.ics' }
            }
          }
        ]
      }
    }

    const mockEventData = {
      data: [
        null,
        null,
        [
          [
            'vevent',
            [
              ['uid', {}, 'text', 'success-event'],
              ['summary', {}, 'text', 'Success Event'],
              ['dtstart', {}, 'date-time', '2024-01-20T10:00:00Z'],
              ['dtend', {}, 'date-time', '2024-01-20T11:00:00Z']
            ],
            []
          ]
        ]
      ],
      _links: {
        self: { href: '/calendars/user1/cal1/success-event.ics' }
      }
    }

    ;(
      fetchSyncTokenChanges.fetchSyncTokenChanges as jest.Mock
    ).mockResolvedValue(mockSyncResponse)
    ;(EventApi.reportEvent as jest.Mock)
      .mockRejectedValueOnce(new Error('Failed to fetch'))
      .mockResolvedValueOnce(mockEventData)

    const store = storeFactory()
    const result = await store.dispatch(
      refreshCalendarWithSyncToken({
        calendar: mockCalendar,
        calendarRange
      })
    )

    expect(result.type).toBe('calendars/refreshWithSyncToken/fulfilled')
    if (result.type === 'calendars/refreshWithSyncToken/fulfilled') {
      const payload = result.payload as SyncTokenUpdates
      expect(payload?.createdOrUpdatedEvents).toHaveLength(1)
      expect(payload?.createdOrUpdatedEvents[0].uid).toBe('success-event')
    }
  })

  it('should update calendar state with new sync token', async () => {
    const mockSyncResponse = {
      'sync-token': 'new-sync-token-state-test',
      _embedded: {
        'dav:item': []
      }
    }

    ;(
      fetchSyncTokenChanges.fetchSyncTokenChanges as jest.Mock
    ).mockResolvedValue(mockSyncResponse)

    const store = storeFactory()
    await store.dispatch(
      refreshCalendarWithSyncToken({
        calendar: mockCalendar,
        calendarRange
      })
    )

    const state = store.getState().calendars
    expect(state.list[mockCalendar.id].syncToken).toBe(
      'new-sync-token-state-test'
    )
  })

  it('should remove deleted events from calendar state', async () => {
    const mockSyncResponse = {
      'sync-token': 'new-token',
      _embedded: {
        'dav:item': [
          {
            status: 404,
            _links: {
              self: { href: '/calendars/user1/cal1/event1.ics' }
            }
          }
        ]
      }
    }

    ;(
      fetchSyncTokenChanges.fetchSyncTokenChanges as jest.Mock
    ).mockResolvedValue(mockSyncResponse)

    const store = storeFactory()
    await store.dispatch(
      refreshCalendarWithSyncToken({
        calendar: mockCalendar,
        calendarRange
      })
    )

    const state = store.getState().calendars
    expect(state.list[mockCalendar.id].events['event1']).toBeUndefined()
  })

  it('should add new events to calendar state', async () => {
    const mockSyncResponse = {
      'sync-token': 'new-token',
      _embedded: {
        'dav:item': [
          {
            status: 200,
            _links: {
              self: { href: '/calendars/user1/cal1/new-event.ics' }
            }
          }
        ]
      }
    }

    const mockEventData = {
      data: [
        null,
        null,
        [
          [
            'vevent',
            [
              ['uid', {}, 'text', 'new-event'],
              ['summary', {}, 'text', 'New Event'],
              ['dtstart', {}, 'date-time', '2024-01-25T10:00:00Z'],
              ['dtend', {}, 'date-time', '2024-01-25T11:00:00Z']
            ],
            []
          ]
        ]
      ],
      _links: {
        self: { href: '/calendars/user1/cal1/new-event.ics' }
      }
    }

    ;(
      fetchSyncTokenChanges.fetchSyncTokenChanges as jest.Mock
    ).mockResolvedValue(mockSyncResponse)
    ;(EventApi.reportEvent as jest.Mock).mockResolvedValue(mockEventData)

    const store = storeFactory()
    await store.dispatch(
      refreshCalendarWithSyncToken({
        calendar: mockCalendar,
        calendarRange
      })
    )

    const state = store.getState().calendars
    expect(state.list[mockCalendar.id].events['new-event']).toBeDefined()
    expect(state.list[mockCalendar.id].events['new-event'].title).toBe(
      'New Event'
    )
  })

  it('should handle recurring events with base UID matching', async () => {
    const calendarWithRecurringEvent = {
      ...mockCalendar,
      events: {
        'recurring-base': {
          uid: 'recurring-base',
          title: 'Recurring Event',
          start: '2024-01-01T10:00:00',
          end: '2024-01-01T11:00:00',
          calId: 'user1/cal1',
          URL: '/calendars/user1/cal1/recurring-base.ics'
        } as CalendarEvent,
        'recurring-base/20240108': {
          uid: 'recurring-base/20240108',
          title: 'Recurring Event Instance',
          start: '2024-01-08T10:00:00',
          end: '2024-01-08T11:00:00',
          calId: 'user1/cal1',
          URL: '/calendars/user1/cal1/recurring-base.ics'
        } as CalendarEvent
      }
    }

    const mockSyncResponse = {
      'sync-token': 'new-token',
      _embedded: {
        'dav:item': [
          {
            status: 404,
            _links: {
              self: { href: '/calendars/user1/cal1/recurring-base.ics' }
            }
          }
        ]
      }
    }

    ;(
      fetchSyncTokenChanges.fetchSyncTokenChanges as jest.Mock
    ).mockResolvedValue(mockSyncResponse)

    const store = configureStore({
      reducer: { calendars: reducer },
      preloadedState: {
        calendars: {
          list: { [calendarWithRecurringEvent.id]: calendarWithRecurringEvent },
          templist: {},
          pending: false,
          error: null
        }
      }
    })

    await store.dispatch(
      refreshCalendarWithSyncToken({
        calendar: calendarWithRecurringEvent,
        calendarRange
      })
    )

    const state = store.getState().calendars
    // Both base event and instance should be removed
    expect(
      state.list[calendarWithRecurringEvent.id].events['recurring-base']
    ).toBeUndefined()
    expect(
      state.list[calendarWithRecurringEvent.id].events[
        'recurring-base/20240108'
      ]
    ).toBeUndefined()
  })

  it('should respect maxConcurrency parameter', async () => {
    const mockSyncResponse = {
      'sync-token': 'new-token',
      _embedded: {
        'dav:item': Array.from({ length: 20 }, (_, i) => ({
          status: 200,
          _links: {
            self: { href: `/calendars/user1/cal1/event${i}.ics` }
          }
        }))
      }
    }

    const mockEventData = (uid: string) => ({
      data: [
        null,
        null,
        [
          [
            'vevent',
            [
              ['uid', {}, 'text', uid],
              ['summary', {}, 'text', `Event ${uid}`],
              ['dtstart', {}, 'date-time', '2024-01-20T10:00:00Z'],
              ['dtend', {}, 'date-time', '2024-01-20T11:00:00Z']
            ],
            []
          ]
        ]
      ],
      _links: {
        self: { href: `/calendars/user1/cal1/${uid}.ics` }
      }
    })

    ;(
      fetchSyncTokenChanges.fetchSyncTokenChanges as jest.Mock
    ).mockResolvedValue(mockSyncResponse)

    let concurrentCalls = 0
    let maxConcurrent = 0

    ;(EventApi.reportEvent as jest.Mock).mockImplementation(async () => {
      concurrentCalls++
      maxConcurrent = Math.max(maxConcurrent, concurrentCalls)
      await new Promise(resolve => setTimeout(resolve, 10))
      concurrentCalls--
      return mockEventData('test-event')
    })

    const store = storeFactory()
    await store.dispatch(
      refreshCalendarWithSyncToken({
        calendar: mockCalendar,
        calendarRange,
        maxConcurrency: 5
      })
    )

    // Max concurrent should not exceed 5
    expect(maxConcurrent).toBeLessThanOrEqual(5)
  })
})
