import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
import { mount, flushPromises } from '@vue/test-utils'
import { createRouter, createMemoryHistory } from 'vue-router'
import EventList from '../EventList.vue'
import type { api } from '../../api/client'

vi.mock('../../api/client', () => ({
  api: {
    PATCH: vi.fn<typeof api.PATCH>(),
    DELETE: vi.fn<typeof api.DELETE>(),
  },
}))

const router = createRouter({
  history: createMemoryHistory(),
  routes: [
    { path: '/', component: { template: '<div />' } },
    { path: '/events/:eventToken', name: 'event', component: { template: '<div />' } },
  ],
})

// Fixed "now": Wednesday, 2026-03-11 12:00
const NOW = new Date(2026, 2, 11, 12, 0, 0)

const mockEvents = [
  { eventToken: 'past-1', title: 'Past Event', dateTime: '2026-03-01T10:00:00' },
  { eventToken: 'later-1', title: 'Later Event', dateTime: '2027-06-15T10:00:00' },
  { eventToken: 'today-1', title: 'Today Event', dateTime: '2026-03-11T18:00:00' },
  { eventToken: 'week-1', title: 'This Week Event', dateTime: '2026-03-13T10:00:00' },
  { eventToken: 'nextweek-1', title: 'Next Week Event', dateTime: '2026-03-16T10:00:00' },
  { eventToken: 'org-1', title: 'Organized Event', dateTime: '2026-03-11T19:00:00', organizerToken: 'org-token' },
  { eventToken: 'rsvp-1', title: 'Attending Event', dateTime: '2026-03-11T20:00:00', rsvpToken: 'rsvp-token', rsvpName: 'Max' },
]

const removeEventMock = vi.fn<(eventToken: string) => void>()

vi.mock('../../composables/useEventStorage', () => ({
  isValidStoredEvent: (e: unknown) => {
    if (typeof e !== 'object' || e === null) return false
    const obj = e as Record<string, unknown>
    return typeof obj.eventToken === 'string' && obj.eventToken.length > 0
      && typeof obj.title === 'string' && obj.title.length > 0
      && typeof obj.dateTime === 'string' && obj.dateTime.length > 0
  },
  useEventStorage: () => ({
    getStoredEvents: () => mockEvents,
    getRsvp: (token: string) => {
      const evt = mockEvents.find((e) => e.eventToken === token)
      if (evt && 'rsvpToken' in evt && 'rsvpName' in evt) {
        return { rsvpToken: evt.rsvpToken, rsvpName: evt.rsvpName }
      }
      return undefined
    },
    getOrganizerToken: (token: string) => {
      const evt = mockEvents.find((e) => e.eventToken === token)
      if (evt && 'organizerToken' in evt) {
        return (evt as Record<string, unknown>).organizerToken as string
      }
      return undefined
    },
    removeEvent: removeEventMock,
  }),
}))

vi.mock('../../composables/useRelativeTime', () => ({
  formatRelativeTime: (dateTime: string) => {
    if (dateTime.includes('03-01')) return '10 days ago'
    if (dateTime.includes('06-15')) return 'in 1 year'
    if (dateTime.includes('03-11T18')) return 'in 6 hours'
    if (dateTime.includes('03-11T19')) return 'in 7 hours'
    if (dateTime.includes('03-11T20')) return 'in 8 hours'
    if (dateTime.includes('03-13')) return 'in 2 days'
    if (dateTime.includes('03-16')) return 'in 5 days'
    return 'sometime'
  },
}))

function mountList() {
  return mount(EventList, {
    global: {
      plugins: [router],
      stubs: { Teleport: true },
    },
  })
}

