import '@testing-library/jest-dom'
import { axe, toHaveNoViolations } from 'jest-axe'
import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest'

expect.extend(toHaveNoViolations)

import './header-menu'
import { PktHeaderMenu } from './header-menu'
import {
  deriveSocialIcon,
  ICON_MAP_ODS_TO_PUNKT,
  mapOdsIcon,
  selectLocaleData,
  selectMegamenu,
} from 'shared-utils/header-menu'
import type { THeaderFooterApi } from 'shared-types/header-menu'

const LOCAL_DATA: THeaderFooterApi = {
  'nb-NO': {
    megamenu: {
      buttons: [{ text: 'Min side', url: 'https://www.oslo.kommune.no/min-side/' }],
      services: {
        title: 'Tjenester og tilbud',
        links: [
          { icon: '24h', text: 'Døgnåpne tjenester', url: 'https://example.test/24h' },
          { icon: 'fire-emblem', text: 'Brannvern', url: 'https://example.test/brannvern' },
          { icon: 'unknown-icon', text: 'Ukjent', url: 'https://example.test/ukjent' },
        ],
      },
      sections: [
        {
          title: 'Oslo vokser',
          links: [{ text: 'Byutvikling', url: 'https://example.test/byutvikling' }],
        },
        {
          title: 'Politikk og innsyn',
          links: [{ text: 'Politikk', url: 'https://example.test/politikk' }],
        },
      ],
      links: [
        { text: 'Kontakt', url: 'https://example.test/kontakt' },
        { text: 'English', url: 'https://example.test/english' },
      ],
      some: [
        { text: 'Facebook', url: 'https://www.facebook.com/Oslo/' },
        { text: 'LinkedIn', url: 'https://www.linkedin.com/company/oslo-kommune' },
      ],
    },
    footer: { sections: [], links: [], some: [] },
    i18n: { menu: 'Meny', search: 'Søk', navAriaLabel: 'Oslo kommune hovedmeny' },
  },
  'en-GB': {
    megamenu: {
      services: {
        title: 'Services and information',
        links: [{ icon: '24h', text: '24h', url: 'https://example.test/en/24h' }],
      },
      sections: [],
      links: [],
      some: [],
    },
    footer: { sections: [], links: [], some: [] },
    i18n: { menu: 'Menu', search: 'Search', navAriaLabel: 'Oslo kommune main menu' },
  },
}

const waitForReady = async () => {
  await customElements.whenDefined('pkt-header-menu')
}

const mountWithData = async (props: Record<string, unknown> = {}): Promise<PktHeaderMenu> => {
  const el = document.createElement('pkt-header-menu') as PktHeaderMenu
  el.data = LOCAL_DATA
  el.open = true
  Object.assign(el, props)
  document.body.appendChild(el)
  await waitForReady()
  await el.updateComplete
  // Give nested icons/accordions a tick to settle.
  await new Promise((resolve) => setTimeout(resolve, 0))
  await el.updateComplete
  return el
}

afterEach(() => {
  document.body.innerHTML = ''
  vi.restoreAllMocks()
})

