import { describe, it, expect } from 'vitest'
import { mount, flushPromises } from '@vue/test-utils'
import { defineComponent, h } from 'vue'

import ErrorBoundary from './ErrorBoundary.vue'

describe('ErrorBoundary', () => {
  describe('normal operation', () => {
    it('renders slot content when no error', () => {
      /**
       * Test that child content is displayed normally.
       *
       * When no errors occur, the error boundary should be
       * transparent and render its slot content as-is.
       */
      const wrapper = mount(ErrorBoundary, {
        slots: {
          default: '<div class="child">Hello World</div>',
        },
      })

      expect(wrapper.find('.child').exists()).toBe(true)
      expect(wrapper.text()).toContain('Hello World')
    })
  })

  describe('error handling', () => {
    it('shows fallback UI when child throws', async () => {
      /**
       * Test that errors in children are caught and handled.
       *
       * When a child component throws, the error boundary should
       * display a friendly fallback UI instead of crashing.
       */
      const ThrowingComponent = defineComponent({
        setup() {
          throw new Error('Test error')
        },
        render() {
          return h('div', 'Should not render')
        },
      })

      const wrapper = mount(ErrorBoundary, {
        slots: {
          default: h(ThrowingComponent),
        },
      })

      await flushPromises()

      expect(wrapper.text()).toContain('Oops!')
      expect(wrapper.text()).toContain('Something went wrong')
      expect(wrapper.find('.child').exists()).toBe(false)
    })

    it('shows custom fallback message', async () => {
      /**
       * Test that custom fallback messages are displayed.
       *
       * Components can customize the error message to provide
       * context-specific guidance to users.
       */
      const ThrowingComponent = defineComponent({
        setup() {
          throw new Error('Test error')
        },
        render() {
          return h('div')
        },
      })

      const wrapper = mount(ErrorBoundary, {
        props: {
          fallbackMessage: 'Failed to load game data.',
        },
        slots: {
          default: h(ThrowingComponent),
        },
      })

      await flushPromises()

      expect(wrapper.text()).toContain('Failed to load game data.')
    })

    it('emits error event when catching', async () => {
      /**
       * Test that error events are emitted for parent handling.
       *
       * Parent components may want to log errors or take
       * additional recovery actions.
       */
      const ThrowingComponent = defineComponent({
        setup() {
          throw new Error('Test error message')
        },
        render() {
          return h('div')
        },
      })

      const wrapper = mount(ErrorBoundary, {
        slots: {
          default: h(ThrowingComponent),
        },
      })

      await flushPromises()

      const errorEvents = wrapper.emitted('error')
      expect(errorEvents).toBeTruthy()
      expect(errorEvents![0][0]).toBeInstanceOf(Error)
      expect((errorEvents![0][0] as Error).message).toBe('Test error message')
    })
  })

  describe('recovery', () => {
    it('has retry button', async () => {
      /**
       * Test that retry button is available.
       *
       * Users should be able to attempt recovery without
       * refreshing the entire page.
       */
      const ThrowingComponent = defineComponent({
        setup() {
          throw new Error('Test error')
        },
        render() {
          return h('div')
        },
      })

      const wrapper = mount(ErrorBoundary, {
        slots: {
          default: h(ThrowingComponent),
        },
      })

      await flushPromises()

      const retryButton = wrapper.find('button')
      expect(retryButton.exists()).toBe(true)
      expect(retryButton.text()).toContain('Try Again')
    })

    it('resets error state on retry', async () => {
      /**
       * Test that clicking retry clears the error state.
       *
       * After retry, the boundary should attempt to re-render
       * the slot content.
       */
      let shouldThrow = true
      const ConditionalThrowComponent = defineComponent({
        setup() {
          if (shouldThrow) {
            throw new Error('Test error')
          }
        },
        render() {
          return h('div', { class: 'success' }, 'Success!')
        },
      })

      const wrapper = mount(ErrorBoundary, {
        slots: {
          default: h(ConditionalThrowComponent),
        },
      })

      await flushPromises()

      // Error state active
      expect(wrapper.text()).toContain('Oops!')

      // Disable throwing and retry
      shouldThrow = false
      await wrapper.find('button').trigger('click')
      await flushPromises()

      // Should attempt to render slot again
      // Note: The slot is still the throwing component from mount time,
      // so we just verify error state is cleared
      expect(wrapper.text()).not.toContain('Oops!')
    })
  })
})
