// Adapter coverage tests for `ProtocolAdapter`.
//
// The goal is one fixture-fed test per operation per protocol, asserting the
// adapter returns the canonical
// client-side shape. We mock `ApiContext` directly (the firewall asserts on
// account type already-trusted upstream) and capture URL + params at the
// `.get()` boundary. Fixtures live under `./fixtures/` and are pinned live
// from a test Pixelfed instance + mastodon.social (refresh quarterly).

import accountMastodonLocal from './fixtures/account.mastodon.local.json'
import accountMastodonSearch from './fixtures/account.mastodon.remote.search.json'
import accountPixelfedLocal from './fixtures/account.pixelfed.local.json'
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),
}))

type GetCall = { path: string; params?: Record<string, unknown> }

/** Build a fake ApiContext-shaped stub that records every .get() call and
 *  returns the next queued response in order. Each test sets up the
 *  response queue and then asserts on the recorded calls. */
function makeApi(responses: unknown[]) {
  const calls: GetCall[] = []
  const queue = [...responses]
  const api = {
    get: jest.fn(async (path: string, params?: Record<string, unknown>) => {
      calls.push({ path, params })
      if (queue.length === 0) {
        throw new Error(`unexpected .get(${path}) — no fixture queued`)
      }
      const next = queue.shift()
      if (next instanceof Error) throw next
      return next
    }),
  }
  return { api, calls }
}

beforeEach(() => {
  mockMmkv.reset()
  mockSecure.reset()
  jest.resetModules()
})

// Pixelfed adapter ---------------------------------------------------------

describe('pixelfedAdapter.lookupAccount', () => {
  it('local handle → /api/v1.1/accounts/username/{u}', async () => {
    const { api, calls } = makeApi([accountPixelfedLocal])
    jest.doMock('../../src/lib/api-context', () => ({
      ApiError: class ApiError extends Error {
        status: number
        constructor(status: number, msg: string) {
          super(msg)
          this.status = status
        }
      },
      pixelfedActiveContext: () => api,
      mastodonActiveContext: () => {
        throw new Error('wrong protocol')
      },
    }))
    const { pixelfedAdapter } = require('../../src/lib/protocol/pixelfedAdapter')
    const { parseHandle } = require('../../src/lib/protocol/handle')

    const res = await pixelfedAdapter.lookupAccount(
      parseHandle('@alice', 'pixelfed.social')
    )

    expect(res).toEqual({ kind: 'found', account: accountPixelfedLocal })
    expect(calls).toHaveLength(1)
    expect(calls[0].path).toBe('api/v1.1/accounts/username/alice')
  })

  it('returns not-found for the empty-array Pixelfed miss sentinel', async () => {
    const { api } = makeApi([[]])
    jest.doMock('../../src/lib/api-context', () => ({
      ApiError: class ApiError extends Error {
        status: number
        constructor(status: number, msg: string) {
          super(msg)
          this.status = status
        }
      },
      pixelfedActiveContext: () => api,
    }))
    const { pixelfedAdapter } = require('../../src/lib/protocol/pixelfedAdapter')
    const { parseHandle } = require('../../src/lib/protocol/handle')

    const res = await pixelfedAdapter.lookupAccount(
      parseHandle('@nobody', 'pixelfed.social')
    )
    expect(res.kind).toBe('not-found')
  })

  it('remote handle → /api/v2/search?resolve=1&type=accounts', async () => {
    const { api, calls } = makeApi([
      {
        accounts: [{ ...accountMastodonLocal, acct: 'COSCUP@hollo.social' }],
        statuses: [],
        hashtags: [],
      },
    ])
    jest.doMock('../../src/lib/api-context', () => ({
      ApiError: class ApiError extends Error {
        status: number
        constructor(status: number, msg: string) {
          super(msg)
          this.status = status
        }
      },
      pixelfedActiveContext: () => api,
    }))
    const { pixelfedAdapter } = require('../../src/lib/protocol/pixelfedAdapter')
    const { parseHandle } = require('../../src/lib/protocol/handle')

    const res = await pixelfedAdapter.lookupAccount(
      parseHandle('@COSCUP@hollo.social', 'pixelfed.social')
    )

    expect(res.kind).toBe('found')
    expect(calls).toHaveLength(1)
    expect(calls[0].path).toBe('api/v2/search')
    expect(calls[0].params).toMatchObject({
      q: 'COSCUP@hollo.social',
      type: 'accounts',
      resolve: 1,
      limit: 1,
    })
  })
})

