/**
 * Appearance Module Tests
 * @countermeasure/web-components
 *
 * Tests for accent theme, font preference, icon set selection,
 * quick switcher, initialization, and server sync.
 */

import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'

import {
  isValidAccent,
  isValidFont,
  getStoredAccent,
  getStoredFont,
  applyAccent,
  applyFont,
  setAccent,
  setFont,
  isValidIconSet,
  getStoredIconSet,
  applyIconSet,
  setIconSet,
  getStoredQuickSwitcherThemes,
  setQuickSwitcherThemes,
  initAppearance,
  syncAppearanceFromServer,
} from './index'
import type { AccentTheme, FontPreference } from './index'
import type { IconSet } from '../icons/index'
import type { ResolvedTheme } from '../ui/index'

const ALL_ACCENTS: AccentTheme[] = ['orange', 'cyan', 'violet', 'rose', 'emerald']
const ALL_FONTS: FontPreference[] = ['sora', 'inter', 'roboto', 'system', 'jetbrains-mono']
const ALL_ICON_SETS: IconSet[] = ['lucide', 'heroicons', 'phosphor', 'material']

interface MockFetchResponse {
  ok: boolean
  status?: number
  json: () => Promise<unknown>
}

type FetchFn = (input: RequestInfo | URL, init?: RequestInit) => Promise<MockFetchResponse>
type FetchMock = ReturnType<typeof vi.fn<FetchFn>>

