import { OFFICIAL_CHANNEL_TYPE } from '@linktr.ee/messaging-taxonomy'
import React from 'react'
import type { Channel } from 'stream-chat'
import { describe, expect, it, vi, beforeEach } from 'vitest'

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

import { ChannelView } from './ChannelView'

let activeChannel: unknown
let activeChannelProps: Record<string, unknown> = {}
let activeComponentOverrides: Record<string, unknown> = {}

vi.mock('stream-chat-react', () => ({
  Channel: (props: {
    channel: unknown
    children: React.ReactNode
    [key: string]: unknown
  }) => {
    activeChannel = props.channel
    activeChannelProps = props
    return <div data-testid="channel">{props.children}</div>
  },
  Window: ({ children }: { children: React.ReactNode }) => (
    <div data-testid="window">{children}</div>
  ),
  MessageList: ({ head }: { head?: React.ReactElement }) => (
    <div data-testid="message-list">{head}</div>
  ),
  WithComponents: ({
    children,
    overrides,
  }: {
    children: React.ReactNode
    overrides?: Record<string, unknown>
  }) => {
    activeComponentOverrides = overrides ?? {}
    return <>{children}</>
  },
  useMessageContext: () => ({ message: { id: 'message-1', text: 'hello' } }),
  useChannelStateContext: () => ({ channel: activeChannel }),
  useChatContext: () => ({
    client: { userID: 'visitor-1', user: { id: 'visitor-1' } },
  }),
  useTypingContext: () => ({ typing: {} }),
  useAIState: () => ({ aiState: 'AI_STATE_IDLE' }),
  AIStates: {
    Error: 'AI_STATE_ERROR',
    ExternalSources: 'AI_STATE_EXTERNAL_SOURCES',
    Generating: 'AI_STATE_GENERATING',
    Idle: 'AI_STATE_IDLE',
    Stop: 'AI_STATE_STOP',
    Thinking: 'AI_STATE_THINKING',
  },
}))

vi.mock('../providers/MessagingProvider', () => ({
  useMessagingContext: () => ({ service: null, debug: false }),
}))

const messageInputProps: Array<Record<string, unknown>> = []
vi.mock('./CustomMessageInput', () => ({
  CustomMessageInput: (props: {
    renderActions?: () => React.ReactNode
    renderHeader?: () => React.ReactNode
    [key: string]: unknown
  }) => {
    messageInputProps.push({ ...props })
    return (
      <div data-testid="message-input">
        {props.renderHeader?.()}
        {props.renderActions?.()}
      </div>
    )
  },
}))

const customMessageMounts = vi.fn()
const customMessageUnmounts = vi.fn()
vi.mock('./CustomMessage', () => ({
  CustomMessage: (props: {
    onOutboundClick?: (outbound: { id: string; name?: string }) => void
  }) => {
    React.useEffect(() => {
      customMessageMounts()
      return customMessageUnmounts
    }, [])

    return (
      <button
        data-testid="custom-message"
        onClick={() =>
          props.onOutboundClick?.({
            id: 'automation-1',
            name: 'Campaign',
          })
        }
      />
    )
  },
}))

vi.mock('./CustomSystemMessage', () => ({
  CustomSystemMessage: () => <div data-testid="custom-system-message" />,
}))

vi.mock('./CustomDateSeparator', () => ({
  CustomDateSeparator: () => <div data-testid="custom-date-separator" />,
}))

const channelActionsMenuProps: Array<Record<string, unknown>> = []
vi.mock('./ChannelActionsMenu', () => ({
  ChannelActionsMenu: (props: Record<string, unknown>) => {
    channelActionsMenuProps.push({ ...props })
    return <div data-testid="channel-actions-menu" />
  },
}))

const avatarRenderCalls: Array<Record<string, unknown>> = []
vi.mock('./Avatar', () => ({
  Avatar: (props: Record<string, unknown>) => {
    avatarRenderCalls.push({ ...props })
    return <div data-testid="avatar" />
  },
}))

let mockIsStarred = false
vi.mock('../hooks/useChannelStar', () => ({
  useChannelStar: () => mockIsStarred,
}))

