// List Organizer membership-map helpers. Locks down the two invariants the
// organizer's cached `lists.membershipMap` query + optimistic drop path rely
// on: (1) `buildMembershipMap` inverts per-list members into an
// accountId→listIds map, caps each list's pagination, and isolates a failing
// list; (2) `addMembershipLocal` is pure + idempotent (no duplicate dot, same
// reference when already a member).

// `mock`-prefixed so babel-plugin-jest-hoist lets the hoisted factory close
// over it; the real `src/lib/api/mastodon` (and its api-context/mmkv deps)
// never loads.
const mockGetMastodonListAccounts = jest.fn()
jest.mock('src/lib/api/mastodon', () => ({
  getMastodonListAccounts: (args: any) => mockGetMastodonListAccounts(args),
}))

import {
  addMembershipLocal,
  buildMembershipMap,
  type MembershipMap,
} from 'src/lib/lists/membershipMap'

type Page = { data: { id: string }[]; nextPage?: string }

// Build a fake pager: one fixed-size page per id in `pages`, threading
// `nextPage` so the loop advances, with the final page closing the chain.
function pager(pages: { id: string }[][]): Page[] {
  return pages.map((data, i) => ({
    data,
    nextPage: i < pages.length - 1 ? `cursor-${i + 1}` : undefined,
  }))
}

function acc(...ids: string[]) {
  return ids.map((id) => ({ id }))
}

beforeEach(() => {
  mockGetMastodonListAccounts.mockReset()
})

describe('addMembershipLocal', () => {
  test('adds a list to an account with no memberships', () => {
    expect(addMembershipLocal({}, 'a', 'L1')).toEqual({ a: ['L1'] })
  })

  test('appends a second list', () => {
    expect(addMembershipLocal({ a: ['L1'] }, 'a', 'L2')).toEqual({ a: ['L1', 'L2'] })
  })

  test('is idempotent — returns the same reference when already a member', () => {
    const prev: MembershipMap = { a: ['L1'] }
    expect(addMembershipLocal(prev, 'a', 'L1')).toBe(prev)
  })

  test('does not mutate the input', () => {
    const prev: MembershipMap = { a: ['L1'] }
    addMembershipLocal(prev, 'a', 'L2')
    expect(prev).toEqual({ a: ['L1'] })
  })
})

describe('buildMembershipMap', () => {
  test('inverts per-list members into accountId → listIds', async () => {
    // L1: {a, b}; L2: {b, c} → b belongs to both.
    const byList: Record<string, Page[]> = {
      L1: pager([acc('a', 'b')]),
      L2: pager([acc('b', 'c')]),
    }
    mockGetMastodonListAccounts.mockImplementation(({ listId, pageParam }: any) => {
      const pages = byList[listId]
      const idx = pageParam ? Number(String(pageParam).split('-')[1]) : 0
      return Promise.resolve(pages[idx])
    })

    const map = await buildMembershipMap(['L1', 'L2'])
    expect(map).toEqual({ a: ['L1'], b: ['L1', 'L2'], c: ['L2'] })
  })

  test('follows pagination across pages of one list', async () => {
    const pages = pager([acc('a', 'b'), acc('c')])
    mockGetMastodonListAccounts.mockImplementation(({ pageParam }: any) => {
      const idx = pageParam ? Number(String(pageParam).split('-')[1]) : 0
      return Promise.resolve(pages[idx])
    })

    const map = await buildMembershipMap(['L1'])
    expect(mockGetMastodonListAccounts).toHaveBeenCalledTimes(2)
    expect(map).toEqual({ a: ['L1'], b: ['L1'], c: ['L1'] })
  })

  test('stops paginating a list once the per-list cap is reached', async () => {
    // Every page is full and always advertises a next page; the cap must halt
    // it rather than the cursor running out.
    mockGetMastodonListAccounts.mockResolvedValue({
      data: acc('x', 'y'),
      nextPage: 'cursor-next',
    })

    await buildMembershipMap(['L1'], /* cap */ 2)
    // First page count = 2, `2 < 2` is false → exactly one fetch despite the
    // ever-present nextPage.
    expect(mockGetMastodonListAccounts).toHaveBeenCalledTimes(1)
  })

  test('a failing list never aborts the rest of the fan-out', async () => {
    mockGetMastodonListAccounts.mockImplementation(({ listId }: any) => {
      if (listId === 'L1') return Promise.reject(new Error('rate limited'))
      return Promise.resolve({ data: acc('c'), nextPage: undefined })
    })

    const map = await buildMembershipMap(['L1', 'L2'])
    expect(map).toEqual({ c: ['L2'] })
  })

  test('returns an empty map for no lists', async () => {
    const map = await buildMembershipMap([])
    expect(map).toEqual({})
    expect(mockGetMastodonListAccounts).not.toHaveBeenCalled()
  })
})
