import {randomUUID} from 'node:crypto'

import {afterAll, beforeEach, expect, test} from 'bun:test'
import {and, eq} from 'drizzle-orm'

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

const setHandle = (u: TestUser, handle: string) =>
  db
    .update(schema.users)
    .set({handle})
    .where(eq(schema.users.id, u.id))

const block = (as: TestUser, target: TestUser) =>
  request(as, 'POST', '/blocks', {userId: target.id})

let alice: TestUser
let bob: TestUser

beforeEach(async () => {
  ;[alice, bob] = await Promise.all([createUser(), createUser()])
})

afterAll(cleanupCreatedUsers)

test('cannot block self', async () => {
  const res = await request(alice, 'POST', '/blocks', {
    userId: alice.id,
  })
  expect(res.status).toBe(400)
})

test('block severs friendship and prevents re-requesting', async () => {
  await befriend(alice, bob)
  expect((await block(alice, bob)).status).toBe(200)

  const edges = await db
    .select({id: schema.friendships.id})
    .from(schema.friendships)
    .where(
      and(
        eq(schema.friendships.requesterId, alice.id),
        eq(schema.friendships.addresseeId, bob.id),
      ),
    )
  expect(edges).toHaveLength(0)

  // neither party can (re)friend across a block
  const outgoing = await request(alice, 'POST', '/friends/requests', {
    addresseeId: bob.id,
  })
  expect(outgoing.status).toBe(403)
  const incoming = await request(bob, 'POST', '/friends/requests', {
    addresseeId: alice.id,
  })
  expect(incoming.status).toBe(403)
})

test('search hides each user from the other', async () => {
  const needle = `needle${randomUUID().slice(0, 8)}`
  await Promise.all([
    setHandle(alice, `${needle}a`),
    setHandle(bob, `${needle}b`),
  ])
  await block(alice, bob)

  const asAlice = await request(alice, 'GET', `/search?q=${needle}`)
  expect(
    asAlice.json.users.map((u: {id: string}) => u.id),
  ).not.toContain(bob.id)
  const asBob = await request(bob, 'GET', `/search?q=${needle}`)
  expect(
    asBob.json.users.map((u: {id: string}) => u.id),
  ).not.toContain(alice.id)
})

test('blocked-by hides the profile; blocker sees a blocked status', async () => {
  await befriend(alice, bob)
  // bob has a crumb the friend alice can normally see
  await request(bob, 'POST', '/crumbs', {name: 'Bob spot'})

  await block(alice, bob)

  // alice (the blocker) still sees bob's profile to unblock, but no content
  const asAlice = await request(alice, 'GET', `/users/${bob.id}`)
  expect(asAlice.status).toBe(200)
  expect(asAlice.json.profile.friendshipStatus).toBe('blocked')
  expect(asAlice.json.profile.crumbCount).toBe(0)

  // bob (blocked) cannot see alice's profile at all
  const asBob = await request(bob, 'GET', `/users/${alice.id}`)
  expect(asBob.status).toBe(404)
})

test('block prevents trail invites either direction', async () => {
  await befriend(alice, bob)
  const trail = await request(alice, 'POST', '/trails', {
    name: 'Blocked trail',
  })
  const trailId = trail.json.trail.id
  await block(alice, bob)

  const invite = await request(
    alice,
    'POST',
    `/trails/${trailId}/invites`,
    {
      inviteeId: bob.id,
    },
  )
  expect(invite.status).toBe(403)
})

test('block hides the conversation from both inboxes', async () => {
  await befriend(alice, bob)
  const convo = await request(alice, 'POST', '/dms', {userId: bob.id})
  expect(convo.status).toBe(200)
  await request(
    alice,
    'POST',
    `/dms/${convo.json.conversation.id}/messages`,
    {
      text: 'hi',
    },
  )

  await block(alice, bob)

  const aliceInbox = await request(alice, 'GET', '/dms')
  expect(
    aliceInbox.json.conversations.map((c: {id: string}) => c.id),
  ).not.toContain(convo.json.conversation.id)
  const bobInbox = await request(bob, 'GET', '/dms')
  expect(
    bobInbox.json.conversations.map((c: {id: string}) => c.id),
  ).not.toContain(convo.json.conversation.id)
})

test('shared trail filters a blocked members posts and reactions', async () => {
  // alice owns a trail with a crumb; bob is an accepted collaborator
  await befriend(alice, bob)
  const crumb = await request(alice, 'POST', '/crumbs', {
    name: 'Meet here',
  })
  const crumbId = crumb.json.crumb.id
  const trail = await request(alice, 'POST', '/trails', {
    name: 'Shared',
    crumbId,
  })
  const trailId = trail.json.trail.id
  await db.insert(schema.trailCollaborators).values({
    trailId,
    userId: bob.id,
    inviterId: alice.id,
    status: 'accepted',
  })

  await request(bob, 'POST', `/trails/${trailId}/posts`, {
    text: 'Excited for this',
  })
  await request(
    bob,
    'PUT',
    `/trails/${trailId}/crumbs/${crumbId}/reactions`,
    {emoji: '🔥'},
  )

  // before blocking, alice sees bob's post and reaction
  const before = await request(alice, 'GET', `/trails/${trailId}`)
  expect(before.json.trail.posts.length).toBe(1)
  const beforeCrumb = before.json.trail.crumbs.find(
    (c: {id: string}) => c.id === crumbId,
  )
  expect(beforeCrumb.reactions).toEqual([
    {emoji: '🔥', count: 1, mine: false},
  ])

  await block(alice, bob)

  // after blocking, bob's post and reaction are filtered from alice's view
  const after = await request(alice, 'GET', `/trails/${trailId}`)
  expect(after.json.trail.posts.length).toBe(0)
  const afterCrumb = after.json.trail.crumbs.find(
    (c: {id: string}) => c.id === crumbId,
  )
  expect(afterCrumb.reactions).toEqual([])

  const reactors = await request(
    alice,
    'GET',
    `/trails/${trailId}/crumbs/${crumbId}/reactions`,
  )
  expect(reactors.json.reactors).toEqual([])
})

test('unblock restores requesting', async () => {
  await block(alice, bob)
  expect(
    (await request(alice, 'DELETE', `/blocks/${bob.id}`)).status,
  ).toBe(200)
  const req = await request(alice, 'POST', '/friends/requests', {
    addresseeId: bob.id,
  })
  expect(req.status).toBe(200)
})

test('report persists a row', async () => {
  const res = await request(alice, 'POST', '/reports', {
    subjectType: 'user',
    subjectId: bob.id,
    reason: 'spam',
    detail: 'unsolicited messages',
  })
  expect(res.status).toBe(200)
  const rows = await db
    .select()
    .from(schema.reports)
    .where(
      and(
        eq(schema.reports.reporterId, alice.id),
        eq(schema.reports.subjectId, bob.id),
      ),
    )
  expect(rows).toHaveLength(1)
  expect(rows[0]!.reason).toBe('spam')
})
