import {afterAll, beforeAll, expect, test} from 'bun:test'

import {
  befriend,
  cleanupCreatedUsers,
  createCrumb as createCrumbFixture,
  createTrail as createTrailFixture,
  createUser,
  request,
  requestFriend,
  type TestUser,
} from '../test-fixtures'

type FeedItem = {
  id: string
  type: 'crumb' | 'trail'
  author: {id: string; handle: string}
  crumb?: {id: string; name: string}
  trail?: {id: string; name: string; pinned: boolean}
  createdAt: string
}

const walkFeed = async (
  as: TestUser,
  path: string,
  limit: number,
) => {
  const items: FeedItem[] = []
  let cursor: string | null = null
  let pages = 0
  do {
    const query: string = cursor
      ? `limit=${limit}&cursor=${encodeURIComponent(cursor)}`
      : `limit=${limit}`
    const res = await request(as, 'GET', `${path}?${query}`)
    expect(res.status).toBe(200)
    items.push(...res.json.items)
    cursor = res.json.nextCursor
    pages += 1
  } while (cursor && pages < 10)
  return items
}

let alice: TestUser
let bob: TestUser
let stranger: TestUser
let pending: TestUser

// created in order, so newest-first is the reverse of this list
const createdNames: string[] = []

const createCrumb = async (as: TestUser, name: string) => {
  const id = await createCrumbFixture(as, name)
  createdNames.push(name)
  return {id}
}

const createTrail = async (as: TestUser, name: string) => {
  const id = await createTrailFixture(as, {name})
  createdNames.push(name)
  return {id}
}

let pinnedTrailId: string

beforeAll(async () => {
  ;[alice, bob, stranger, pending] = await Promise.all([
    createUser(),
    createUser(),
    createUser(),
    createUser(),
  ])
  await befriend(alice, bob)
  await requestFriend(pending, bob)

  await createCrumb(alice, 'alice crumb one')
  await createTrail(bob, 'bob trail one')
  await createCrumb(alice, 'alice crumb two')
  pinnedTrailId = (await createTrail(alice, 'alice trail one')).id
  await createCrumb(bob, 'bob crumb one')

  await request(stranger, 'POST', '/crumbs', {name: 'stranger crumb'})
  await request(pending, 'POST', '/crumbs', {name: 'pending crumb'})
})

afterAll(cleanupCreatedUsers)

const itemName = (item: FeedItem) =>
  item.type === 'crumb' ? item.crumb?.name : item.trail?.name

test('feed interleaves own and friend activity newest first', async () => {
  const res = await request(bob, 'GET', '/feed')
  expect(res.status).toBe(200)

  const items: FeedItem[] = res.json.items
  expect(items.map(itemName)).toEqual([...createdNames].reverse())
  expect(items.map((i) => i.type)).toEqual([
    'crumb',
    'trail',
    'crumb',
    'trail',
    'crumb',
  ])
  for (const item of items) {
    const entity = item.type === 'crumb' ? item.crumb : item.trail
    expect(entity).toBeTruthy()
    expect(item.id).toBe(`${item.type}:${entity!.id}`)
    const author = [alice, bob].find((u) => u.id === item.author.id)
    expect(author).toBeTruthy()
    expect(item.author.handle).toBe(author!.handle)
  }
})

test('feed excludes non-friends and pending requesters', async () => {
  const res = await request(bob, 'GET', '/feed')
  const authorIds = new Set(
    res.json.items.map((i: FeedItem) => i.author.id),
  )
  expect(authorIds.has(stranger.id)).toBe(false)
  expect(authorIds.has(pending.id)).toBe(false)
})

test('feed paginates across mixed types', async () => {
  const first = await request(bob, 'GET', '/feed?limit=2')
  expect(first.json.items).toHaveLength(2)
  expect(first.json.nextCursor).toBeTruthy()

  const items = await walkFeed(bob, '/feed', 2)
  const ids = new Set(items.map((i) => i.id))
  expect(ids.size).toBe(items.length)
  expect(items.map(itemName)).toEqual([...createdNames].reverse())
})

test("trail feed items carry the viewer's pin state, not the author's", async () => {
  await request(alice, 'PUT', `/trails/${pinnedTrailId}/pin`)

  const findPinned = (items: FeedItem[]) =>
    items.find((i) => i.id === `trail:${pinnedTrailId}`)

  const own = await request(alice, 'GET', '/feed')
  expect(findPinned(own.json.items)?.trail?.pinned).toBe(true)

  const viewed = await request(bob, 'GET', '/feed')
  expect(findPinned(viewed.json.items)?.trail?.pinned).toBe(false)
})

test('profile activity returns only that author to a friend', async () => {
  const res = await request(bob, 'GET', `/users/${alice.id}/activity`)
  expect(res.status).toBe(200)

  const items: FeedItem[] = res.json.items
  expect(items.map(itemName)).toEqual([
    'alice trail one',
    'alice crumb two',
    'alice crumb one',
  ])
  for (const item of items) expect(item.author.id).toBe(alice.id)
})

test('profile activity is friends-only', async () => {
  const denied = await request(
    stranger,
    'GET',
    `/users/${alice.id}/activity`,
  )
  expect(denied.status).toBe(403)

  const self = await request(
    alice,
    'GET',
    `/users/${alice.id}/activity`,
  )
  expect(self.status).toBe(200)
})

test('profile activity paginates', async () => {
  const first = await request(
    bob,
    'GET',
    `/users/${alice.id}/activity?limit=2`,
  )
  expect(first.json.items).toHaveLength(2)
  expect(first.json.nextCursor).toBeTruthy()

  const items = await walkFeed(bob, `/users/${alice.id}/activity`, 2)
  expect(items).toHaveLength(3)
  expect(new Set(items.map((i) => i.id)).size).toBe(3)
})
