/**
 * Tests for ThemeContext - theme-context.tsx
 *
 * Tests theme switching, system theme detection, and localStorage persistence.
 */

import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
import { render, screen, fireEvent, act, waitFor } from '@testing-library/react'
import { ThemeProvider, useTheme, type ThemeContextValue } from '../../context/theme-context'

// Helper component to consume theme context
function ThemeConsumer({ onTheme }: { onTheme: (theme: ThemeContextValue) => void }) {
  const themeCtx = useTheme()
  onTheme(themeCtx)
  return (
    <div>
      <span data-testid="theme">{themeCtx.theme}</span>
      <span data-testid="resolved-theme">{themeCtx.resolvedTheme}</span>
      <button data-testid="toggle-btn" onClick={themeCtx.toggleTheme}>
        Toggle
      </button>
      <button data-testid="set-light-btn" onClick={() => themeCtx.setTheme('light')}>
        Light
      </button>
      <button data-testid="set-dark-btn" onClick={() => themeCtx.setTheme('dark')}>
        Dark
      </button>
      <button data-testid="set-system-btn" onClick={() => themeCtx.setTheme('system')}>
        System
      </button>
    </div>
  )
}

// Mock matchMedia for system theme detection
function createMatchMediaMock(prefersDark: boolean) {
  return vi.fn().mockImplementation((query: string) => ({
    matches: query === '(prefers-color-scheme: dark)' && prefersDark,
    media: query,
    onchange: null,
    addListener: vi.fn(),
    removeListener: vi.fn(),
    addEventListener: vi.fn(),
    removeEventListener: vi.fn(),
    dispatchEvent: vi.fn(),
  }))
}

