import React from 'react'
import { beforeEach, describe, expect, it, vi } from 'vitest'

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

import { triggerDownload } from './_shared/triggerDownload'

import MessageAttachment, { bubbleGroupPositionFromStream } from '.'

vi.mock('./_shared/triggerDownload', () => ({
  triggerDownload: vi.fn(() => Promise.resolve()),
}))

const mockTriggerDownload = vi.mocked(triggerDownload)

beforeEach(() => {
  mockTriggerDownload.mockClear()
})

// The viewer's `<dialog>` stays mounted across open/close so the
// platform open / close transitions (via `@starting-style` +
// `allow-discrete`) can play. That means we can't gate openness on
// "is the dialog in the DOM" — we have to look at the `[open]`
// attribute the platform sets on a `<dialog>` while it's visible.
const expectViewerClosed = (testId: string) => {
  const dialog = screen.queryByTestId(testId)
  if (dialog) expect(dialog).not.toHaveAttribute('open')
}
const expectViewerOpen = (testId: string) => {
  const dialog = screen.getByTestId(testId)
  expect(dialog).toHaveAttribute('open')
  return dialog
}

describe('MessageAttachment.Image', () => {
  it('optimizes Linktree-hosted composer previews without changing the viewer source', () => {
    renderWithProviders(
      <MessageAttachment.Image.Composer
        src="https://assets.linktr.ee/photo.jpg"
        alt="Photo"
      />
    )

    expect(screen.getByRole('img', { name: 'Photo' })).toHaveAttribute(
      'src',
      'https://assets.linktr.ee/photo.jpg?io=true&size=messaging-attachment-v1_0'
    )

    fireEvent.click(screen.getByLabelText('Open image'))
    const viewer = expectViewerOpen('image-viewer')
    expect(viewer.querySelector('img')?.getAttribute('src')).toBe(
      'https://assets.linktr.ee/photo.jpg'
    )
  })

  it('renders an image bubble with caption', () => {
    renderWithProviders(
      <MessageAttachment.Image.Sent
        src="https://cdn.example.com/photo.jpg"
        alt="Photo"
        text="Here is the image"
      />
    )
    const bubble = screen.getByTestId('image-attachment')
    expect(bubble).toBeInTheDocument()
    expect(screen.getByText('Here is the image')).toBeInTheDocument()
    expect(bubble.querySelector('img')?.getAttribute('src')).toBe(
      'https://cdn.example.com/photo.jpg'
    )
  })

  it('opens the ImageViewer lightbox when clicked', () => {
    renderWithProviders(
      <MessageAttachment.Image.Received
        src="https://cdn.example.com/photo.jpg"
        alt="Photo"
        filename="photo.jpg"
      />
    )

    expectViewerClosed('image-viewer')

    fireEvent.click(screen.getByLabelText('Open image 1 of 1'))

    expectViewerOpen('image-viewer')
  })

  it('renders the dismiss button only on Composer state', () => {
    const onDismiss = vi.fn()
    const { rerender } = renderWithProviders(
      <MessageAttachment.Image.Composer
        src="https://cdn.example.com/photo.jpg"
        onDismiss={onDismiss}
      />
    )
    expect(screen.getByLabelText('Dismiss attachment')).toBeInTheDocument()

    rerender(
      <MessageAttachment.Image.Sent src="https://cdn.example.com/photo.jpg" />
    )
    expect(screen.queryByLabelText('Dismiss attachment')).toBeNull()
  })

  it('renders a stacked grid for multiple items', () => {
    renderWithProviders(
      <MessageAttachment.Image.Sent
        items={[
          { src: 'https://cdn.example.com/a.jpg', alt: 'A' },
          { src: 'https://cdn.example.com/b.jpg', alt: 'B' },
          { src: 'https://cdn.example.com/c.jpg', alt: 'C' },
        ]}
      />
    )
    expect(screen.getByLabelText('Open image 1 of 3')).toBeInTheDocument()
    expect(screen.getByLabelText('Open image 2 of 3')).toBeInTheDocument()
    expect(screen.getByLabelText('Open image 3 of 3')).toBeInTheDocument()
  })

  it('renders a download button inside the image viewer', () => {
    renderWithProviders(
      <MessageAttachment.Image.Received
        src="https://cdn.example.com/photo.jpg"
        alt="Photo"
        filename="photo.jpg"
      />
    )
    fireEvent.click(screen.getByLabelText('Open image 1 of 1'))
    const viewer = expectViewerOpen('image-viewer')
    // The viewer chrome carries its own download action — separate
    // from any sibling download button on the bubble surface — so the
    // user can grab the image without leaving the lightbox.
    expect(
      viewer.querySelector('button[aria-label="Download photo.jpg"]')
    ).not.toBeNull()
  })

  it('does not render carousel nav controls for a single image', () => {
    renderWithProviders(
      <MessageAttachment.Image.Received
        src="https://cdn.example.com/photo.jpg"
        filename="photo.jpg"
      />
    )
    fireEvent.click(screen.getByLabelText('Open image 1 of 1'))
    expectViewerOpen('image-viewer')
    expect(screen.queryByLabelText('Next image')).toBeNull()
    expect(screen.queryByLabelText('Previous image')).toBeNull()
  })

  it('renders the stacked images as a carousel inside the viewer', () => {
    renderWithProviders(
      <MessageAttachment.Image.Sent
        filename="album"
        items={[
          { src: 'https://cdn.example.com/a.jpg', alt: 'A' },
          { src: 'https://cdn.example.com/b.jpg', alt: 'B' },
          { src: 'https://cdn.example.com/c.jpg', alt: 'C' },
        ]}
      />
    )

    // Open the second tile — the viewer should land on image 2.
    fireEvent.click(screen.getByLabelText('Open image 2 of 3'))
    const viewer = expectViewerOpen('image-viewer')
    expect(viewer.querySelector('img')?.getAttribute('src')).toBe(
      'https://cdn.example.com/b.jpg'
    )
    // Counter + nav chrome only appear on multi-item viewers.
    expect(viewer.textContent).toContain('2 / 3')

    // Next button advances by one.
    fireEvent.click(screen.getByLabelText('Next image'))
    expect(viewer.querySelector('img')?.getAttribute('src')).toBe(
      'https://cdn.example.com/c.jpg'
    )
    expect(viewer.textContent).toContain('3 / 3')

    // Next wraps from the last item back to the first.
    fireEvent.click(screen.getByLabelText('Next image'))
    expect(viewer.querySelector('img')?.getAttribute('src')).toBe(
      'https://cdn.example.com/a.jpg'
    )

    // Previous wraps from the first item to the last.
    fireEvent.click(screen.getByLabelText('Previous image'))
    expect(viewer.querySelector('img')?.getAttribute('src')).toBe(
      'https://cdn.example.com/c.jpg'
    )
  })

  it('uses ArrowRight / ArrowLeft to navigate stacked images', () => {
    renderWithProviders(
      <MessageAttachment.Image.Sent
        items={[
          { src: 'https://cdn.example.com/a.jpg', alt: 'A' },
          { src: 'https://cdn.example.com/b.jpg', alt: 'B' },
        ]}
      />
    )
    fireEvent.click(screen.getByLabelText('Open image 1 of 2'))
    const viewer = expectViewerOpen('image-viewer')
    expect(viewer.querySelector('img')?.getAttribute('src')).toBe(
      'https://cdn.example.com/a.jpg'
    )

    fireEvent.keyDown(window, { key: 'ArrowRight' })
    expect(viewer.querySelector('img')?.getAttribute('src')).toBe(
      'https://cdn.example.com/b.jpg'
    )

    fireEvent.keyDown(window, { key: 'ArrowLeft' })
    expect(viewer.querySelector('img')?.getAttribute('src')).toBe(
      'https://cdn.example.com/a.jpg'
    )
  })

  it('shows a +N overflow tile for 5+ items', () => {
    renderWithProviders(
      <MessageAttachment.Image.Sent
        items={Array.from({ length: 6 }, (_, i) => ({
          src: `https://cdn.example.com/${i}.jpg`,
        }))}
      />
    )
    expect(screen.getByText('+2')).toBeInTheDocument()
  })
})