describe('pixelfedAdapter.fetchHashtagInfo', () => {
  it('strips leading # before hitting the wire', async () => {
    const { api, calls } = makeApi([{ name: 'japanese', url: '' }])
    jest.doMock('../../src/lib/api-context', () => ({
      ApiError: class ApiError extends Error {},
      pixelfedActiveContext: () => api,
    }))
    const { pixelfedAdapter } = require('../../src/lib/protocol/pixelfedAdapter')

    await pixelfedAdapter.fetchHashtagInfo('#japanese')
    expect(calls[0].path).toBe('api/v1/tags/japanese')
  })

  it('encodeURIComponents Unicode tag names', async () => {
    const { api, calls } = makeApi([{ name: 'ひらがな', url: '' }])
    jest.doMock('../../src/lib/api-context', () => ({
      ApiError: class ApiError extends Error {},
      pixelfedActiveContext: () => api,
    }))
    const { pixelfedAdapter } = require('../../src/lib/protocol/pixelfedAdapter')

    await pixelfedAdapter.fetchHashtagInfo('ひらがな')
    expect(calls[0].path).toBe(`api/v1/tags/${encodeURIComponent('ひらがな')}`)
  })
})

describe('pixelfedAdapter.fetchHashtagFeed', () => {
  it('omits max_id on the first page; filters pf_type === photo', async () => {
    const page = [
      { id: '10', pf_type: 'photo', media_attachments: [{}], sensitive: false },
      { id: '9', pf_type: 'photo', media_attachments: [], sensitive: false }, // no media → drop
      { id: '8', pf_type: 'text', media_attachments: [{}], sensitive: false }, // wrong type → drop
      { id: '7', pf_type: 'photo', media_attachments: [{}], sensitive: true }, // NSFW → drop
    ]
    const { api, calls } = makeApi([page])
    jest.doMock('../../src/lib/api-context', () => ({
      ApiError: class ApiError extends Error {},
      pixelfedActiveContext: () => api,
    }))
    const { pixelfedAdapter } = require('../../src/lib/protocol/pixelfedAdapter')

    const res = await pixelfedAdapter.fetchHashtagFeed('food', { cursor: {} })

    expect(calls[0].path).toBe('api/v1/timelines/tag/food')
    expect(calls[0].params?.max_id).toBeUndefined()
    expect(calls[0].params?.only_media).toBe('true')
    expect(res.items).toHaveLength(1)
    expect(res.items[0].id).toBe('10')
    expect(res.nextCursor?.maxId).toBe('9') // (BigInt(10) - 1n).toString()
  })

  it('forwards max_id for subsequent pages', async () => {
    const { api, calls } = makeApi([[]])
    jest.doMock('../../src/lib/api-context', () => ({
      ApiError: class ApiError extends Error {},
      pixelfedActiveContext: () => api,
    }))
    const { pixelfedAdapter } = require('../../src/lib/protocol/pixelfedAdapter')

    await pixelfedAdapter.fetchHashtagFeed('food', { cursor: { maxId: '42' } })
    expect(calls[0].params?.max_id).toBe('42')
  })
})

// Mastodon adapter ---------------------------------------------------------