describe('shared-utils: icon-map', () => {
  test('mapOdsIcon translates known legacy names', () => {
    expect(mapOdsIcon('fire-emblem')).toBe('shield-fire')
    expect(mapOdsIcon('document')).toBe('document-plain')
    expect(mapOdsIcon('linked-in')).toBe('linkedin')
  })

  test('mapOdsIcon passes unknown names through unchanged', () => {
    expect(mapOdsIcon('totally-made-up')).toBe('totally-made-up')
  })

  test('mapOdsIcon handles empty input', () => {
    expect(mapOdsIcon('')).toBe('')
  })

  test('ICON_MAP_ODS_TO_PUNKT only stores actual renames', () => {
    for (const [from, to] of Object.entries(ICON_MAP_ODS_TO_PUNKT)) {
      expect(from).not.toBe(to)
    }
  })

  test('mapOdsIcon resolves the icons the live API ships', () => {
    // Identity passes (already valid Punkt names) and renames both reach
    // a non-empty mapped value.
    const liveIcons = ['24h', 'swingset', 'backpack', 'heart-plus', 'crane', 'fire-emblem']
    for (const icon of liveIcons) {
      expect(mapOdsIcon(icon)).toBeTruthy()
    }
  })

  test('deriveSocialIcon resolves known hostnames from the URL', () => {
    expect(deriveSocialIcon('https://www.facebook.com/Oslo/')).toBe('facebook')
    expect(deriveSocialIcon('https://www.instagram.com/oslo.kommune/')).toBe('instagram')
    expect(deriveSocialIcon('https://www.linkedin.com/company/oslo-kommune')).toBe('linkedin')
    expect(deriveSocialIcon('https://x.com/oslo_kommune')).toBe('x')
  })

  test('deriveSocialIcon strips www./m. subdomains', () => {
    expect(deriveSocialIcon('https://m.facebook.com/Oslo/')).toBe('facebook')
    expect(deriveSocialIcon('https://facebook.com/Oslo/')).toBe('facebook')
  })

  test('deriveSocialIcon ignores localized text when URL resolves', () => {
    expect(
      deriveSocialIcon('https://www.facebook.com/Oslo/', 'Følg oss på Facebook'),
    ).toBe('facebook')
  })

  test('deriveSocialIcon falls back to text when the URL is unknown or invalid', () => {
    expect(deriveSocialIcon('mailto:foo@example.com', 'Facebook')).toBe('facebook')
    expect(deriveSocialIcon('not-a-url', 'LinkedIn')).toBe('linkedin')
  })

  test('deriveSocialIcon falls back to slugified text for unknown platforms', () => {
    expect(deriveSocialIcon(undefined, 'Threads')).toBe('threads')
  })

  test('deriveSocialIcon returns undefined when both inputs are empty', () => {
    expect(deriveSocialIcon()).toBeUndefined()
    expect(deriveSocialIcon('', '')).toBeUndefined()
    expect(deriveSocialIcon('not-a-url', '   ')).toBeUndefined()
  })
})

describe('shared-utils: select-megamenu', () => {
  test('selectLocaleData returns the requested locale slice', () => {
    expect(selectLocaleData(LOCAL_DATA, 'en-GB')?.i18n?.navAriaLabel).toBe('Oslo kommune main menu')
  })

  test('selectLocaleData falls back to nb-NO for missing locales', () => {
    expect(selectLocaleData(LOCAL_DATA, 'fr-FR')?.i18n?.navAriaLabel).toBe('Oslo kommune hovedmeny')
  })

  test('selectMegamenu returns the megamenu slice for the locale', () => {
    expect(selectMegamenu(LOCAL_DATA, 'nb-NO')?.services.title).toBe('Tjenester og tilbud')
    expect(selectMegamenu(LOCAL_DATA, 'en-GB')?.services.title).toBe('Services and information')
  })

  test('selectLocaleData returns undefined for missing input', () => {
    expect(selectLocaleData(undefined)).toBeUndefined()
  })
})