describe('Appearance Module', () => {
  beforeEach(() => {
    localStorage.clear()
    // Reset document state
    document.documentElement.removeAttribute('data-accent')
    document.documentElement.removeAttribute('data-font')
    document.documentElement.removeAttribute('data-icon-set')
    document.body.innerHTML = ''
  })

  afterEach(() => {
    localStorage.clear()
    document.documentElement.removeAttribute('data-accent')
    document.documentElement.removeAttribute('data-font')
    document.documentElement.removeAttribute('data-icon-set')
    document.body.innerHTML = ''
    vi.restoreAllMocks()
  })

  // ===========================================================================
  // Accent validation
  // ===========================================================================

  describe('isValidAccent', () => {
    it('returns true for all valid accent values', () => {
      for (const accent of ALL_ACCENTS) {
        expect(isValidAccent(accent), `${accent} should be valid`).toBe(true)
      }
    })

    it('returns false for invalid string values', () => {
      expect(isValidAccent('blue')).toBe(false)
      expect(isValidAccent('red')).toBe(false)
      expect(isValidAccent('')).toBe(false)
      expect(isValidAccent('ORANGE')).toBe(false)
    })

    it('returns false for null and undefined', () => {
      expect(isValidAccent(null)).toBe(false)
      expect(isValidAccent(undefined)).toBe(false)
    })
  })

  // ===========================================================================
  // Font validation
  // ===========================================================================

  describe('isValidFont', () => {
    it('returns true for all valid font values', () => {
      for (const font of ALL_FONTS) {
        expect(isValidFont(font), `${font} should be valid`).toBe(true)
      }
    })

    it('returns false for invalid string values', () => {
      expect(isValidFont('arial')).toBe(false)
      expect(isValidFont('helvetica')).toBe(false)
      expect(isValidFont('')).toBe(false)
      expect(isValidFont('SORA')).toBe(false)
    })

    it('returns false for null and undefined', () => {
      expect(isValidFont(null)).toBe(false)
      expect(isValidFont(undefined)).toBe(false)
    })
  })

  // ===========================================================================
  // getStoredAccent
  // ===========================================================================

  describe('getStoredAccent', () => {
    it('returns "orange" when nothing is stored', () => {
      expect(getStoredAccent()).toBe('orange')
    })

    it('returns the stored accent value', () => {
      localStorage.setItem('accent_theme', 'cyan')
      expect(getStoredAccent()).toBe('cyan')
    })

    it('returns "orange" when an invalid value is stored', () => {
      localStorage.setItem('accent_theme', 'invalid')
      expect(getStoredAccent()).toBe('orange')
    })
  })

  // ===========================================================================
  // getStoredFont
  // ===========================================================================

  describe('getStoredFont', () => {
    it('returns "sora" when nothing is stored', () => {
      expect(getStoredFont()).toBe('sora')
    })

    it('returns the stored font value', () => {
      localStorage.setItem('typography', 'inter')
      expect(getStoredFont()).toBe('inter')
    })

    it('returns "sora" when an invalid value is stored', () => {
      localStorage.setItem('typography', 'comic-sans')
      expect(getStoredFont()).toBe('sora')
    })
  })

  // ===========================================================================
  // applyAccent
  // ===========================================================================

  describe('applyAccent', () => {
    it('sets data-accent attribute for non-orange accents', () => {
      applyAccent('cyan')
      expect(document.documentElement.getAttribute('data-accent')).toBe('cyan')
    })

    it('sets data-accent for violet', () => {
      applyAccent('violet')
      expect(document.documentElement.getAttribute('data-accent')).toBe('violet')
    })

    it('sets data-accent for rose', () => {
      applyAccent('rose')
      expect(document.documentElement.getAttribute('data-accent')).toBe('rose')
    })

    it('sets data-accent for emerald', () => {
      applyAccent('emerald')
      expect(document.documentElement.getAttribute('data-accent')).toBe('emerald')
    })

    it('removes data-accent attribute for orange (the default)', () => {
      // First set a non-default accent
      document.documentElement.setAttribute('data-accent', 'cyan')
      // Then apply orange
      applyAccent('orange')
      expect(document.documentElement.hasAttribute('data-accent')).toBe(false)
    })
  })

  // ===========================================================================
  // applyFont
  // ===========================================================================

  describe('applyFont', () => {
    it('sets data-font attribute for non-sora fonts', () => {
      applyFont('inter')
      expect(document.documentElement.getAttribute('data-font')).toBe('inter')
    })

    it('sets data-font for roboto', () => {
      applyFont('roboto')
      expect(document.documentElement.getAttribute('data-font')).toBe('roboto')
    })

    it('sets data-font for system', () => {
      applyFont('system')
      expect(document.documentElement.getAttribute('data-font')).toBe('system')
    })

    it('sets data-font for jetbrains-mono', () => {
      applyFont('jetbrains-mono')
      expect(document.documentElement.getAttribute('data-font')).toBe('jetbrains-mono')
    })

    it('removes data-font attribute for sora (the default)', () => {
      document.documentElement.setAttribute('data-font', 'inter')
      applyFont('sora')
      expect(document.documentElement.hasAttribute('data-font')).toBe(false)
    })
  })

  // ===========================================================================
  // setAccent
  // ===========================================================================

  describe('setAccent', () => {
    it('saves accent to localStorage and applies it', () => {
      setAccent('violet')
      expect(localStorage.getItem('accent_theme')).toBe('violet')
      expect(document.documentElement.getAttribute('data-accent')).toBe('violet')
    })

    it('saves and removes attribute for orange', () => {
      setAccent('cyan')
      setAccent('orange')
      expect(localStorage.getItem('accent_theme')).toBe('orange')
      expect(document.documentElement.hasAttribute('data-accent')).toBe(false)
    })
  })

  // ===========================================================================
  // setFont
  // ===========================================================================

  describe('setFont', () => {
    it('saves font to localStorage and applies it', () => {
      setFont('roboto')
      expect(localStorage.getItem('typography')).toBe('roboto')
      expect(document.documentElement.getAttribute('data-font')).toBe('roboto')
    })

    it('saves and removes attribute for sora', () => {
      setFont('inter')
      setFont('sora')
      expect(localStorage.getItem('typography')).toBe('sora')
      expect(document.documentElement.hasAttribute('data-font')).toBe(false)
    })
  })

  // ===========================================================================
  // Icon Set validation
  // ===========================================================================

  describe('isValidIconSet', () => {
    it('returns true for all valid icon set values', () => {
      for (const set of ALL_ICON_SETS) {
        expect(isValidIconSet(set), `${set} should be valid`).toBe(true)
      }
    })

    it('returns false for invalid values', () => {
      expect(isValidIconSet('fontawesome')).toBe(false)
      expect(isValidIconSet('')).toBe(false)
      expect(isValidIconSet('LUCIDE')).toBe(false)
    })

    it('returns false for null and undefined', () => {
      expect(isValidIconSet(null)).toBe(false)
      expect(isValidIconSet(undefined)).toBe(false)
    })
  })

  // ===========================================================================
  // getStoredIconSet
  // ===========================================================================

  describe('getStoredIconSet', () => {
    it('returns "lucide" when nothing is stored', () => {
      expect(getStoredIconSet()).toBe('lucide')
    })

    it('returns the stored icon set value', () => {
      localStorage.setItem('icon_set', 'phosphor')
      expect(getStoredIconSet()).toBe('phosphor')
    })

    it('returns "lucide" when an invalid value is stored', () => {
      localStorage.setItem('icon_set', 'fontawesome')
      expect(getStoredIconSet()).toBe('lucide')
    })
  })

  // ===========================================================================
  // applyIconSet
  // ===========================================================================

  describe('applyIconSet', () => {
    it('sets data-icon-set attribute for non-lucide sets', () => {
      applyIconSet('heroicons')
      expect(document.documentElement.getAttribute('data-icon-set')).toBe('heroicons')
    })

    it('removes data-icon-set attribute for lucide (the default)', () => {
      document.documentElement.setAttribute('data-icon-set', 'phosphor')
      applyIconSet('lucide')
      expect(document.documentElement.hasAttribute('data-icon-set')).toBe(false)
    })

    it('updates existing [data-icon] elements with new icon set content', () => {
      document.body.innerHTML = `
        <span data-icon="dashboard">old-svg-content</span>
        <span data-icon="settings">old-svg-content</span>
      `
      applyIconSet('material')

      const icons = document.querySelectorAll('[data-icon]')
      icons.forEach(icon => {
        // The content should have been replaced (not the original placeholder)
        expect(icon.innerHTML).not.toBe('old-svg-content')
      })
    })
  })

  // ===========================================================================
  // setIconSet
  // ===========================================================================

  describe('setIconSet', () => {
    it('saves icon set to localStorage and applies it', () => {
      setIconSet('phosphor')
      expect(localStorage.getItem('icon_set')).toBe('phosphor')
    })

    it('sets data-icon-set on <html> for non-default sets', () => {
      setIconSet('material')
      expect(document.documentElement.getAttribute('data-icon-set')).toBe('material')
    })
  })

  // ===========================================================================
  // Quick Switcher Themes
  // ===========================================================================

  describe('getStoredQuickSwitcherThemes', () => {
    it('returns all 4 resolved themes by default when nothing stored', () => {
      const themes = getStoredQuickSwitcherThemes()
      expect(themes).toHaveLength(4)
      expect(themes).toContain('light')
      expect(themes).toContain('dark')
      expect(themes).toContain('monokai')
      expect(themes).toContain('glass')
    })

    it('parses stored JSON and returns theme array', () => {
      localStorage.setItem('quick_switcher_themes', JSON.stringify(['light', 'dark']))
      const themes = getStoredQuickSwitcherThemes()
      expect(themes).toEqual(['light', 'dark'])
    })

    it('returns defaults when stored JSON is invalid', () => {
      localStorage.setItem('quick_switcher_themes', 'not-json')
      const themes = getStoredQuickSwitcherThemes()
      expect(themes).toHaveLength(4)
    })
  })

  describe('setQuickSwitcherThemes', () => {
    it('stores themes as JSON in localStorage', () => {
      const themes: ResolvedTheme[] = ['dark', 'monokai']
      setQuickSwitcherThemes(themes)
      const stored = localStorage.getItem('quick_switcher_themes')
      expect(stored).toBe(JSON.stringify(['dark', 'monokai']))
    })
  })

  // ===========================================================================
  // initAppearance
  // ===========================================================================

  describe('initAppearance', () => {
    it('applies all stored preferences on init', () => {
      localStorage.setItem('accent_theme', 'emerald')
      localStorage.setItem('typography', 'jetbrains-mono')
      localStorage.setItem('icon_set', 'heroicons')

      initAppearance()

      expect(document.documentElement.getAttribute('data-accent')).toBe('emerald')
      expect(document.documentElement.getAttribute('data-font')).toBe('jetbrains-mono')
      expect(document.documentElement.getAttribute('data-icon-set')).toBe('heroicons')
    })

    it('applies defaults when nothing is stored', () => {
      initAppearance()

      // Defaults: orange (no attr), sora (no attr), lucide (no attr)
      expect(document.documentElement.hasAttribute('data-accent')).toBe(false)
      expect(document.documentElement.hasAttribute('data-font')).toBe(false)
      expect(document.documentElement.hasAttribute('data-icon-set')).toBe(false)
    })

    it('calls onToggleCycleUpdate callback with current themes', () => {
      const callback = vi.fn()
      initAppearance({ onToggleCycleUpdate: callback })

      expect(callback).toHaveBeenCalledOnce()
      expect(callback).toHaveBeenCalledWith(expect.any(Array))
    })
  })

  // ===========================================================================
  // syncAppearanceFromServer
  // ===========================================================================

  describe('syncAppearanceFromServer', () => {
    it('fetches appearance from the default endpoint', async () => {
      const mockFetch = vi.fn().mockResolvedValue({
        ok: true,
        json: () =>
          Promise.resolve({
            accent: 'rose',
            font: 'inter',
            icon_set: 'phosphor',
          }),
      })
      globalThis.fetch = mockFetch

      await syncAppearanceFromServer()

      expect(mockFetch).toHaveBeenCalledWith(expect.stringContaining('/api/'), expect.any(Object))
    })

    it('fetches from a configurable endpoint', async () => {
      const mockFetch = vi.fn().mockResolvedValue({
        ok: true,
        json: () =>
          Promise.resolve({
            accent: 'cyan',
            font: 'roboto',
            icon_set: 'material',
          }),
      })
      globalThis.fetch = mockFetch

      await syncAppearanceFromServer({ endpoint: '/custom/api/appearance' })

      expect(mockFetch).toHaveBeenCalledWith('/custom/api/appearance', expect.any(Object))
    })

    it('applies server preferences that differ from local', async () => {
      // Local starts as defaults (orange, sora, lucide)
      const mockFetch = vi.fn().mockResolvedValue({
        ok: true,
        json: () =>
          Promise.resolve({
            accent: 'violet',
            font: 'jetbrains-mono',
            icon_set: 'heroicons',
          }),
      })
      globalThis.fetch = mockFetch

      await syncAppearanceFromServer()

      expect(document.documentElement.getAttribute('data-accent')).toBe('violet')
      expect(document.documentElement.getAttribute('data-font')).toBe('jetbrains-mono')
      expect(document.documentElement.getAttribute('data-icon-set')).toBe('heroicons')
    })

    it('uses getToken for authorization header when provided', async () => {
      const mockFetch: FetchMock = vi.fn<FetchFn>().mockResolvedValue({
        ok: true,
        json: () => Promise.resolve({ accent: 'orange', font: 'sora', icon_set: 'lucide' }),
      })
      globalThis.fetch = mockFetch as unknown as typeof fetch

      await syncAppearanceFromServer({
        getToken: () => 'my-auth-token',
      })

      expect(mockFetch).toHaveBeenCalledOnce()
      expect(mockFetch.mock.calls[0]?.[1]?.headers).toEqual({
        Authorization: 'Bearer my-auth-token',
      })
    })

    it('does not throw when fetch fails', async () => {
      globalThis.fetch = vi.fn().mockRejectedValue(new Error('Network error'))

      // Should not throw
      await expect(syncAppearanceFromServer()).resolves.not.toThrow()
    })

    it('does not throw when response is not ok', async () => {
      globalThis.fetch = vi.fn().mockResolvedValue({
        ok: false,
        status: 500,
        json: () => Promise.resolve({}),
      })

      await expect(syncAppearanceFromServer()).resolves.not.toThrow()
    })
  })
})