describe('ThemeContext', () => {
  let originalMatchMedia: typeof window.matchMedia
  let localStorageMock: { [key: string]: string }

  beforeEach(() => {
    // Mock localStorage
    localStorageMock = {}
    vi.spyOn(Storage.prototype, 'getItem').mockImplementation((key: string) => localStorageMock[key] || null)
    vi.spyOn(Storage.prototype, 'setItem').mockImplementation((key: string, value: string) => {
      localStorageMock[key] = value
    })
    vi.spyOn(Storage.prototype, 'removeItem').mockImplementation((key: string) => {
      delete localStorageMock[key]
    })

    // Mock matchMedia
    originalMatchMedia = window.matchMedia
    window.matchMedia = createMatchMediaMock(false) // Default to light mode

    // Mock document.documentElement
    document.documentElement.classList.remove('light', 'dark')
  })

  afterEach(() => {
    window.matchMedia = originalMatchMedia
    vi.restoreAllMocks()
  })

  describe('ThemeProvider', () => {
    it('should provide theme state to children', () => {
      let themeValue: ThemeContextValue | undefined

      render(
        <ThemeProvider defaultTheme="light">
          <ThemeConsumer onTheme={(t) => { themeValue = t }} />
        </ThemeProvider>
      )

      expect(themeValue).toBeDefined()
      expect(screen.getByTestId('theme').textContent).toBe('light')
      expect(screen.getByTestId('resolved-theme').textContent).toBe('light')
    })

    it('should use defaultTheme when no localStorage value', () => {
      render(
        <ThemeProvider defaultTheme="dark">
          <ThemeConsumer onTheme={() => {}} />
        </ThemeProvider>
      )

      expect(screen.getByTestId('theme').textContent).toBe('dark')
      expect(screen.getByTestId('resolved-theme').textContent).toBe('dark')
    })

    it('should read theme from localStorage on mount', () => {
      localStorageMock['mdxui-app-theme'] = 'dark'

      render(
        <ThemeProvider defaultTheme="light">
          <ThemeConsumer onTheme={() => {}} />
        </ThemeProvider>
      )

      expect(screen.getByTestId('theme').textContent).toBe('dark')
    })

    it('should use custom storageKey', () => {
      localStorageMock['my-custom-theme'] = 'dark'

      render(
        <ThemeProvider defaultTheme="light" storageKey="my-custom-theme">
          <ThemeConsumer onTheme={() => {}} />
        </ThemeProvider>
      )

      expect(screen.getByTestId('theme').textContent).toBe('dark')
    })

    it('should apply theme class to document root', async () => {
      render(
        <ThemeProvider defaultTheme="dark">
          <ThemeConsumer onTheme={() => {}} />
        </ThemeProvider>
      )

      await waitFor(() => {
        expect(document.documentElement.classList.contains('dark')).toBe(true)
      })
    })
  })

  describe('theme switching', () => {
    it('should switch from light to dark', async () => {
      render(
        <ThemeProvider defaultTheme="light">
          <ThemeConsumer onTheme={() => {}} />
        </ThemeProvider>
      )

      expect(screen.getByTestId('theme').textContent).toBe('light')

      act(() => {
        fireEvent.click(screen.getByTestId('set-dark-btn'))
      })

      await waitFor(() => {
        expect(screen.getByTestId('theme').textContent).toBe('dark')
        expect(screen.getByTestId('resolved-theme').textContent).toBe('dark')
      })
    })

    it('should switch from dark to light', async () => {
      render(
        <ThemeProvider defaultTheme="dark">
          <ThemeConsumer onTheme={() => {}} />
        </ThemeProvider>
      )

      expect(screen.getByTestId('theme').textContent).toBe('dark')

      act(() => {
        fireEvent.click(screen.getByTestId('set-light-btn'))
      })

      await waitFor(() => {
        expect(screen.getByTestId('theme').textContent).toBe('light')
        expect(screen.getByTestId('resolved-theme').textContent).toBe('light')
      })
    })

    it('should persist theme to localStorage', async () => {
      render(
        <ThemeProvider defaultTheme="light">
          <ThemeConsumer onTheme={() => {}} />
        </ThemeProvider>
      )

      act(() => {
        fireEvent.click(screen.getByTestId('set-dark-btn'))
      })

      await waitFor(() => {
        expect(localStorageMock['mdxui-app-theme']).toBe('dark')
      })
    })

    it('should update document class on theme change', async () => {
      render(
        <ThemeProvider defaultTheme="light">
          <ThemeConsumer onTheme={() => {}} />
        </ThemeProvider>
      )

      await waitFor(() => {
        expect(document.documentElement.classList.contains('light')).toBe(true)
      })

      act(() => {
        fireEvent.click(screen.getByTestId('set-dark-btn'))
      })

      await waitFor(() => {
        expect(document.documentElement.classList.contains('dark')).toBe(true)
        expect(document.documentElement.classList.contains('light')).toBe(false)
      })
    })
  })

  describe('toggleTheme', () => {
    it('should toggle from light to dark', async () => {
      render(
        <ThemeProvider defaultTheme="light">
          <ThemeConsumer onTheme={() => {}} />
        </ThemeProvider>
      )

      expect(screen.getByTestId('resolved-theme').textContent).toBe('light')

      act(() => {
        fireEvent.click(screen.getByTestId('toggle-btn'))
      })

      await waitFor(() => {
        expect(screen.getByTestId('resolved-theme').textContent).toBe('dark')
      })
    })

    it('should toggle from dark to light', async () => {
      render(
        <ThemeProvider defaultTheme="dark">
          <ThemeConsumer onTheme={() => {}} />
        </ThemeProvider>
      )

      expect(screen.getByTestId('resolved-theme').textContent).toBe('dark')

      act(() => {
        fireEvent.click(screen.getByTestId('toggle-btn'))
      })

      await waitFor(() => {
        expect(screen.getByTestId('resolved-theme').textContent).toBe('light')
      })
    })

    it('should toggle from system (light) to dark', async () => {
      window.matchMedia = createMatchMediaMock(false) // System prefers light

      render(
        <ThemeProvider defaultTheme="system">
          <ThemeConsumer onTheme={() => {}} />
        </ThemeProvider>
      )

      await waitFor(() => {
        expect(screen.getByTestId('theme').textContent).toBe('system')
        expect(screen.getByTestId('resolved-theme').textContent).toBe('light')
      })

      act(() => {
        fireEvent.click(screen.getByTestId('toggle-btn'))
      })

      await waitFor(() => {
        // Toggle should switch resolved theme, changing actual theme from system
        expect(screen.getByTestId('theme').textContent).toBe('dark')
        expect(screen.getByTestId('resolved-theme').textContent).toBe('dark')
      })
    })
  })

  describe('system theme detection', () => {
    it('should resolve to light when system prefers light', async () => {
      window.matchMedia = createMatchMediaMock(false)

      render(
        <ThemeProvider defaultTheme="system">
          <ThemeConsumer onTheme={() => {}} />
        </ThemeProvider>
      )

      await waitFor(() => {
        expect(screen.getByTestId('theme').textContent).toBe('system')
        expect(screen.getByTestId('resolved-theme').textContent).toBe('light')
      })
    })

    it('should resolve to dark when system prefers dark', async () => {
      window.matchMedia = createMatchMediaMock(true)

      render(
        <ThemeProvider defaultTheme="system">
          <ThemeConsumer onTheme={() => {}} />
        </ThemeProvider>
      )

      await waitFor(() => {
        expect(screen.getByTestId('theme').textContent).toBe('system')
        expect(screen.getByTestId('resolved-theme').textContent).toBe('dark')
      })
    })

    it('should register listener for system theme changes', async () => {
      let addEventListenerCalled = false

      const mockMatchMedia = vi.fn().mockImplementation((query: string) => ({
        matches: query === '(prefers-color-scheme: dark)' && false, // Start light
        media: query,
        addEventListener: vi.fn((event: string) => {
          if (event === 'change') addEventListenerCalled = true
        }),
        removeEventListener: vi.fn(),
      }))
      window.matchMedia = mockMatchMedia

      render(
        <ThemeProvider defaultTheme="system">
          <ThemeConsumer onTheme={() => {}} />
        </ThemeProvider>
      )

      await waitFor(() => {
        expect(screen.getByTestId('resolved-theme').textContent).toBe('light')
      })

      // Verify the event listener was registered
      expect(addEventListenerCalled).toBe(true)
    })
  })

  describe('useTheme', () => {
    it('should throw error when used outside ThemeProvider', () => {
      const consoleError = vi.spyOn(console, 'error').mockImplementation(() => {})

      expect(() => {
        render(<ThemeConsumer onTheme={() => {}} />)
      }).toThrow('useTheme must be used within a ThemeProvider')

      consoleError.mockRestore()
    })
  })

  describe('edge cases', () => {
    it('should handle rapid theme changes', async () => {
      render(
        <ThemeProvider defaultTheme="light">
          <ThemeConsumer onTheme={() => {}} />
        </ThemeProvider>
      )

      // Rapidly change themes
      act(() => {
        fireEvent.click(screen.getByTestId('set-dark-btn'))
        fireEvent.click(screen.getByTestId('set-light-btn'))
        fireEvent.click(screen.getByTestId('set-dark-btn'))
      })

      await waitFor(() => {
        expect(screen.getByTestId('theme').textContent).toBe('dark')
        expect(localStorageMock['mdxui-app-theme']).toBe('dark')
      })
    })

    it('should handle invalid localStorage value gracefully', () => {
      localStorageMock['mdxui-app-theme'] = 'invalid-theme'

      // Should use localStorage value as-is since it's typed as Theme
      render(
        <ThemeProvider defaultTheme="light">
          <ThemeConsumer onTheme={() => {}} />
        </ThemeProvider>
      )

      // The implementation reads from localStorage as Theme type
      // This tests that no crash occurs with unexpected values
      expect(screen.getByTestId('theme')).toBeDefined()
      // The invalid value is read as-is from localStorage
      expect(screen.getByTestId('theme').textContent).toBe('invalid-theme')
    })
  })
})