const createChannel = ({
  currentUserId = 'visitor-1',
  currentUserRole = 'owner',
  channelType,
  messages = [],
  otherUser = {
    id: 'linker-1' as const,
    name: 'Linker' as string | undefined,
    username: undefined as string | undefined,
    is_account: true,
  },
  channelCustomerTag,
  otherMemberIsFollower,
}: {
  currentUserId?: 'visitor-1' | 'linker-1'
  currentUserRole?: 'owner' | 'member'
  channelType?: string
  messages?: Channel['state']['messages']
  channelCustomerTag?: string
  otherMemberIsFollower?: boolean
  otherUser?: {
    id: string
    name?: string
    username?: string
    is_account?: boolean
  }
} = {}) =>
  ({
    id: 'channel-1',
    cid: 'messaging:channel-1',
    type: channelType,
    data: {
      customer_tag: channelCustomerTag,
    },
    _client: { userID: currentUserId },
    state: {
      members: {
        current: {
          user: {
            id: currentUserId,
            name: currentUserId === 'visitor-1' ? 'Visitor' : 'Linker',
            is_account: currentUserId !== 'visitor-1',
          },
          role: currentUserRole,
        },
        other: {
          user: {
            ...otherUser,
            id:
              otherUser.id ??
              (currentUserId === 'visitor-1' ? 'linker-1' : 'visitor-1'),
            is_account: otherUser.is_account ?? currentUserId === 'visitor-1',
          },
          is_follower: otherMemberIsFollower,
          role: currentUserRole === 'owner' ? 'member' : 'owner',
        },
      },
      membership: {},
      messages,
    },
    on: vi.fn(),
    off: vi.fn(),
    sendMessage: vi.fn(),
    hide: vi.fn(),
    pin: vi.fn(),
    unpin: vi.fn(),
  }) as unknown as Channel