describe('EventList', () => {
  beforeEach(() => {
    vi.useFakeTimers()
    vi.setSystemTime(NOW)
  })

  afterEach(() => {
    vi.useRealTimers()
  })

  it('renders section headers for each non-empty section', () => {
    const wrapper = mountList()
    const headers = wrapper.findAll('.section-header')
    expect(headers).toHaveLength(5)
    expect(headers[0]!.text()).toBe('Today')
    expect(headers[1]!.text()).toBe('This Week')
    expect(headers[2]!.text()).toBe('Next Week')
    expect(headers[3]!.text()).toBe('Later')
    expect(headers[4]!.text()).toBe('Past')
  })

  it('renders events within their correct sections', () => {
    const wrapper = mountList()
    const sections = wrapper.findAll('.event-section')
    expect(sections).toHaveLength(5)

    expect(sections[0]!.text()).toContain('Today Event')
    expect(sections[1]!.text()).toContain('This Week Event')
    expect(sections[2]!.text()).toContain('Next Week Event')
    expect(sections[3]!.text()).toContain('Later Event')
    expect(sections[4]!.text()).toContain('Past Event')
  })

  it('renders all valid events as cards', () => {
    const wrapper = mountList()
    const cards = wrapper.findAll('.event-card')
    expect(cards).toHaveLength(7)
  })

  it('marks past events with isPast class', () => {
    const wrapper = mountList()
    const pastSection = wrapper.findAll('.event-section')[4]!
    const pastCards = pastSection.findAll('.event-card')
    expect(pastCards).toHaveLength(1)
    expect(pastCards[0]!.classes()).toContain('event-card--past')
  })

  it('does not mark non-past events with isPast class', () => {
    const wrapper = mountList()
    const todaySection = wrapper.findAll('.event-section')[0]!
    const cards = todaySection.findAll('.event-card')
    expect(cards[0]!.classes()).not.toContain('event-card--past')
  })

  it('sections have aria-label attributes', () => {
    const wrapper = mountList()
    const sections = wrapper.findAll('section')
    expect(sections[0]!.attributes('aria-label')).toBe('Today')
    expect(sections[1]!.attributes('aria-label')).toBe('This Week')
    expect(sections[2]!.attributes('aria-label')).toBe('Next Week')
    expect(sections[3]!.attributes('aria-label')).toBe('Later')
    expect(sections[4]!.attributes('aria-label')).toBe('Past')
  })

  it('does not render date subheader in "Today" section', () => {
    const wrapper = mountList()
    const todaySection = wrapper.findAll('.event-section')[0]!
    expect(todaySection.find('.date-subheader').exists()).toBe(false)
  })

  it('renders date subheaders in non-today sections', () => {
    const wrapper = mountList()
    const thisWeekSection = wrapper.findAll('.event-section')[1]!
    expect(thisWeekSection.find('.date-subheader').exists()).toBe(true)

    const nextWeekSection = wrapper.findAll('.event-section')[2]!
    expect(nextWeekSection.find('.date-subheader').exists()).toBe(true)

    const laterSection = wrapper.findAll('.event-section')[3]!
    expect(laterSection.find('.date-subheader').exists()).toBe(true)

    const pastSection = wrapper.findAll('.event-section')[4]!
    expect(pastSection.find('.date-subheader').exists()).toBe(true)
  })

  it('assigns watcher role when event has no organizerToken and no rsvpToken', () => {
    const wrapper = mountList()
    const badges = wrapper.findAll('.event-card__badge--watcher')
    expect(badges.length).toBeGreaterThanOrEqual(1)
    expect(badges[0]!.text()).toBe('Watching')
  })

  it('assigns organizer role when event has organizerToken', () => {
    const wrapper = mountList()
    const badge = wrapper.find('.event-card__badge--organizer')
    expect(badge.exists()).toBe(true)
    expect(badge.text()).toBe('Organizing')
  })

  it('assigns attendee role when event has rsvpToken', () => {
    const wrapper = mountList()
    const badge = wrapper.find('.event-card__badge--attendee')
    expect(badge.exists()).toBe(true)
    expect(badge.text()).toBe('Attending')
  })
})

