// `patchAnchorReplyCount` cache-write tests. The helper bumps the cached
// `posts.byId(anchorId).reply_count` (and Mastodon's `replies_count`) to
// `walkedSize` whenever the freshly-walked subtree size exceeds the
// stamped server-side count. Defines the freshness pipeline the post
// screen relies on so the FeedPost row on Home / Profile / Search
// reflects the walked subtree without a refetch.
//
// Three load-bearing invariants under test:
// 1. Never lower an existing count — the server may have a higher
//    truthful count we haven't walked.
// 2. Never throw on a missing cache entry — first-time threads land
//    here before `posts.byId` is populated.
// 3. Always patch BOTH wire fields — Mastodon and Pixelfed renderers
//    each read a different field, so writing only one silently leaves
//    the other zero on the cross-server consumer.

import { createMmkvInstances } from './_helpers/mockMmkv'
import { createSecureStoreMock } from './_helpers/mockSecureStore'

const mockMmkv = createMmkvInstances()
const mockSecure = createSecureStoreMock()

jest.mock('react-native-mmkv', () => ({
  createMMKV: ({
    id,
    compareBeforeSet,
  }: {
    id: string
    compareBeforeSet?: boolean
  }) => mockMmkv.getOrCreate(id, { compareBeforeSet }),
}))

jest.mock('expo-secure-store', () => ({
  getItem: (key: string) => mockSecure.getItem(key),
  setItem: (key: string, value: string) => mockSecure.setItem(key, value),
  deleteItemAsync: (key: string) => mockSecure.deleteItemAsync(key),
}))

// Lazy require: `patchAnchorReplyCount` imports `./client` (which constructs
// the singleton QueryClient with the MMKV-backed persister) and `queryKeys`
// (which calls `acct()` on every key construction). Both must run AFTER the
// MMKV / SecureStore mocks are registered.
type AccountStoreModule = typeof import('../src/state/accountStore')
type PostsModule = typeof import('../src/lib/queries/posts')
type ClientModule = typeof import('../src/lib/queries/client')
type KeysModule = typeof import('../src/lib/queries/keys')
type Status = import('../src/lib/api-types').Status

let AccountStore: AccountStoreModule['AccountStore']
let patchAnchorReplyCount: PostsModule['patchAnchorReplyCount']
let queryClient: ClientModule['queryClient']
let queryKeys: KeysModule['queryKeys']

beforeEach(() => {
  mockMmkv.reset()
  mockSecure.reset()
  // Fresh module isolation per test so the singleton QueryClient's cache
  // doesn't bleed between cases. Same shape `accountStore.ts` uses for the
  // store's own snapshot caches.
  jest.isolateModules(() => {
    AccountStore = (
      require('../src/state/accountStore') as AccountStoreModule
    ).AccountStore
    patchAnchorReplyCount = (
      require('../src/lib/queries/posts') as PostsModule
    ).patchAnchorReplyCount
    queryClient = (require('../src/lib/queries/client') as ClientModule).queryClient
    queryKeys = (require('../src/lib/queries/keys') as KeysModule).queryKeys
  })

  // queryKeys.posts.byId requires an active account — every key is
  // implicitly per-account-scoped. Set one up so the helper can reach
  // its slot.
  const a = AccountStore.upsert({
    type: 'pixelfed',
    instance: 'test.example',
    userId: 'u_a',
    username: 'tester',
    acct: 'tester@test.example',
    displayName: 'Tester',
    avatarUrl: null,
  })
  AccountStore.setActive(a.accountId)
})

function statusFixture(overrides: Partial<Status>): Status {
  // Production code only reads `reply_count` / `replies_count` here; pad
  // the rest with `as unknown as Status` rather than dragging in 80 fields
  // of MastoAPI noise.
  return overrides as unknown as Status
}