describe('ChannelView', () => {
  beforeEach(() => {
    activeChannel = undefined
    activeChannelProps = {}
    activeComponentOverrides = {}
    avatarRenderCalls.length = 0
    channelActionsMenuProps.length = 0
    messageInputProps.length = 0
    customMessageMounts.mockClear()
    customMessageUnmounts.mockClear()
    mockIsStarred = false
  })

  const lastActionsMenuProps = () =>
    channelActionsMenuProps[channelActionsMenuProps.length - 1]
  const lastInputProps = () => messageInputProps[messageInputProps.length - 1]

  it('keeps block and report visible by default in the actions menu', () => {
    renderWithProviders(<ChannelView channel={createChannel()} />)

    expect(lastActionsMenuProps().showBlockParticipant).not.toBe(false)
    expect(lastActionsMenuProps().showReportParticipant).not.toBe(false)
  })

  it('widens the content column in the official (read-only) channel via the width var', () => {
    const { container } = renderWithProviders(
      <ChannelView
        channel={createChannel({ channelType: OFFICIAL_CHANNEL_TYPE })}
      />
    )

    const root = container.querySelector(
      '.messaging-channel-view'
    ) as HTMLElement
    expect(root.style.getPropertyValue('--lt-msg-card-width')).toBe('368px')
  })

  it('leaves the card-width var unset in a regular channel (default widths)', () => {
    const { container } = renderWithProviders(
      <ChannelView channel={createChannel({ channelType: 'messaging' })} />
    )

    const root = container.querySelector(
      '.messaging-channel-view'
    ) as HTMLElement
    expect(root.style.getPropertyValue('--lt-msg-card-width')).toBe('')
  })

  it('hides block and report in the actions menu when restricted', () => {
    renderWithProviders(
      <ChannelView
        channel={createChannel()}
        showBlockParticipant={false}
        showReportParticipant={false}
      />
    )

    expect(lastActionsMenuProps().showBlockParticipant).toBe(false)
    expect(lastActionsMenuProps().showReportParticipant).toBe(false)
  })

  it('renders the redesigned header without a participant click trigger by default', () => {
    renderWithProviders(<ChannelView channel={createChannel()} />)

    expect(
      screen.queryByRole('button', { name: /view details for linker/i })
    ).not.toBeInTheDocument()
    expect(screen.getAllByText('Linker').length).toBeGreaterThanOrEqual(1)

    const avatars = screen.getAllByTestId('avatar')
    expect(avatars.length).toBeGreaterThanOrEqual(1)
    expect(avatars[0].closest('button')).toBeNull()
    expect(screen.getByTestId('channel-actions-menu')).toBeInTheDocument()
  })

  it('renders the redesigned header button when onParticipantNameClick is provided', () => {
    const onParticipantNameClick = vi.fn()
    renderWithProviders(
      <ChannelView
        channel={createChannel()}
        onParticipantNameClick={onParticipantNameClick}
      />
    )

    const button = screen.getByRole('button', {
      name: /view details for linker/i,
    })
    expect(button).toHaveClass(
      'appearance-none',
      'border-0',
      'bg-transparent',
      'flex-col'
    )

    const avatars = screen.getAllByTestId('avatar')
    expect(avatars.length).toBeGreaterThanOrEqual(1)
    expect(avatars[0].closest('button')).toBe(button)
    expect(screen.getByTestId('channel-actions-menu')).toBeInTheDocument()

    button.click()
    expect(onParticipantNameClick).toHaveBeenCalledTimes(1)
  })

  it('renders redesigned header badges for paid and starred conversations', () => {
    mockIsStarred = true

    renderWithProviders(
      <ChannelView
        channel={createChannel({
          channelCustomerTag: 'CUSTOMER_PAID',
          otherMemberIsFollower: true,
        })}
        showStarButton={true}
      />
    )

    expect(screen.getByLabelText('Paid customer')).toBeInTheDocument()
    expect(screen.getByLabelText('Starred conversation')).toBeInTheDocument()
  })

  it('hides the paid customer badge when the participant is not a follower', () => {
    renderWithProviders(
      <ChannelView
        channel={createChannel({
          channelCustomerTag: 'CUSTOMER_PAID',
          otherMemberIsFollower: false,
        })}
        onParticipantNameClick={vi.fn()}
      />
    )

    expect(screen.queryByLabelText('Paid customer')).not.toBeInTheDocument()
  })

  it('renders the redesigned header outside MessageList head when all messages are deleted', () => {
    renderWithProviders(
      <ChannelView
        channel={createChannel({
          messages: [
            {
              id: 'deleted-message-1',
              type: 'deleted',
            } as Channel['state']['messages'][number],
          ],
        })}
      />
    )

    expect(
      screen.getByText('Linker').closest('[data-testid="message-list"]')
    ).toBeNull()
  })

  it('renders the actions menu by default', () => {
    renderWithProviders(<ChannelView channel={createChannel()} />)

    expect(
      screen.queryAllByTestId('channel-actions-menu').length
    ).toBeGreaterThan(0)
  })

  it('suppresses stream unread message UI overrides', () => {
    renderWithProviders(<ChannelView channel={createChannel()} />)

    expect(activeComponentOverrides.UnreadMessagesSeparator).toBeTypeOf(
      'function'
    )
    expect(activeComponentOverrides.UnreadMessagesNotification).toBeTypeOf(
      'function'
    )
  })

  it('keeps message rows mounted while updating the outbound click handler', () => {
    const firstHandler = vi.fn()
    const secondHandler = vi.fn()
    const { rerender: rerenderChannel } = renderWithProviders(
      <ChannelView channel={createChannel()} onOutboundClick={firstHandler} />
    )
    const firstMessageOverride =
      activeComponentOverrides.Message as React.ComponentType
    const MessageRowHarness = ({
      Message,
    }: {
      Message: React.ComponentType
    }) => <Message />
    const { rerender: rerenderMessageRow } = renderWithProviders(
      <MessageRowHarness Message={firstMessageOverride} />
    )

    expect(customMessageMounts).toHaveBeenCalledOnce()

    rerenderChannel(
      <ChannelView channel={createChannel()} onOutboundClick={secondHandler} />
    )
    const secondMessageOverride =
      activeComponentOverrides.Message as React.ComponentType

    expect(secondMessageOverride).toBe(firstMessageOverride)
    rerenderMessageRow(<MessageRowHarness Message={secondMessageOverride} />)
    expect(customMessageMounts).toHaveBeenCalledOnce()
    expect(customMessageUnmounts).not.toHaveBeenCalled()

    screen.getByTestId('custom-message').click()
    expect(firstHandler).not.toHaveBeenCalled()
    expect(secondHandler).toHaveBeenCalledWith({
      id: 'automation-1',
      name: 'Campaign',
    })
  })

  it('hides the actions menu (both desktop and mobile) when showChannelInfo is false', () => {
    renderWithProviders(
      <ChannelView channel={createChannel()} showChannelInfo={false} />
    )

    expect(screen.queryAllByTestId('channel-actions-menu')).toHaveLength(0)
  })

  it('passes composer disabled state and reason to the message input', () => {
    renderWithProviders(
      <ChannelView
        channel={createChannel()}
        composerDisabled
        composerDisabledReason="Message Linktree from your profile to reply"
      />
    )

    expect(lastInputProps().disabled).toBe(true)
    expect(lastInputProps().disabledReason).toBe(
      'Message Linktree from your profile to reply'
    )
  })

  it('does not disable the composer by default', () => {
    renderWithProviders(<ChannelView channel={createChannel()} />)

    expect(lastInputProps().disabled).not.toBe(true)
  })

  it('renders conversation footer between message list and message input', () => {
    const channel = createChannel()
    const renderConversationFooter = vi.fn((currentChannel: Channel) => (
      <div data-testid="conversation-footer">footer-{currentChannel.id}</div>
    ))

    renderWithProviders(
      <ChannelView
        channel={channel}
        renderConversationFooter={renderConversationFooter}
      />
    )

    const messageList = screen.getByTestId('message-list')
    const conversationFooter = screen.getByTestId('conversation-footer')
    const messageInput = screen.getByTestId('message-input')

    expect(renderConversationFooter).toHaveBeenCalledWith(channel)
    expect(conversationFooter).toHaveTextContent('footer-channel-1')
    expect(
      messageList.compareDocumentPosition(conversationFooter) &
        Node.DOCUMENT_POSITION_FOLLOWING
    ).toBeTruthy()
    expect(
      conversationFooter.compareDocumentPosition(messageInput) &
        Node.DOCUMENT_POSITION_FOLLOWING
    ).toBeTruthy()
  })

  it('keeps conversation footer inside the floating composer via renderHeader', () => {
    const channel = createChannel()
    const renderConversationFooter = vi.fn((currentChannel: Channel) => (
      <div data-testid="conversation-footer">footer-{currentChannel.id}</div>
    ))

    renderWithProviders(
      <ChannelView
        channel={channel}
        composerInput={() => <div data-testid="floating-composer" />}
        renderConversationFooter={renderConversationFooter}
      />
    )

    const messageInput = screen.getByTestId('message-input')
    const conversationFooter = screen.getByTestId('conversation-footer')

    expect(renderConversationFooter).toHaveBeenCalledWith(channel)
    expect(lastInputProps().renderHeader).toEqual(expect.any(Function))
    // Floating layout must keep the footer inside the absolute composer chrome,
    // not as a covered sibling underneath it.
    expect(messageInput).toContainElement(conversationFooter)
    expect(screen.getAllByTestId('conversation-footer')).toHaveLength(1)
  })

  it('does not render conversation footer when no renderer is provided', () => {
    renderWithProviders(<ChannelView channel={createChannel()} />)

    expect(screen.queryByTestId('conversation-footer')).not.toBeInTheDocument()
  })

  it('does not show a starred badge when showStarButton is false', () => {
    mockIsStarred = true

    renderWithProviders(
      <ChannelView channel={createChannel()} showStarButton={false} />
    )

    expect(
      screen.queryByLabelText('Starred conversation')
    ).not.toBeInTheDocument()
  })

  it('shows a starred badge when showStarButton is true and channel is starred', () => {
    mockIsStarred = true

    renderWithProviders(
      <ChannelView channel={createChannel()} showStarButton={true} />
    )

    expect(screen.getByLabelText('Starred conversation')).toBeInTheDocument()
  })

  it('does not show a starred badge by default (showStarButton defaults to false)', () => {
    mockIsStarred = true

    renderWithProviders(<ChannelView channel={createChannel()} />)

    expect(
      screen.queryByLabelText('Starred conversation')
    ).not.toBeInTheDocument()
  })

  it('keeps channel banner and input action renderers working', () => {
    const channel = createChannel()
    const renderMessageInputActions = vi.fn((currentChannel: Channel) => (
      <button data-testid="message-input-action">{currentChannel.id}</button>
    ))

    renderWithProviders(
      <ChannelView
        channel={channel}
        renderChannelBanner={() => (
          <div data-testid="channel-banner">channel-banner</div>
        )}
        renderMessageInputActions={renderMessageInputActions}
      />
    )

    expect(screen.getByTestId('channel-banner')).toBeInTheDocument()
    expect(screen.getByTestId('message-input-action')).toHaveTextContent(
      'channel-1'
    )
    expect(renderMessageInputActions).toHaveBeenCalledWith(channel)
  })

  it('passes sendButton to Channel as SendButton when provided', () => {
    const CustomSendButton = () => (
      <button type="button" aria-label="Custom Send" />
    )

    renderWithProviders(
      <ChannelView channel={createChannel()} sendButton={CustomSendButton} />
    )

    expect(activeChannelProps.SendButton).toBe(CustomSendButton)
  })

  it('does not pass SendButton to Channel when sendButton is not provided', () => {
    renderWithProviders(<ChannelView channel={createChannel()} />)

    expect(activeChannelProps.SendButton).toBeUndefined()
  })

  it('renders stable classes for channel header back buttons when enabled', () => {
    renderWithProviders(
      <ChannelView
        channel={createChannel()}
        showBackButton={true}
        onBack={vi.fn()}
      />
    )

    expect(
      document.querySelectorAll('.messaging-channel-view-back-button-mobile')
    ).toHaveLength(1)
    expect(
      document.querySelectorAll('.messaging-channel-view-back-button-desktop')
    ).toHaveLength(1)
  })

  it('does not render channel header back button classes when disabled', () => {
    renderWithProviders(
      <ChannelView
        channel={createChannel()}
        showBackButton={false}
        onBack={vi.fn()}
      />
    )

    expect(
      document.querySelectorAll('.messaging-channel-view-back-button-mobile')
    ).toHaveLength(0)
    expect(
      document.querySelectorAll('.messaging-channel-view-back-button-desktop')
    ).toHaveLength(0)
  })

  it('uses username when participant name is UUID-like', () => {
    renderWithProviders(
      <ChannelView
        channel={createChannel({
          otherUser: {
            id: 'linker-1',
            name: 'a1b2c3d4-e5f6-4789-a012-3456789abcde',
            username: 'linkeruser',
            is_account: true,
          },
        })}
      />
    )

    expect(screen.getAllByText('linkeruser').length).toBeGreaterThan(0)
    expect(avatarRenderCalls.some((call) => call.name === 'linkeruser')).toBe(
      true
    )
  })

  it('shows Unknown member when participant has no usable name or username', () => {
    renderWithProviders(
      <ChannelView
        channel={createChannel({
          otherUser: {
            id: 'opaque-user-id',
            name: 'opaque-user-id',
            is_account: true,
          },
        })}
      />
    )

    expect(screen.getAllByText('Unknown member').length).toBeGreaterThan(0)
  })

  it('fires onMessageLinkClick with the href and undefined message when the link is not inside a message wrapper', () => {
    const handler = vi.fn()
    const { container } = renderWithProviders(
      <ChannelView channel={createChannel()} onMessageLinkClick={handler} />
    )

    const anchor = document.createElement('a')
    anchor.href = 'https://example.com/clicked'
    anchor.addEventListener('click', (e) => e.preventDefault())
    container.querySelector('.messaging-channel-view')?.appendChild(anchor)
    anchor.click()

    expect(handler).toHaveBeenCalledOnce()
    expect(handler).toHaveBeenCalledWith(
      'https://example.com/clicked',
      undefined
    )
  })

  it('resolves the surrounding message from channel state and passes it to onMessageLinkClick', () => {
    const handler = vi.fn()
    const message = {
      id: 'message-42',
      text: 'check this out https://example.com/clicked',
      metadata: { custom_type: 'MESSAGE_CHATBOT' },
    } as unknown as ReturnType<Channel['state']['formatMessage']>
    const channel = createChannel()
    ;(channel.state as unknown as { messages: (typeof message)[] }).messages = [
      message,
    ]

    const { container } = renderWithProviders(
      <ChannelView channel={channel} onMessageLinkClick={handler} />
    )

    const wrapper = document.createElement('div')
    wrapper.setAttribute('data-message-id', 'message-42')
    const anchor = document.createElement('a')
    anchor.href = 'https://example.com/clicked'
    anchor.addEventListener('click', (e) => e.preventDefault())
    wrapper.appendChild(anchor)
    container.querySelector('.messaging-channel-view')?.appendChild(wrapper)
    anchor.click()

    expect(handler).toHaveBeenCalledOnce()
    expect(handler).toHaveBeenCalledWith('https://example.com/clicked', message)
  })

  it('passes undefined when the wrapper data-message-id does not match any message in state', () => {
    const handler = vi.fn()
    const { container } = renderWithProviders(
      <ChannelView channel={createChannel()} onMessageLinkClick={handler} />
    )

    const wrapper = document.createElement('div')
    wrapper.setAttribute('data-message-id', 'unknown-id')
    const anchor = document.createElement('a')
    anchor.href = 'https://example.com/clicked'
    anchor.addEventListener('click', (e) => e.preventDefault())
    wrapper.appendChild(anchor)
    container.querySelector('.messaging-channel-view')?.appendChild(wrapper)
    anchor.click()

    expect(handler).toHaveBeenCalledOnce()
    expect(handler).toHaveBeenCalledWith(
      'https://example.com/clicked',
      undefined
    )
  })

  it('does not throw when a link is clicked without onMessageLinkClick set', () => {
    const { container } = renderWithProviders(
      <ChannelView channel={createChannel()} />
    )

    const anchor = document.createElement('a')
    anchor.href = 'https://example.com'
    anchor.addEventListener('click', (e) => e.preventDefault())
    container.querySelector('.messaging-channel-view')?.appendChild(anchor)
    expect(() => anchor.click()).not.toThrow()
  })
})
