import { render, screen, fireEvent, waitFor } from '@testing-library/react'
import { ChatMessage } from '../chat/ChatMessage'
import { ChatThread } from '../chat/ChatThread'
import { ChatComposer } from '../chat/ChatComposer'
import type { ChatMessageData } from '../chat/types'

beforeAll(() => {
  // jsdom has no layout; stub scrollIntoView used by autoscroll
  Element.prototype.scrollIntoView = jest.fn()
})

describe('ChatMessage', () => {
  it('renders text content and tags the role', () => {
    const { container } = render(
      <ChatMessage message={{ id: '1', role: 'user', content: 'Hello there' }} />
    )
    expect(screen.getByText('Hello there')).toBeInTheDocument()
    expect(container.querySelector('[data-role="user"]')).toBeInTheDocument()
  })

  it('marks a streaming assistant message as busy', () => {
    const { container } = render(
      <ChatMessage message={{ id: '2', role: 'assistant', content: 'thinking', status: 'streaming' }} />
    )
    const el = container.querySelector('[data-status="streaming"]')
    expect(el).toBeInTheDocument()
    expect(el).toHaveAttribute('aria-busy', 'true')
  })

  it('renders a tool/status message with its tool name', () => {
    render(
      <ChatMessage
        message={{ id: '3', role: 'tool', kind: 'tool', toolName: 'generateSlide', content: 'Generating slide 1…' }}
      />
    )
    expect(screen.getByText(/generateSlide/i)).toBeInTheDocument()
    expect(screen.getByText('Generating slide 1…')).toBeInTheDocument()
  })

  it('renders message attachment chips (image thumbnail + file chip)', () => {
    render(
      <ChatMessage
        message={{
          id: '4',
          role: 'user',
          content: 'Match this brand',
          attachments: [
            { id: 'att-1', fileUrl: 'logo.png', filename: 'logo.png', contentType: 'image/png' },
            { id: 'att-2', fileUrl: 'deck.pdf', filename: 'deck.pdf', contentType: 'application/pdf' },
          ],
        }}
      />
    )
    // image attachment renders an <img> thumbnail with its filename as alt
    const img = screen.getByAltText('logo.png') as HTMLImageElement
    expect(img).toBeInTheDocument()
    expect(img.src).toContain('logo.png')
    // non-image attachment renders its filename as text
    expect(screen.getByText('deck.pdf')).toBeInTheDocument()
  })
})

describe('ChatThread', () => {
  const messages: ChatMessageData[] = [
    { id: 'a', role: 'user', content: 'First' },
    { id: 'b', role: 'assistant', content: 'Second' },
    { id: 'c', role: 'assistant', content: 'Third' },
  ]

  it('renders all messages in order', () => {
    render(<ChatThread messages={messages} />)
    expect(screen.getByText('First')).toBeInTheDocument()
    expect(screen.getByText('Second')).toBeInTheDocument()
    expect(screen.getByText('Third')).toBeInTheDocument()
  })

  it('shows an empty state placeholder when there are no messages', () => {
    render(<ChatThread messages={[]} emptyState={<div>Start chatting</div>} />)
    expect(screen.getByText('Start chatting')).toBeInTheDocument()
  })

  it('autoscrolls to the bottom when messages change', () => {
    const { rerender } = render(<ChatThread messages={messages} />)
    rerender(<ChatThread messages={[...messages, { id: 'd', role: 'user', content: 'Fourth' }]} />)
    expect(Element.prototype.scrollIntoView).toHaveBeenCalled()
  })

  it('shows an animated working indicator when busy (startsim-ku8o)', () => {
    render(<ChatThread messages={messages} busy busyLabel="Working on your deck…" />)
    expect(screen.getByTestId('chat-working-indicator')).toBeInTheDocument()
    expect(screen.getByText('Working on your deck…')).toBeInTheDocument()
  })

  it('hides the working indicator when not busy', () => {
    render(<ChatThread messages={messages} />)
    expect(screen.queryByTestId('chat-working-indicator')).not.toBeInTheDocument()
  })

  it('shows the working indicator even when the thread is empty (busy overrides empty state)', () => {
    render(<ChatThread messages={[]} busy emptyState={<div>Start chatting</div>} />)
    expect(screen.getByTestId('chat-working-indicator')).toBeInTheDocument()
    expect(screen.queryByText('Start chatting')).not.toBeInTheDocument()
  })
})

