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

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

import { SentMessageDeliveryStatus } from './SentMessageDeliveryStatus'

// stream-chat-react's `MessageStatus` owns own-message / not-error gating and
// only exposes a terminal state once a message is stored — Stream's tested
// code. Its threadList guard skips `sending`, so this component owns
// thread-list suppression plus the "Delivered"/"Sending" labels and the
// "latest message in the thread" gate from live channel state. Mock
// `MessageStatus` to render the slots it was handed, and the context hooks
// the label reads.
vi.mock('stream-chat-react', () => ({
  useMessageContext: vi.fn(),
  useChannelStateContext: vi.fn(),
  MessageStatus: ({
    MessageSentStatus,
    MessageDeliveredStatus,
    MessageReadStatus,
    MessageSendingStatus,
  }: Record<string, React.ComponentType>) => (
    <div>
      <div data-testid="sent-slot">
        <MessageSentStatus />
      </div>
      <div data-testid="delivered-slot">
        <MessageDeliveredStatus />
      </div>
      <div data-testid="read-slot">
        <MessageReadStatus />
      </div>
      <div data-testid="sending-slot">
        <MessageSendingStatus />
      </div>
    </div>
  ),
}))

const mockedUseMessageContext = vi.mocked(useMessageContext)
const mockedUseChannelStateContext = vi.mocked(useChannelStateContext)

// A run where the viewer ('me') sent the first and last messages, with an
// incoming message in between — the label must sit on the last message ('own-2')
// and clear from the earlier one.
const CHANNEL_MESSAGES = [
  { id: 'own-1', user: { id: 'me' } },
  { id: 'their-1', user: { id: 'them' } },
  { id: 'own-2', user: { id: 'me' } },
]

const TERMINAL_SLOTS = ['sent-slot', 'delivered-slot', 'read-slot'] as const

