// Shared adapter assertion suite.
//
// The Pixelfed and Mastodon adapters implement the same `ProtocolAdapter`
// interface — the wire mechanics diverge but the user-facing contract is
// identical. The fixture-tier tests in `__tests__/protocol/adapters.ts` pin
// the wire shapes against canned JSON; this suite pins the *behaviour*
// against a real server. Same six operations, same expectations.
//
// Called from each adapters.*.it.ts test file with the appropriate adapter
// + rig entry; everything else is identical. Adding a new operation to
// `ProtocolAdapter` means adding one assertion here, and both protocol
// test files exercise it automatically.

import { activeProtocol, parseHandle } from 'src/lib/protocol'
import { AccountStore } from 'src/state/accountStore'
import type { RigAccount } from '../../_helpers/loadTestRig'
import { seedAndActivate } from './seedAccount'
import '../_setup/jestMocks' // hoisted; ensures mocks load before src/

/**
 * Run the canonical adapter assertion set against `entry`. The describe
 * wrapper outside is the protocol-name handle; everything inside is
 * shared.
 *
 * Keep this suite read-only — any operation that creates or mutates
 * server-side state belongs in `compose.it.ts` (with cleanup) or its own
 * tracker-aware file. Read-only ops are safe to run on every push.
 */
export function runAdapterContractSuite(entry: RigAccount): void {
  beforeAll(() => {
    seedAndActivate(entry)
  })

  it('lookupAccount resolves the bot itself (local handle)', async () => {
    const handle = parseHandle(entry.acct, entry.instance)
    const result = await activeProtocol().lookupAccount(handle)
    expect(result.kind).toBe('found')
    if (result.kind === 'found') {
      expect(String(result.account.id)).toBe(entry.userId)
      expect(result.account.username).toBe(entry.username)
    }
  })

  it('lookupAccount returns not-found for a clearly missing handle', async () => {
    const handle = parseHandle(
      'this-handle-definitely-does-not-exist-0123456789abcdef',
      entry.instance
    )
    const result = await activeProtocol().lookupAccount(handle)
    // 'not-found' is the green path; 'rejected' is acceptable when the
    // server returns 5xx mid-test (we don't want a flaky network to fail
    // CI). We just refuse 'found'.
    expect(result.kind).not.toBe('found')
  })

  it('fetchHashtagInfo returns a tag object with a matching name', async () => {
    // 'photography' is broadly populated on both protocols and stable across
    // years. If a tag-namespace policy ever takes it down on a specific
    // server, swap to another high-frequency tag.
    const tag = await activeProtocol().fetchHashtagInfo('photography')
    expect(tag).toBeTruthy()
    expect(typeof tag.name).toBe('string')
    expect(tag.name?.toLowerCase()).toBe('photography')
  })

  it('fetchHashtagFeed returns a HashtagFeedPage with photo items', async () => {
    const page = await activeProtocol().fetchHashtagFeed('photography', {
      cursor: {},
      onlyMedia: true,
      filterSensitive: true,
    })
    expect(Array.isArray(page.items)).toBe(true)
    // Empty page is acceptable on a freshly created bot's view; presence of
    // `nextCursor` shape is what we pin.
    if (page.items.length > 0) {
      const first = page.items[0]
      expect(first.id).toBeTruthy()
      expect(Array.isArray(first.media_attachments)).toBe(true)
    }
    if (page.nextCursor) {
      expect(
        typeof page.nextCursor.maxId === 'string' || page.nextCursor.maxId === undefined
      ).toBe(true)
    }
  })

  it('fetchHashtagRelated returns an array (possibly empty on Mastodon)', async () => {
    const related = await activeProtocol().fetchHashtagRelated('photography')
    expect(Array.isArray(related)).toBe(true)
    if (entry.type === 'mastodon') {
      // Mastodon has no related-tags endpoint — adapter MUST return [].
      expect(related).toEqual([])
    }
  })

  it('searchAccounts returns an Account[] for a generic query', async () => {
    const results = await activeProtocol().searchAccounts('admin', {
      resolve: false,
      limit: 5,
    })
    expect(Array.isArray(results)).toBe(true)
    for (const r of results) {
      expect(r.id).toBeTruthy()
      expect(typeof r.username).toBe('string')
    }
  })

  it('getNotificationUnreadCount returns a non-negative number', async () => {
    const n = await activeProtocol().getNotificationUnreadCount()
    expect(typeof n).toBe('number')
    expect(Number.isFinite(n)).toBe(true)
    expect(n).toBeGreaterThanOrEqual(0)
  })

  it('AccountStore.activeAccount() points at the rig entry', () => {
    const active = AccountStore.activeAccount()
    expect(active).toBeTruthy()
    expect(active?.instance).toBe(entry.instance)
    expect(active?.userId).toBe(entry.userId)
    expect(active?.type).toBe(entry.type)
  })
}