describe('mastodonAdapter.lookupAccount', () => {
  it('local handle → /api/v1/accounts/lookup', async () => {
    const { api, calls } = makeApi([accountMastodonLocal])
    jest.doMock('../../src/lib/api-context', () => ({
      ApiError: class ApiError extends Error {
        status: number
        constructor(status: number, msg: string) {
          super(msg)
          this.status = status
        }
      },
      mastodonActiveContext: () => api,
    }))
    const { mastodonAdapter } = require('../../src/lib/protocol/mastodonAdapter')
    const { parseHandle } = require('../../src/lib/protocol/handle')

    const res = await mastodonAdapter.lookupAccount(
      parseHandle('@bob', 'mastodon.social')
    )
    expect(res).toEqual({ kind: 'found', account: accountMastodonLocal })
    expect(calls).toHaveLength(1)
    expect(calls[0].path).toBe('api/v1/accounts/lookup')
    expect(calls[0].params?.acct).toBe('bob')
  })

  it('local handle 404 returns not-found without falling through to search', async () => {
    class ApiError extends Error {
      status = 404
    }
    const { api, calls } = makeApi([new ApiError('missing')])
    jest.doMock('../../src/lib/api-context', () => ({
      ApiError,
      mastodonActiveContext: () => api,
    }))
    const { mastodonAdapter } = require('../../src/lib/protocol/mastodonAdapter')
    const { parseHandle } = require('../../src/lib/protocol/handle')

    const res = await mastodonAdapter.lookupAccount(
      parseHandle('nobody', 'mastodon.social')
    )
    expect(res.kind).toBe('not-found')
    expect(calls).toHaveLength(1)
  })

  it('remote handle 404 falls through to /api/v2/search?resolve=1', async () => {
    class ApiError extends Error {
      status = 404
    }
    const { api, calls } = makeApi([new ApiError('missing'), accountMastodonSearch])
    jest.doMock('../../src/lib/api-context', () => ({
      ApiError,
      mastodonActiveContext: () => api,
    }))
    const { mastodonAdapter } = require('../../src/lib/protocol/mastodonAdapter')
    const { parseHandle } = require('../../src/lib/protocol/handle')

    const res = await mastodonAdapter.lookupAccount(
      parseHandle('@COSCUP@hollo.social', 'mastodon.social')
    )
    expect(res.kind).toBe('found')
    expect(calls).toHaveLength(2)
    expect(calls[0].path).toBe('api/v1/accounts/lookup')
    expect(calls[1].path).toBe('api/v2/search')
    expect(calls[1].params?.resolve).toBe(1)
  })
})

describe('mastodonAdapter.fetchHashtagInfo', () => {
  it('encodeURIComponents the name and strips #', async () => {
    const { api, calls } = makeApi([{ name: 'food', url: '' }])
    jest.doMock('../../src/lib/api-context', () => ({
      ApiError: class ApiError extends Error {},
      mastodonActiveContext: () => api,
    }))
    const { mastodonAdapter } = require('../../src/lib/protocol/mastodonAdapter')

    await mastodonAdapter.fetchHashtagInfo('#food')
    expect(calls[0].path).toBe('api/v1/tags/food')
  })
})

