import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'
import { act, render, screen } from '@testing-library/react'
import { State } from './widget-state'
import { Provider } from '../provider/widget-provider'
import { clearAllWidgetStores, getCaptureEl, getWidgetStore } from '../stores'

beforeEach(() => clearAllWidgetStores())
afterEach(() => clearAllWidgetStores())

describe('<Widget.State>', () => {
  it('renders skeleton when isLoading is true (loading wins)', () => {
    render(
      <Provider id='s1' data={null} isLoading>
        <State skeleton={<div data-testid='skeleton' />}>
          <div>content</div>
        </State>
      </Provider>,
    )
    expect(screen.getByTestId('skeleton')).toBeTruthy()
  })

  it('renders default error UI when store error is set', () => {
    render(
      <Provider id='s2' data={[1, 2]} error={new Error('boom')}>
        <State skeleton={null}>
          <div>content</div>
        </State>
      </Provider>,
    )
    expect(screen.getByRole('alert').textContent).toContain('boom')
  })

  it('renders custom fallback when provided', () => {
    render(
      <Provider id='s3' data={[1, 2]} error={new Error('x')}>
        <State
          skeleton={null}
          fallback={(e) => <div data-testid='fb'>{e.message}</div>}
        >
          <div>content</div>
        </State>
      </Provider>,
    )
    expect(screen.getByTestId('fb').textContent).toBe('x')
  })

  it('renders empty UI when rawData is empty (default predicate)', () => {
    render(
      <Provider id='s4' data={[]}>
        <State skeleton={null} labels={{ emptyTitle: 'NOTHING' }}>
          <div>content</div>
        </State>
      </Provider>,
    )
    expect(screen.getByText('NOTHING')).toBeTruthy()
  })

  it('renders content when there is data and no error/loading', () => {
    render(
      <Provider id='s5' data={[1]}>
        <State skeleton={null}>
          <div data-testid='ok'>content</div>
        </State>
      </Provider>,
    )
    expect(screen.getByTestId('ok')).toBeTruthy()
  })

  it('catches a render error in children and routes through the error UI', () => {
    const spy = vi.spyOn(console, 'error').mockImplementation(() => undefined)
    function Bomb(): null {
      throw new Error('render-throw')
    }
    render(
      <Provider id='s6' data={[1]}>
        <State skeleton={null}>
          <Bomb />
        </State>
      </Provider>,
    )
    expect(screen.getByRole('alert').textContent).toContain('render-throw')
    spy.mockRestore()
  })

  it('uses custom fallback for render errors too (single render path)', () => {
    const spy = vi.spyOn(console, 'error').mockImplementation(() => undefined)
    function Bomb(): null {
      throw new Error('render-throw-2')
    }
    render(
      <Provider id='s7' data={[1]}>
        <State
          skeleton={null}
          fallback={(e) => <div data-testid='rb'>{e.message}</div>}
        >
          <Bomb />
        </State>
      </Provider>,
    )
    expect(screen.getByTestId('rb').textContent).toBe('render-throw-2')
    spy.mockRestore()
  })

  it('uses custom isEmpty predicate against rawData', () => {
    render(
      <Provider id='s8' data={{ rows: [] }}>
        <State
          skeleton={null}
          labels={{ emptyTitle: 'NO ROWS' }}
          isEmpty={(d) =>
            !!d &&
            typeof d === 'object' &&
            Array.isArray((d as { rows: unknown[] }).rows) &&
            (d as { rows: unknown[] }).rows.length === 0
          }
        >
          <div>content</div>
        </State>
      </Provider>,
    )
    expect(screen.getByText('NO ROWS')).toBeTruthy()
  })

  it('hides errors during fetch (loading wins)', () => {
    render(
      <Provider id='s9' data={[1]} isLoading error={new Error('x')}>
        <State skeleton={<div data-testid='skel' />}>
          <div>content</div>
        </State>
      </Provider>,
    )
    expect(screen.getByTestId('skel')).toBeTruthy()
    expect(screen.queryByRole('alert')).toBeNull()
  })

  describe('captureEl registry', () => {
    it('registers the success-path content wrapper as captureEl', () => {
      render(
        <Provider id='cap-success' data={[1]}>
          <State skeleton={null}>
            <div data-testid='inner'>content</div>
          </State>
        </Provider>,
      )
      const el = getCaptureEl('cap-success')
      expect(el).not.toBeNull()
      // The captured element wraps the rendered content.
      expect(el?.contains(screen.getByTestId('inner'))).toBe(true)
    })

    it('does not register a captureEl on the loading branch', () => {
      render(
        <Provider id='cap-loading' data={null} isLoading>
          <State skeleton={<div data-testid='skel' />}>
            <div>content</div>
          </State>
        </Provider>,
      )
      expect(getCaptureEl('cap-loading')).toBeNull()
    })

    it('does not register a captureEl on the error branch', () => {
      render(
        <Provider id='cap-error' data={[1]} error={new Error('x')}>
          <State skeleton={null}>
            <div>content</div>
          </State>
        </Provider>,
      )
      expect(getCaptureEl('cap-error')).toBeNull()
    })

    it('does not register a captureEl on the empty branch', () => {
      render(
        <Provider id='cap-empty' data={[]}>
          <State skeleton={null}>
            <div>content</div>
          </State>
        </Provider>,
      )
      expect(getCaptureEl('cap-empty')).toBeNull()
    })

    it('clears the captureEl on unmount', () => {
      const { unmount } = render(
        <Provider id='cap-unmount' data={[1]}>
          <State skeleton={null}>
            <div>content</div>
          </State>
        </Provider>,
      )
      expect(getCaptureEl('cap-unmount')).not.toBeNull()
      unmount()
      expect(getCaptureEl('cap-unmount')).toBeNull()
    })
  })

  describe('default isEmpty predicate', () => {
    it('treats an empty object literal as empty', () => {
      render(
        <Provider id='isEmpty-empty-obj' data={{}}>
          <State skeleton={null} labels={{ emptyTitle: 'EMPTY' }}>
            <div>content</div>
          </State>
        </Provider>,
      )
      expect(screen.getByText('EMPTY')).toBeTruthy()
    })

    it('treats an array of empty arrays as empty', () => {
      render(
        <Provider id='isEmpty-empty-rows' data={[[], []]}>
          <State skeleton={null} labels={{ emptyTitle: 'EMPTY-ROWS' }}>
            <div>content</div>
          </State>
        </Provider>,
      )
      expect(screen.getByText('EMPTY-ROWS')).toBeTruthy()
    })

    it('treats a populated array of non-empty sub-arrays as non-empty', () => {
      render(
        <Provider id='isEmpty-populated' data={[[1], [2]]}>
          <State skeleton={null}>
            <div data-testid='content'>content</div>
          </State>
        </Provider>,
      )
      expect(screen.getByTestId('content')).toBeTruthy()
    })
  })

  describe('toError coercion', () => {
    it('coerces a string error to an Error instance', () => {
      render(
        <Provider id='err-string' data={[1]} error='string-msg'>
          <State skeleton={null}>
            <div>content</div>
          </State>
        </Provider>,
      )
      expect(screen.getByRole('alert').textContent).toContain('string-msg')
    })

    it('coerces an object-with-message to an Error instance', () => {
      render(
        <Provider id='err-objmsg' data={[1]} error={{ message: 'object-msg' }}>
          <State skeleton={null}>
            <div>content</div>
          </State>
        </Provider>,
      )
      expect(screen.getByRole('alert').textContent).toContain('object-msg')
    })

    it('falls back to "Unknown error" for unknown shapes', () => {
      render(
        <Provider id='err-unknown' data={[1]} error={42}>
          <State skeleton={null}>
            <div>content</div>
          </State>
        </Provider>,
      )
      expect(screen.getByRole('alert').textContent).toContain('Unknown error')
    })
  })

  it('routes a transform-throw error (set by middleware) through the same UI', () => {
    render(
      <Provider id='s10' data={[1, 2]}>
        <State skeleton={null}>
          <div>content</div>
        </State>
      </Provider>,
    )
    // Simulate a transform throw setting the error field
    act(() => {
      getWidgetStore('s10').setState({
        dataTransforms: [
          {
            id: 'bad',
            type: 'data',
            order: 1,
            enabled: true,
            fn: () => {
              throw new Error('transform-throw')
            },
          },
        ],
      })
    })
    expect(screen.getByRole('alert').textContent).toContain('transform-throw')
  })
})