describe('MessageAttachment.Image single-image aspect ratio', () => {
  // A single image used to be force-cropped to 1:1. It now fits inside
  // a max 326x436 content box on web and 309x436 on mobile, preserving
  // its own ratio, and the card adopts the fitted box so `object-cover`
  // performs no crop. The width caps differ per breakpoint; the 436px
  // height cap deliberately does not, which is why Figma's 9:16
  // portrait renders 246x436 on web *and* mobile.
  const WIDTH_CAP_MOBILE_PX = 309
  const WIDTH_CAP_WEB_PX = 326

  const mediaBox = (label: string) =>
    screen.getByLabelText(label).parentElement as HTMLElement

  /** The `min(var(--…), Npx)` term the height cap contributes. */
  const heightBoundWidthPx = (box: HTMLElement) =>
    Number(/([\d.]+)px\)\s*$/.exec(box.style.width)?.[1])

  it('adopts the source ratio instead of forcing a square', () => {
    renderWithProviders(
      <MessageAttachment.Image.Sent
        items={[
          {
            src: 'https://cdn.example.com/tall.jpg',
            alt: 'Tall',
            width: 1080,
            height: 1920,
          },
        ]}
      />
    )
    const box = mediaBox('Open image 1 of 1')
    expect(box).not.toHaveClass('aspect-square')
    expect(Number(box.style.aspectRatio)).toBeCloseTo(0.5625, 4)
    expect(box).toHaveClass(
      '[--mes-single-width-cap:309px]',
      'sm:[--mes-single-width-cap:326px]'
    )
  })

  it('resolves a portrait source against the height cap on both breakpoints', () => {
    renderWithProviders(
      <MessageAttachment.Image.Sent
        items={[
          {
            src: 'https://cdn.example.com/tall.jpg',
            width: 1080,
            height: 1920,
          },
        ]}
      />
    )
    // 436 x 0.5625 = 245.25, under both width caps — so the height cap
    // governs everywhere and the card resolves to 249 x 440.
    const bound = heightBoundWidthPx(mediaBox('Open image 1 of 1'))
    expect(bound).toBeCloseTo(245.25, 2)
    expect(bound).toBeLessThan(WIDTH_CAP_MOBILE_PX)
  })

  it('resolves a landscape source against the breakpoint width cap', () => {
    renderWithProviders(
      <MessageAttachment.Image.Sent
        items={[
          {
            src: 'https://cdn.example.com/wide.jpg',
            width: 1920,
            height: 1080,
          },
        ]}
      />
    )
    // 436 x 1.7778 = 775.11, over both width caps — so 326px (309px on
    // mobile) governs and the card resolves to 330 x 187.
    expect(heightBoundWidthPx(mediaBox('Open image 1 of 1'))).toBeGreaterThan(
      WIDTH_CAP_WEB_PX
    )
  })

  it('reserves the 4:3 landscape box until the image decodes, then adopts its real ratio', () => {
    renderWithProviders(
      <MessageAttachment.Image.Sent src="https://cdn.example.com/unsized.jpg" />
    )
    expect(Number(mediaBox('Open image 1 of 1').style.aspectRatio)).toBeCloseTo(
      326 / 246,
      4
    )

    // Scoped to the tile: the always-mounted `ImageViewer` renders a
    // second `<img>` for the same source.
    const img = mediaBox('Open image 1 of 1').querySelector(
      'img'
    ) as HTMLImageElement
    Object.defineProperty(img, 'naturalWidth', {
      value: 1080,
      configurable: true,
    })
    Object.defineProperty(img, 'naturalHeight', {
      value: 1920,
      configurable: true,
    })
    fireEvent.load(img)

    expect(Number(mediaBox('Open image 1 of 1').style.aspectRatio)).toBeCloseTo(
      0.5625,
      4
    )
  })

  it('leaves a stacked grid on its fixed aspect', () => {
    renderWithProviders(
      <MessageAttachment.Image.Sent
        items={Array.from({ length: 4 }, (_, i) => ({
          src: `https://cdn.example.com/${i}.jpg`,
          width: 1080,
          height: 1920,
        }))}
      />
    )
    // 379x286 is the content box of Figma's 383x290 stacked card.
    const grid = mediaBox('Open image 1 of 4')
    expect(grid).toHaveClass('aspect-[379/286]')
    expect(grid.style.width).toBe('')
    expect(grid.style.aspectRatio).toBe('')
  })
})

