// Thread-tree builder tests. Locks down the pure transform from the
// `/api/v1/statuses/{id}/context` `descendants` wire shape (DFS preorder
// with per-level self-reply promotion, per Halflight audit H3-fix-A) to
// the parent→children tree the threaded renderer consumes.
//
// The audit's worked example is the canonical fixture: input `[B, D, C, F, E]`
// must produce `roots: [B, F, E]`, `B.children: [D, C]`, depths
// `[0, 1, 1, 0, 0]`. If a future server change flips to Mastodon's flat
// partition (the path the audit explicitly rejected), this test fails loud.
//
// Orphan promotion (parent dropped by the server's StatusVisibilityResolver
// ahead of the walk) is the "fail visible, not silent" guard — the
// __DEV__-gated console.warn is the regression detector for visibility-
// filter changes.

import { buildThreadTree, flattenWithDepth } from 'src/lib/comments/tree'
import type { Status } from 'src/lib/api-types'

// `buildThreadTree` only reads `id` and `in_reply_to_id` — fixturing the
// rest of the Status shape would be 80 fields of noise. Cast through unknown
// to avoid leaking a partial-Status type into the production module.
function s(id: string, in_reply_to_id: string | null): Status {
  return { id, in_reply_to_id } as unknown as Status
}

const ANCHOR = 'anchor'

describe('buildThreadTree', () => {
  it('returns empty containers for an empty descendants list', () => {
    const tree = buildThreadTree([], ANCHOR)
    expect(tree.roots).toEqual([])
    expect(tree.preorder).toEqual([])
    expect(tree.hasChildrenById.size).toBe(0)
  })

  it('places every direct reply at depth 0 in `roots`', () => {
    // Flat conversation: every reply is to the anchor, no nesting.
    const desc = [s('A', ANCHOR), s('B', ANCHOR), s('C', ANCHOR)]
    const tree = buildThreadTree(desc, ANCHOR)
    expect(tree.roots.map((n) => n.status.id)).toEqual(['A', 'B', 'C'])
    expect(tree.preorder.map((n) => n.depth)).toEqual([0, 0, 0])
    expect(tree.hasChildrenById.size).toBe(0)
  })

  it('threads a linear A → B → C → D chain at increasing depth', () => {
    const desc = [s('A', ANCHOR), s('B', 'A'), s('C', 'B'), s('D', 'C')]
    const tree = buildThreadTree(desc, ANCHOR)
    expect(tree.roots.map((n) => n.status.id)).toEqual(['A'])
    expect(tree.roots[0].children.map((n) => n.status.id)).toEqual(['B'])
    expect(tree.roots[0].children[0].children.map((n) => n.status.id)).toEqual(['C'])
    expect(tree.preorder.map((n) => [n.status.id, n.depth])).toEqual([
      ['A', 0],
      ['B', 1],
      ['C', 2],
      ['D', 3],
    ])
    expect(tree.hasChildrenById.get('A')).toBe(true)
    expect(tree.hasChildrenById.get('B')).toBe(true)
    expect(tree.hasChildrenById.get('C')).toBe(true)
    expect(tree.hasChildrenById.has('D')).toBe(false)
  })

  it("preserves the audit's H3-fix-A per-level self-reply promotion", () => {
    // Worked example from HALFLIGHT_RECURSIVE_THREAD_CONTEXT_AUDIT.md:
    //   anchor has self-replies B (by author) and F (by author), plus
    //   non-self E. B has children C (non-self) and D (self).
    // The server emits `[B, D, C, F, E]` — D before C because the OP's
    // continuation subtree gets promoted under each parent. The builder
    // must round-trip the order verbatim; if a future server flips to
    // Mastodon's flat partition, depths and `roots` would change shape
    // and this test would fail.
    const desc = [
      s('B', ANCHOR),
      s('D', 'B'),
      s('C', 'B'),
      s('F', ANCHOR),
      s('E', ANCHOR),
    ]
    const tree = buildThreadTree(desc, ANCHOR)
    expect(tree.roots.map((n) => n.status.id)).toEqual(['B', 'F', 'E'])
    expect(tree.roots[0].children.map((n) => n.status.id)).toEqual(['D', 'C'])
    expect(tree.preorder.map((n) => [n.status.id, n.depth])).toEqual([
      ['B', 0],
      ['D', 1],
      ['C', 1],
      ['F', 0],
      ['E', 0],
    ])
    expect(tree.hasChildrenById.get('B')).toBe(true)
    expect(tree.hasChildrenById.has('D')).toBe(false)
    expect(tree.hasChildrenById.has('F')).toBe(false)
  })

  it('promotes mid-tree orphans to depth 0 and warns under __DEV__', () => {
    // C's parent D is not in the descendants list — visibility-filtered
    // out by the server. C must still render (failing visible beats
    // failing silent) and a dev-only warn fires so the regression is
    // observable.
    const warn = jest.spyOn(console, 'warn').mockImplementation(() => {})
    try {
      const desc = [s('B', ANCHOR), s('C', 'D')]
      const tree = buildThreadTree(desc, ANCHOR)
      expect(tree.roots.map((n) => n.status.id)).toEqual(['B', 'C'])
      // Orphan stays at depth 0 in the preorder; B is its own depth-0 root.
      expect(tree.preorder.map((n) => [n.status.id, n.depth])).toEqual([
        ['B', 0],
        ['C', 0],
      ])
      expect(warn).toHaveBeenCalledTimes(1)
      expect(warn.mock.calls[0][0]).toContain('orphan reply C')
      expect(warn.mock.calls[0][0]).toContain('parent D')
    } finally {
      warn.mockRestore()
    }
  })

  it('orphans every row when the anchor id matches nothing (fails closed)', () => {
    // Pass an anchor id no descendant points at — every row would be a
    // depth-1 reply with a missing parent in the tree. The builder must
    // surface them as orphans, not silently drop them.
    const warn = jest.spyOn(console, 'warn').mockImplementation(() => {})
    try {
      const desc = [s('A', 'wrong'), s('B', 'wrong')]
      const tree = buildThreadTree(desc, 'anchor-mismatch')
      expect(tree.roots.map((n) => n.status.id)).toEqual(['A', 'B'])
      expect(tree.preorder.length).toBe(2)
      expect(warn).toHaveBeenCalledTimes(2)
    } finally {
      warn.mockRestore()
    }
  })

  it('appends orphans after roots in the preorder traversal', () => {
    // Mixed: legitimate root B, then mid-tree orphan X. Roots come first
    // so the renderer's natural top-to-bottom scroll pins valid threads
    // above the orphan tail.
    const warn = jest.spyOn(console, 'warn').mockImplementation(() => {})
    try {
      const desc = [s('B', ANCHOR), s('X', 'gone')]
      const tree = buildThreadTree(desc, ANCHOR)
      expect(tree.preorder.map((n) => n.status.id)).toEqual(['B', 'X'])
      expect(tree.roots.map((n) => n.status.id)).toEqual(['B', 'X'])
    } finally {
      warn.mockRestore()
    }
  })
})

