import * as calAPI from '@/features/Calendars/CalendarApi'
import reducer, {
  addEvent,
  createCalendar,
  removeEvent,
  removeTempCal,
  updateEventLocal
} from '@/features/Calendars/CalendarSlice'
import { Calendar } from '@/features/Calendars/CalendarTypes'
import {
  addSharedCalendarAsync,
  createCalendarAsync,
  getCalendarDetailAsync,
  getCalendarsListAsync,
  getEventAsync,
  getTempCalendarsListAsync,
  patchACLCalendarAsync
} from '@/features/Calendars/services'
import { CalendarEvent } from '@/features/Events/EventsTypes'
import * as userAPI from '@/features/User/userAPI'
import userReducer, { setUserData } from '@/features/User/userSlice'
import { configureStore } from '@reduxjs/toolkit'

jest.mock('@/features/Calendars/CalendarApi')
jest.mock('@/features/User/userAPI')
jest.mock('@/features/Events/EventApi')
jest.mock('@/features/Events/utils')
jest.mock('@/utils/apiUtils')

describe('CalendarSlice', () => {
  const initialState = {
    list: {},
    templist: {},
    pending: false,
    error: null
  }

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

  describe('reducers', () => {
    it('createCalendar adds new calendar', () => {
      const action = createCalendar({
        name: 'Test Cal',
        color: '#ff0000',
        description: 'desc'
      })
      const state = reducer(initialState, action)
      const values = Object.values(state.list)
      expect(values[0].name).toBe('Test Cal')
      expect(values[0].color).toBe('#ff0000')
    })

    it('addEvent adds event to calendar', () => {
      const calId = 'user/cal'
      const event = { uid: 'event1', title: 'My Event' } as any
      const stateWithCal = {
        ...initialState,
        list: { [calId]: { id: calId, events: {} } as any }
      }
      const state = reducer(
        stateWithCal,
        addEvent({ calendarUid: calId, event })
      )
      expect(state.list[calId].events['event1']).toEqual(
        expect.objectContaining({ uid: 'event1' })
      )
    })

    it('removeEvent deletes event', () => {
      const calId = 'user/cal'
      const stateWithEvent = {
        ...initialState,
        list: {
          [calId]: {
            id: calId,
            events: { e1: { uid: 'e1' } }
          } as unknown as Calendar
        }
      }
      const state = reducer(
        stateWithEvent,
        removeEvent({ calendarUid: calId, eventUid: 'e1' })
      )
      expect(state.list[calId].events).toEqual({})
    })

    it('updateEventLocal updates an event', () => {
      const calId = 'user/cal'
      const stateWithEvent = {
        ...initialState,
        list: {
          [calId]: {
            id: calId,
            events: { e1: { uid: 'e1', title: 'Old' } }
          } as unknown as Calendar
        }
      }
      const state = reducer(
        stateWithEvent,
        updateEventLocal({ calId, event: { uid: 'e1', title: 'New' } as any })
      )
      expect(state.list[calId].events.e1.title).toBe('New')
    })

    it('removeTempCal deletes temp calendar', () => {
      const stateWithTemp = {
        ...initialState,
        templist: { temp1: { id: 'temp1' } as any }
      }
      const state = reducer(stateWithTemp, removeTempCal('temp1'))
      expect(state.templist).toEqual({})
    })
  })

  describe('extraReducers (thunks)', () => {
    const storeFactory = () =>
      configureStore({
        reducer: { calendars: reducer, user: userReducer }
      })
    beforeEach(() => {
      jest.resetAllMocks()
    })
    it('getCalendarsListAsync.fulfilled replaces list', async () => {
      ;(userAPI.getOpenPaasUser as jest.Mock).mockResolvedValue({ id: 'u1' })
      ;(calAPI.getCalendars as jest.Mock).mockResolvedValue({
        _embedded: { 'dav:calendar': [] }
      })
      ;(userAPI.getUserDetails as jest.Mock).mockResolvedValue({
        firstname: 'Alice',
        lastname: 'Smith',
        emails: ['a@b.com']
      })

      const store = storeFactory()
      await store.dispatch(getCalendarsListAsync() as any)
      const state = store.getState().calendars
      expect(state.list).toEqual({})
    })

    it('getCalendarsListAsync loads user details in parallel for multiple owners', async () => {
      const mockCalendars = [
        {
          _links: { self: { href: '/calendars/u1/cal1.json' } },
          'dav:name': 'Calendar 1',
          'apple:color': '#FF0000',
          acl: []
        },
        {
          _links: { self: { href: '/calendars/u2/cal2.json' } },
          'dav:name': 'Calendar 2',
          'apple:color': '#00FF00',
          acl: []
        },
        {
          _links: { self: { href: '/calendars/u3/cal3.json' } },
          'dav:name': 'Calendar 3',
          'apple:color': '#0000FF',
          acl: []
        }
      ]

      ;(userAPI.getOpenPaasUser as jest.Mock).mockResolvedValue({ id: 'u1' })
      ;(calAPI.getCalendars as jest.Mock).mockResolvedValue({
        _embedded: { 'dav:calendar': mockCalendars }
      })

      const getUserDetailsMock = userAPI.getUserDetails as jest.Mock
      getUserDetailsMock
        .mockResolvedValueOnce({
          firstname: 'Alice',
          lastname: 'Smith',
          emails: ['alice@example.com']
        })
        .mockResolvedValueOnce({
          firstname: 'Bob',
          lastname: 'Jones',
          emails: ['bob@example.com']
        })
        .mockResolvedValueOnce({
          firstname: 'Charlie',
          lastname: 'Brown',
          emails: ['charlie@example.com']
        })

      const store = storeFactory()
      await store.dispatch(getCalendarsListAsync() as any)

      expect(getUserDetailsMock).toHaveBeenCalledTimes(3)
      expect(getUserDetailsMock).toHaveBeenCalledWith('u1')
      expect(getUserDetailsMock).toHaveBeenCalledWith('u2')
      expect(getUserDetailsMock).toHaveBeenCalledWith('u3')

      const state = store.getState().calendars
      expect(state.list['u1/cal1'].owner.firstname).toContain('Alice')
      expect(state.list['u2/cal2'].owner.firstname).toContain('Bob')
      expect(state.list['u3/cal3'].owner.firstname).toContain('Charlie')
    })

    it('getCalendarsListAsync deduplicates getUserDetails calls for same ownerId', async () => {
      const mockCalendars = [
        {
          _links: { self: { href: '/calendars/u1/cal1.json' } },
          'dav:name': 'Calendar 1',
          'apple:color': '#FF0000',
          acl: []
        },
        {
          _links: { self: { href: '/calendars/u1/cal2.json' } },
          'dav:name': 'Calendar 2',
          'apple:color': '#00FF00',
          acl: []
        },
        {
          _links: { self: { href: '/calendars/u1/cal3.json' } },
          'dav:name': 'Calendar 3',
          'apple:color': '#0000FF',
          acl: []
        }
      ]

      ;(userAPI.getOpenPaasUser as jest.Mock).mockResolvedValue({ id: 'u1' })
      ;(calAPI.getCalendars as jest.Mock).mockResolvedValue({
        _embedded: { 'dav:calendar': mockCalendars }
      })

      const getUserDetailsMock = userAPI.getUserDetails as jest.Mock
      getUserDetailsMock.mockResolvedValue({
        firstname: 'Alice',
        lastname: 'Smith',
        emails: ['alice@example.com']
      })

      const store = storeFactory()
      await store.dispatch(getCalendarsListAsync() as any)

      expect(getUserDetailsMock).toHaveBeenCalledTimes(1)
      expect(getUserDetailsMock).toHaveBeenCalledWith('u1')
    })

    it('getCalendarsListAsync processes owners in batches of 20', async () => {
      const mockCalendars = Array.from({ length: 45 }, (_, i) => ({
        _links: { self: { href: `/calendars/u${i + 1}/cal${i + 1}.json` } },
        'dav:name': `Calendar ${i + 1}`,
        'apple:color': '#FF0000',
        acl: []
      }))

      ;(userAPI.getOpenPaasUser as jest.Mock).mockResolvedValue({ id: 'u1' })
      ;(calAPI.getCalendars as jest.Mock).mockResolvedValue({
        _embedded: { 'dav:calendar': mockCalendars }
      })

      const getUserDetailsMock = userAPI.getUserDetails as jest.Mock
      getUserDetailsMock.mockImplementation((ownerId: string) =>
        Promise.resolve({
          firstname: 'User',
          lastname: ownerId,
          emails: [`${ownerId}@example.com`]
        })
      )

      const store = storeFactory()
      await store.dispatch(getCalendarsListAsync() as any)

      expect(getUserDetailsMock).toHaveBeenCalledTimes(45)
      const state = store.getState().calendars
      expect(Object.keys(state.list)).toHaveLength(45)
    })

    it('getCalendarsListAsync doesnt call getUserDetails if userdata exist in store', async () => {
      const existingCalendars = {
        'u1/cal1': {
          id: 'u1/cal1',
          name: 'Existing Calendar',
          events: {}
        } as Calendar
      }

      const store = storeFactory()
      store.dispatch({
        type: 'calendars/getCalendars/fulfilled',
        payload: { importedCalendars: existingCalendars, errors: '' }
      })
      store.dispatch(setUserData({ openpaasId: 'bla' }))
      const getUserDetailsMock = userAPI.getUserDetails as jest.Mock

      await store.dispatch(getCalendarsListAsync() as any)

      expect(getUserDetailsMock).not.toHaveBeenCalled()

      const state = store.getState().calendars
      expect(state.list).toEqual(existingCalendars)
    })

    it('getCalendarsListAsync handles errors in getUserDetails gracefully', async () => {
      const mockCalendars = [
        {
          _links: { self: { href: '/calendars/u1/cal1.json' } },
          'dav:name': 'Calendar 1',
          'apple:color': '#FF0000',
          acl: []
        },
        {
          _links: { self: { href: '/calendars/u2/cal2.json' } },
          'dav:name': 'Calendar 2',
          'apple:color': '#00FF00',
          acl: []
        }
      ]

      ;(userAPI.getOpenPaasUser as jest.Mock).mockResolvedValue({ id: 'u1' })
      ;(calAPI.getCalendars as jest.Mock).mockResolvedValue({
        _embedded: { 'dav:calendar': mockCalendars }
      })

      const getUserDetailsMock = userAPI.getUserDetails as jest.Mock
      getUserDetailsMock
        .mockResolvedValueOnce({
          firstname: 'Alice',
          lastname: 'Smith',
          emails: ['alice@example.com']
        })
        .mockRejectedValueOnce(new Error('Failed to fetch user'))

      const store = storeFactory()
      const result = await store.dispatch(getCalendarsListAsync() as any)

      expect(getUserDetailsMock).toHaveBeenCalledTimes(2)
      const state = store.getState().calendars
      expect(state.list['u1/cal1'].owner.firstname).toContain('Alice')
      expect(state.list['u2/cal2'].owner.lastname).toContain('Unknown User')
      expect(result.payload.errors).toBeTruthy()
    })

    it('patchACLCalendarAsync.fulfilled sets visibility', () => {
      const prev = {
        ...initialState,
        list: { c1: { id: 'c1', visibility: 'public' } as any }
      }
      const state = reducer(
        prev,
        patchACLCalendarAsync.fulfilled(
          { calId: 'c1', calLink: 'l', request: '' },
          'req3',
          { calId: 'c1', calLink: 'l', request: '' }
        )
      )
      expect(state.list.c1.visibility).toBe('private')
    })

    it('createCalendarAsync.fulfilled adds a new calendar', () => {
      const payload = {
        userData: {
          openpaasId: 'u1',
          family_name: 'Owner',
          email: 'o@example.com',
          sid: '',
          sub: '',
          given_name: 'Test',
          name: 'Test Owner'
        },
        calId: 'cal1',
        color: { 'apple:color': '#f00' },
        name: 'Test',
        desc: 'Desc'
      }

      const payloadResponse = {
        userId: 'u1',
        calId: 'cal1',
        color: { 'apple:color': '#f00' },
        name: 'Test',
        desc: 'Desc',
        owner: { firstname: 'Owner', emails: ['o@example.com'] }
      }
      const state = reducer(
        initialState,
        createCalendarAsync.fulfilled(payloadResponse, 'req4', payload)
      )
      expect(state.list['u1/cal1'].name).toBe('Test')
      expect(state.list['u1/cal1'].color?.['apple:color']).toBe('#f00')
    })

    it('addSharedCalendarAsync.fulfilled adds shared calendar', () => {
      const payload = {
        calId: 'c1',
        color: { 'apple:color': '#0f0' },
        link: '/calendars/u1/c1.json',
        name: 'Shared',
        desc: 'Shared Desc',
        owner: { firstname: 'O', emails: ['o@example.com'] }
      }
      const mockCal = {
        cal: {
          _links: { self: { href: '/calendars/u1/c1.json' } },
          'apple:color': '#0f0',
          'caldav:description': 'Shared Desc',
          'dav:name': 'Shared'
        }
      }
      const state = reducer(
        initialState,
        addSharedCalendarAsync.fulfilled(payload, 'req5', {
          userId: 'u1',
          calId: 'c1',
          cal: mockCal
        })
      )
      expect(state.list['c1'].name).toBe('Shared')
    })

    it('getTempCalendarsListAsync.fulfilled updates templist', () => {
      const payload = {
        t1: {
          id: 't1',
          name: 'Temp',
          color: { 'apple:color': '#aaa' },
          events: {},
          visibility: 'public',
          owner: { firstname: 'O', emails: ['o@o.com'] },
          link: '/calendars/t1.json',
          description: 'desc'
        } as Calendar
      }
      const state = reducer(
        initialState,
        getTempCalendarsListAsync.fulfilled(payload, 'req7', {
          openpaasId: 'u1',
          color: { 'apple:color': '#aaa' },
          displayName: 'test',
          avatarUrl: '',
          email: 'test@test.com'
        })
      )
      expect(state.templist.t1.name).toBe('Temp')
    })

    it('getEventAsync.fulfilled adds single event', () => {
      const payload = { calId: 'c1', event: { uid: 'e1' } as any }
      const state = reducer(
        initialState,
        getEventAsync.fulfilled(payload, 'req9', { uid: 'e1' } as any)
      )
      expect(state.list.c1.events.e1.uid).toBe('e1')
    })

    it('getCalendarDetailAsync.fulfilled adds calendar events', () => {
      const payload = { calId: 'c1', events: [{ uid: 'e1' }] as any[] }
      const state = reducer(
        {
          ...initialState,
          list: {
            ['c1']: {
              id: 'c1',
              events: {}
            } as unknown as Calendar
          }
        },
        getCalendarDetailAsync.fulfilled(payload, 'req11', {
          calId: 'c1',
          match: { start: '', end: '' }
        })
      )
      expect(state.list.c1.events.e1.uid).toBe('e1')
    })

    it('getEventAsync.fulfilled doesnt create new events when there are already event with base UID', () => {
      const baseUid = 'recurring-event-base'
      const existingEvent = {
        uid: `${baseUid}/20240115`,
        title: 'Existing Recurring Event',
        recurrenceId: null
      } as unknown as CalendarEvent

      const newEventInstance = {
        uid: baseUid,
        title: 'Fetched Master Event',
        recurrenceId: '20240115'
      } as CalendarEvent

      const stateWithEvent = {
        ...initialState,
        list: {
          c1: {
            id: 'c1',
            events: { [`${baseUid}/20240115`]: existingEvent }
          } as unknown as Calendar
        }
      }

      const payload = { calId: 'c1', event: newEventInstance }
      const state = reducer(
        stateWithEvent,
        getEventAsync.fulfilled(payload, 'req', newEventInstance)
      )

      // Should still only have the base event, not the instance
      expect(Object.keys(state.list.c1.events)).toHaveLength(1)
      expect(state.list.c1.events[`${baseUid}/20240115`]).toBeDefined()
      expect(state.list.c1.events[baseUid]).toBeUndefined()
    })

    it("getEventAsync.fulfilled create new event when there isn't any event with base UID", () => {
      const eventUid = 'new-event-uid'
      const newEvent = {
        uid: eventUid,
        title: 'New Event',
        recurrenceId: null
      } as unknown as CalendarEvent

      const stateWithoutEvent = {
        ...initialState,
        list: {
          c1: {
            id: 'c1',
            events: {}
          } as unknown as Calendar
        }
      }

      const payload = { calId: 'c1', event: newEvent }
      const state = reducer(
        stateWithoutEvent,
        getEventAsync.fulfilled(payload, 'req', newEvent)
      )

      // Should create the new event
      expect(Object.keys(state.list.c1.events)).toHaveLength(1)
      expect(state.list.c1.events[eventUid]).toBeDefined()
      expect(state.list.c1.events[eventUid].uid).toBe(eventUid)
      expect(state.list.c1.events[eventUid].title).toBe('New Event')
    })
  })
})