describe('MessageAttachment.Bubble geometry overrides', () => {
  // `widthClassName` / `paddingClassName` / `radiusClassName` REPLACE
  // their defaults outright rather than merging: an appended radius and
  // the corner table have identical specificity, so Tailwind's emitted
  // source order — not argument order — would pick the winner. Document
  // and audio rows keep the 280px / 8px defaults; media cards override
  // all three.
  it('keeps the default width, padding and corner utilities when nothing is overridden', () => {
    renderWithProviders(
      <MessageAttachment.File.Sent
        src="https://cdn.example.com/file.zip"
        filename="file.zip"
      />
    )
    expect(screen.getByTestId('file-attachment')).toHaveClass(
      'w-[280px]',
      'px-2',
      'py-2',
      'rounded-tl-[18px]'
    )
  })

  it('replaces every default on a media card instead of merging', () => {
    renderWithProviders(
      <MessageAttachment.Image.Sent src="https://cdn.example.com/photo.jpg" />
    )
    const bubble = screen.getByTestId('image-attachment')
    expect(bubble).toHaveClass('w-fit', 'p-0.5', 'rounded-[20px]')
    expect(bubble.className).not.toMatch(
      /w-\[280px\]|px-2|py-2|rounded-tl-\[18px\]/
    )
  })
})

describe('MessageAttachment.Pdf', () => {
  it('renders the compact row with filename + meta', () => {
    renderWithProviders(
      <MessageAttachment.Pdf.Received
        src="https://cdn.example.com/doc.pdf"
        filename="ESOP-summary.pdf"
        fileSize={388_658}
      />
    )
    expect(screen.getByText('ESOP-summary.pdf')).toBeInTheDocument()
    expect(screen.getByText('PDF · 379.5 KB')).toBeInTheDocument()
  })

  it('opens the PdfViewer when clicked', () => {
    renderWithProviders(
      <MessageAttachment.Pdf.Sent
        src="https://cdn.example.com/doc.pdf"
        filename="doc.pdf"
      />
    )
    expectViewerClosed('pdf-viewer')
    fireEvent.click(screen.getByLabelText('Open doc.pdf'))
    expectViewerOpen('pdf-viewer')
  })

  it('forwards the onClick handler before opening the viewer', () => {
    const onClick = vi.fn()
    renderWithProviders(
      <MessageAttachment.Pdf.Received
        src="https://cdn.example.com/doc.pdf"
        filename="doc.pdf"
        onClick={onClick}
      />
    )
    fireEvent.click(screen.getByLabelText('Open doc.pdf'))
    expect(onClick).toHaveBeenCalledTimes(1)
  })

  it('renders caption inside the same bubble', () => {
    renderWithProviders(
      <MessageAttachment.Pdf.Sent
        src="https://cdn.example.com/doc.pdf"
        filename="doc.pdf"
        text="Here is the file"
      />
    )
    const bubble = screen.getByTestId('pdf-attachment')
    expect(bubble.textContent).toContain('Here is the file')
  })

  it('renders a row per item for stacked PDFs', () => {
    renderWithProviders(
      <MessageAttachment.Pdf.Sent
        items={[
          { src: 'https://cdn.example.com/a.pdf', filename: 'a.pdf' },
          { src: 'https://cdn.example.com/b.pdf', filename: 'b.pdf' },
          { src: 'https://cdn.example.com/c.pdf', filename: 'c.pdf' },
        ]}
      />
    )
    expect(screen.getByLabelText('Open a.pdf')).toBeInTheDocument()
    expect(screen.getByLabelText('Open b.pdf')).toBeInTheDocument()
    expect(screen.getByLabelText('Open c.pdf')).toBeInTheDocument()
  })

  it('opens the viewer on the activated row in a stacked PDF bubble', () => {
    renderWithProviders(
      <MessageAttachment.Pdf.Received
        items={[
          { src: 'https://cdn.example.com/a.pdf', filename: 'a.pdf' },
          { src: 'https://cdn.example.com/b.pdf', filename: 'b.pdf' },
        ]}
      />
    )
    expectViewerClosed('pdf-viewer')
    fireEvent.click(screen.getByLabelText('Open b.pdf'))
    const viewer = expectViewerOpen('pdf-viewer')
    // The active filename surfaces as the dialog's accessible name —
    // the lightbox chrome is intentionally just close + carousel chrome
    // so the filename isn't rendered as visible text inside the modal.
    expect(viewer.getAttribute('aria-label')).toBe('b.pdf')
    // Carousel chrome is present because items.length > 1.
    expect(screen.getByLabelText('Next document')).toBeInTheDocument()
    expect(screen.getByLabelText('Previous document')).toBeInTheDocument()
    expect(viewer.textContent).toContain('2 / 2')
  })

  it('navigates between stacked PDFs via the carousel controls', () => {
    renderWithProviders(
      <MessageAttachment.Pdf.Received
        items={[
          { src: 'https://cdn.example.com/a.pdf', filename: 'a.pdf' },
          { src: 'https://cdn.example.com/b.pdf', filename: 'b.pdf' },
          { src: 'https://cdn.example.com/c.pdf', filename: 'c.pdf' },
        ]}
      />
    )
    fireEvent.click(screen.getByLabelText('Open a.pdf'))
    const viewer = expectViewerOpen('pdf-viewer')
    expect(viewer.getAttribute('aria-label')).toBe('a.pdf')

    fireEvent.click(screen.getByLabelText('Next document'))
    expect(viewer.getAttribute('aria-label')).toBe('b.pdf')

    fireEvent.click(screen.getByLabelText('Next document'))
    expect(viewer.getAttribute('aria-label')).toBe('c.pdf')

    // Wraps back to the start.
    fireEvent.click(screen.getByLabelText('Next document'))
    expect(viewer.getAttribute('aria-label')).toBe('a.pdf')
  })

  it('renders a sibling Download button on Sent / Received rows', () => {
    renderWithProviders(
      <MessageAttachment.Pdf.Received
        src="https://cdn.example.com/notes.pdf"
        filename="notes.pdf"
        fileSize={1_024}
      />
    )
    const open = screen.getByLabelText('Open notes.pdf')
    const download = screen.getByLabelText('Download notes.pdf')
    expect(open).toBeInTheDocument()
    expect(download).toBeInTheDocument()
    // The download button must be a real sibling button — not nested
    // inside the open button — so the two activations are independent.
    expect(open.contains(download)).toBe(false)
    expect(download.contains(open)).toBe(false)
  })

  it('renders one Download button per row for stacked PDFs', () => {
    renderWithProviders(
      <MessageAttachment.Pdf.Sent
        items={[
          { src: 'https://cdn.example.com/a.pdf', filename: 'a.pdf' },
          { src: 'https://cdn.example.com/b.pdf', filename: 'b.pdf' },
          { src: 'https://cdn.example.com/c.pdf', filename: 'c.pdf' },
        ]}
      />
    )
    expect(screen.getByLabelText('Download a.pdf')).toBeInTheDocument()
    expect(screen.getByLabelText('Download b.pdf')).toBeInTheDocument()
    expect(screen.getByLabelText('Download c.pdf')).toBeInTheDocument()
  })

  it('does not open the viewer when the Download button is clicked', () => {
    renderWithProviders(
      <MessageAttachment.Pdf.Sent
        src="https://cdn.example.com/notes.pdf"
        filename="notes.pdf"
      />
    )
    expectViewerClosed('pdf-viewer')
    fireEvent.click(screen.getByLabelText('Download notes.pdf'))
    // Download should fire on its own without bubbling up to the row
    // activation that opens the viewer.
    expectViewerClosed('pdf-viewer')
  })

  it('hides the Download button on the Composer state (dismiss takes its place)', () => {
    renderWithProviders(
      <MessageAttachment.Pdf.Composer
        src="https://cdn.example.com/notes.pdf"
        filename="notes.pdf"
        onDismiss={() => undefined}
      />
    )
    expect(screen.queryByLabelText('Download notes.pdf')).toBeNull()
    expect(screen.getByLabelText('Dismiss attachment')).toBeInTheDocument()
  })

  it('does not crash when items shrinks after the viewer opens (P1 regression)', () => {
    // Regression test for the PR #151 P1 reviewer finding:
    // `viewerIndex` was dereferenced directly against `resolvedItems`,
    // so a re-render with fewer items than the open index would crash
    // with "Cannot read properties of undefined". The component now
    // clamps the index defensively.
    const { rerender } = renderWithProviders(
      <MessageAttachment.Pdf.Received
        items={[
          { src: 'https://cdn.example.com/a.pdf', filename: 'a.pdf' },
          { src: 'https://cdn.example.com/b.pdf', filename: 'b.pdf' },
          { src: 'https://cdn.example.com/c.pdf', filename: 'c.pdf' },
        ]}
      />
    )
    fireEvent.click(screen.getByLabelText('Open c.pdf'))
    expectViewerOpen('pdf-viewer')

    expect(() =>
      rerender(
        <MessageAttachment.Pdf.Received
          items={[{ src: 'https://cdn.example.com/a.pdf', filename: 'a.pdf' }]}
        />
      )
    ).not.toThrow()

    // Bubble still renders the surviving row instead of blowing up.
    expect(screen.getByLabelText('Open a.pdf')).toBeInTheDocument()
    // Viewer falls back to the last available item (the surviving one)
    // — surfaced as the dialog's aria-label since the chrome no longer
    // renders the filename as visible text.
    expect(screen.getByTestId('pdf-viewer').getAttribute('aria-label')).toBe(
      'a.pdf'
    )
  })

  it('does not open the viewer when onClick returns false', () => {
    const onClick = vi.fn(() => false as const)
    renderWithProviders(
      <MessageAttachment.Pdf.Received
        src="https://cdn.example.com/doc.pdf"
        filename="doc.pdf"
        onClick={onClick}
      />
    )
    fireEvent.click(screen.getByLabelText('Open doc.pdf'))
    expect(onClick).toHaveBeenCalledTimes(1)
    expectViewerClosed('pdf-viewer')
  })
})

