import { faker } from '@faker-js/faker'
import { PrismaClient, type Post, type User } from '@prisma/client'
import { afterAll, afterEach, beforeAll, describe, expect, test } from 'vitest'
import { applyExtension } from './soft-delete'

const prisma = new PrismaClient()

describe('Soft Delete', () => {
  afterAll(async () => {
    await prisma.post.deleteMany()
    await prisma.user.deleteMany()
    await prisma.$disconnect()
  })

  describe('All Models', () => {
    const xprisma = prisma.$extends(applyExtension())

    afterEach(async () => {
      await xprisma.post.deleteMany()
      await xprisma.user.deleteMany()
    })

    describe('Mutations', () => {
      test('.softDelete', async () => {
        const user = await xprisma.user.create({
          data: {
            firstName: faker.name.firstName(),
            lastName: faker.name.lastName(),
          },
        })

        const deleteOp = await xprisma.user.softDelete({
          where: { id: user.id },
        })

        expect(deleteOp).toEqual(
          expect.objectContaining({
            ...user,
            deletedAt: expect.any(Date),
            updatedAt: expect.any(Date),
          }),
        )
      })

      test('.softDeleteMany', async () => {
        const ids: number[] = []

        const createUser = async () => {
          const post = await xprisma.user.create({
            data: {
              firstName: faker.name.firstName(),
              lastName: faker.name.lastName(),
            },
          })

          ids.push(post.id)
        }

        await Promise.allSettled([createUser(), createUser(), createUser()])

        const deleteOp = await xprisma.user.softDeleteMany()
        expect(deleteOp.count).toEqual(3)

        const findOp = await xprisma.user.findMany({
          where: { id: { in: ids } },
        })

        expect(findOp.length).toEqual(0)
      })

      test('.restore', async () => {
        const user = await xprisma.user.create({
          data: {
            firstName: faker.name.firstName(),
            lastName: faker.name.lastName(),
            deletedAt: new Date(),
          },
        })

        const restoredUser = await xprisma.user.softRestore({
          where: { id: user.id },
        })
        expect(restoredUser.deletedAt).toBeNull()
      })

      test('.restoreMany', async () => {
        const deletedAt = new Date()
        const ids: number[] = []

        const createUser = async () => {
          const user = await xprisma.user.create({
            data: {
              firstName: faker.name.firstName(),
              lastName: faker.name.lastName(),
              deletedAt,
            },
          })

          ids.push(user.id)
        }

        await Promise.allSettled([createUser(), createUser(), createUser()])

        const deleteOp = await xprisma.user.softRestoreMany()
        expect(deleteOp.count).toEqual(3)

        const findOp = await xprisma.user.findMany({
          where: { id: { in: ids } },
        })

        expect(findOp.map((p) => p.id).sort()).toEqual(ids.sort())
      })
    })

    describe('Queries', () => {
      let post: Post

      beforeAll(async () => {
        const author = await prisma.user.create({
          data: {
            firstName: faker.name.firstName(),
            lastName: faker.name.lastName(),
          },
        })

        post = await xprisma.post.create({
          data: {
            authorId: author.id,
            content: faker.lorem.paragraphs(3),
            title: faker.lorem.sentence(),
            deletedAt: new Date(),
            commentCount: 5,
          },
        })
      })

      test('.aggregate', async () => {
        const { _avg } = await xprisma.post.aggregate({
          _avg: {
            commentCount: true,
          },
        })

        expect(_avg.commentCount).toBeNull()
      })

      test('.count', async () => {
        const count = await xprisma.post.count()

        expect(count).toBe(0)
      })

      test('.findFirst', async () => {
        const res = await xprisma.post.findFirst({ where: { id: post.id } })
        expect(res).toBeNull()
      })

      test.todo('.findFirstOrThrow', async () => {
        // Prisma Client does not invoke the extended functions

        await expect(
          prisma.post.findFirstOrThrow({
            where: { id: post.id },
          }),
        ).rejects.toThrowError()
      })

      test.todo('.findMany', async () => {
        // Prisma Client does not invoke the extended functions

        const res = await prisma.post.findMany({
          where: { id: { in: post.id } },
        })

        expect(res).toStrictEqual([])
      })

      test('.groupBy', async () => {
        const res = await xprisma.post.groupBy({
          by: ['authorId'],
          _count: {
            authorId: true,
          },
        })

        expect(res).toStrictEqual([])
      })
    })
  })

  describe('Selected Models', () => {
    let author: User

    const xprisma = prisma.$extends(applyExtension({ models: ['post'] }))

    beforeAll(async () => {
      author = await prisma.user.create({
        data: {
          firstName: faker.name.firstName(),
          lastName: faker.name.lastName(),
        },
      })
    })

    describe('Mutations', () => {
      afterEach(async () => {
        await xprisma.post.deleteMany()
      })

      describe('Enabled', () => {
        test('.softDelete', async () => {
          const post = await xprisma.post.create({
            data: {
              authorId: author.id,
              content: faker.lorem.paragraphs(3),
              title: faker.lorem.sentence(),
            },
          })

          const deleteOp = await xprisma.post.softDelete({
            where: { id: post.id },
          })

          expect(deleteOp).toEqual(
            expect.objectContaining({
              ...post,
              deletedAt: expect.any(Date),
              updatedAt: expect.any(Date),
            }),
          )

          const findOp = await xprisma.post.findFirst({
            where: { id: post.id },
          })

          expect(findOp).toBeNull()
        })

        test('.softDeleteMany', async () => {
          const ids: number[] = []

          const createPost = async () => {
            const post = await xprisma.post.create({
              data: {
                authorId: author.id,
                content: faker.lorem.paragraphs(3),
                title: faker.lorem.sentence(),
              },
            })

            ids.push(post.id)
          }

          await Promise.allSettled([createPost(), createPost(), createPost()])

          const deleteOp = await xprisma.post.softDeleteMany()
          expect(deleteOp.count).toEqual(3)

          const findOp = await xprisma.post.findMany({
            where: { id: { in: ids } },
          })

          expect(findOp.length).toEqual(0)
        })

        test('.restore', async () => {
          const post = await xprisma.post.create({
            data: {
              authorId: author.id,
              content: faker.lorem.paragraphs(3),
              deletedAt: new Date(),
              title: faker.lorem.sentence(),
            },
          })

          const restoredPost = await xprisma.post.softRestore({
            where: { id: post.id },
          })
          expect(restoredPost.deletedAt).toBeNull()
        })

        test('.restoreMany', async () => {
          const deletedAt = new Date()
          const ids: number[] = []

          const createPost = async () => {
            const post = await xprisma.post.create({
              data: {
                authorId: author.id,
                content: faker.lorem.paragraphs(3),
                deletedAt,
                title: faker.lorem.sentence(),
              },
            })

            ids.push(post.id)
          }

          await Promise.allSettled([createPost(), createPost(), createPost()])

          const deleteOp = await xprisma.post.softRestoreMany()
          expect(deleteOp.count).toEqual(3)

          const findOp = await xprisma.post.findMany({
            where: { id: { in: ids } },
          })

          expect(findOp.map((p) => p.id).sort()).toEqual(ids.sort())
        })
      })
    })

    describe('Queries', () => {
      describe('Modified ', () => {
        let post: Post

        beforeAll(async () => {
          post = await xprisma.post.create({
            data: {
              authorId: author.id,
              content: faker.lorem.paragraphs(3),
              title: faker.lorem.sentence(),
              deletedAt: new Date(),
              commentCount: 5,
            },
          })
        })

        test('.aggregate', async () => {
          const { _avg } = await xprisma.post.aggregate({
            _avg: {
              commentCount: true,
            },
          })

          expect(_avg.commentCount).toBeNull()
        })

        test('.count', async () => {
          const count = await xprisma.post.count()

          expect(count).toBe(0)
        })

        test('.findFirst', async () => {
          const res = await xprisma.post.findFirst({ where: { id: post.id } })
          expect(res).toBeNull()
        })

        test.todo('.findFirstOrThrow', async () => {
          // Prisma Client does not invoke the extended functions

          await expect(
            prisma.post.findFirstOrThrow({
              where: { id: post.id },
            }),
          ).rejects.toThrowError()
        })

        test.todo('.findMany', async () => {
          // Prisma Client does not invoke the extended functions

          const res = await prisma.post.findMany({
            where: { id: { in: post.id } },
          })

          expect(res).toStrictEqual([])
        })

        test('.groupBy', async () => {
          const res = await xprisma.post.groupBy({
            by: ['authorId'],
            _count: {
              authorId: true,
            },
          })

          expect(res).toStrictEqual([])
        })
      })

      describe('Unmodified', () => {
        test('.aggregate', async () => {
          const { _avg } = await xprisma.user.aggregate({
            _avg: {
              id: true,
            },
          })

          expect(_avg.id).toEqual(author.id)
        })

        test('.count', async () => {
          const count = await xprisma.user.count()
          expect(count).toBe(1)
        })

        test('.findFirst', async () => {
          const res = await xprisma.user.findFirst({ where: { id: author.id } })
          expect(res).toStrictEqual(author)
        })

        test('.findFirstOrThrow - resolves', async () => {
          const res = await prisma.user.findFirstOrThrow({
            where: { id: author.id },
          })

          expect(res).toStrictEqual(author)
        })

        test('.findFirstOrThrow - throws', async () => {
          await expect(
            prisma.user.findFirstOrThrow({
              where: { firstName: 'no-name' },
            }),
          ).rejects.toThrowError()
        })

        test('.findMany', async () => {
          const res = await prisma.user.findMany({
            where: { id: { in: author.id } },
          })

          expect(res).toStrictEqual([author])
        })

        test('.groupBy', async () => {
          const res = await xprisma.user.groupBy({
            by: ['id'],
            _count: {
              id: true,
            },
          })

          expect(res).toStrictEqual([
            {
              _count: {
                id: 1,
              },
              id: author.id,
            },
          ])
        })
      })
    })
  })
})