describe('EventList — Organizer Cancel (US1)', () => {
  beforeEach(() => {
    vi.useFakeTimers()
    vi.setSystemTime(NOW)
  })

  afterEach(() => {
    vi.useRealTimers()
  })

  it('T005: confirmDelete calls PATCH cancel-event API when role is organizer', async () => {
    const { api } = await import('../../api/client')
    const patchMock = vi.mocked(api.PATCH)
    patchMock.mockResolvedValue({ response: { status: 204 } } as never)

    const wrapper = mountList()

    // Find the organizer event delete button and click it
    const orgCard = wrapper.findAll('.event-card').find((c) => c.text().includes('Organized Event'))
    expect(orgCard).toBeTruthy()
    await orgCard!.find('.event-card__delete').trigger('click')

    // Confirm the dialog
    const confirmBtn = wrapper.find('.confirm-dialog__btn--confirm')
    await confirmBtn.trigger('click')
    await flushPromises()

    expect(patchMock).toHaveBeenCalledWith(
      '/events/{eventToken}',
      expect.objectContaining({
        params: expect.objectContaining({
          path: { eventToken: 'org-1' },
          query: { organizerToken: 'org-token' },
        }),
        body: { cancelled: true },
      }),
    )
  })

  it('T006: confirmDelete treats 409 response as success (removes event from list)', async () => {
    const { api } = await import('../../api/client')
    const patchMock = vi.mocked(api.PATCH)
    patchMock.mockResolvedValue({ response: { status: 409 } } as never)

    removeEventMock.mockClear()

    const wrapper = mountList()

    const orgCard = wrapper.findAll('.event-card').find((c) => c.text().includes('Organized Event'))
    await orgCard!.find('.event-card__delete').trigger('click')

    const confirmBtn = wrapper.find('.confirm-dialog__btn--confirm')
    await confirmBtn.trigger('click')
    await flushPromises()

    // 409 should be treated as success — removeEvent should have been called
    expect(removeEventMock).toHaveBeenCalledWith('org-1')
  })

  it('T006b: confirmDelete treats 404 response as success (removes event from list)', async () => {
    const { api } = await import('../../api/client')
    const patchMock = vi.mocked(api.PATCH)
    patchMock.mockResolvedValue({ response: { status: 404 } } as never)

    removeEventMock.mockClear()

    const wrapper = mountList()

    const orgCard = wrapper.findAll('.event-card').find((c) => c.text().includes('Organized Event'))
    await orgCard!.find('.event-card__delete').trigger('click')

    const confirmBtn = wrapper.find('.confirm-dialog__btn--confirm')
    await confirmBtn.trigger('click')
    await flushPromises()

    expect(removeEventMock).toHaveBeenCalledWith('org-1')
  })
})

describe('EventList — Dialog Differentiation (US2)', () => {
  beforeEach(() => {
    vi.useFakeTimers()
    vi.setSystemTime(NOW)
  })

  afterEach(() => {
    vi.useRealTimers()
  })

  it('T013: deleteDialogTitle returns organizer-specific text when role is organizer', async () => {
    const wrapper = mountList()

    // Click delete on organizer event
    const orgCard = wrapper.findAll('.event-card').find((c) => c.text().includes('Organized Event'))
    await orgCard!.find('.event-card__delete').trigger('click')

    const dialogTitle = wrapper.find('.confirm-dialog__title')
    expect(dialogTitle.text()).toBe('Cancel event?')
  })

  it('T014: deleteDialogMessage returns existing attendee text when role is attendee', async () => {
    const wrapper = mountList()

    // Click delete on attendee event
    const attCard = wrapper.findAll('.event-card').find((c) => c.text().includes('Attending Event'))
    await attCard!.find('.event-card__delete').trigger('click')

    const dialogTitle = wrapper.find('.confirm-dialog__title')
    expect(dialogTitle.text()).toBe('Remove event?')

    const dialogMsg = wrapper.find('.confirm-dialog__message')
    expect(dialogMsg.text()).toContain('attendance will be cancelled')
  })
})