describe('MessageAttachment.File', () => {
  it('renders the row with the trailing download icon', () => {
    renderWithProviders(
      <MessageAttachment.File.Received
        src="https://cdn.example.com/file.zip"
        filename="workout-plan.zip"
        mimeType="application/zip"
        fileSize={2_457_600}
      />
    )
    expect(screen.getByText('workout-plan.zip')).toBeInTheDocument()
    expect(screen.getByText('ZIP · 2.34 MB')).toBeInTheDocument()
    expect(
      screen.getByLabelText('Download workout-plan.zip')
    ).toBeInTheDocument()
  })

  it('falls back to the URL filename when none is provided', () => {
    renderWithProviders(
      <MessageAttachment.File.Sent src="https://cdn.example.com/notes.pdf" />
    )
    expect(screen.getByText('notes.pdf')).toBeInTheDocument()
  })

  it('renders one row per item for stacked files', () => {
    renderWithProviders(
      <MessageAttachment.File.Sent
        items={[
          {
            src: 'https://cdn.example.com/a.zip',
            filename: 'a.zip',
            mimeType: 'application/zip',
            fileSize: 1_024,
          },
          {
            src: 'https://cdn.example.com/b.docx',
            filename: 'b.docx',
            mimeType:
              'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
            fileSize: 2_048,
          },
          {
            src: 'https://cdn.example.com/c.pdf',
            filename: 'c.pdf',
            mimeType: 'application/pdf',
            fileSize: 4_096,
          },
        ]}
      />
    )
    expect(screen.getByLabelText('Download a.zip')).toBeInTheDocument()
    expect(screen.getByLabelText('Download b.docx')).toBeInTheDocument()
    expect(screen.getByLabelText('Download c.pdf')).toBeInTheDocument()
  })

  it('downloads the file when the row is clicked and onClick allows it', () => {
    const onClick = vi.fn()
    renderWithProviders(
      <MessageAttachment.File.Received
        src="https://cdn.example.com/file.zip"
        filename="workout-plan.zip"
        onClick={onClick}
      />
    )
    fireEvent.click(screen.getByLabelText('Download workout-plan.zip'))
    expect(onClick).toHaveBeenCalledWith(0)
    expect(mockTriggerDownload).toHaveBeenCalledTimes(1)
    expect(mockTriggerDownload).toHaveBeenCalledWith(
      'https://cdn.example.com/file.zip',
      'workout-plan.zip'
    )
  })

  it('lets onClick cancel the automatic download by returning false (P2 regression)', () => {
    // Regression test for the PR #151 P2 reviewer finding:
    // the docs advertised `onClick` as a hook for intercepting the
    // download (e.g. confirmation modal), but the handler used to fire
    // the default download unconditionally. Returning `false` from
    // `onClick` now suppresses the built-in trigger.
    const onClick = vi.fn(() => false as const)
    renderWithProviders(
      <MessageAttachment.File.Received
        src="https://cdn.example.com/file.zip"
        filename="workout-plan.zip"
        onClick={onClick}
      />
    )
    fireEvent.click(screen.getByLabelText('Download workout-plan.zip'))
    expect(onClick).toHaveBeenCalledTimes(1)
    expect(mockTriggerDownload).not.toHaveBeenCalled()
  })

  it('still downloads when onClick returns void', () => {
    // Belt-and-braces: void / undefined returns must not be confused
    // with `false` (the only cancellation sentinel).
    const onClick = vi.fn(() => undefined)
    renderWithProviders(
      <MessageAttachment.File.Received
        src="https://cdn.example.com/file.zip"
        filename="workout-plan.zip"
        onClick={onClick}
      />
    )
    fireEvent.click(screen.getByLabelText('Download workout-plan.zip'))
    expect(mockTriggerDownload).toHaveBeenCalledTimes(1)
  })
})

