// `<ReplyIndicator>` snapshot tests. Locks the in-modal banner the user
// sees above the textarea once they've tapped Reply: avatar + "Replying
// to @acct" + a 100-char snippet of the parent's content. The banner is
// the answer to "what does this composer reply to?" — without it the
// user has to navigate-back to find out, which the prior pass's chip
// design specifically tried to avoid.
//
// Two contracts under test:
//   1. Null / missing-target gracefully renders nothing (the camera
//      screen mounts the banner unconditionally; the route-driven
//      target query may be cold on first mount).
//   2. The optional `[×]` clear button only renders when `onClear` is
//      provided — the route-driven default case has no clear path
//      (the URL param is the source of truth), so the icon's absence
//      is meaningful.

import React from 'react'
import { act, create, type ReactTestRenderer } from 'react-test-renderer'

// Tamagui mock — every primitive becomes a host-tag passthrough that
// renders its children. `useTheme` returns a stub of every token the
// banner reads (border, backgroundTertiary, colorTertiary,
// colorSecondary). Same shape used by `federatedRepliesPill.tsx`.
jest.mock('tamagui', () => {
  const React = require('react') as typeof import('react')
  const passthrough =
    (tag: string) =>
    ({ children, ...rest }: { children?: React.ReactNode; [k: string]: any }) =>
      React.createElement(tag, rest, children)
  return {
    Text: passthrough('Text'),
    View: passthrough('View'),
    XStack: passthrough('XStack'),
    YStack: passthrough('YStack'),
    useTheme: () => ({
      borderColor: { val: '#000' },
      backgroundTertiary: { val: '#222' },
      colorTertiary: { val: '#aaa' },
      colorSecondary: { val: '#888' },
    }),
  }
})

// Subordinate components — render their content / props as inert host
// nodes so the rendered tree's content + structure is observable.
jest.mock('src/components/common/UserAvatar', () => {
  const React = require('react') as typeof import('react')
  return {
    __esModule: true,
    default: ({ url, size }: { url?: string; size?: string }) =>
      React.createElement('UserAvatar', { url, size }, null),
  }
})

jest.mock('src/components/common/EmojiText', () => {
  const React = require('react') as typeof import('react')
  return {
    __esModule: true,
    default: ({ text }: { text?: string }) =>
      React.createElement('EmojiText', null, text ?? ''),
  }
})

jest.mock('@expo/vector-icons', () => {
  const React = require('react') as typeof import('react')
  return {
    Feather: ({ name }: { name?: string }) =>
      React.createElement('Feather', { name }, null),
  }
})

// expo-web-browser is reached transitively via `src/utils.ts`'s
// `htmlToTextWithLineBreaks` import chain. Stub it before the production
// module loads so the jest-expo preset's Platform.select call doesn't
// blow up. We don't override `react-native` itself — its real Pressable
// renders fine under react-test-renderer and several upstream modules
// (gesture-handler etc.) depend on the full surface.
jest.mock('expo-web-browser', () => ({ openBrowserAsync: jest.fn() }))

type ReplyIndicatorModule = typeof import('../src/components/composer/ReplyIndicator')
let ReplyIndicator: ReplyIndicatorModule['ReplyIndicator']

beforeAll(() => {
  ReplyIndicator = (
    require('../src/components/composer/ReplyIndicator') as ReplyIndicatorModule
  ).ReplyIndicator
})

type Status = import('../src/lib/api-types').Status

function statusFixture(overrides: Partial<Status> = {}): Status {
  return {
    id: 'parent-1',
    content: '<p>hello world</p>',
    account: {
      acct: 'tester@test.example',
      avatar: 'https://test.example/a.png',
      emojis: [],
    },
    ...overrides,
  } as unknown as Status
}

function render(
  target: Status | null,
  onClear?: () => void
): { root: ReactTestRenderer; tree: any } {
  let root: ReactTestRenderer = null as unknown as ReactTestRenderer
  act(() => {
    root = create(
      React.createElement(ReplyIndicator, { target, onClear })
    )
  })
  return { root, tree: root.toJSON() }
}

