import {rm} from 'node:fs/promises'
import {join} from 'node:path'

import {afterAll, expect, test} from 'bun:test'
import {eq, inArray} from 'drizzle-orm'
import sharp from 'sharp'

import app from '../app'
import {createSession} from '../auth/session'
import {db, schema} from '../db/client'
import {env} from '../env'

const createdIds: string[] = []

const makeUser = async () => {
  const [user] = await db
    .insert(schema.users)
    .values({
      email: `avatar-${crypto.randomUUID()}@example.com`,
      handle: `avatar-${crypto.randomUUID().slice(0, 8)}`,
    })
    .returning()
  createdIds.push(user!.id)
  const token = await createSession(user!.id)
  return {user: user!, token}
}

const samplePng = async () =>
  sharp({
    create: {
      width: 8,
      height: 8,
      channels: 3,
      background: {r: 10, g: 120, b: 200},
    },
  })
    .png()
    .toBuffer()

const putAvatar = (token: string | null, body: Uint8Array) =>
  app.request('/auth/avatar', {
    method: 'PUT',
    headers: {
      ...(token ? {Authorization: `Bearer ${token}`} : {}),
      'Content-Type': 'application/octet-stream',
    },
    body: new Blob([Uint8Array.from(body)]),
  })

afterAll(async () => {
  if (!createdIds.length) return
  await db
    .delete(schema.sessions)
    .where(inArray(schema.sessions.userId, createdIds))
  await db
    .delete(schema.users)
    .where(inArray(schema.users.id, createdIds))
  await Promise.all(
    createdIds.map((id) =>
      rm(join(env.UPLOADS_DIR, 'avatars', id), {
        recursive: true,
        force: true,
      }),
    ),
  )
})

test('rejects an unauthenticated upload', async () => {
  const res = await putAvatar(null, await samplePng())
  expect(res.status).toBe(401)
})

test('rejects bytes that are not an image', async () => {
  const {token} = await makeUser()
  const junk = new Uint8Array(64).fill(7)
  const res = await putAvatar(token, junk)
  expect(res.status).toBe(400)
})

test('processes, stores, and serves an uploaded avatar', async () => {
  const {user, token} = await makeUser()
  const res = await putAvatar(token, await samplePng())
  expect(res.status).toBe(200)
  const {user: updated} = (await res.json()) as {
    user: {avatarUrl: string | null}
  }
  expect(updated.avatarUrl).toContain(`/static/avatars/${user.id}/`)
  expect(updated.avatarUrl).toEndWith('.webp')

  const [row] = await db
    .select({avatarRef: schema.users.avatarRef})
    .from(schema.users)
    .where(eq(schema.users.id, user.id))
    .limit(1)
  expect(row!.avatarRef).toBeTruthy()

  // the object is publicly fetchable as a webp
  const path = new URL(updated.avatarUrl!).pathname
  const served = await app.request(path)
  expect(served.status).toBe(200)
  expect(served.headers.get('content-type')).toContain('image/webp')

  const me = await app.request('/auth/me', {
    headers: {Authorization: `Bearer ${token}`},
  })
  const {user: meUser} = (await me.json()) as {
    user: {avatarUrl: string | null}
  }
  expect(meUser.avatarUrl).toBe(updated.avatarUrl)
})

test('removes the avatar and its object', async () => {
  const {user, token} = await makeUser()
  const put = await putAvatar(token, await samplePng())
  const {user: uploaded} = (await put.json()) as {
    user: {avatarUrl: string}
  }
  const path = new URL(uploaded.avatarUrl).pathname

  const res = await app.request('/auth/avatar', {
    method: 'DELETE',
    headers: {Authorization: `Bearer ${token}`},
  })
  expect(res.status).toBe(200)
  const {user: cleared} = (await res.json()) as {
    user: {avatarUrl: string | null}
  }
  expect(cleared.avatarUrl).toBeNull()

  const [row] = await db
    .select({avatarRef: schema.users.avatarRef})
    .from(schema.users)
    .where(eq(schema.users.id, user.id))
    .limit(1)
  expect(row!.avatarRef).toBeNull()

  const served = await app.request(path)
  expect(served.status).toBe(404)
})