describe('MessageAttachment.Audio', () => {
  it('renders the native audio player only (no title, no extra download)', () => {
    renderWithProviders(
      <MessageAttachment.Audio.Sent
        src="https://cdn.example.com/song.mp3"
        mimeType="audio/mpeg"
        filename="song.mp3"
      />
    )
    const bubble = screen.getByTestId('audio-attachment')
    expect(bubble.querySelector('audio')).not.toBeNull()
    // No filename title — the native `<audio>` element doesn't expose
    // a title slot, so we render just the player.
    expect(screen.queryByText('song.mp3')).toBeNull()
    // Native player handles its own download in the kebab menu, so
    // no extra Download button should be rendered.
    expect(screen.queryByRole('button', { name: /download/i })).toBeNull()
  })

  it('shows an inline dismiss button next to the player on Composer state', () => {
    const onDismiss = vi.fn()
    renderWithProviders(
      <MessageAttachment.Audio.Composer
        src="https://cdn.example.com/song.mp3"
        onDismiss={onDismiss}
      />
    )
    const dismiss = screen.getByLabelText('Dismiss attachment')
    expect(dismiss).toBeInTheDocument()
    fireEvent.click(dismiss)
    expect(onDismiss).toHaveBeenCalledTimes(1)
  })

  it('renders one native player per item for stacked audio', () => {
    renderWithProviders(
      <MessageAttachment.Audio.Sent
        items={[
          { src: 'https://cdn.example.com/a.mp3', mimeType: 'audio/mpeg' },
          { src: 'https://cdn.example.com/b.mp3', mimeType: 'audio/mpeg' },
          { src: 'https://cdn.example.com/c.mp3', mimeType: 'audio/mpeg' },
        ]}
      />
    )
    const bubble = screen.getByTestId('audio-attachment')
    expect(bubble.querySelectorAll('audio')).toHaveLength(3)
  })
})

describe('MessageAttachment.Video', () => {
  it('opens the VideoViewer when clicked', () => {
    renderWithProviders(
      <MessageAttachment.Video.Received
        src="https://cdn.example.com/clip.mp4"
        poster="https://cdn.example.com/poster.jpg"
        mimeType="video/mp4"
        filename="clip.mp4"
      />
    )
    expectViewerClosed('video-viewer')
    fireEvent.click(screen.getByLabelText('Play video 1 of 1'))
    expectViewerOpen('video-viewer')
    // Single-item viewer has no carousel chrome.
    expect(screen.queryByLabelText('Next video')).toBeNull()
  })

  it('navigates between stacked videos via the carousel controls', () => {
    renderWithProviders(
      <MessageAttachment.Video.Sent
        filename="clip"
        items={[
          {
            src: 'https://cdn.example.com/a.mp4',
            poster: 'https://cdn.example.com/a.jpg',
            mimeType: 'video/mp4',
          },
          {
            src: 'https://cdn.example.com/b.mp4',
            poster: 'https://cdn.example.com/b.jpg',
            mimeType: 'video/mp4',
          },
        ]}
      />
    )
    fireEvent.click(screen.getByLabelText('Play video 1 of 2'))
    const viewer = expectViewerOpen('video-viewer')
    expect(viewer.querySelector('video')?.getAttribute('src')).toBe(
      'https://cdn.example.com/a.mp4'
    )

    fireEvent.click(screen.getByLabelText('Next video'))
    expect(viewer.querySelector('video')?.getAttribute('src')).toBe(
      'https://cdn.example.com/b.mp4'
    )
  })
})

// The grid only *displays* four tiles; the viewer is a carousel over the
// whole array. Every number here is an off-by-one waiting to happen —
// `total - visible` vs `total - 1`, index 3 vs index 4, `4 / 9` vs
// `4 / 4` — and none of it was covered.
describe('MessageAttachment.Video overflow', () => {
  const NINE_VIDEOS = Array.from({ length: 9 }, (_, i) => ({
    src: `https://cdn.example.com/${i}.mp4`,
    poster: `https://cdn.example.com/${i}.jpg`,
    mimeType: 'video/mp4',
  }))

  it('renders four tiles and a +5 badge for nine videos', () => {
    renderWithProviders(<MessageAttachment.Video.Sent items={NINE_VIDEOS} />)

    const tiles = screen.getAllByRole('button', { name: /^Play video/ })
    expect(tiles).toHaveLength(4)
    // `total - visible`, not `total - 1` and not a count that swallows
    // the tile the scrim sits on.
    expect(screen.getByText('+5')).toBeInTheDocument()
    // Tiles 5..9 are never rendered, so they stay out of the tab order.
    expect(screen.queryByLabelText('Play video 5 of 9')).toBeNull()
  })

  it('opens the viewer on the fourth video from the overflow tile and pages to the ninth', () => {
    renderWithProviders(<MessageAttachment.Video.Sent items={NINE_VIDEOS} />)

    fireEvent.click(screen.getByLabelText('Play video 4 of 9'))
    const viewer = expectViewerOpen('video-viewer')
    expect(viewer.querySelector('video')?.getAttribute('src')).toBe(
      'https://cdn.example.com/3.mp4'
    )
    // How the user learns what "+5" resolved to.
    expect(viewer.textContent).toContain('4 / 9')

    // The carousel spans all nine, not just the four the grid drew.
    for (let i = 0; i < 5; i += 1) {
      fireEvent.click(screen.getByLabelText('Next video'))
    }
    expect(viewer.querySelector('video')?.getAttribute('src')).toBe(
      'https://cdn.example.com/8.mp4'
    )
    expect(viewer.textContent).toContain('9 / 9')
  })

  it('drops the play badge on the overflow tile only', () => {
    renderWithProviders(<MessageAttachment.Video.Sent items={NINE_VIDEOS} />)

    const tiles = screen.getAllByRole('button', { name: /^Play video/ })
    // The badge is the only `svg` a poster tile renders.
    expect(tiles[0].querySelector('svg')).not.toBeNull()
    expect(tiles[3].querySelector('svg')).toBeNull()
  })
})

