/* eslint-disable @typescript-eslint/no-unsafe-call */
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
/* eslint-disable @typescript-eslint/no-unsafe-return */
import type { WithOptionalId } from '@furystack/core'
import { InMemoryStore, addStore } from '@furystack/core'
import { Injector } from '@furystack/inject'
import { usingAsync } from '@furystack/utils'
import { describe, expect, it, vi } from 'vitest'
import type { AuthorizationResult, DataSetSettings } from './data-set-setting.js'
import { getDataSetFor, getRepository } from './helpers.js'
import { Repository } from './repository.js'

class TestClass {
  public id = 1
  public value = ''
}

describe('DataSet', () => {
  describe('Construction', () => {
    it('can be retrieved from an extension method with class', async () => {
      await usingAsync(new Injector(), async (i) => {
        addStore(
          i,
          new InMemoryStore({
            model: TestClass,
            primaryKey: 'id',
          }),
        )
        getRepository(i).createDataSet(TestClass, 'id')
        const dataSet = getDataSetFor(i, TestClass, 'id')
        expect(dataSet.settings.physicalStore.model).toBe(TestClass)
      })
    })

    it('can be retrieved from an extension method with string', async () => {
      await usingAsync(new Injector(), async (i) => {
        addStore(
          i,
          new InMemoryStore({
            model: TestClass,
            primaryKey: 'id',
          }),
        )
        getRepository(i).createDataSet(TestClass, 'id')
        const dataSet = getDataSetFor(i, TestClass, 'id')
        expect(dataSet.settings.physicalStore.model).toBe(TestClass)
      })
    })

    it('Should throw if dataset is not registered through extension', async () => {
      await usingAsync(new Injector(), async (i) => {
        expect(() => getDataSetFor(i, TestClass, 'id')).toThrowError(`No DataSet found for 'class TestClass {
  id = 1;
  value = "";
}'`)
      })
    })

    it('Should throw if dataset is not registered through service', async () => {
      await usingAsync(new Injector(), async (i) => {
        expect(() => i.getInstance(Repository).getDataSetFor(TestClass, 'id'))
          .toThrowError(`No DataSet found for 'class TestClass {
  id = 1;
  value = "";
}'`)
      })
    })

    it('Should throw error on mismatched primary key', async () => {
      await usingAsync(new Injector(), async (i) => {
        addStore(i, new InMemoryStore({ model: TestClass, primaryKey: 'id' }))
        getRepository(i).createDataSet(TestClass, 'id', {})

        expect(() => getDataSetFor(i, TestClass, 'name' as any)).toThrowError()
      })
    })
  })

  describe('Authorizers', () => {
    describe('Add', () => {
      it('should add an entity if no settings are provided', async () => {
        await usingAsync(new Injector(), async (i) => {
          addStore(i, new InMemoryStore({ model: TestClass, primaryKey: 'id' }))

          getRepository(i).createDataSet(TestClass, 'id')

          const dataSet = getDataSetFor(i, TestClass, 'id')
          await dataSet.add(i, { id: 1, value: 'asd' })
          const result = await dataSet.get(i, 1)
          expect(result && result.value).toBe('asd')
        })
      })

      it('should call the add async authorizer and add the entity on pass', async () => {
        await usingAsync(new Injector(), async (i) => {
          const authorizeAdd = vi.fn(async () => ({ isAllowed: true }) as AuthorizationResult)
          addStore(i, new InMemoryStore({ model: TestClass, primaryKey: 'id' }))

          getRepository(i).createDataSet(TestClass, 'id', { authorizeAdd })
          const dataSet = getDataSetFor(i, TestClass, 'id')
          await dataSet.add(i, { id: 1, value: 'asd' })
          expect(authorizeAdd).toBeCalled()
          const added = await dataSet.get(i, 1)
          expect(added && added.value).toBe('asd')
        })
      })

      it('should throw if the add authorizer returns a non-valid result and should not add a value to the store', async () => {
        await usingAsync(new Injector(), async (i) => {
          const authorizeAdd = vi.fn(async () => ({ isAllowed: false, message: '...' }) as AuthorizationResult)
          addStore(i, new InMemoryStore({ model: TestClass, primaryKey: 'id' }))

          getRepository(i).createDataSet(TestClass, 'id', { authorizeAdd })

          const dataSet = getDataSetFor(i, TestClass, 'id')

          try {
            await dataSet.add(i, { id: 1, value: 'asd' })
            throw Error('Should throw')
          } catch (error) {
            /** */
            expect(authorizeAdd).toBeCalled()
            const added = await dataSet.get(i, 1)
            expect(added).toBeUndefined()
          }
        })
      })

      it('should modify an entity on add, if modifyOnAdd is provided', async () => {
        await usingAsync(new Injector(), async (i) => {
          const modifyOnAdd = vi.fn(
            async (options: { injector: Injector; entity: WithOptionalId<TestClass, 'id'> }) => ({
              ...options.entity,
              value: options.entity.value.toUpperCase(),
            }),
          ) as any

          addStore(i, new InMemoryStore({ model: TestClass, primaryKey: 'id' }))
          getRepository(i).createDataSet(TestClass, 'id', { modifyOnAdd })

          const dataSet = getDataSetFor(i, TestClass, 'id')
          await dataSet.add(i, { id: 1, value: 'asd' })
          const result = await dataSet.get(i, 1)
          expect(modifyOnAdd).toBeCalled()
          expect(result && result.value).toBe('ASD')
        })
      })

      it('should call the onEntityAdded callback if an entity has been added', async () => {
        await usingAsync(new Injector(), async (i) => {
          expect.assertions(1)
          addStore(i, new InMemoryStore({ model: TestClass, primaryKey: 'id' }))

          getRepository(i).createDataSet(TestClass, 'id', {})

          getRepository(i)
            .getDataSetFor(TestClass, 'id')
            .subscribe('onEntityAdded', ({ entity }) => {
              expect(entity.value).toBe('asd')
            })

          const dataSet = getDataSetFor(i, TestClass, 'id')
          await dataSet.add(i, { id: 1, value: 'asd' })
        })
      })
    })

    describe('Update', () => {
      it('should update an entity if no settings are provided', async () => {
        await usingAsync(new Injector(), async (i) => {
          addStore(i, new InMemoryStore({ model: TestClass, primaryKey: 'id' }))
          getRepository(i).createDataSet(TestClass, 'id')

          const dataSet = getDataSetFor(i, TestClass, 'id')
          await dataSet.add(i, { id: 1, value: 'asd' })
          await dataSet.update(i, 1, { id: 1, value: 'asd2' })
          const result = await dataSet.get(i, 1)
          expect(result && result.value).toBe('asd2')
        })
      })

      it('should call the authorizeUpdate authorizer and add the entity on pass', async () => {
        await usingAsync(new Injector(), async (i) => {
          const authorizeUpdate = vi.fn(async () => ({ isAllowed: true }) as AuthorizationResult)
          addStore(i, new InMemoryStore({ model: TestClass, primaryKey: 'id' }))
          getRepository(i).createDataSet(TestClass, 'id', { authorizeUpdate })
          const dataSet = getDataSetFor(i, TestClass, 'id')
          await dataSet.add(i, { id: 1, value: 'asd' })
          await dataSet.update(i, 1, { id: 1, value: 'asd2' })
          expect(authorizeUpdate).toBeCalled()
          const added = await dataSet.get(i, 1)
          expect(added && added.value).toBe('asd2')
        })
      })

      it('should throw if the authorizeUpdateEntity returns a non-valid result and should not update a value to the store', async () => {
        await usingAsync(new Injector(), async (i) => {
          const authorizeUpdateEntity = vi.fn(async () => ({ isAllowed: false, message: '...' }) as AuthorizationResult)
          addStore(i, new InMemoryStore({ model: TestClass, primaryKey: 'id' }))
          getRepository(i).createDataSet(TestClass, 'id', { authorizeUpdateEntity })

          const dataSet = getDataSetFor(i, TestClass, 'id')

          try {
            await dataSet.add(i, { id: 1, value: 'asd' })
            await dataSet.update(i, 1, { id: 1, value: 'asd2' })
            throw Error('Should throw')
          } catch (error) {
            /** */
            expect(authorizeUpdateEntity).toBeCalled()
            const added = await dataSet.get(i, 1)
            expect(added && added.value).toBe('asd')
          }
        })
      })
      it('should call the authorizeUpdateEntity authorizer and add the entity on pass', async () => {
        await usingAsync(new Injector(), async (i) => {
          const authorizeUpdateEntity = vi.fn(async () => ({ isAllowed: true }) as AuthorizationResult)
          addStore(i, new InMemoryStore({ model: TestClass, primaryKey: 'id' }))
          getRepository(i).createDataSet(TestClass, 'id', { authorizeUpdateEntity })
          const dataSet = getDataSetFor(i, TestClass, 'id')
          await dataSet.add(i, { id: 1, value: 'asd' })
          await dataSet.update(i, 1, { id: 1, value: 'asd2' })
          expect(authorizeUpdateEntity).toBeCalled()
          const added = await dataSet.get(i, 1)
          expect(added && added.value).toBe('asd2')
        })
      })

      it('should throw if the authorizeUpdate returns a non-valid result and should not update a value to the store', async () => {
        await usingAsync(new Injector(), async (i) => {
          const authorizeUpdate = vi.fn(async () => ({ isAllowed: false, message: '...' }) as AuthorizationResult)
          addStore(i, new InMemoryStore({ model: TestClass, primaryKey: 'id' }))
          getRepository(i).createDataSet(TestClass, 'id', { authorizeUpdate })

          const dataSet = getDataSetFor(i, TestClass, 'id')

          try {
            await dataSet.add(i, { id: 1, value: 'asd' })
            await dataSet.update(i, 1, { id: 1, value: 'asd2' })
            throw Error('Should throw')
          } catch (error) {
            /** */
            expect(authorizeUpdate).toBeCalled()
            const added = await dataSet.get(i, 1)
            expect(added && added.value).toBe('asd')
          }
        })
      })

      it('should modify an entity on update, if modifyOnAdd is provided', async () => {
        await usingAsync(new Injector(), async (i) => {
          const modifyOnUpdate: DataSetSettings<TestClass, 'id'>['modifyOnUpdate'] = vi.fn(async (options) => ({
            ...options.entity,
            value: options.entity.value?.toUpperCase(),
          }))

          addStore(i, new InMemoryStore({ model: TestClass, primaryKey: 'id' }))
          getRepository(i).createDataSet(TestClass, 'id', { modifyOnUpdate })

          const dataSet = getDataSetFor(i, TestClass, 'id')
          await dataSet.add(i, { id: 1, value: 'asd' })
          await dataSet.update(i, 1, { id: 1, value: 'asd2' })
          const result = await dataSet.get(i, 1)
          expect(modifyOnUpdate).toBeCalled()
          expect(result && result.value).toBe('ASD2')
        })
      })

      it('should publish to the onEntityUpdated observable if an entity has been updated', async () => {
        await usingAsync(new Injector(), async (i) => {
          expect.assertions(1)
          addStore(i, new InMemoryStore({ model: TestClass, primaryKey: 'id' }))
          getRepository(i).createDataSet(TestClass, 'id')

          getRepository(i)
            .getDataSetFor(TestClass, 'id')
            .subscribe('onEntityUpdated', ({ change }) => {
              expect(change).toEqual({ id: 1, value: 'asd2' })
            })

          const dataSet = getDataSetFor(i, TestClass, 'id')
          await dataSet.add(i, { id: 1, value: 'asd' })
          await dataSet.update(i, 1, { id: 1, value: 'asd2' })
        })
      })
    })

    describe('Count', () => {
      it('should return the count if no settings are provided', async () => {
        await usingAsync(new Injector(), async (i) => {
          addStore(i, new InMemoryStore({ model: TestClass, primaryKey: 'id' }))
          getRepository(i).createDataSet(TestClass, 'id')

          const dataSet = getDataSetFor(i, TestClass, 'id')
          await dataSet.add(i, { id: 1, value: 'asd' })
          const result = await dataSet.count(i)
          expect(result).toBe(1)
        })
      })

      it('should return the count if authorizeGet returns valid result', async () => {
        await usingAsync(new Injector(), async (i) => {
          const authorizeGet = vi.fn(async () => ({ isAllowed: true, message: '' }))
          addStore(i, new InMemoryStore({ model: TestClass, primaryKey: 'id' }))
          getRepository(i).createDataSet(TestClass, 'id', { authorizeGet })

          const dataSet = getDataSetFor(i, TestClass, 'id')
          await dataSet.add(i, { id: 1, value: 'asd' })
          const result = await dataSet.count(i)
          expect(result).toBe(1)
        })
      })

      it('should throw if authorizeGet returns invalid result', async () => {
        await usingAsync(new Injector(), async (i) => {
          const authorizeGet = vi.fn(async () => ({ isAllowed: false, message: ':(' }))
          addStore(i, new InMemoryStore({ model: TestClass, primaryKey: 'id' }))
          getRepository(i).createDataSet(TestClass, 'id', { authorizeGet })

          const dataSet = getDataSetFor(i, TestClass, 'id')
          await dataSet.add(i, { id: 1, value: 'asd' })
          try {
            await dataSet.count(i)
            throw Error('Should throw')
          } catch (error) {
            /** */
          }
        })
      })
    })
  })

  describe('filter', () => {
    it('should return the unfiltered result if no settings are provided', async () => {
      await usingAsync(new Injector(), async (i) => {
        addStore(i, new InMemoryStore({ model: TestClass, primaryKey: 'id' }))
        getRepository(i).createDataSet(TestClass, 'id')

        const dataSet = getDataSetFor(i, TestClass, 'id')
        await dataSet.add(i, { id: 1, value: 'asd' })
        const result = await dataSet.find(i, {})
        expect(result.length).toBe(1)
      })
    })

    it('should return the unfiltered result if authorizeGet returns valid result', async () => {
      await usingAsync(new Injector(), async (i) => {
        const authorizeGet = vi.fn(async () => ({ isAllowed: true, message: '' }))
        addStore(i, new InMemoryStore({ model: TestClass, primaryKey: 'id' }))
        getRepository(i).createDataSet(TestClass, 'id', { authorizeGet })

        const dataSet = getDataSetFor(i, TestClass, 'id')
        await dataSet.add(i, { id: 1, value: 'asd' })
        const result = await dataSet.find(i, {})
        expect(result.length).toBe(1)
      })
    })

    it('should throw if authorizeGet returns invalid result', async () => {
      await usingAsync(new Injector(), async (i) => {
        const authorizeGet = vi.fn(async () => ({ isAllowed: false, message: ':(' }))
        addStore(i, new InMemoryStore({ model: TestClass, primaryKey: 'id' }))
        getRepository(i).createDataSet(TestClass, 'id', { authorizeGet })

        const dataSet = getDataSetFor(i, TestClass, 'id')
        await dataSet.add(i, { id: 1, value: 'asd' })
        try {
          await dataSet.find(i, {})
          throw Error('Should throw')
        } catch (error) {
          /** */
        }
      })
    })
  })

  describe('get', () => {
    it('should return the entity if no settings are provided', async () => {
      await usingAsync(new Injector(), async (i) => {
        addStore(i, new InMemoryStore({ model: TestClass, primaryKey: 'id' }))
        getRepository(i).createDataSet(TestClass, 'id')

        const dataSet = getDataSetFor(i, TestClass, 'id')
        await dataSet.add(i, { id: 1, value: 'asd' })
        const result = await dataSet.get(i, 1)
        expect(result && result.id).toBe(1)
      })
    })

    it('should return the entity if authorizeGet returns valid result', async () => {
      await usingAsync(new Injector(), async (i) => {
        const authorizeGet = vi.fn(async () => ({ isAllowed: true, message: '' }))
        addStore(i, new InMemoryStore({ model: TestClass, primaryKey: 'id' }))
        getRepository(i).createDataSet(TestClass, 'id', { authorizeGet })

        const dataSet = getDataSetFor(i, TestClass, 'id')
        await dataSet.add(i, { id: 1, value: 'asd' })
        const result = await dataSet.get(i, 1)
        expect(result && result.id).toBe(1)
      })
    })

    it('should throw if authorizeGet returns invalid result', async () => {
      await usingAsync(new Injector(), async (i) => {
        const authorizeGet = vi.fn(async () => ({ isAllowed: false, message: ':(' }))
        addStore(i, new InMemoryStore({ model: TestClass, primaryKey: 'id' }))
        getRepository(i).createDataSet(TestClass, 'id', { authorizeGet })

        const dataSet = getDataSetFor(i, TestClass, 'id')
        await dataSet.add(i, { id: 1, value: 'asd' })
        try {
          await dataSet.get(i, 1)
          throw Error('Should throw')
        } catch (error) {
          /** */
        }
      })
    })

    it('should return the entity if authorizeGetEntity returns valid result', async () => {
      await usingAsync(new Injector(), async (i) => {
        const authorizeGetEntity = vi.fn(async () => ({ isAllowed: true, message: '' }))
        addStore(i, new InMemoryStore({ model: TestClass, primaryKey: 'id' }))
        getRepository(i).createDataSet(TestClass, 'id', { authorizeGetEntity })

        const dataSet = getDataSetFor(i, TestClass, 'id')
        await dataSet.add(i, { id: 1, value: 'asd' })
        const result = await dataSet.get(i, 1)
        expect(result && result.id).toBe(1)
      })
    })

    it('should throw if authorizeGetEntity returns invalid result', async () => {
      await usingAsync(new Injector(), async (i) => {
        const authorizeGetEntity = vi.fn(async () => ({ isAllowed: false, message: ':(' }))
        addStore(i, new InMemoryStore({ model: TestClass, primaryKey: 'id' }))
        getRepository(i).createDataSet(TestClass, 'id', { authorizeGetEntity })

        const dataSet = getDataSetFor(i, TestClass, 'id')
        await dataSet.add(i, { id: 1, value: 'asd' })
        try {
          await dataSet.get(i, 1)
          throw Error('Should throw')
        } catch (error) {
          /** */
        }
      })
    })
  })
  describe('remove', () => {
    it('should remove the entity if no settings are provided', async () => {
      await usingAsync(new Injector(), async (i) => {
        addStore(i, new InMemoryStore({ model: TestClass, primaryKey: 'id' }))
        getRepository(i).createDataSet(TestClass, 'id')

        const dataSet = getDataSetFor(i, TestClass, 'id')
        await dataSet.add(i, { id: 1, value: 'asd' })
        await dataSet.remove(i, 1)
        const countValue = await dataSet.count(i)
        expect(countValue).toBe(0)
      })
    })

    it('should remove the entity if authorizeRemove returns valid result', async () => {
      await usingAsync(new Injector(), async (i) => {
        const authorizeRemove = vi.fn(async () => ({ isAllowed: true, message: '' }))
        addStore(i, new InMemoryStore({ model: TestClass, primaryKey: 'id' }))
        getRepository(i).createDataSet(TestClass, 'id', { authorizeRemove })

        const dataSet = getDataSetFor(i, TestClass, 'id')
        await dataSet.add(i, { id: 1, value: 'asd' })
        await dataSet.remove(i, 1)
        const count = await dataSet.count(i)
        expect(count).toBe(0)
      })
    })

    it('should throw if authorizeRemove returns invalid result', async () => {
      await usingAsync(new Injector(), async (i) => {
        const authorizeRemove = vi.fn(async () => ({ isAllowed: false, message: ':(' }))
        addStore(i, new InMemoryStore({ model: TestClass, primaryKey: 'id' }))
        getRepository(i).createDataSet(TestClass, 'id', { authorizeRemove })

        const dataSet = getDataSetFor(i, TestClass, 'id')
        await dataSet.add(i, { id: 1, value: 'asd' })
        try {
          await dataSet.remove(i, 1)
          throw Error('Should throw')
        } catch (error) {
          /** */
        }
        const count = await dataSet.count(i)
        expect(count).toBe(1)
      })
    })

    it('should remove the entity if authroizeRemoveEntity returns valid result', async () => {
      await usingAsync(new Injector(), async (i) => {
        const authroizeRemoveEntity = vi.fn(async () => ({ isAllowed: true, message: '' }))
        addStore(i, new InMemoryStore({ model: TestClass, primaryKey: 'id' }))
        getRepository(i).createDataSet(TestClass, 'id', { authroizeRemoveEntity })

        const dataSet = getDataSetFor(i, TestClass, 'id')
        await dataSet.add(i, { id: 1, value: 'asd' })
        await dataSet.remove(i, 1)
        const count = await dataSet.count(i)
        expect(count).toBe(0)
      })
    })

    it('should throw if authroizeRemoveEntity returns invalid result', async () => {
      await usingAsync(new Injector(), async (i) => {
        const authroizeRemoveEntity = vi.fn(async () => ({ isAllowed: false, message: ':(' }))
        addStore(i, new InMemoryStore({ model: TestClass, primaryKey: 'id' }))
        getRepository(i).createDataSet(TestClass, 'id', { authroizeRemoveEntity })

        const dataSet = getDataSetFor(i, TestClass, 'id')
        await dataSet.add(i, { id: 1, value: 'asd' })
        try {
          await dataSet.remove(i, 1)
          throw Error('Should throw')
        } catch (error) {
          /** */
        }
        const count = await dataSet.count(i)
        expect(count).toBe(1)
      })
    })
    it('should publish to the onEntityRemoved observable if an entity has been removed', async () => {
      await usingAsync(new Injector(), async (i) => {
        expect.assertions(1)
        addStore(i, new InMemoryStore({ model: TestClass, primaryKey: 'id' }))
        getRepository(i).createDataSet(TestClass, 'id')

        getRepository(i)
          .getDataSetFor(TestClass, 'id')
          .subscribe('onEntityRemoved', ({ key }) => {
            expect(key).toEqual(1)
          })

        const dataSet = getDataSetFor(i, TestClass, 'id')
        await dataSet.add(i, { id: 1, value: 'asd' })
        await dataSet.remove(i, 1)
      })
    })
  })
})