describe('ChatComposer', () => {
  it('submits trimmed text on Enter and clears (uncontrolled)', () => {
    const onSubmit = jest.fn()
    render(<ChatComposer onSubmit={onSubmit} />)
    const textarea = screen.getByRole('textbox')
    fireEvent.change(textarea, { target: { value: '  paste my outline  ' } })
    fireEvent.keyDown(textarea, { key: 'Enter' })
    expect(onSubmit).toHaveBeenCalledWith('paste my outline')
  })

  it('does NOT submit on Shift+Enter (newline)', () => {
    const onSubmit = jest.fn()
    render(<ChatComposer onSubmit={onSubmit} />)
    const textarea = screen.getByRole('textbox')
    fireEvent.change(textarea, { target: { value: 'line one' } })
    fireEvent.keyDown(textarea, { key: 'Enter', shiftKey: true })
    expect(onSubmit).not.toHaveBeenCalled()
  })

  it('does NOT submit on bare Enter when the content is multi-line (pasted outline stays intact)', () => {
    const onSubmit = jest.fn()
    render(<ChatComposer onSubmit={onSubmit} />)
    const textarea = screen.getByRole('textbox')
    fireEvent.change(textarea, { target: { value: 'My Deck\nSlide 1: Problem\nSlide 2: Solution' } })
    fireEvent.keyDown(textarea, { key: 'Enter' })
    expect(onSubmit).not.toHaveBeenCalled()
  })

  it('submits multi-line content on Cmd/Ctrl+Enter', () => {
    const onSubmit = jest.fn()
    render(<ChatComposer onSubmit={onSubmit} />)
    const textarea = screen.getByRole('textbox')
    fireEvent.change(textarea, { target: { value: 'My Deck\nSlide 1: Problem' } })
    fireEvent.keyDown(textarea, { key: 'Enter', metaKey: true })
    expect(onSubmit).toHaveBeenCalledWith('My Deck\nSlide 1: Problem')
  })

  it('does not submit empty/whitespace', () => {
    const onSubmit = jest.fn()
    render(<ChatComposer onSubmit={onSubmit} />)
    const textarea = screen.getByRole('textbox')
    fireEvent.change(textarea, { target: { value: '   ' } })
    fireEvent.keyDown(textarea, { key: 'Enter' })
    expect(onSubmit).not.toHaveBeenCalled()
  })

  it('submits via the send button', () => {
    const onSubmit = jest.fn()
    render(<ChatComposer onSubmit={onSubmit} />)
    fireEvent.change(screen.getByRole('textbox'), { target: { value: 'go' } })
    fireEvent.click(screen.getByRole('button', { name: /send/i }))
    expect(onSubmit).toHaveBeenCalledWith('go')
  })

  it('disables submission while busy', () => {
    const onSubmit = jest.fn()
    render(<ChatComposer onSubmit={onSubmit} busy />)
    const textarea = screen.getByRole('textbox')
    fireEvent.change(textarea, { target: { value: 'hi' } })
    fireEvent.keyDown(textarea, { key: 'Enter' })
    expect(onSubmit).not.toHaveBeenCalled()
  })

  describe('attachments', () => {
    function makeFile(name: string, type = 'image/png') {
      return new File(['data'], name, { type })
    }

    it('fires onAttachFiles when files are picked via the attach button input', () => {
      const onAttachFiles = jest.fn()
      render(<ChatComposer onSubmit={jest.fn()} onAttachFiles={onAttachFiles} />)
      // The attach button is present and labelled
      expect(screen.getByRole('button', { name: /attach/i })).toBeInTheDocument()
      const input = screen.getByTestId('chat-attach-input') as HTMLInputElement
      fireEvent.change(input, { target: { files: [makeFile('a.png'), makeFile('b.pdf', 'application/pdf')] } })
      expect(onAttachFiles).toHaveBeenCalledTimes(1)
      expect(onAttachFiles.mock.calls[0][0]).toHaveLength(2)
      expect(onAttachFiles.mock.calls[0][0][0].name).toBe('a.png')
    })

    it('does NOT render an attach button when onAttachFiles is omitted', () => {
      render(<ChatComposer onSubmit={jest.fn()} />)
      expect(screen.queryByRole('button', { name: /attach/i })).not.toBeInTheDocument()
    })

    it('renders chips for controlled attachments with a remove control', () => {
      const onRemoveAttachment = jest.fn()
      render(
        <ChatComposer
          onSubmit={jest.fn()}
          onAttachFiles={jest.fn()}
          onRemoveAttachment={onRemoveAttachment}
          attachments={[
            { id: 'p1', name: 'brand.png', status: 'ready', thumbnailUrl: 'blob:brand', sizeBytes: 2048 },
            { id: 'p2', name: 'notes.pdf', status: 'ready', sizeBytes: 1024 },
          ]}
        />
      )
      expect(screen.getByText('notes.pdf')).toBeInTheDocument()
      // image chip uses thumbnail with the name as alt
      expect(screen.getByAltText('brand.png')).toBeInTheDocument()
      fireEvent.click(screen.getByRole('button', { name: /remove brand\.png/i }))
      expect(onRemoveAttachment).toHaveBeenCalledWith('p1')
    })

    it('shows upload progress while a chip is uploading and an error state on failure', () => {
      render(
        <ChatComposer
          onSubmit={jest.fn()}
          onAttachFiles={jest.fn()}
          attachments={[
            { id: 'u1', name: 'big.pptx', status: 'uploading', progress: 0.42 },
            { id: 'e1', name: 'bad.png', status: 'error' },
          ]}
        />
      )
      const uploading = screen.getByTestId('chat-attachment-u1')
      expect(uploading).toHaveAttribute('data-status', 'uploading')
      expect(uploading.querySelector('[role="progressbar"]')).toBeInTheDocument()
      const errored = screen.getByTestId('chat-attachment-e1')
      expect(errored).toHaveAttribute('data-status', 'error')
    })

    it('still submits text with attachments present (attachments are presentation-only)', () => {
      const onSubmit = jest.fn()
      render(
        <ChatComposer
          onSubmit={onSubmit}
          onAttachFiles={jest.fn()}
          attachments={[{ id: 'p1', name: 'a.png', status: 'ready' }]}
        />
      )
      fireEvent.change(screen.getByRole('textbox'), { target: { value: 'use this' } })
      fireEvent.keyDown(screen.getByRole('textbox'), { key: 'Enter' })
      expect(onSubmit).toHaveBeenCalledWith('use this')
    })

    it('routes dropped files to onAttachFiles', () => {
      const onAttachFiles = jest.fn()
      const { container } = render(
        <ChatComposer onSubmit={jest.fn()} onAttachFiles={onAttachFiles} />
      )
      const root = container.firstChild as HTMLElement
      fireEvent.drop(root, { dataTransfer: { files: [makeFile('dropped.png')] } })
      expect(onAttachFiles).toHaveBeenCalledTimes(1)
      expect(onAttachFiles.mock.calls[0][0][0].name).toBe('dropped.png')
    })

    it('routes pasted image/file data to onAttachFiles', () => {
      const onAttachFiles = jest.fn()
      render(<ChatComposer onSubmit={jest.fn()} onAttachFiles={onAttachFiles} />)
      const textarea = screen.getByRole('textbox')
      const file = makeFile('pasted.png')
      fireEvent.paste(textarea, {
        clipboardData: { files: [file], items: [{ kind: 'file', type: 'image/png' }] },
      })
      expect(onAttachFiles).toHaveBeenCalledTimes(1)
      expect(onAttachFiles.mock.calls[0][0][0].name).toBe('pasted.png')
    })

    describe('self-managed uploader mode (startsim-dd9o)', () => {
      beforeAll(() => {
        if (!('createObjectURL' in URL)) {
          // @ts-expect-error test shim
          URL.createObjectURL = jest.fn(() => 'blob:preview')
        }
        if (!('revokeObjectURL' in URL)) {
          // @ts-expect-error test shim
          URL.revokeObjectURL = jest.fn()
        }
      })

      function fakeUploader() {
        return {
          uploadAttachment: jest.fn(async (_id: string, file: File) => ({
            id: `srv-${file.name}`,
            fileUrl: `https://cdn/${file.name}`,
          })),
          deleteAttachment: jest.fn(async () => undefined),
        }
      }

      it('uploads picked files through the uploader and shows a ready chip — no controlled props needed', async () => {
        const uploader = fakeUploader()
        render(<ChatComposer onSubmit={jest.fn()} uploader={uploader} documentId="doc-1" />)
        const input = screen.getByTestId('chat-attach-input') as HTMLInputElement
        fireEvent.change(input, { target: { files: [makeFile('hero.png')] } })
        await waitFor(() =>
          expect(uploader.uploadAttachment).toHaveBeenCalledWith('doc-1', expect.any(File), expect.any(Object))
        )
        await waitFor(() => {
          const chips = screen.getByTestId('chat-attachment-chips')
          expect(chips.querySelector('[data-status="ready"]')).toBeInTheDocument()
        })
      })

      it('shows the attach affordance even without onAttachFiles when an uploader is present', () => {
        render(<ChatComposer onSubmit={jest.fn()} uploader={fakeUploader()} documentId="doc-1" />)
        expect(screen.getByRole('button', { name: /attach/i })).toBeInTheDocument()
      })

      it('still submits text in self-managed mode', () => {
        const onSubmit = jest.fn()
        render(<ChatComposer onSubmit={onSubmit} uploader={fakeUploader()} documentId="doc-1" />)
        fireEvent.change(screen.getByRole('textbox'), { target: { value: 'use these' } })
        fireEvent.keyDown(screen.getByRole('textbox'), { key: 'Enter' })
        expect(onSubmit).toHaveBeenCalledWith('use these')
      })
    })
  })
})