// A fixed-width card with a free height is one bad ratio away from a
// 587px-tall bubble, so the clamp is the whole point of the feature.
describe('MessageAttachment.Video single-card aspect', () => {
  // jsdom never decodes an `<img>`, so `naturalWidth` stays 0 and `load`
  // never fires on its own — stub the intrinsic size and fire it.
  const loadPosterAt = (naturalWidth: number, naturalHeight: number) => {
    const poster = screen.getByTestId('video-attachment').querySelector('img')
    if (!poster) throw new Error('no poster rendered')
    Object.defineProperty(poster, 'naturalWidth', {
      configurable: true,
      value: naturalWidth,
    })
    Object.defineProperty(poster, 'naturalHeight', {
      configurable: true,
      value: naturalHeight,
    })
    fireEvent.load(poster)
  }

  const mediaAspect = () =>
    screen.getByTestId('video-attachment').querySelector<HTMLElement>('[style]')
      ?.style.aspectRatio

  it('stays square while the poster is still undecoded', () => {
    // Square, not 16:9. The fallback covers "we know nothing yet", and this
    // card was square before any of this existed — guessing landscape put
    // portrait clips in a letterbox, which is more wrong (MES-1353).
    renderWithProviders(
      <MessageAttachment.Video.Sent
        src="https://cdn.example.com/clip.mp4"
        poster="https://cdn.example.com/poster.jpg"
      />
    )
    expect(mediaAspect()).toBe(String(1))
  })

  it('sizes a posterless clip from the dimensions the sender recorded', () => {
    // The case that made this necessary: an uploaded clip with no poster.
    // Nothing can be measured, so without recorded dimensions the card sat at
    // whatever the fallback guessed, regardless of the clip's real shape.
    renderWithProviders(
      <MessageAttachment.Video.Sent
        src="https://cdn.example.com/clip.mp4"
        naturalAspectRatio={9 / 16}
      />
    )
    expect(
      screen.getByTestId('video-attachment').querySelector('img')
    ).toBeNull()
    // Clamped to MIN_ASPECT: a true 9:16 would render a 587px-tall card, so
    // portrait pins at 4:5 until design asks for a full-height card.
    expect(mediaAspect()).toBe(String(4 / 5))
  })

  it('prefers the recorded dimensions over the decoded poster', () => {
    // Recorded dimensions describe the clip; the poster is only a proxy for it
    // and can be cropped or letterboxed by whatever produced it.
    renderWithProviders(
      <MessageAttachment.Video.Sent
        src="https://cdn.example.com/clip.mp4"
        poster="https://cdn.example.com/poster.jpg"
        naturalAspectRatio={4 / 3}
      />
    )
    expect(mediaAspect()).toBe(String(4 / 3))

    loadPosterAt(1920, 1080)
    expect(mediaAspect()).toBe(String(4 / 3))
  })

  it('stays square when there is neither a poster nor recorded dimensions', () => {
    renderWithProviders(
      <MessageAttachment.Video.Sent src="https://cdn.example.com/clip.mp4" />
    )
    expect(mediaAspect()).toBe(String(1))
  })

  it('adopts the poster ratio inside the clamp and pins it outside', () => {
    renderWithProviders(
      <MessageAttachment.Video.Sent
        src="https://cdn.example.com/clip.mp4"
        poster="https://cdn.example.com/poster.jpg"
      />
    )

    // 4:3 sits inside [4/5, 16/9] and passes through untouched.
    loadPosterAt(1440, 1080)
    expect(mediaAspect()).toBe(String(4 / 3))

    // 9:16 is taller than the 4/5 floor.
    loadPosterAt(1080, 1920)
    expect(mediaAspect()).toBe(String(4 / 5))

    // 21:9 is wider than the 16/9 ceiling.
    loadPosterAt(2560, 1080)
    expect(mediaAspect()).toBe(String(16 / 9))
  })

  it('measures a poster the browser served from cache, which never fires `load`', () => {
    // A cached `<img>` can be `complete` before React attaches its `load`
    // listener — a second card on the same URL, a remount while scrolling a
    // thread, a Storybook HMR update. Measuring on `load` alone left the card
    // stuck on the fallback with no way out, which is why the hook also reads
    // from the ref (MES-1353).
    const saved = (['naturalWidth', 'naturalHeight'] as const).map(
      (name) =>
        [
          name,
          Object.getOwnPropertyDescriptor(HTMLImageElement.prototype, name),
        ] as const
    )
    // Stubbed on the prototype so the poster already reports its intrinsic
    // size when the ref callback first sees it, and no `load` is fired below.
    Object.defineProperty(HTMLImageElement.prototype, 'naturalWidth', {
      configurable: true,
      value: 1440,
    })
    Object.defineProperty(HTMLImageElement.prototype, 'naturalHeight', {
      configurable: true,
      value: 1080,
    })

    try {
      renderWithProviders(
        <MessageAttachment.Video.Sent
          src="https://cdn.example.com/clip.mp4"
          poster="https://cdn.example.com/poster.jpg"
        />
      )
      expect(mediaAspect()).toBe(String(4 / 3))
    } finally {
      for (const [name, descriptor] of saved) {
        if (descriptor) {
          Object.defineProperty(HTMLImageElement.prototype, name, descriptor)
        } else {
          delete (
            HTMLImageElement.prototype as unknown as Record<string, unknown>
          )[name]
        }
      }
    }
  })

  it('stays square on a received clip with no poster', () => {
    // Was asserted as 16:9. That guess is what put a portrait clip in a
    // letterbox box, since nothing here knows the clip's shape — the sender
    // has to record it. Square is the honest answer and matches what this
    // card rendered before the feature existed (MES-1353).
    renderWithProviders(
      <MessageAttachment.Video.Received src="https://cdn.example.com/clip.mp4" />
    )
    expect(
      screen.getByTestId('video-attachment').querySelector('img')
    ).toBeNull()
    expect(mediaAspect()).toBe(String(1))
  })

  it('leaves multi-video grids on their fixed geometry', () => {
    renderWithProviders(
      <MessageAttachment.Video.Sent
        items={[
          {
            src: 'https://cdn.example.com/a.mp4',
            poster: 'https://cdn.example.com/a.jpg',
          },
          {
            src: 'https://cdn.example.com/b.mp4',
            poster: 'https://cdn.example.com/b.jpg',
          },
        ]}
      />
    )
    // No inline aspect-ratio: the 2-tile layout owns its own class.
    expect(mediaAspect()).toBeUndefined()
  })
})

