import React from 'react'
import type { Channel } from 'stream-chat'
import { describe, expect, it, vi, beforeEach } from 'vitest'

import { renderWithProviders } from '../../test/utils'

import { ChannelList } from '.'

const streamChannelListMock = vi.fn()
const {
  ChannelListProviderMock,
  CustomChannelPreviewMock,
  InfiniteScrollMock,
  restorePendingMessagesMock,
  streamMountMock,
  messagingContextMock,
} = vi.hoisted(() => ({
  ChannelListProviderMock: vi.fn(
    ({ children }: { children: React.ReactNode; value: unknown }) => children
  ),
  CustomChannelPreviewMock: () => null,
  InfiniteScrollMock: () => null,
  restorePendingMessagesMock: vi.fn(),
  streamMountMock: vi.fn(),
  messagingContextMock: { current: { debug: false } },
}))

vi.mock('stream-chat-react', () => ({
  ChannelList: (props: unknown) => {
    React.useEffect(() => {
      streamMountMock()
    }, [])
    streamChannelListMock(props)
    return <div data-testid="stream-channel-list" />
  },
  InfiniteScroll: InfiniteScrollMock,
}))

vi.mock('../../providers/MessagingProvider', () => ({
  useMessagingContext: () => messagingContextMock.current,
}))

vi.mock('../../hooks/useRestorePendingMessages', () => ({
  restorePendingMessages: restorePendingMessagesMock,
}))

vi.mock('./ChannelListContext', () => ({
  ChannelListProvider: ChannelListProviderMock,
}))

vi.mock('./CustomChannelPreview', () => ({
  default: CustomChannelPreviewMock,
}))

