import {randomUUID} from 'node:crypto'
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 {db, schema} from '../db/client'
import {env} from '../env'
import {cleanupCreatedUsers, createUser} from '../test-fixtures'

const createdCrumbIds: string[] = []
const createdSourceIds: string[] = []

const makeUser = async () => {
  const user = await createUser()
  return {user, token: user.token}
}

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

const createCrumb = async (
  token: string,
  body: object = {name: 'pic crumb'},
) => {
  const res = await app.request('/crumbs', {
    method: 'POST',
    headers: {
      Authorization: `Bearer ${token}`,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify(body),
  })
  const {crumb} = (await res.json()) as {crumb: {id: string}}
  createdCrumbIds.push(crumb.id)
  return crumb
}

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

const imageUrlOf = async (res: Response) =>
  ((await res.json()) as {crumb: {imageUrl: string | null}}).crumb
    .imageUrl

afterAll(async () => {
  await cleanupCreatedUsers()
  if (createdSourceIds.length)
    await db
      .delete(schema.sources)
      .where(inArray(schema.sources.id, createdSourceIds))
  await Promise.all(
    createdCrumbIds.map((id) =>
      rm(join(env.UPLOADS_DIR, 'crumb-images', id), {
        recursive: true,
        force: true,
      }),
    ),
  )
})

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

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

test('processes, stores, and serves a manual crumb image', async () => {
  const {token} = await makeUser()
  const crumb = await createCrumb(token)

  const res = await putImage(token, crumb.id, await samplePng())
  expect(res.status).toBe(200)
  const imageUrl = await imageUrlOf(res)
  expect(imageUrl).toContain(`/static/crumb-images/${crumb.id}/`)
  expect(imageUrl).toEndWith('.webp')

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

  const served = await app.request(new URL(imageUrl!).pathname)
  expect(served.status).toBe(200)
  expect(served.headers.get('content-type')).toContain('image/webp')
})

test('replacing the image deletes the previous object', async () => {
  const {token} = await makeUser()
  const crumb = await createCrumb(token)

  const first = await putImage(
    token,
    crumb.id,
    await samplePng({r: 1, g: 2, b: 3}),
  )
  const firstPath = new URL((await imageUrlOf(first))!).pathname
  const second = await putImage(
    token,
    crumb.id,
    await samplePng({r: 200, g: 100, b: 50}),
  )
  const secondPath = new URL((await imageUrlOf(second))!).pathname

  expect(secondPath).not.toBe(firstPath)
  expect((await app.request(firstPath)).status).toBe(404)
  expect((await app.request(secondPath)).status).toBe(200)
})

test('deleting clears the image and removes the object', async () => {
  const {token} = await makeUser()
  const crumb = await createCrumb(token)
  const put = await putImage(token, crumb.id, await samplePng())
  const path = new URL((await imageUrlOf(put))!).pathname

  const res = await app.request(`/crumbs/${crumb.id}/image`, {
    method: 'DELETE',
    headers: {Authorization: `Bearer ${token}`},
  })
  expect(res.status).toBe(200)
  expect(await imageUrlOf(res)).toBeNull()
  expect((await app.request(path)).status).toBe(404)
})

test('only the author can upload', async () => {
  const {token: alice} = await makeUser()
  const {token: bob} = await makeUser()
  const crumb = await createCrumb(alice)
  const res = await putImage(bob, crumb.id, await samplePng())
  expect(res.status).toBe(404)
})

test('a manual image wins over the source image', async () => {
  const {user, token} = await makeUser()
  const [source] = await db
    .insert(schema.sources)
    .values({
      kind: 'link',
      url: `https://example.com/${randomUUID()}`,
      sharedById: user.id,
      imageRef: 'source-images/seed.webp',
    })
    .returning({id: schema.sources.id})
  createdSourceIds.push(source!.id)

  const crumb = await createCrumb(token, {
    name: 'linked',
    source: 'link',
    sourceId: source!.id,
  })
  // the source's display image surfaces on the crumb before any manual upload
  const fetched = await app.request(`/crumbs/${crumb.id}`, {
    headers: {Authorization: `Bearer ${token}`},
  })
  expect(await imageUrlOf(fetched)).toContain(
    '/static/source-images/seed.webp',
  )

  const res = await putImage(token, crumb.id, await samplePng())
  expect(await imageUrlOf(res)).toContain(
    `/static/crumb-images/${crumb.id}/`,
  )
})