describe('ViewerShell focus management', () => {
  it('moves focus into the dialog when a viewer opens', () => {
    renderWithProviders(
      <MessageAttachment.Pdf.Received
        src="https://cdn.example.com/doc.pdf"
        filename="doc.pdf"
      />
    )
    fireEvent.click(screen.getByLabelText('Open doc.pdf'))

    // The close button is the primary focus target in the viewer
    // toolbar — keyboard users should land on it on open so Escape
    // / Tab work without an extra click.
    const close = screen.getByLabelText('Close viewer')
    expect(document.activeElement).toBe(close)
  })

  it('restores focus to the opener when the viewer closes', () => {
    renderWithProviders(
      <MessageAttachment.Pdf.Received
        src="https://cdn.example.com/doc.pdf"
        filename="doc.pdf"
      />
    )
    const opener = screen.getByLabelText('Open doc.pdf')
    opener.focus()
    expect(document.activeElement).toBe(opener)

    fireEvent.click(opener)
    expectViewerOpen('pdf-viewer')

    fireEvent.click(screen.getByLabelText('Close viewer'))

    // After close, focus should hop back to whatever was focused
    // before the viewer mounted (the row's Open button), and the
    // dialog should no longer carry the `[open]` attribute.
    expectViewerClosed('pdf-viewer')
    expect(document.activeElement).toBe(opener)
  })
})

describe('MessageAttachment lazy-loading defaults', () => {
  describe('Image', () => {
    it('renders every `<img>` with loading="lazy" decoding="async" by default', () => {
      renderWithProviders(
        <MessageAttachment.Image.Sent
          src="https://cdn.example.com/photo.jpg"
          alt="Photo"
        />
      )
      const img = screen.getByTestId('image-attachment').querySelector('img')
      expect(img?.getAttribute('loading')).toBe('lazy')
      expect(img?.getAttribute('decoding')).toBe('async')
    })

    it('honors loading="eager" when passed at the attachment level', () => {
      renderWithProviders(
        <MessageAttachment.Image.Received
          src="https://cdn.example.com/photo.jpg"
          alt="Photo"
          loading="eager"
        />
      )
      const img = screen.getByTestId('image-attachment').querySelector('img')
      expect(img?.getAttribute('loading')).toBe('eager')
      expect(img?.getAttribute('decoding')).toBe('async')
    })

    it('honors per-tile ImageItem.loading overrides in a stack', () => {
      renderWithProviders(
        <MessageAttachment.Image.Sent
          items={[
            {
              src: 'https://cdn.example.com/a.jpg',
              alt: 'A',
              loading: 'eager',
            },
            { src: 'https://cdn.example.com/b.jpg', alt: 'B' },
          ]}
        />
      )
      const [first, second] = Array.from(
        screen.getByTestId('image-attachment').querySelectorAll('img')
      )
      expect(first?.getAttribute('loading')).toBe('eager')
      expect(second?.getAttribute('loading')).toBe('lazy')
    })

    it('does not mount the viewer `<img>` while the lightbox is closed', () => {
      renderWithProviders(
        <MessageAttachment.Image.Sent
          src="https://cdn.example.com/photo.jpg"
          alt="Photo"
        />
      )
      // `ViewerShell` keeps its `<dialog>` mounted so the open / close
      // transition can play, and the viewer image is `loading="eager"`
      // on the raw (un-optimised) source. The element being absent
      // while closed is therefore the only thing stopping a scrolled
      // thread from downloading every full-resolution original.
      expect(screen.getByTestId('image-viewer').querySelector('img')).toBeNull()

      fireEvent.click(screen.getByLabelText('Open image 1 of 1'))
      expect(
        screen.getByTestId('image-viewer').querySelector('img')
      ).not.toBeNull()
    })
  })

  describe('Video', () => {
    it('renders the poster `<img>` with loading="lazy" decoding="async"', () => {
      renderWithProviders(
        <MessageAttachment.Video.Received
          src="https://cdn.example.com/clip.mp4"
          poster="https://cdn.example.com/poster.jpg"
          mimeType="video/mp4"
        />
      )
      const poster = screen.getByTestId('video-attachment').querySelector('img')
      expect(poster?.getAttribute('src')).toBe(
        'https://cdn.example.com/poster.jpg'
      )
      expect(poster?.getAttribute('loading')).toBe('lazy')
      expect(poster?.getAttribute('decoding')).toBe('async')
    })

    it('opens the viewer with `<video preload="metadata">` for the active item', () => {
      renderWithProviders(
        <MessageAttachment.Video.Sent
          src="https://cdn.example.com/clip.mp4"
          poster="https://cdn.example.com/poster.jpg"
          mimeType="video/mp4"
          filename="clip.mp4"
        />
      )
      fireEvent.click(screen.getByLabelText('Play video 1 of 1'))
      const video = screen.getByTestId('video-viewer').querySelector('video')
      expect(video?.getAttribute('preload')).toBe('metadata')
    })

    // The single most load-bearing clause of the preload policy: a nine
    // -video message must cost four lazy poster images, never nine (or
    // four) media pipelines. Any future hover-to-preview would break
    // this, which is the point.
    it('mounts no `<video>` on the bubble surface, however many items', () => {
      renderWithProviders(
        <MessageAttachment.Video.Sent
          items={Array.from({ length: 9 }, (_, i) => ({
            src: `https://cdn.example.com/${i}.mp4`,
            poster: `https://cdn.example.com/${i}.jpg`,
          }))}
        />
      )
      const bubble = screen.getByTestId('video-attachment')
      expect(bubble.querySelectorAll('video')).toHaveLength(0)
      expect(bubble.querySelectorAll('img')).toHaveLength(4)
    })
  })

  describe('Audio', () => {
    it('defaults a single-track bubble to preload="metadata"', () => {
      renderWithProviders(
        <MessageAttachment.Audio.Received
          src="https://cdn.example.com/song.mp3"
          mimeType="audio/mpeg"
        />
      )
      const audio = screen
        .getByTestId('audio-attachment')
        .querySelector('audio')
      expect(audio?.getAttribute('preload')).toBe('metadata')
    })

    it('defaults a stacked (3-track) bubble to preload="none" on every player', () => {
      renderWithProviders(
        <MessageAttachment.Audio.Sent
          items={[
            { src: 'https://cdn.example.com/a.mp3', mimeType: 'audio/mpeg' },
            { src: 'https://cdn.example.com/b.mp3', mimeType: 'audio/mpeg' },
            { src: 'https://cdn.example.com/c.mp3', mimeType: 'audio/mpeg' },
          ]}
        />
      )
      const audios = Array.from(
        screen.getByTestId('audio-attachment').querySelectorAll('audio')
      )
      expect(audios).toHaveLength(3)
      audios.forEach((audio) => {
        expect(audio.getAttribute('preload')).toBe('none')
      })
    })

    it('honors a per-track AudioItem.preload override', () => {
      renderWithProviders(
        <MessageAttachment.Audio.Sent
          items={[
            { src: 'https://cdn.example.com/a.mp3', mimeType: 'audio/mpeg' },
            {
              src: 'https://cdn.example.com/b.mp3',
              mimeType: 'audio/mpeg',
              preload: 'metadata',
            },
          ]}
        />
      )
      const audios = Array.from(
        screen.getByTestId('audio-attachment').querySelectorAll('audio')
      )
      expect(audios[0]?.getAttribute('preload')).toBe('none')
      expect(audios[1]?.getAttribute('preload')).toBe('metadata')
    })
  })
})

