// List Organizer (Surface E) data-spine round-trip — the live-server contract
// behind the drag-to-sort organizer. Mastodon-only: Pixelfed's /api/v1/lists is
// a stub, and the screen renders a "Mastodon-only" notice under a Pixelfed
// session, so there is nothing to assert there.
//
// The organizer never drags anything in this tier — the long-press-armed pan is
// a device-test item (Maestro can't drive it; see the E2E flow). What this tier
// pins is every network operation the screen leans on, end to end against a real
// Mastodon instance:
//
//   getMastodonLists / createMastodonList / deleteMastodonList — the right rail.
//   getMastodonListAccounts (empty + populated)                — membershipMap fan-out.
//   followAccountById + addMastodonListMembers                 — the drop write-path.
//   removeMastodonListMembers                                  — undo / cleanup.
//   getRelationshipsBatch                                      — the mutual-follow star.
//   getAccountFollowing                                        — the left column.
//
// Mastodon enforces "you must follow an account before listing it" (422
// "Account must be a followed account", verified live), so the member-add
// assertions follow a discovered local account first. The target is discovered
// at runtime from the instance directory so the test is portable to any Mastodon
// rig; if the directory is empty or disabled, the member-add block skips cleanly
// rather than failing. We only mutate follow state we created — a target the bot
// already followed (e.g. a durable E2E fixture) is left untouched.

import {
  addMastodonListMembers,
  createMastodonList,
  deleteMastodonList,
  followAccountById,
  getAccountFollowing,
  getMastodonListAccounts,
  getMastodonLists,
  getRelationshipsBatch,
  removeMastodonListMembers,
} from 'src/lib/api'
import { ContextForActiveAccount } from 'src/lib/api-context'
import type { Account } from 'src/lib/api-types'
import { describeWithRigType } from '../_helpers/loadTestRig'
import { RigCleanupTracker } from './_setup/cleanup'
import { __resetIntegrationMocks } from './_setup/jestMocks'
import { seedAndActivate } from './_setup/seedAccount'

type DirectoryAccount = { id: string; acct: string }

describeWithRigType('mastodon', 'list organizer data spine (mastodon, live)', (entry) => {
  const cleanup = new RigCleanupTracker()
  let listId = ''
  let followTarget: string | null = null
  let wasAlreadyFollowing = false

  beforeAll(async () => {
    __resetIntegrationMocks()
    seedAndActivate(entry)

    // The throwaway list every test in this block sorts against. A unique title
    // keeps it distinct from any durable fixture list the E2E tier maintains.
    const list = await createMastodonList({ title: `halflight-it-${Date.now()}` })
    listId = list.id
    cleanup.track(async () => {
      await deleteMastodonList(listId)
    })

    // Discover a followable local account from the instance directory. Local
    // (not remote) so the follow + list-add land synchronously rather than
    // federating asynchronously. Skip the member-add assertions if none exists.
    try {
      const api = ContextForActiveAccount()
      const dir = (await api.get('api/v1/directory', {
        local: true,
        limit: 8,
        order: 'active',
      })) as DirectoryAccount[]
      followTarget = dir.find((a) => String(a.id) !== String(entry.userId))?.id ?? null
    } catch {
      followTarget = null
    }

    // Record the starting follow state so cleanup only undoes a follow we add.
    if (followTarget) {
      const rels = await getRelationshipsBatch([followTarget])
      wasAlreadyFollowing = rels.some(
        (r) => String(r.id) === String(followTarget) && r.following === true
      )
    }
  })

  afterAll(async () => {
    await cleanup.drain()
  })

  it('creates a list that surfaces in the catalogue', async () => {
    const lists = await getMastodonLists()
    expect(lists.some((l) => l.id === listId)).toBe(true)
  })

  it('reports an empty member set for a fresh list', async () => {
    const page = await getMastodonListAccounts({ listId })
    expect(Array.isArray(page.data)).toBe(true)
    expect(page.data).toHaveLength(0)
  })

  it('refuses to list an account the bot does not follow (422)', async () => {
    if (!followTarget || wasAlreadyFollowing) return // not in a clean unfollowed state
    await expect(addMastodonListMembers(listId, [followTarget])).rejects.toThrow()
  })

  it('adds a followed account and reads it back', async () => {
    if (!followTarget) {
      // eslint-disable-next-line no-console
      console.warn('[lists.it] no followable directory account — member-add skipped')
      return
    }
    if (!wasAlreadyFollowing) {
      await followAccountById(followTarget)
      cleanup.trackFollow(ContextForActiveAccount(), followTarget)
    }

    // Deleting the list (tracked in beforeAll) cascades the membership away, so
    // we don't track a separate remove here — the explicit removal test below
    // already leaves the list empty.
    await addMastodonListMembers(listId, [followTarget])
    const page = await getMastodonListAccounts({ listId })
    expect(page.data.map((a) => String(a.id))).toContain(String(followTarget))
  })

  it('flags the follow direction via the batch relationships call', async () => {
    if (!followTarget) return
    const rels = await getRelationshipsBatch([followTarget, entry.userId])
    const target = rels.find((r) => String(r.id) === String(followTarget))
    expect(target?.following).toBe(true)
  })

  it("surfaces the target in the bot's following set (organizer left column)", async () => {
    if (!followTarget) return
    const page = await getAccountFollowing(entry.userId, '')
    const ids = (page.data as Account[]).map((a) => String(a.id))
    expect(ids).toContain(String(followTarget))
  })

  it('removes a member, leaving the list empty again', async () => {
    if (!followTarget) return
    await removeMastodonListMembers(listId, [followTarget])
    const page = await getMastodonListAccounts({ listId })
    expect(page.data.map((a) => String(a.id))).not.toContain(String(followTarget))
  })
})