function extractText(node: any): string {
  if (node == null) return ''
  if (typeof node === 'string') return node
  // Concatenate without separators — React's `<Text>literal{var}</Text>`
  // serialises as a children array `['literal', var]`; joining with a
  // space would split `@{acct}` into `@ tester` and break exact-match
  // assertions on rendered copy.
  if (Array.isArray(node)) return node.map(extractText).join('')
  if (typeof node === 'object' && 'children' in node) return extractText(node.children)
  return ''
}

function findByType(node: any, type: string): any[] {
  const out: any[] = []
  const walk = (n: any) => {
    if (!n) return
    if (Array.isArray(n)) {
      n.forEach(walk)
      return
    }
    if (typeof n !== 'object') return
    if (n.type === type) out.push(n)
    walk(n.children)
  }
  walk(node)
  return out
}

describe('ReplyIndicator', () => {
  it('renders null when target is missing (defensive — route-param query is cold)', () => {
    // The camera screen mounts the banner unconditionally; the
    // `postQueries.detail(inReplyToId)` query may not have settled on
    // first mount. Rendering null keeps the modal layout from jumping
    // between mount and target-arrival.
    const { tree } = render(null)
    expect(tree).toBeNull()
  })

  it('renders the @acct line and snippet when target is set', () => {
    const { tree } = render(
      statusFixture({
        content: '<p>just a quick reply</p>',
        account: {
          acct: 'op@home.example',
          avatar: 'https://home.example/op.png',
          emojis: [],
        } as any,
      })
    )
    const text = extractText(tree)
    expect(text).toContain('Replying to @op@home.example')
    expect(text).toContain('just a quick reply')
  })

  it('omits the [×] clear button when onClear is not provided (route-driven default)', () => {
    // The route-param drives the target — there's no in-app gesture to
    // "clear" it (back-swipe cancels the whole modal). The Feather "x"
    // glyph should be absent so the user isn't offered a control that
    // would surprise them with no observable effect.
    const { tree } = render(statusFixture())
    expect(findByType(tree, 'Feather').length).toBe(0)
  })

  it('renders the [×] clear button when onClear is provided', () => {
    // Forward-looking: a hosted (in-page) reply composer would pass
    // `onClear` so the user can drop the reply target without leaving
    // the screen. Pin the contract so a future caller's wiring is
    // mechanical. (Detect the clear control by the Feather "x" glyph
    // rather than `<Pressable>` — react-native's real Pressable renders
    // as a different host tag depending on platform stubbing.)
    const onClear = jest.fn()
    const { tree } = render(statusFixture(), onClear)
    const feathers = findByType(tree, 'Feather')
    expect(feathers.length).toBe(1)
    expect((feathers[0].props as { name?: string } | null)?.name).toBe('x')
  })

  it('truncates long content via the project-canonical helper (shortcode-safe cut)', () => {
    // `truncateKeepingShortcodes` backs off a `:shortcode:` straddling
    // the 100-char cut so the snippet never leaks a bare `:`. Tested
    // here at 200 chars; the full-blown truncation invariants live in
    // the `src/utils.ts` test net.
    const long = 'a'.repeat(200)
    const { tree } = render(statusFixture({ content: `<p>${long}</p>` }))
    const text = extractText(tree)
    expect(text).toMatch(/a{1,100}…/)
    // Sanity: the full 200-char run must not survive verbatim — the
    // snippet would otherwise overflow the banner's 2-line cap.
    expect(text).not.toContain('a'.repeat(150))
  })

  it('renders without a snippet when target.content is empty (text-stripped to nothing)', () => {
    // Edge case: a media-only post with no caption — `htmlToTextWithLineBreaks`
    // returns ''; the banner should still render the avatar + @acct line
    // but skip the EmojiText snippet entirely (rendering an empty
    // EmojiText would still consume vertical space).
    const { tree } = render(
      statusFixture({ content: '<p></p>' })
    )
    expect(findByType(tree, 'EmojiText').length).toBe(0)
    expect(extractText(tree)).toContain('Replying to @')
  })
})