describe('patchAnchorReplyCount', () => {
  it('is a no-op when no anchor entry is cached (defence against early calls)', () => {
    // First-time thread fetch lands the descendants-walk before the anchor
    // row is in cache. The helper must accept that shape — never throw,
    // never insert a half-baked status under `posts.byId`.
    const id = 'anchor-1'
    expect(queryClient.getQueryData(queryKeys.posts.byId(id))).toBeUndefined()
    expect(() => patchAnchorReplyCount(id, 5)).not.toThrow()
    expect(queryClient.getQueryData(queryKeys.posts.byId(id))).toBeUndefined()
  })

  it('bumps reply_count + replies_count when walked size exceeds cached count', () => {
    const id = 'anchor-2'
    const before = statusFixture({ id, reply_count: 3 })
    queryClient.setQueryData(queryKeys.posts.byId(id), before)

    patchAnchorReplyCount(id, 5)

    const after = queryClient.getQueryData<Status>(queryKeys.posts.byId(id))
    expect(after).toBeDefined()
    // Both wire shapes are written so every consumer reads correctly,
    // regardless of which field they prefer.
    expect((after as any)?.reply_count).toBe(5)
    expect((after as any)?.replies_count).toBe(5)
  })

  it('preserves a higher cached count (never lowers — server may know more)', () => {
    // The walker bound is the *minimum* truthful subtree size. The server
    // may have stamped a higher count from work we haven't seen yet
    // (e.g. a federated reply that's not visible to this viewer but is
    // still counted by the server's `reply_count` stamp). Never overwrite
    // downward.
    const id = 'anchor-3'
    const before = statusFixture({ id, reply_count: 10 })
    queryClient.setQueryData(queryKeys.posts.byId(id), before)

    patchAnchorReplyCount(id, 5)

    const after = queryClient.getQueryData<Status>(queryKeys.posts.byId(id))
    expect((after as any)?.reply_count).toBe(10)
    // Mastodon's plural field stays untouched too — no half-write.
    expect((after as any)?.replies_count).toBeUndefined()
  })

  it('promotes a Mastodon-shaped row to both fields when walked size beats replies_count', () => {
    // Mastodon-served accounts cache `replies_count` (plural) only.
    // `getReplyCount` falls back to it inside the helper's read; the
    // write must populate both fields so cross-server consumers
    // (FeedPost.tsx, PostActions.tsx, EventDetailView.tsx, etc.) all
    // see the walked size on the next render.
    const id = 'anchor-4'
    const before = statusFixture({ id, replies_count: 3 })
    queryClient.setQueryData(queryKeys.posts.byId(id), before)

    patchAnchorReplyCount(id, 7)

    const after = queryClient.getQueryData<Status>(queryKeys.posts.byId(id))
    expect((after as any)?.reply_count).toBe(7)
    expect((after as any)?.replies_count).toBe(7)
  })

  it('is a no-op when walked size equals cached count (no churn write)', () => {
    // Equal sizes should not produce a new object reference — preserves
    // referential equality for downstream React renderers that bail on
    // `===` comparisons (FlashList in particular reuses cells aggressively
    // on key match, and a churn write with the same numbers would mark
    // the row dirty for no reason).
    const id = 'anchor-5'
    const before = statusFixture({ id, reply_count: 5 })
    queryClient.setQueryData(queryKeys.posts.byId(id), before)
    const handle = queryClient.getQueryData<Status>(queryKeys.posts.byId(id))

    patchAnchorReplyCount(id, 5)

    // Same reference — `getReplyCount(prev) === walkedSize` returns
    // `prev` verbatim from the setQueryData updater.
    expect(queryClient.getQueryData<Status>(queryKeys.posts.byId(id))).toBe(handle)
  })

  it('preserves unrelated fields on the cached anchor row', () => {
    // Spread-update guard: the helper must not drop fields that other
    // surfaces care about (favourited, account, content, ...). Test a
    // representative subset — if the spread were ever broken, every
    // consumer would silently lose state.
    const id = 'anchor-6'
    const before = statusFixture({
      id,
      reply_count: 2,
      favourited: true,
      favourites_count: 17,
      content: '<p>hello</p>',
      created_at: '2026-04-30T00:00:00Z',
    })
    queryClient.setQueryData(queryKeys.posts.byId(id), before)

    patchAnchorReplyCount(id, 9)

    const after = queryClient.getQueryData<Status>(queryKeys.posts.byId(id))
    expect((after as any)?.reply_count).toBe(9)
    expect((after as any)?.replies_count).toBe(9)
    expect(after?.favourited).toBe(true)
    expect(after?.favourites_count).toBe(17)
    expect(after?.content).toBe('<p>hello</p>')
    expect(after?.created_at).toBe('2026-04-30T00:00:00Z')
  })
})
