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

vi.mock('@/api/client', () => ({
  api: {
    GET: vi.fn<typeof api.GET>(),
    POST: vi.fn<typeof api.POST>(),
  },
}))

const mockSaveRsvp = vi.fn<(...args: unknown[]) => void>()
const mockGetRsvp = vi.fn<(eventToken: string) => { rsvpToken: string; rsvpName: string } | undefined>()
const mockGetOrganizerToken = vi.fn<(eventToken: string) => string | undefined>()
const mockSaveWatch = vi.fn<(...args: unknown[]) => void>()
const mockIsStored = vi.fn<(eventToken: string) => boolean>()
const mockRemoveEvent = vi.fn<(eventToken: string) => void>()

vi.mock('@/composables/useEventStorage', () => ({
  useEventStorage: vi.fn<() => unknown>(() => ({
    saveCreatedEvent: vi.fn<() => void>(),
    getStoredEvents: vi.fn<() => unknown[]>(() => []),
    getOrganizerToken: mockGetOrganizerToken,
    saveRsvp: mockSaveRsvp,
    getRsvp: mockGetRsvp,
    removeRsvp: vi.fn<() => void>(),
    saveWatch: mockSaveWatch,
    isStored: mockIsStored,
    removeEvent: mockRemoveEvent,
  })),
}))

function createTestRouter(_token?: string) {
  return createRouter({
    history: createMemoryHistory(),
    routes: [
      { path: '/', name: 'home', component: { template: '<div />' } },
      { path: '/events/:eventToken', name: 'event', component: EventDetailView },
    ],
  })
}

async function mountWithToken(token = 'test-token') {
  const router = createTestRouter(token)
  await router.push(`/events/${token}`)
  await router.isReady()
  return mount(EventDetailView, {
    global: { plugins: [router] },
    attachTo: document.body,
  })
}

const fullEvent = {
  eventToken: 'abc-123',
  title: 'Summer BBQ',
  description: 'Bring your own drinks!',
  dateTime: '2026-03-15T20:00:00+01:00',
  timezone: 'Europe/Berlin',
  location: 'Central Park, NYC',
  attendeeCount: 12,
}

function mockLoadedEvent(eventOverrides = {}) {
  vi.mocked(api.GET).mockResolvedValue({
    data: { ...fullEvent, ...eventOverrides },
    error: undefined,
    response: new Response(null, { status: 200 }),
  } as never)
}

beforeEach(() => {
  vi.restoreAllMocks()
  mockGetRsvp.mockReturnValue(undefined)
  mockGetOrganizerToken.mockReturnValue(undefined)
  mockIsStored.mockReturnValue(false)
  mockSaveWatch.mockClear()
  mockRemoveEvent.mockClear()

  // Provide Teleport target for kebab menu
  if (!document.getElementById('header-actions')) {
    const target = document.createElement('div')
    target.id = 'header-actions'
    document.body.appendChild(target)
  }
})

