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

import {
  cleanupCreatedUsers,
  createUser,
  request,
  type TestUser,
} from '../test-fixtures'

let owner: TestUser
let joiner: TestUser

beforeAll(async () => {
  ;[owner, joiner] = await Promise.all([createUser(), createUser()])
})

afterAll(cleanupCreatedUsers)

const createTrailWithCrumb = async () => {
  const crumb = await request(owner, 'POST', '/crumbs', {
    name: 'Shared spot',
  })
  const trail = await request(owner, 'POST', '/trails', {
    name: 'Social trail',
    crumbId: crumb.json.crumb.id,
  })
  return {
    trailId: trail.json.trail.id as string,
    crumbId: crumb.json.crumb.id as string,
  }
}

test('join link lifecycle: create, join, revoke', async () => {
  const {trailId} = await createTrailWithCrumb()

  const link = await request(
    owner,
    'POST',
    `/trails/${trailId}/share-link`,
  )
  expect(link.status).toBe(200)
  const url: string = link.json.url
  const token = url.split('/').pop()!

  // idempotent: same active link returned
  const again = await request(
    owner,
    'POST',
    `/trails/${trailId}/share-link`,
  )
  expect(again.json.url).toBe(url)

  // strangers cannot mint links
  const denied = await request(
    joiner,
    'POST',
    `/trails/${trailId}/share-link`,
  )
  expect(denied.status).toBe(404)

  const joined = await request(
    joiner,
    'POST',
    `/trails/join/${token}`,
  )
  expect(joined.status).toBe(200)
  expect(joined.json.trailId).toBe(trailId)

  // joiner is now an accepted member and sees the trail
  const trails = await request(joiner, 'GET', '/trails')
  expect(
    trails.json.trails.some((t: {id: string}) => t.id === trailId),
  ).toBe(true)

  const revoked = await request(
    owner,
    'DELETE',
    `/trails/${trailId}/share-link`,
  )
  expect(revoked.status).toBe(200)
  const afterRevoke = await request(
    owner,
    'POST',
    `/trails/join/${token}`,
  )
  expect(afterRevoke.status).toBe(404)
})

test('reactions: members react, summaries aggregate', async () => {
  const {trailId, crumbId} = await createTrailWithCrumb()
  const link = await request(
    owner,
    'POST',
    `/trails/${trailId}/share-link`,
  )
  const token = (link.json.url as string).split('/').pop()!
  await request(joiner, 'POST', `/trails/join/${token}`)

  const react = await request(
    joiner,
    'PUT',
    `/trails/${trailId}/crumbs/${crumbId}/reactions`,
    {emoji: '🔥'},
  )
  expect(react.status).toBe(200)
  await request(
    owner,
    'PUT',
    `/trails/${trailId}/crumbs/${crumbId}/reactions`,
    {emoji: '🔥'},
  )

  const detail = await request(owner, 'GET', `/trails/${trailId}`)
  const crumb = detail.json.trail.crumbs.find(
    (cr: {id: string}) => cr.id === crumbId,
  )
  expect(crumb.reactions).toEqual([
    {emoji: '🔥', count: 2, mine: true},
  ])

  // re-reacting replaces, deleting removes
  await request(
    joiner,
    'PUT',
    `/trails/${trailId}/crumbs/${crumbId}/reactions`,
    {emoji: '🤔'},
  )
  await request(
    owner,
    'DELETE',
    `/trails/${trailId}/crumbs/${crumbId}/reactions`,
  )
  const after = await request(joiner, 'GET', `/trails/${trailId}`)
  const afterCrumb = after.json.trail.crumbs.find(
    (cr: {id: string}) => cr.id === crumbId,
  )
  expect(afterCrumb.reactions).toEqual([
    {emoji: '🤔', count: 1, mine: true},
  ])
})

test('reactors: lists who reacted with their emoji and profile', async () => {
  const {trailId, crumbId} = await createTrailWithCrumb()
  const link = await request(
    owner,
    'POST',
    `/trails/${trailId}/share-link`,
  )
  const token = (link.json.url as string).split('/').pop()!
  await request(joiner, 'POST', `/trails/join/${token}`)

  await request(
    owner,
    'PUT',
    `/trails/${trailId}/crumbs/${crumbId}/reactions`,
    {emoji: '❤️'},
  )
  await request(
    joiner,
    'PUT',
    `/trails/${trailId}/crumbs/${crumbId}/reactions`,
    {emoji: '🔥'},
  )

  const res = await request(
    owner,
    'GET',
    `/trails/${trailId}/crumbs/${crumbId}/reactions`,
  )
  expect(res.status).toBe(200)
  const byUser = new Map<string, {emoji: string; handle: string}>(
    res.json.reactors.map(
      (r: {user: {id: string; handle: string}; emoji: string}) => [
        r.user.id,
        {emoji: r.emoji, handle: r.user.handle},
      ],
    ),
  )
  expect(byUser.get(owner.id)).toMatchObject({
    emoji: '❤️',
    handle: owner.handle,
  })
  expect(byUser.get(joiner.id)).toMatchObject({
    emoji: '🔥',
    handle: joiner.handle,
  })

  // clearing a reaction drops the reactor
  await request(
    owner,
    'DELETE',
    `/trails/${trailId}/crumbs/${crumbId}/reactions`,
  )
  const after = await request(
    joiner,
    'GET',
    `/trails/${trailId}/crumbs/${crumbId}/reactions`,
  )
  expect(
    after.json.reactors.map((r: {user: {id: string}}) => r.user.id),
  ).toEqual([joiner.id])
})

test('reactors: non-members cannot list reactors', async () => {
  const {trailId, crumbId} = await createTrailWithCrumb()
  await request(
    owner,
    'PUT',
    `/trails/${trailId}/crumbs/${crumbId}/reactions`,
    {emoji: '❤️'},
  )
  // a stranger with no view access is refused
  const stranger = await createUser()
  const denied = await request(
    stranger,
    'GET',
    `/trails/${trailId}/crumbs/${crumbId}/reactions`,
  )
  expect(denied.status).toBe(403)
})

test('pin ordering and unseen counts', async () => {
  const {trailId} = await createTrailWithCrumb()

  const pin = await request(owner, 'PUT', `/trails/${trailId}/pin`)
  expect(pin.status).toBe(200)
  const trails = await request(owner, 'GET', '/trails')
  const pinned = trails.json.trails.find(
    (t: {id: string}) => t.id === trailId,
  )
  expect(pinned.pinned).toBe(true)
  expect(trails.json.trails[0].id).toBe(trailId)

  // viewing marks read; adding later makes it unseen
  await request(owner, 'GET', `/trails/${trailId}`)
  const crumb = await request(owner, 'POST', '/crumbs', {
    name: 'Later addition',
  })
  await request(owner, 'POST', `/trails/${trailId}/crumbs`, {
    crumbId: crumb.json.crumb.id,
  })
  const after = await request(owner, 'GET', '/trails')
  const withUnseen = after.json.trails.find(
    (t: {id: string}) => t.id === trailId,
  )
  expect(withUnseen.unseenCount).toBe(1)

  await request(owner, 'DELETE', `/trails/${trailId}/pin`)
  const unpinned = await request(owner, 'GET', '/trails')
  expect(
    unpinned.json.trails.find((t: {id: string}) => t.id === trailId)
      .pinned,
  ).toBe(false)
})