describe('bubbleGroupPositionFromStream', () => {
  it.each([
    {
      groupedByUser: false,
      firstOfGroup: true,
      endOfGroup: true,
      expected: 'single',
    },
    {
      groupedByUser: true,
      firstOfGroup: true,
      endOfGroup: true,
      expected: 'single',
    },
    {
      groupedByUser: true,
      firstOfGroup: true,
      endOfGroup: false,
      expected: 'first',
    },
    {
      groupedByUser: true,
      firstOfGroup: false,
      endOfGroup: true,
      expected: 'end',
    },
    {
      groupedByUser: true,
      firstOfGroup: false,
      endOfGroup: false,
      expected: 'middle',
    },
  ])(
    'maps groupedByUser=$groupedByUser firstOfGroup=$firstOfGroup endOfGroup=$endOfGroup → $expected',
    ({ groupedByUser, firstOfGroup, endOfGroup, expected }) => {
      expect(
        bubbleGroupPositionFromStream({
          groupedByUser,
          firstOfGroup,
          endOfGroup,
        })
      ).toBe(expected)
    }
  )
})

describe('MessageAttachment.Bubble grouping', () => {
  // Sender bubbles (Sent) cluster against the right edge — the corner
  // facing the previous bubble in a run is `top-right`, the corner
  // facing the next bubble is `bottom-right`. Receiver bubbles
  // (Received) mirror those onto the left edge.
  //
  // We assert on the `data-group-position` attribute the Bubble
  // serializes, not on the Tailwind class strings. The class strings
  // encode the same intent, but asserting on them ties the test to
  // the current `rounded-tr-[Xpx]` token format — if the bubble ever
  // migrates to a CSS variable / theme token (see `Bubble.tsx` TODO),
  // every one of those assertions would break for an implementation
  // detail. `data-group-position` is the documented contract.
  const FILE_PROPS = {
    src: 'https://cdn.example.com/notes.pdf',
    filename: 'notes.pdf',
    fileSize: 1_024,
    mimeType: 'application/pdf',
  }

  it.each(['single', 'first', 'middle', 'end'] as const)(
    'serializes groupPosition="%s" on the bubble',
    (groupPosition) => {
      renderWithProviders(
        <MessageAttachment.File.Sent
          {...FILE_PROPS}
          groupPosition={groupPosition}
        />
      )
      expect(
        screen
          .getByTestId('file-attachment')
          .getAttribute('data-group-position')
      ).toBe(groupPosition)
    }
  )

  it('serializes groupPosition on receiver-side bubbles too', () => {
    renderWithProviders(
      <MessageAttachment.File.Received {...FILE_PROPS} groupPosition="middle" />
    )
    expect(
      screen.getByTestId('file-attachment').getAttribute('data-group-position')
    ).toBe('middle')
  })

  it('forwards groupPosition through every attachment type', () => {
    const { rerender } = renderWithProviders(
      <MessageAttachment.Audio.Sent
        src="https://cdn.example.com/song.mp3"
        groupPosition="middle"
      />
    )
    expect(
      screen.getByTestId('audio-attachment').getAttribute('data-group-position')
    ).toBe('middle')

    rerender(
      <MessageAttachment.Pdf.Sent
        src="https://cdn.example.com/notes.pdf"
        filename="notes.pdf"
        groupPosition="end"
      />
    )
    expect(
      screen.getByTestId('pdf-attachment').getAttribute('data-group-position')
    ).toBe('end')

    rerender(
      <MessageAttachment.Image.Sent
        src="https://cdn.example.com/photo.jpg"
        groupPosition="first"
      />
    )
    expect(
      screen.getByTestId('image-attachment').getAttribute('data-group-position')
    ).toBe('first')
  })
})