describe('SentMessageDeliveryStatus', () => {
  beforeEach(() => {
    mockedUseChannelStateContext.mockReturnValue({
      messages: CHANNEL_MESSAGES,
    } as never)
  })

  it('renders "Delivered" in every terminal slot for the last message in the thread', () => {
    // delivered/read fire ahead of sent in receipt channels, so all three
    // must show the label — never Stream's default delivered icon / read
    // avatars.
    mockedUseMessageContext.mockReturnValue({
      message: { id: 'own-2' },
      isMyMessage: () => true,
    } as never)

    renderWithProviders(<SentMessageDeliveryStatus />)

    for (const slot of TERMINAL_SLOTS) {
      expect(
        within(screen.getByTestId(slot)).getByText('Delivered')
      ).toBeInTheDocument()
    }
  })

  it('renders nothing for an earlier own message once a later message exists', () => {
    mockedUseMessageContext.mockReturnValue({
      message: { id: 'own-1' },
      isMyMessage: () => true,
    } as never)

    renderWithProviders(<SentMessageDeliveryStatus />)

    for (const slot of TERMINAL_SLOTS) {
      expect(screen.getByTestId(slot)).toBeEmptyDOMElement()
    }
  })

  it('renders nothing for an own message once a reply arrives after it', () => {
    // 'own-2' was the viewer's last sent message, but 'their-1' is now the
    // latest message in the thread — the label must clear from the bubble.
    mockedUseChannelStateContext.mockReturnValue({
      messages: [
        { id: 'own-1', user: { id: 'me' } },
        { id: 'own-2', user: { id: 'me' } },
        { id: 'their-1', user: { id: 'them' } },
      ],
    } as never)
    mockedUseMessageContext.mockReturnValue({
      message: { id: 'own-2' },
      isMyMessage: () => true,
    } as never)

    renderWithProviders(<SentMessageDeliveryStatus />)

    for (const slot of TERMINAL_SLOTS) {
      expect(screen.getByTestId(slot)).toBeEmptyDOMElement()
    }
  })

  it('ignores a trailing system row when the last real message is still the viewer’s own', () => {
    mockedUseChannelStateContext.mockReturnValue({
      messages: [
        { id: 'own-1', user: { id: 'me' } },
        { id: 'their-1', user: { id: 'them' } },
        { id: 'own-2', user: { id: 'me' } },
        { id: 'system-1', type: 'system' },
      ],
    } as never)
    mockedUseMessageContext.mockReturnValue({
      message: { id: 'own-2' },
      isMyMessage: () => true,
    } as never)

    renderWithProviders(<SentMessageDeliveryStatus />)

    for (const slot of TERMINAL_SLOTS) {
      expect(
        within(screen.getByTestId(slot)).getByText('Delivered')
      ).toBeInTheDocument()
    }
  })

  it('renders "Sending" in the sending slot for the latest message', () => {
    mockedUseMessageContext.mockReturnValue({
      message: { id: 'own-2', status: 'sending' },
      isMyMessage: () => true,
      threadList: false,
    } as never)

    renderWithProviders(<SentMessageDeliveryStatus />)

    expect(screen.getByTestId('sending-slot')).toHaveTextContent(/sending/i)
  })

  it('does not render "Sending" for an earlier message', () => {
    mockedUseMessageContext.mockReturnValue({
      message: { id: 'own-1', status: 'sending' },
      isMyMessage: () => true,
      threadList: false,
    } as never)

    renderWithProviders(<SentMessageDeliveryStatus />)

    expect(screen.getByTestId('sending-slot')).toBeEmptyDOMElement()
  })

  it('does not render "Sending" inside a thread list', () => {
    // Stream's MessageStatus still invokes MessageSendingStatus when
    // threadList is true; we suppress the stamp ourselves.
    mockedUseMessageContext.mockReturnValue({
      message: { id: 'own-2', status: 'sending' },
      isMyMessage: () => true,
      threadList: true,
    } as never)

    renderWithProviders(<SentMessageDeliveryStatus />)

    expect(screen.queryByTestId('sending-slot')).not.toBeInTheDocument()
    expect(
      screen.queryByTestId('sent-message-sending-status')
    ).not.toBeInTheDocument()
  })

  it('renders an outbound label for every own outbound message, including non-latest messages', () => {
    mockedUseMessageContext.mockReturnValue({
      message: {
        id: 'own-1',
        metadata: {
          outbound_id: 'outbound-1',
        },
      },
      isMyMessage: () => true,
      threadList: false,
    } as never)

    renderWithProviders(
      <SentMessageDeliveryStatus
        resolvedOutbound={{
          id: 'automation-1',
          name: 'July 9th Class attendees',
        }}
      />
    )

    expect(
      screen.getByTestId('sent-message-outbound-status')
    ).toHaveTextContent('July 9th Class attendees')
    expect(
      screen.queryByTestId('sent-message-delivery-status')
    ).not.toBeInTheDocument()
    expect(screen.queryByText('•')).not.toBeInTheDocument()
  })

  it('uses a generic label and invokes the outbound callback when the name is missing', () => {
    const onOutboundClick = vi.fn()
    mockedUseMessageContext.mockReturnValue({
      message: { id: 'own-2', metadata: { outbound_id: 'outbound-1' } },
      isMyMessage: () => true,
      threadList: false,
    } as never)

    renderWithProviders(
      <SentMessageDeliveryStatus
        onOutboundClick={onOutboundClick}
        resolvedOutbound={{ id: 'automation-1' }}
      />
    )

    expect(
      screen.getByTestId('sent-message-outbound-status')
    ).toHaveTextContent('Broadcast')
    expect(
      screen.getByRole('button', { name: 'View broadcast Broadcast' })
    ).toBeInTheDocument()
    const button = screen.getByRole('button', {
      name: 'View broadcast Broadcast',
    })
    expect(
      button.querySelector('.sent-message-outbound-badge')
    ).toBeInTheDocument()
    expect(
      button.querySelector('.sent-message-outbound-name')
    ).toHaveTextContent('Broadcast')
    expect(button).not.toHaveTextContent('•')
    expect(screen.getAllByText('•')).toHaveLength(1)
    button.click()
    expect(onOutboundClick).toHaveBeenCalledWith({ id: 'automation-1' })
  })

  it('keeps the latest separator outside the button and underlines only the name', () => {
    const onOutboundClick = vi.fn()
    mockedUseMessageContext.mockReturnValue({
      message: {
        id: 'own-2',
        metadata: {
          outbound_id: 'outbound-1',
        },
      },
      isMyMessage: () => true,
      threadList: false,
    } as never)

    renderWithProviders(
      <SentMessageDeliveryStatus
        onOutboundClick={onOutboundClick}
        resolvedOutbound={{
          id: 'automation-1',
          name: 'July 9th Class attendees',
        }}
      />
    )

    const button = screen.getByRole('button', {
      name: 'View broadcast July 9th Class attendees',
    })
    const name = within(button).getByTestId('sent-message-outbound-status')

    expect(screen.getAllByText('•')).toHaveLength(1)
    expect(button).not.toHaveTextContent('•')
    expect(name).toHaveClass('sent-message-outbound-name')
    expect(
      button.querySelector('.sent-message-outbound-badge')
    ).toHaveAttribute('aria-hidden', 'true')
    expect(screen.getAllByTestId('sent-message-delivery-status')).toHaveLength(
      3
    )
  })

  it('passes the resolved outbound name to the click callback', () => {
    const onOutboundClick = vi.fn()
    mockedUseMessageContext.mockReturnValue({
      message: {
        id: 'own-2',
        metadata: {
          outbound_id: 'outbound-1',
        },
      },
      isMyMessage: () => true,
      threadList: false,
    } as never)

    renderWithProviders(
      <SentMessageDeliveryStatus
        onOutboundClick={onOutboundClick}
        resolvedOutbound={{
          id: 'automation-1',
          name: 'July 9th Class absentee',
        }}
      />
    )

    screen
      .getByRole('button', {
        name: 'View broadcast July 9th Class absentee',
      })
      .click()

    expect(onOutboundClick).toHaveBeenCalledWith({
      id: 'automation-1',
      name: 'July 9th Class absentee',
    })
  })

  it('reports a campaign named Broadcast on click', () => {
    const onOutboundClick = vi.fn()
    mockedUseMessageContext.mockReturnValue({
      message: {
        id: 'own-3',
        metadata: {
          outbound_id: 'outbound-1',
        },
      },
      isMyMessage: () => true,
      threadList: false,
    } as never)

    renderWithProviders(
      <SentMessageDeliveryStatus
        onOutboundClick={onOutboundClick}
        resolvedOutbound={{ id: 'automation-1', name: 'Broadcast' }}
      />
    )

    screen
      .getByRole('button', {
        name: 'View broadcast Broadcast',
      })
      .click()

    expect(onOutboundClick).toHaveBeenCalledWith({
      id: 'automation-1',
      name: 'Broadcast',
    })
  })

  it('does not render an outbound label for a visitor viewing an outbound message', () => {
    mockedUseMessageContext.mockReturnValue({
      message: {
        id: 'automation-1',
        metadata: {
          outbound_id: 'outbound-1',
        },
      },
      isMyMessage: () => false,
      threadList: false,
    } as never)

    renderWithProviders(
      <SentMessageDeliveryStatus
        onOutboundClick={vi.fn()}
        resolvedOutbound={{
          id: 'automation-1',
          name: 'July 9th Class attendees',
        }}
      />
    )

    expect(
      screen.queryByTestId('sent-message-outbound-status')
    ).not.toBeInTheDocument()
  })

  it('renders "Failed to send" for every own failed message', () => {
    mockedUseMessageContext.mockReturnValue({
      message: { id: 'own-1', status: 'failed', type: 'regular' },
      isMyMessage: () => true,
      threadList: false,
    } as never)

    renderWithProviders(<SentMessageDeliveryStatus />)

    expect(screen.getByTestId('sent-message-failed-status')).toHaveTextContent(
      /failed to send/i
    )
    expect(screen.queryByTestId('sending-slot')).not.toBeInTheDocument()
  })

  it.each([
    ['a paid message with text', 'thanks!'],
    ['a textless paid message', undefined],
  ])('does not render the delivery stamp on %s', (_label, text) => {
    mockedUseMessageContext.mockReturnValue({
      message: {
        id: 'own-2',
        text,
        metadata: { custom_type: 'MESSAGE_PAID', amount_text: '$10.00' },
      },
      isMyMessage: () => true,
      threadList: false,
    } as never)

    renderWithProviders(<SentMessageDeliveryStatus />)

    for (const slot of TERMINAL_SLOTS) {
      expect(screen.queryByTestId(slot)).not.toBeInTheDocument()
    }
    expect(screen.queryByTestId('sending-slot')).not.toBeInTheDocument()
  })

  it('keeps the delivery stamp on a tip, which renders a pill instead of a footer tag', () => {
    mockedUseMessageContext.mockReturnValue({
      message: {
        id: 'own-2',
        text: 'thanks!',
        metadata: { custom_type: 'MESSAGE_TIP', amount_text: '$5.00' },
      },
      isMyMessage: () => true,
      threadList: false,
    } as never)

    renderWithProviders(<SentMessageDeliveryStatus />)

    for (const slot of TERMINAL_SLOTS) {
      expect(screen.getByTestId(slot)).toHaveTextContent(/delivered/i)
    }
  })

  it('does not render the sending stamp on a paid message', () => {
    mockedUseMessageContext.mockReturnValue({
      message: {
        id: 'own-2',
        status: 'sending',
        text: 'thanks!',
        metadata: { custom_type: 'MESSAGE_PAID', amount_text: '$10.00' },
      },
      isMyMessage: () => true,
      threadList: false,
    } as never)

    renderWithProviders(<SentMessageDeliveryStatus />)

    expect(screen.queryByTestId('sending-slot')).not.toBeInTheDocument()
    expect(
      screen.queryByTestId('sent-message-sending-status')
    ).not.toBeInTheDocument()
  })

  it('renders "Delivered" instead of "Sending" on a pending textless tip', () => {
    mockedUseMessageContext.mockReturnValue({
      message: {
        id: 'own-2',
        status: 'sending',
        metadata: { custom_type: 'MESSAGE_TIP', amount_text: '$5.00' },
      },
      isMyMessage: () => true,
      threadList: false,
    } as never)

    renderWithProviders(<SentMessageDeliveryStatus />)

    expect(screen.getByTestId('sending-slot')).toHaveTextContent(/delivered/i)
    expect(
      screen.queryByTestId('sent-message-sending-status')
    ).not.toBeInTheDocument()
  })

  it('keeps "Failed to send" on a tip that fails', () => {
    mockedUseMessageContext.mockReturnValue({
      message: {
        id: 'own-2',
        status: 'failed',
        type: 'regular',
        metadata: { custom_type: 'MESSAGE_TIP', amount_text: '$5.00' },
      },
      isMyMessage: () => true,
      threadList: false,
    } as never)

    renderWithProviders(<SentMessageDeliveryStatus />)

    expect(screen.getByTestId('sent-message-failed-status')).toHaveTextContent(
      /failed to send/i
    )
  })

  it('keeps the failed and outbound stamps on a paid message', () => {
    mockedUseMessageContext.mockReturnValue({
      message: {
        id: 'own-2',
        status: 'failed',
        type: 'regular',
        text: 'thanks!',
        metadata: { custom_type: 'MESSAGE_PAID', amount_text: '$10.00' },
      },
      isMyMessage: () => true,
      threadList: false,
    } as never)

    const { rerender } = renderWithProviders(<SentMessageDeliveryStatus />)

    expect(screen.getByTestId('sent-message-failed-status')).toHaveTextContent(
      /failed to send/i
    )

    mockedUseMessageContext.mockReturnValue({
      message: {
        id: 'own-2',
        text: 'thanks!',
        metadata: {
          custom_type: 'MESSAGE_PAID',
          amount_text: '$10.00',
          outbound_id: 'outbound-1',
        },
      },
      isMyMessage: () => true,
      threadList: false,
    } as never)
    rerender(
      <SentMessageDeliveryStatus
        resolvedOutbound={{ id: 'automation-1', name: 'July 9th Class' }}
      />
    )

    expect(
      screen.getByTestId('sent-message-outbound-status')
    ).toHaveTextContent('July 9th Class')
    expect(screen.queryByText('•')).not.toBeInTheDocument()
  })

  it('does not render failure status for a received message or thread message', () => {
    mockedUseMessageContext.mockReturnValue({
      message: { id: 'own-1', status: 'failed', type: 'regular' },
      isMyMessage: () => false,
      threadList: false,
    } as never)

    const { rerender } = renderWithProviders(<SentMessageDeliveryStatus />)

    expect(
      screen.queryByTestId('sent-message-failed-status')
    ).not.toBeInTheDocument()

    mockedUseMessageContext.mockReturnValue({
      message: { id: 'own-1', status: 'failed', type: 'regular' },
      isMyMessage: () => true,
      threadList: true,
    } as never)
    rerender(<SentMessageDeliveryStatus />)

    expect(
      screen.queryByTestId('sent-message-failed-status')
    ).not.toBeInTheDocument()
  })
})