describe('ChannelList', () => {
  const defaultProps = {
    filters: { type: 'messaging' },
    onChannelSelect: vi.fn(),
  }

  beforeEach(() => {
    ChannelListProviderMock.mockClear()
    streamChannelListMock.mockClear()
    restorePendingMessagesMock.mockClear()
    streamMountMock.mockClear()
    messagingContextMock.current = { debug: false }
  })

  it('defaults unseen-channel promotion to false', () => {
    renderWithProviders(<ChannelList {...defaultProps} />)

    expect(streamChannelListMock).toHaveBeenCalledOnce()
    expect(streamChannelListMock.mock.calls[0][0]).toMatchObject({
      allowNewMessagesFromUnfilteredChannels: false,
    })
  })

  it('uses InfiniteScroll as the channel list paginator', () => {
    renderWithProviders(<ChannelList {...defaultProps} />)

    expect(streamChannelListMock).toHaveBeenCalledOnce()
    expect(streamChannelListMock.mock.calls[0][0]).toMatchObject({
      Paginator: InfiniteScrollMock,
    })
  })

  it('uses a custom channel list paginator when provided', () => {
    const ChannelListPaginator = () => null

    renderWithProviders(
      <ChannelList
        {...defaultProps}
        channelListPaginator={ChannelListPaginator}
      />
    )

    expect(streamChannelListMock).toHaveBeenCalledOnce()
    expect(streamChannelListMock.mock.calls[0][0]).toMatchObject({
      Paginator: ChannelListPaginator,
    })
  })

  it('uses the built-in channel preview by default', () => {
    renderWithProviders(<ChannelList {...defaultProps} />)

    expect(streamChannelListMock).toHaveBeenCalledOnce()
    expect(streamChannelListMock.mock.calls[0][0]).toMatchObject({
      Preview: CustomChannelPreviewMock,
    })
  })

  it('keeps the built-in channel preview bridge when a custom preview is provided', () => {
    const ChannelPreview = () => null

    renderWithProviders(
      <ChannelList {...defaultProps} channelPreview={ChannelPreview} />
    )

    expect(streamChannelListMock).toHaveBeenCalledOnce()
    const contextValue = ChannelListProviderMock.mock.calls[0][0].value as {
      channelPreview?: unknown
    }

    expect(streamChannelListMock.mock.calls[0][0]).toMatchObject({
      Preview: CustomChannelPreviewMock,
    })
    expect(contextValue.channelPreview).toBe(ChannelPreview)
  })

  it('passes allowNewMessagesFromUnfilteredChannels through to Stream ChannelList', () => {
    const props: React.ComponentProps<typeof ChannelList> = {
      ...defaultProps,
      allowNewMessagesFromUnfilteredChannels: true,
    }

    renderWithProviders(React.createElement(ChannelList, props))

    expect(streamChannelListMock).toHaveBeenCalledOnce()
    expect(streamChannelListMock.mock.calls[0][0]).toMatchObject({
      allowNewMessagesFromUnfilteredChannels: true,
    })
  })

  it('leaves live-reorder controls undefined by default so Stream keeps its default ordering', () => {
    renderWithProviders(<ChannelList {...defaultProps} />)

    expect(streamChannelListMock).toHaveBeenCalledOnce()
    const streamProps = streamChannelListMock.mock.calls[0][0] as Record<
      string,
      unknown
    >
    expect(streamProps.lockChannelOrder).toBeUndefined()
    expect(streamProps.onMessageNewHandler).toBeUndefined()
    expect(streamProps.onMessageNew).toBeUndefined()
    expect(streamProps.onChannelVisible).toBeUndefined()
    expect(streamProps.onAddedToChannel).toBeUndefined()
    expect(streamProps.onChannelUpdated).toBeUndefined()
  })

  it('forwards live-reorder controls to Stream ChannelList when provided', () => {
    const onMessageNewHandler = vi.fn()
    const onMessageNew = vi.fn()
    const onChannelVisible = vi.fn()
    const onAddedToChannel = vi.fn()
    const onChannelUpdated = vi.fn()

    renderWithProviders(
      <ChannelList
        {...defaultProps}
        lockChannelOrder
        onMessageNewHandler={onMessageNewHandler}
        onMessageNew={onMessageNew}
        onChannelVisible={onChannelVisible}
        onAddedToChannel={onAddedToChannel}
        onChannelUpdated={onChannelUpdated}
      />
    )

    expect(streamChannelListMock).toHaveBeenCalledOnce()
    expect(streamChannelListMock.mock.calls[0][0]).toMatchObject({
      lockChannelOrder: true,
      onMessageNewHandler,
      onMessageNew,
      onChannelVisible,
      onAddedToChannel,
      onChannelUpdated,
    })
  })

  it('wraps channelRenderFilterFn to restore pending messages and delegates to consumer filter', () => {
    const filterFn = vi.fn((channels: Channel[]) => channels)

    renderWithProviders(
      React.createElement(ChannelList, {
        ...defaultProps,
        channelRenderFilterFn: filterFn,
      })
    )

    expect(streamChannelListMock).toHaveBeenCalledOnce()
    const streamProps = streamChannelListMock.mock.calls[0][0] as {
      channelRenderFilterFn?: (channels: unknown[]) => unknown[]
    }

    // The wrapper should not be the same reference as the original filter
    expect(streamProps.channelRenderFilterFn).not.toBe(filterFn)
    expect(typeof streamProps.channelRenderFilterFn).toBe('function')

    // When the wrapper is called, it should delegate to the consumer's filter
    const mockChannels = [
      {
        cid: 'ch-1',
        state: { pending_messages: [], addMessageSorted: vi.fn() },
      },
    ]
    streamProps.channelRenderFilterFn!(mockChannels)
    expect(filterFn).toHaveBeenCalledWith(mockChannels)
  })

  it('restores pending messages for every channel and preserves channels without a filter', () => {
    const channels = [
      { cid: 'ch-1', state: { pending_messages: [] } },
      { cid: 'ch-2', state: { pending_messages: [{ id: 'pending-2' }] } },
    ] as unknown as Channel[]

    renderWithProviders(<ChannelList {...defaultProps} />)
    const streamProps = streamChannelListMock.mock.calls[0][0] as {
      channelRenderFilterFn: (channels: Channel[]) => Channel[]
    }

    expect(streamProps.channelRenderFilterFn(channels)).toBe(channels)
    expect(restorePendingMessagesMock).toHaveBeenCalledTimes(2)
    expect(restorePendingMessagesMock).toHaveBeenNthCalledWith(1, channels[0])
    expect(restorePendingMessagesMock).toHaveBeenNthCalledWith(2, channels[1])
  })

  it('remounts Stream ChannelList when filters change but not when they stay the same', () => {
    const { rerender } = renderWithProviders(<ChannelList {...defaultProps} />)

    expect(streamMountMock).toHaveBeenCalledOnce()

    rerender(<ChannelList {...defaultProps} />)
    expect(streamMountMock).toHaveBeenCalledOnce()

    rerender(
      <ChannelList
        {...defaultProps}
        filters={{ type: 'messaging', members: { $in: ['another-user'] } }}
      />
    )
    expect(streamMountMock).toHaveBeenCalledTimes(2)
  })

  it('forwards the default and explicit sort values and the channel limit', () => {
    renderWithProviders(<ChannelList {...defaultProps} />)
    expect(streamChannelListMock.mock.calls[0][0]).toMatchObject({
      sort: { last_message_at: -1 },
      options: { limit: 30 },
    })

    const explicitSort = { updated_at: 1 as const }
    renderWithProviders(<ChannelList {...defaultProps} sort={explicitSort} />)
    expect(streamChannelListMock.mock.calls[1][0]).toMatchObject({
      sort: explicitSort,
      options: { limit: 30 },
    })
  })

  it('provides channel list context values from props and messaging context', () => {
    const selectedChannel = { id: 'selected-channel' } as Channel
    const onChannelSelect = vi.fn()
    const renderMessagePreview = vi.fn()
    messagingContextMock.current = { debug: true }

    renderWithProviders(
      <ChannelList
        {...defaultProps}
        selectedChannel={selectedChannel}
        onChannelSelect={onChannelSelect}
        renderMessagePreview={renderMessagePreview}
        viewerLanguage="fr"
      />
    )

    const contextValue = ChannelListProviderMock.mock.calls[0][0].value
    expect(contextValue).toMatchObject({
      selectedChannel,
      onChannelSelect,
      debug: true,
      renderMessagePreview,
      viewerLanguage: 'fr',
    })
  })

  it('logs the channel list render when messaging debug is enabled', () => {
    const consoleLog = vi.spyOn(console, 'log').mockImplementation(() => {})
    messagingContextMock.current = { debug: true }

    renderWithProviders(<ChannelList {...defaultProps} />)

    expect(consoleLog).toHaveBeenCalledWith(
      '📺 [ChannelList] 🔄 RENDER START',
      expect.objectContaining({
        renderCount: 1,
        selectedChannelId: undefined,
        filters: defaultProps.filters,
      })
    )
  })

  it('passes wrapper classes and the custom empty state indicator through', () => {
    const EmptyStateIndicator = () => <div>Empty state</div>
    const { container } = renderWithProviders(
      <ChannelList
        {...defaultProps}
        className="custom-channel-list"
        customEmptyStateIndicator={EmptyStateIndicator}
      />
    )

    expect(container.firstElementChild).toHaveClass(
      'messaging-channel-list',
      'h-full',
      'flex',
      'flex-col',
      'min-w-0',
      'overflow-hidden',
      'custom-channel-list'
    )
    expect(streamChannelListMock.mock.calls[0][0]).toMatchObject({
      EmptyStateIndicator,
    })
  })
})