// `flattenWithDepth` is the depth-annotated DFS preorder companion to
// `buildThreadTree.roots` — it feeds `CommentThreadList`'s renderItem with
// `{ comment, depth }` so each row can spread `threadRowStyle(depth)`
// without knowing about its siblings, parents, or children. Behaviour
// must mirror the tree's own preorder traversal: same row order, same
// depths, no reorder, no merge with the orphan tail (orphans live in
// `tree.roots` and are walked by the same function).
describe('flattenWithDepth', () => {
  it('returns an empty array for an empty roots list', () => {
    expect(flattenWithDepth([])).toEqual([])
  })

  it('emits direct replies at depth 0 with the input row order preserved', () => {
    const desc = [s('A', ANCHOR), s('B', ANCHOR), s('C', ANCHOR)]
    const tree = buildThreadTree(desc, ANCHOR)
    const rows = flattenWithDepth(tree.roots)
    expect(rows.map((r) => [r.comment.id, r.depth])).toEqual([
      ['A', 0],
      ['B', 0],
      ['C', 0],
    ])
  })

  it('walks a linear chain in DFS preorder with monotonically increasing depth', () => {
    const desc = [s('A', ANCHOR), s('B', 'A'), s('C', 'B'), s('D', 'C')]
    const tree = buildThreadTree(desc, ANCHOR)
    const rows = flattenWithDepth(tree.roots)
    expect(rows.map((r) => [r.comment.id, r.depth])).toEqual([
      ['A', 0],
      ['B', 1],
      ['C', 2],
      ['D', 3],
    ])
  })

  it("preserves the audit's H3-fix-A worked-example depth annotations", () => {
    // Same fixture as `buildThreadTree`'s H3 test — flattenWithDepth must
    // produce the same row order as `tree.preorder` (the audit's contract).
    // Lock both, so a future refactor that rewrites either has to update
    // both in lockstep.
    const desc = [
      s('B', ANCHOR),
      s('D', 'B'),
      s('C', 'B'),
      s('F', ANCHOR),
      s('E', ANCHOR),
    ]
    const tree = buildThreadTree(desc, ANCHOR)
    const rows = flattenWithDepth(tree.roots)
    expect(rows.map((r) => [r.comment.id, r.depth])).toEqual([
      ['B', 0],
      ['D', 1],
      ['C', 1],
      ['F', 0],
      ['E', 0],
    ])
    // Same shape as `tree.preorder`, modulo the wrapper key.
    expect(rows.map((r) => [r.comment.id, r.depth])).toEqual(
      tree.preorder.map((n) => [n.status.id, n.depth])
    )
  })

  it('walks orphans alongside roots since both live in tree.roots', () => {
    const warn = jest.spyOn(console, 'warn').mockImplementation(() => {})
    try {
      const desc = [s('B', ANCHOR), s('X', 'gone')]
      const tree = buildThreadTree(desc, ANCHOR)
      const rows = flattenWithDepth(tree.roots)
      // Orphans surface at depth 0 (the builder doesn't try to reconstruct
      // a parent chain for them) — same shape as the preorder traversal.
      expect(rows.map((r) => [r.comment.id, r.depth])).toEqual([
        ['B', 0],
        ['X', 0],
      ])
    } finally {
      warn.mockRestore()
    }
  })
})