describe('mastodonAdapter.fetchHashtagFeed', () => {
  it('filters on media_attachments[].type instead of pf_type', async () => {
    const page = [
      { id: '10', media_attachments: [{ type: 'image' }], sensitive: false },
      { id: '9', media_attachments: [{ type: 'gifv' }], sensitive: false },
      { id: '8', media_attachments: [{ type: 'audio' }], sensitive: false }, // drop
      { id: '7', media_attachments: [], sensitive: false }, // drop
      { id: '6', media_attachments: [{ type: 'image' }], sensitive: true }, // drop
    ]
    const { api, calls } = makeApi([page])
    jest.doMock('../../src/lib/api-context', () => ({
      ApiError: class ApiError extends Error {},
      mastodonActiveContext: () => api,
    }))
    const { mastodonAdapter } = require('../../src/lib/protocol/mastodonAdapter')

    const res = await mastodonAdapter.fetchHashtagFeed('food', { cursor: {} })

    expect(calls[0].params?.max_id).toBeUndefined()
    expect(calls[0].params?.only_media).toBe('true')
    expect(res.items.map((i: { id: string }) => i.id)).toEqual(['10', '9'])
    expect(res.nextCursor?.maxId).toBe('8')
  })

  it('does not send max_id=undefined for the first page (Bug #4 regression)', async () => {
    const { api, calls } = makeApi([[]])
    jest.doMock('../../src/lib/api-context', () => ({
      ApiError: class ApiError extends Error {},
      mastodonActiveContext: () => api,
    }))
    const { mastodonAdapter } = require('../../src/lib/protocol/mastodonAdapter')

    await mastodonAdapter.fetchHashtagFeed('food', { cursor: {} })
    expect(calls[0].params).not.toHaveProperty('max_id')
  })
})

describe('mastodonAdapter.fetchHashtagRelated', () => {
  it('returns an empty array without hitting the network', async () => {
    const { api, calls } = makeApi([])
    jest.doMock('../../src/lib/api-context', () => ({
      ApiError: class ApiError extends Error {},
      mastodonActiveContext: () => api,
    }))
    const { mastodonAdapter } = require('../../src/lib/protocol/mastodonAdapter')

    const res = await mastodonAdapter.fetchHashtagRelated('food')
    expect(res).toEqual([])
    expect(calls).toHaveLength(0)
  })
})

describe('mastodonAdapter.getNotificationUnreadCount', () => {
  it('reads /api/v1/notifications/unread_count', async () => {
    const { api, calls } = makeApi([{ count: 17 }])
    jest.doMock('../../src/lib/api-context', () => ({
      ApiError: class ApiError extends Error {},
      mastodonActiveContext: () => api,
    }))
    const { mastodonAdapter } = require('../../src/lib/protocol/mastodonAdapter')

    const n = await mastodonAdapter.getNotificationUnreadCount()
    expect(n).toBe(17)
    expect(calls[0].path).toBe('api/v1/notifications/unread_count')
  })

  it('falls back to 0 when the response is malformed', async () => {
    const { api } = makeApi([{ wrong: 'shape' }])
    jest.doMock('../../src/lib/api-context', () => ({
      ApiError: class ApiError extends Error {},
      mastodonActiveContext: () => api,
    }))
    const { mastodonAdapter } = require('../../src/lib/protocol/mastodonAdapter')

    const n = await mastodonAdapter.getNotificationUnreadCount()
    expect(n).toBe(0)
  })
})

describe('pixelfedAdapter.getNotificationUnreadCount', () => {
  it('derives count from /api/v1/markers + /api/v1/notifications min_id', async () => {
    const { api, calls } = makeApi([
      { notifications: { last_read_id: '100' } },
      [{ id: '101' }, { id: '102' }],
    ])
    jest.doMock('../../src/lib/api-context', () => ({
      ApiError: class ApiError extends Error {},
      pixelfedActiveContext: () => api,
    }))
    const { pixelfedAdapter } = require('../../src/lib/protocol/pixelfedAdapter')

    const n = await pixelfedAdapter.getNotificationUnreadCount()
    expect(n).toBe(2)
    expect(calls[0].path).toBe('api/v1/markers')
    expect(calls[1].path).toBe('api/v1/notifications')
    expect(calls[1].params?.min_id).toBe('100')
  })

  it('returns 0 on a fresh install (no marker)', async () => {
    const { api } = makeApi([{ notifications: null }])
    jest.doMock('../../src/lib/api-context', () => ({
      ApiError: class ApiError extends Error {},
      pixelfedActiveContext: () => api,
    }))
    const { pixelfedAdapter } = require('../../src/lib/protocol/pixelfedAdapter')

    const n = await pixelfedAdapter.getNotificationUnreadCount()
    expect(n).toBe(0)
  })
})