describe('EventDetailView', () => {
  // Loading state
  it('renders skeleton shimmer placeholders while loading', async () => {
    vi.mocked(api.GET).mockReturnValue(new Promise(() => {}))

    const wrapper = await mountWithToken()

    expect(wrapper.find('[aria-busy="true"]').exists()).toBe(true)
    expect(wrapper.findAll('.skeleton').length).toBeGreaterThanOrEqual(3)
    wrapper.unmount()
  })

  // Loaded state — all fields
  it('renders all event fields when loaded', async () => {
    mockLoadedEvent()

    const wrapper = await mountWithToken()
    await flushPromises()

    expect(wrapper.find('.detail__title').text()).toBe('Summer BBQ')
    expect(wrapper.text()).toContain('Bring your own drinks!')
    expect(wrapper.text()).toContain('Central Park, NYC')
    expect(wrapper.text()).toContain('12')
    expect(wrapper.text()).toContain('Europe/Berlin')
    wrapper.unmount()
  })

  // Loaded state — locale-formatted date/time
  it('formats date/time with Intl.DateTimeFormat and timezone', async () => {
    mockLoadedEvent()

    const wrapper = await mountWithToken()
    await flushPromises()

    const dateField = wrapper.findAll('.detail__meta-text')[0]!
    expect(dateField.text()).toContain('(Europe/Berlin)')
    expect(dateField.text()).toContain('2026')
    wrapper.unmount()
  })

  // Loaded state — optional fields absent
  it('does not render description and location when absent', async () => {
    mockLoadedEvent({ description: undefined, location: undefined, attendeeCount: 0 })

    const wrapper = await mountWithToken()
    await flushPromises()

    expect(wrapper.text()).not.toContain('Description')
    expect(wrapper.text()).not.toContain('Location')
    expect(wrapper.text()).toContain('0')
    wrapper.unmount()
  })

  // Not found state
  it('renders "event not found" when API returns 404', async () => {
    vi.mocked(api.GET).mockResolvedValue({
      data: undefined,
      error: { type: 'about:blank', title: 'Not Found', status: 404 },
      response: new Response(null, { status: 404 }),
    } as never)

    const wrapper = await mountWithToken()
    await flushPromises()

    expect(wrapper.text()).toContain('Event not found.')
    expect(wrapper.find('.detail__title').exists()).toBe(false)
    wrapper.unmount()
  })

  // Server error + retry
  it('renders error state with retry button on server error', async () => {
    vi.mocked(api.GET).mockResolvedValue({
      data: undefined,
      error: { type: 'about:blank', title: 'Internal Server Error', status: 500 },
      response: new Response(null, { status: 500 }),
    } as never)

    const wrapper = await mountWithToken()
    await flushPromises()

    expect(wrapper.text()).toContain('Something went wrong.')
    expect(wrapper.find('button').text()).toBe('Retry')
    wrapper.unmount()
  })

  // Retry button re-fetches
  it('retry button triggers a new fetch', async () => {
    vi.mocked(api.GET)
      .mockResolvedValueOnce({
        data: undefined,
        error: { type: 'about:blank', title: 'Error', status: 500 },
        response: new Response(null, { status: 500 }),
      } as never)
      .mockResolvedValueOnce({
        data: fullEvent,
        error: undefined,
        response: new Response(null, { status: 200 }),
      } as never)

    const wrapper = await mountWithToken()
    await flushPromises()

    expect(wrapper.text()).toContain('Something went wrong.')

    await wrapper.find('button').trigger('click')
    await flushPromises()

    expect(wrapper.find('.detail__title').text()).toBe('Summer BBQ')
    wrapper.unmount()
  })

  // RSVP bar
  it('shows RSVP CTA bar on active event', async () => {
    mockLoadedEvent()

    const wrapper = await mountWithToken()
    await flushPromises()

    expect(wrapper.find('.bar-cta').exists()).toBe(true)
    expect(wrapper.find('.bar-cta').text()).toBe("I'm attending!")
    wrapper.unmount()
  })

  it('does not show RSVP bar for organizer', async () => {
    mockGetOrganizerToken.mockReturnValue('org-token-123')
    mockLoadedEvent()

    const wrapper = await mountWithToken()
    await flushPromises()

    expect(wrapper.find('.rsvp-bar').exists()).toBe(false)
    wrapper.unmount()
  })

  it('shows RSVP status bar when localStorage has RSVP', async () => {
    mockGetRsvp.mockReturnValue({ rsvpToken: 'rsvp-1', rsvpName: 'Max' })
    mockLoadedEvent()

    const wrapper = await mountWithToken()
    await flushPromises()

    expect(wrapper.find('.rsvp-bar__status').exists()).toBe(true)
    expect(wrapper.find('.rsvp-bar__text').text()).toBe("You're attending!")
    expect(wrapper.find('.bar-cta').exists()).toBe(false)
    wrapper.unmount()
  })

  // RSVP form submission
  it('opens bottom sheet when CTA is clicked', async () => {
    mockLoadedEvent()

    const wrapper = await mountWithToken()
    await flushPromises()

    expect(document.body.querySelector('[role="dialog"]')).toBeNull()

    await wrapper.find('.bar-cta-btn').trigger('click')
    await flushPromises()

    expect(document.body.querySelector('[role="dialog"]')).not.toBeNull()
    wrapper.unmount()
  })

  it('shows validation error when submitting empty name', async () => {
    mockLoadedEvent()

    const wrapper = await mountWithToken()
    await flushPromises()

    await wrapper.find('.bar-cta-btn').trigger('click')
    await flushPromises()

    // Form is inside Teleport — find via document.body
    const form = document.body.querySelector('.rsvp-form')! as HTMLFormElement
    form.dispatchEvent(new Event('submit', { bubbles: true }))
    await flushPromises()

    expect(document.body.querySelector('.rsvp-form__field-error')?.textContent).toBe('Please enter your name.')
    expect(vi.mocked(api.POST)).not.toHaveBeenCalled()
    wrapper.unmount()
  })

  it('submits RSVP, saves to storage, and shows status', async () => {
    mockLoadedEvent()
    vi.mocked(api.POST).mockResolvedValue({
      data: { rsvpToken: 'rsvp-token-1', name: 'Max' },
      error: undefined,
      response: new Response(null, { status: 201 }),
    } as never)

    const wrapper = await mountWithToken()
    await flushPromises()

    // Open sheet
    await wrapper.find('.bar-cta-btn').trigger('click')
    await flushPromises()

    // Fill name via Teleported input
    const input = document.body.querySelector('#rsvp-name')! as HTMLInputElement
    input.value = 'Max'
    input.dispatchEvent(new Event('input', { bubbles: true }))
    await flushPromises()

    // Submit form
    const form = document.body.querySelector('.rsvp-form')! as HTMLFormElement
    form.dispatchEvent(new Event('submit', { bubbles: true }))
    await flushPromises()

    // Verify API call
    expect(vi.mocked(api.POST)).toHaveBeenCalledWith('/events/{eventToken}/rsvps', {
      params: { path: { eventToken: 'test-token' } },
      body: { name: 'Max' },
    })

    // Verify storage
    expect(mockSaveRsvp).toHaveBeenCalledWith(
      'abc-123',
      'rsvp-token-1',
      'Max',
      'Summer BBQ',
      '2026-03-15T20:00:00+01:00',
    )

    // Verify UI switched to status
    expect(wrapper.find('.rsvp-bar__text').text()).toBe("You're attending!")
    expect(wrapper.find('.bar-cta').exists()).toBe(false)

    // Verify attendee count incremented
    expect(wrapper.text()).toContain('13')

    wrapper.unmount()
  })

  // Attendee list (organizer)
  it('shows attendee list for organizer', async () => {
    mockGetOrganizerToken.mockReturnValue('org-token-123')
    mockLoadedEvent()
    vi.mocked(api.GET)
      .mockResolvedValueOnce({
        data: fullEvent,
        error: undefined,
        response: new Response(null, { status: 200 }),
      } as never)
      .mockResolvedValueOnce({
        data: { attendees: [{ name: 'Alice' }, { name: 'Bob' }] },
        error: undefined,
        response: new Response(null, { status: 200 }),
      } as never)

    const wrapper = await mountWithToken()
    await flushPromises()

    expect(wrapper.find('.attendee-list').exists()).toBe(true)
    expect(wrapper.text()).toContain('Alice')
    expect(wrapper.text()).toContain('Bob')
    expect(wrapper.find('.attendee-list__heading').text()).toBe('2 Attendees')
    wrapper.unmount()
  })

  it('does not show attendee list for visitor', async () => {
    mockLoadedEvent()

    const wrapper = await mountWithToken()
    await flushPromises()

    expect(wrapper.find('.attendee-list').exists()).toBe(false)
    wrapper.unmount()
  })

  it('shows error when RSVP submission fails', async () => {
    mockLoadedEvent()
    vi.mocked(api.POST).mockResolvedValue({
      data: undefined,
      error: { type: 'about:blank', title: 'Bad Request', status: 400 },
      response: new Response(null, { status: 400 }),
    } as never)

    const wrapper = await mountWithToken()
    await flushPromises()

    await wrapper.find('.bar-cta-btn').trigger('click')
    await flushPromises()

    const input = document.body.querySelector('#rsvp-name')! as HTMLInputElement
    input.value = 'Max'
    input.dispatchEvent(new Event('input', { bubbles: true }))
    await flushPromises()

    const form = document.body.querySelector('.rsvp-form')! as HTMLFormElement
    form.dispatchEvent(new Event('submit', { bubbles: true }))
    await flushPromises()

    expect(document.body.textContent).toContain('Could not submit RSVP. Please try again.')
    wrapper.unmount()
  })

  // Bookmark — T007: bookmark state is passed to RsvpBar via props
  it('passes bookmarked=false to RsvpBar when event is not in storage', async () => {
    mockIsStored.mockReturnValue(false)
    mockLoadedEvent()

    const wrapper = await mountWithToken()
    await flushPromises()

    const rsvpBar = wrapper.findComponent({ name: 'RsvpBar' })
    expect(rsvpBar.props('bookmarked')).toBe(false)
    wrapper.unmount()
  })

  it('passes bookmarked=true to RsvpBar when event is in storage', async () => {
    mockIsStored.mockReturnValue(true)
    mockLoadedEvent()

    const wrapper = await mountWithToken()
    await flushPromises()

    const rsvpBar = wrapper.findComponent({ name: 'RsvpBar' })
    expect(rsvpBar.props('bookmarked')).toBe(true)
    wrapper.unmount()
  })

  it('bookmark event emitted from RsvpBar calls saveWatch', async () => {
    mockIsStored.mockReturnValue(false)
    mockLoadedEvent()

    const wrapper = await mountWithToken()
    await flushPromises()

    const rsvpBar = wrapper.findComponent({ name: 'RsvpBar' })
    rsvpBar.vm.$emit('bookmark')
    await flushPromises()

    expect(mockSaveWatch).toHaveBeenCalledWith('test-token', 'Summer BBQ', '2026-03-15T20:00:00+01:00')
    wrapper.unmount()
  })

  it('bookmark event emitted from RsvpBar calls removeEvent when user is watcher', async () => {
    mockIsStored.mockReturnValue(true)
    mockLoadedEvent()

    const wrapper = await mountWithToken()
    await flushPromises()

    const rsvpBar = wrapper.findComponent({ name: 'RsvpBar' })
    rsvpBar.vm.$emit('bookmark')
    await flushPromises()

    expect(mockRemoveEvent).toHaveBeenCalledWith('test-token')
    wrapper.unmount()
  })

  it('bookmark event ignored when user is attendee', async () => {
    mockGetRsvp.mockReturnValue({ rsvpToken: 'rsvp-1', rsvpName: 'Max' })
    mockIsStored.mockReturnValue(true)
    mockLoadedEvent()

    const wrapper = await mountWithToken()
    await flushPromises()

    const rsvpBar = wrapper.findComponent({ name: 'RsvpBar' })
    rsvpBar.vm.$emit('bookmark')
    await flushPromises()

    expect(mockRemoveEvent).not.toHaveBeenCalled()
    expect(mockSaveWatch).not.toHaveBeenCalled()
    wrapper.unmount()
  })

  it('passes bookmarked=true to RsvpBar after removeRsvp (event still in storage)', async () => {
    mockIsStored.mockReturnValue(true)
    mockGetRsvp.mockReturnValue(undefined)
    mockLoadedEvent()

    const wrapper = await mountWithToken()
    await flushPromises()

    const rsvpBar = wrapper.findComponent({ name: 'RsvpBar' })
    expect(rsvpBar.props('bookmarked')).toBe(true)
    wrapper.unmount()
  })
})