describe('PktHeaderMenu rendering', () => {
  test('renders services, sections, and footer when given pre-fetched data', async () => {
    const el = await mountWithData()

    expect(el.querySelector('.pkt-header-menu__services-title')?.textContent).toContain(
      'Tjenester og tilbud',
    )
    expect(el.querySelectorAll('.pkt-header-menu__service')).toHaveLength(3)
    expect(el.querySelectorAll('.pkt-header-menu__section')).toHaveLength(2)
    expect(el.querySelectorAll('.pkt-header-menu__footer-link')).toHaveLength(2)
  })

  test('does not fetch when data prop is supplied', async () => {
    const fetchSpy = vi.spyOn(globalThis, 'fetch').mockRejectedValue(new Error('should not run'))
    await mountWithData()
    expect(fetchSpy).not.toHaveBeenCalled()
  })

  test('maps ODS icon names on service links', async () => {
    const el = await mountWithData()
    const serviceIcons = el.querySelectorAll<HTMLElement>('.pkt-header-menu__service-icon')
    expect(serviceIcons[0].getAttribute('name')).toBe('24h')
    expect(serviceIcons[1].getAttribute('name')).toBe('shield-fire')
    // Unmapped names pass through
    expect(serviceIcons[2].getAttribute('name')).toBe('unknown-icon')
  })

  test('uses the locale-specific nav aria-label', async () => {
    const el = await mountWithData()
    const nav = el.querySelector('nav')
    expect(nav?.getAttribute('aria-label')).toBe('Oslo kommune hovedmeny')
  })

  test('switches to en-GB content when locale changes', async () => {
    const el = await mountWithData()
    el.locale = 'en-GB'
    await el.updateComplete
    const nav = el.querySelector('nav')
    expect(nav?.getAttribute('aria-label')).toBe('Oslo kommune main menu')
    expect(el.querySelector('.pkt-header-menu__services-title')?.textContent).toContain(
      'Services and information',
    )
  })

  test('renders social links with derived aria-labels and icons', async () => {
    const el = await mountWithData()
    const socialLinks = el.querySelectorAll<HTMLElement>('.pkt-header-menu__social-link')
    expect(socialLinks).toHaveLength(2)
    expect(socialLinks[0].getAttribute('aria-label')).toBe('Facebook')
    expect(socialLinks[0].querySelector('pkt-icon')?.getAttribute('name')).toBe('facebook')
    expect(socialLinks[1].getAttribute('aria-label')).toBe('LinkedIn')
    expect(socialLinks[1].querySelector('pkt-icon')?.getAttribute('name')).toBe('linkedin')
  })

  test('reflects the open prop to a host class', async () => {
    const el = await mountWithData()
    expect(el.classList.contains('pkt-header-menu')).toBe(true)
    expect(el.classList.contains('pkt-header-menu--open')).toBe(true)
    el.open = false
    await el.updateComplete
    expect(el.classList.contains('pkt-header-menu--open')).toBe(false)
  })

  test('emits data-loaded with the supplied payload', async () => {
    const el = document.createElement('pkt-header-menu') as PktHeaderMenu
    el.data = LOCAL_DATA
    const events: CustomEvent[] = []
    el.addEventListener('data-loaded', (e) => events.push(e as CustomEvent))
    document.body.appendChild(el)
    await waitForReady()
    await el.updateComplete
    expect(events.length).toBeGreaterThanOrEqual(1)
    expect(events[0].detail.data).toBe(LOCAL_DATA)
  })
})

describe('PktHeaderMenu fetching', () => {
  beforeEach(() => {
    vi.spyOn(globalThis, 'fetch').mockImplementation(() =>
      Promise.resolve(
        new Response(JSON.stringify(LOCAL_DATA), {
          status: 200,
          headers: { 'Content-Type': 'application/json' },
        }),
      ),
    )
  })

  test('fetches the default URL on connect when no data is supplied', async () => {
    const el = document.createElement('pkt-header-menu') as PktHeaderMenu
    el.open = true
    document.body.appendChild(el)
    await waitForReady()
    // Wait for the async fetch chain to resolve.
    await new Promise((resolve) => setTimeout(resolve, 0))
    await el.updateComplete
    expect(globalThis.fetch).toHaveBeenCalledWith(
      'https://cdn.web.oslo.kommune.no/header-footer/header-footer.json',
      expect.any(Object),
    )
    expect(el.querySelector('.pkt-header-menu__services-title')?.textContent).toContain(
      'Tjenester og tilbud',
    )
  })

  test('honors a custom data-url attribute', async () => {
    const el = document.createElement('pkt-header-menu') as PktHeaderMenu
    el.setAttribute('data-url', 'https://custom.test/payload.json')
    document.body.appendChild(el)
    await waitForReady()
    await new Promise((resolve) => setTimeout(resolve, 0))
    expect(globalThis.fetch).toHaveBeenCalledWith(
      'https://custom.test/payload.json',
      expect.any(Object),
    )
  })

  test('emits data-error when the fetch fails', async () => {
    vi.restoreAllMocks()
    vi.spyOn(globalThis, 'fetch').mockResolvedValue(
      new Response('boom', { status: 500 }) as unknown as Response,
    )
    const el = document.createElement('pkt-header-menu') as PktHeaderMenu
    const events: CustomEvent[] = []
    el.addEventListener('data-error', (e) => events.push(e as CustomEvent))
    document.body.appendChild(el)
    await waitForReady()
    await new Promise((resolve) => setTimeout(resolve, 0))
    await el.updateComplete
    expect(events.length).toBe(1)
    expect((events[0].detail.error as Error).message).toMatch(/HTTP 500/)
    expect(el.querySelector('.pkt-header-menu__error')).not.toBeNull()
  })
})

describe('PktHeaderMenu accessibility', () => {
  test('has no axe violations with the local-data payload', async () => {
    const el = await mountWithData()
    const results = await axe(el)
    expect(results).toHaveNoViolations()
  })
})
