import { describe, test, it, expect } from 'vitest'
import EntityManager from "~/services/data/entityManager";
import ApiResource from "~/models/ApiResource";
import ApiModel from "~/models/ApiModel";
import ApiRequestService from "~/services/data/apiRequestService";
import {Element, Repository} from "pinia-orm";
import models from "~/models/models";



class DummyApiResource extends ApiResource {
    static entity = 'dummyResource'

}

class DummyApiModel extends ApiModel {
    static entity = 'dummyModel'
}

let _console: any = {
    'log': console.log,
    'warn': console.warn,
    'error': console.error,
}

vi.mock("~/models/models", async () => {
    class MyModel {
        static entity = 'myModel'
    }

    const models: Record<string, any> = {'myModel': MyModel}

    return {
        default: models
    }
})

let apiRequestService: ApiRequestService
let entityManager: EntityManager
let repo: Repository<ApiResource>
let _getRepo: (model: typeof ApiResource) => Repository<ApiResource>

beforeEach(() => {
    // @ts-ignore
    repo = vi.fn() as Repository<ApiResource>

    // @ts-ignore
    apiRequestService = vi.fn() as ApiRequestService
    _getRepo = vi.fn((model: typeof ApiResource) => repo)

    entityManager = new EntityManager(apiRequestService, _getRepo)
})

afterEach(() => {
    // Reset console methods after mock
    console.log = _console['log']
    console.warn = _console['warn']
    console.error = _console['error']
})

describe('getRepository', () => {
  test('simple call', () => {
      entityManager.getRepository(DummyApiResource)

      expect(_getRepo).toHaveBeenCalledWith(DummyApiResource)
  })
})

describe('cast', () => {
    test('simple cast', () => {
        // @ts-ignore
        const result = entityManager.cast(DummyApiResource, { id: 1 })

        expect(result instanceof DummyApiResource).toEqual(true)
    })
})

describe('getModelFor', () => {
    test('simple call', () => {
        expect(entityManager.getModelFor('myModel').entity).toEqual('myModel')
    })
})

describe('getModelFromIri', () => {
    test('simple call', () => {
        // @ts-ignore
        entityManager.getModelFor = vi.fn((entityName: string) => entityName === 'dummy' ? DummyApiResource : null)

        // @ts-ignore
        const result = entityManager.getModelFromIri('/api/dummy/123')

        expect(result).toEqual(DummyApiResource)
    })
    test('invalide Iri', () => {
        expect(() => entityManager.getModelFromIri('/invalid')).toThrowError('cannot parse the IRI')
    })
})

describe('newInstance', () => {

    test('simple call', () => {
        const properties = { 'id': 1 }

        // @ts-ignore
        entityManager.getRepository = vi.fn((model: typeof ApiResource) => {
            return model === DummyApiResource ? repo : null
        })

        // @ts-ignore
        const entity = new DummyApiResource(properties)

        // @ts-ignore
        repo.make = vi.fn((properties: object) => {
            // @ts-ignore
            entity.id = properties.id
            return entity
        })

        // @ts-ignore
        entityManager.save = vi.fn((model: typeof ApiResource, entity: ApiResource, permanent: boolean) => entity)

        const result = entityManager.newInstance(DummyApiResource, properties)

        expect(repo.make).toHaveBeenCalledWith(properties)
        expect(entityManager.save).toHaveBeenCalledWith(DummyApiResource, entity, true)

        expect(result.id).toEqual(properties.id)
    })

    test('with no id provided', () => {
        const properties = { 'name': 'bob' }

        // @ts-ignore
        const repo = vi.fn() as Repository<ApiResource>

        // @ts-ignore
        entityManager.getRepository = vi.fn((model: typeof ApiResource) => {
            return model === DummyApiResource ? repo : null
        })

        // @ts-ignore
        entityManager.saveInitialState = vi.fn((model: typeof ApiResource, entity: ApiResource) => null)

        // @ts-ignore
        const entity = new DummyApiResource(properties)
        entity.setModel = vi.fn((model: typeof ApiResource) => null)

        // @ts-ignore
        repo.make = vi.fn((properties: object) => {
            // @ts-ignore
            entity.name = properties.name
            return entity
        })
        // @ts-ignore
        repo.save = vi.fn((record: Element) => entity)

        const result = entityManager.newInstance(DummyApiResource, properties)

        expect(
            result.id,
            'id is \'tmp\' followed by a valid uuid-V4'
        ).toMatch(/tmp[0-9a-f]{8}-[0-9a-f]{4}-[0-5][0-9a-f]{3}-[089ab][0-9a-f]{3}-[0-9a-f]{12}/)

        expect(result.name).toEqual(properties.name)
    })
})

describe('save', () => {
    test('simple call', () => {
        // @ts-ignore
        const repo = vi.fn() as Repository<ApiResource>

        // @ts-ignore
        entityManager.getRepository = vi.fn((model: typeof ApiResource) => {
            return model === DummyApiResource ? repo : null
        })

        // @ts-ignore
        repo.save = vi.fn((record: Element) => entity)

        const entity = new DummyApiResource()
        entityManager.save(DummyApiResource, entity)

        expect(repo.save).toHaveBeenCalledWith(entity)
    })
})

describe('find', () => {
    test('simple call', () => {
        // @ts-ignore
        const repo = vi.fn() as Repository<ApiResource>

        // @ts-ignore
        entityManager.getRepository = vi.fn((model: typeof ApiResource) => {
            return model === DummyApiResource ? repo : null
        })

        const entity = new DummyApiResource({ id: 1 })
        // @ts-ignore
        repo.find = vi.fn((id: string | number) => entity)

        entityManager.find(DummyApiResource, 1)

        expect(entityManager.getRepository).toHaveBeenCalledWith(DummyApiResource)
        expect(repo.find).toHaveBeenCalledWith(1)
    })
})

describe('fetch', () => {
    test('not in store, no force refresh', async () => {

        const properties = { id: 1 }

        const entity = new DummyApiResource({ id: 1 })

        // @ts-ignore
        entityManager.find = vi.fn((model: typeof ApiResource, id: number) => undefined)

        // @ts-ignore
        apiRequestService.get = vi.fn(async (url: string) => properties)

        // @ts-ignore
        entityManager.newInstance = vi.fn((model: typeof ApiResource, props: object) => entity)

        const result = await entityManager.fetch(DummyApiResource, 1)

        expect(entityManager.find).toHaveBeenCalledWith(DummyApiResource, 1)
        expect(apiRequestService.get).toHaveBeenCalledWith('api/dummyResource/1')
        expect(entityManager.newInstance).toHaveBeenCalledWith(DummyApiResource, properties)

        expect(result).toEqual(entity)
    })

    test('in store, no force refresh', async () => {

        const properties = { id: 1 }

        const entity = new DummyApiResource({ id: 1 })

        // @ts-ignore
        entityManager.find = vi.fn((model: typeof ApiResource, id: number) => entity)

        const result = await entityManager.fetch(DummyApiResource, 1)

        expect(entityManager.find).toHaveBeenCalledWith(DummyApiResource, 1)
        expect(result).toEqual(entity)
    })

    test('in store, but with force refresh', async () => {

        const properties = { id: 1 }

        const entity = new DummyApiResource({ id: 1 })

        // @ts-ignore
        entityManager.find = vi.fn((model: typeof ApiResource, id: number) => undefined)

        // @ts-ignore
        apiRequestService.get = vi.fn(async (url: string) => properties)

        // @ts-ignore
        entityManager.newInstance = vi.fn((model: typeof ApiResource, props: object) => entity)

        const result = await entityManager.fetch(DummyApiResource, 1, true)

        expect(entityManager.find).toHaveBeenCalledTimes(0)
        expect(apiRequestService.get).toHaveBeenCalledWith('api/dummyResource/1')
        expect(entityManager.newInstance).toHaveBeenCalledWith(DummyApiResource, properties)

        expect(result).toEqual(entity)
    })
})

describe('fetchCollection', () => {
    test('simple call', async () => {

        const collection = {
            '@type': 'hydra:Collection',
            'hydra:totalItems': 3,
            'hydra:member': [
                {id: 1},
                {id: 2},
                {id: 3},
            ]
        }

        // @ts-ignore
        apiRequestService.get = vi.fn(async (url: string) => collection)

        // @ts-ignore
        entityManager.newInstance = vi.fn((model: typeof ApiResource, props: object) => {
            return new DummyApiResource(props)
        })

        const result = await entityManager.fetchCollection(DummyApiResource, null)

        expect(apiRequestService.get).toHaveBeenCalledWith('api/dummyResource', [])
        expect(entityManager.newInstance).toHaveBeenCalledTimes(3)
        expect(entityManager.newInstance).toHaveBeenCalledWith(DummyApiResource, {id: 1})
        expect(entityManager.newInstance).toHaveBeenCalledWith(DummyApiResource, {id: 2})
        expect(entityManager.newInstance).toHaveBeenCalledWith(DummyApiResource, {id: 3})

        expect(result.items).toEqual([
            new DummyApiResource({id: 1}),
            new DummyApiResource({id: 2}),
            new DummyApiResource({id: 3})
        ])

        expect(result.pagination, 'default pagination').toEqual({
            first: 1,
            last: 1,
            next: undefined,
            previous: undefined
        })
    })

    test('with a parent', async () => {

        const collection = {
            '@type': 'hydra:Collection',
            'hydra:totalItems': 3,
            'hydra:member': [
                {id: 1},
                {id: 2},
                {id: 3},
            ]
        }

        // @ts-ignore
        apiRequestService.get = vi.fn(async (url: string) => collection)

        // @ts-ignore
        entityManager.newInstance = vi.fn((model: typeof ApiResource, props: object) => {
            return new DummyApiResource(props)
        })

        const parent = new DummyApiModel()
        parent.id = 100
        parent.entity = 'dummyModel'  // TODO: je ne comprend pas pqoi cette ligne est nécessaire...

        await entityManager.fetchCollection(DummyApiResource, parent)

        expect(apiRequestService.get).toHaveBeenCalledWith('api/dummyModel/100/dummyResource', [])
    })

    test('with a query', async () => {

        const collection = {
            '@type': 'hydra:Collection',
            'hydra:totalItems': 3,
            'hydra:member': [
                {id: 1},
                {id: 2},
                {id: 3},
            ]
        }

        // @ts-ignore
        apiRequestService.get = vi.fn(async (url: string) => collection)

        // @ts-ignore
        entityManager.newInstance = vi.fn((model: typeof ApiResource, props: object) => {
            return new DummyApiResource(props)
        })

        await entityManager.fetchCollection(DummyApiResource, null, { page: 10 })

        expect(apiRequestService.get).toHaveBeenCalledWith('api/dummyResource', { page: 10 })
    })

    test('with pagination', async () => {

        const collection = {
            '@type': 'hydra:Collection',
            'hydra:totalItems': 1000,
            'hydra:member': [],
            'hydra:view': {
                "@id": "/api/subdomains?organization=498&page=50",
                'hydra:first': '/api/subdomains?organization=498&page=1',
                'hydra:last': '/api/subdomains?organization=498&page=100',
                'hydra:next': '/api/subdomains?organization=498&page=51',
                'hydra:previous': '/api/subdomains?organization=498&page=49'
            }
        }

        // @ts-ignore
        apiRequestService.get = vi.fn(async (url: string) => collection)

        const result = await entityManager.fetchCollection(DummyApiResource, null)

        expect(result.totalItems).toEqual(1000)
        expect(result.pagination.first).toEqual(1)
        expect(result.pagination.last).toEqual(100)
        expect(result.pagination.previous).toEqual(49)
        expect(result.pagination.next).toEqual(51)
    })
})

describe('persist', () => {
    test('new entity (POST)', async () => {
        const instance = new DummyApiModel({id: 'tmp1', name: 'bob'})
        instance.isNew = vi.fn(() => true)

        // @ts-ignore
        instance.$toJson = vi.fn(() => {
            return {id: 'tmp1', name: 'bob'}
        })

        // TODO: attendre de voir si cet appel est nécessaire dans l'entity manager
        // entityManager.cast = vi.fn((model: typeof ApiResource, entity: ApiResource): ApiResource => entity)

        const response = { id: 1, name: 'bob' }
        // @ts-ignore
        apiRequestService.post = vi.fn((url, data) => response)

        // @ts-ignore
        entityManager.newInstance = vi.fn((model, response) => {
            const newEntity = new DummyApiModel(response)
            // @ts-ignore
            newEntity.id = response.id
            // @ts-ignore
            newEntity.name = response.name

            return newEntity
        })

        // @ts-ignore
        entityManager.removeTempAfterPersist = vi.fn()

        const result = await entityManager.persist(DummyApiModel, instance)

        // temp id should have been purged from the posted data
        expect(apiRequestService.post).toHaveBeenCalledWith('api/dummyModel', {name: 'bob'})
        expect(entityManager.newInstance).toHaveBeenCalledWith(DummyApiModel, response)
        // @ts-ignore
        expect(entityManager.removeTempAfterPersist).toHaveBeenCalledWith(DummyApiModel, instance.id)

        expect(result.id).toEqual(1)
        expect(result.name).toEqual('bob')
    })

    test('existing entity (PUT)', async () => {
        const props = {id: 1, name: 'bob'}
        const entity = new DummyApiModel(props)
        entity.id = 1
        entity.isNew = vi.fn(() => false)

        // @ts-ignore
        entity.$toJson = vi.fn(() => props)

        // TODO: attendre de voir si cet appel est nécessaire dans l'entity manager
        // entityManager.cast = vi.fn((model: typeof ApiResource, entity: ApiResource): ApiResource => entity)

        // @ts-ignore
        apiRequestService.put = vi.fn((url, data) => props)

        // @ts-ignore
        entityManager.newInstance = vi.fn((model, response) => {
            const newEntity = new DummyApiModel(response)
            // @ts-ignore
            newEntity.id = response.id
            // @ts-ignore
            newEntity.name = response.name

            return newEntity
        })

        // @ts-ignore
        entityManager.removeTempAfterPersist = vi.fn()

        const result = await entityManager.persist(DummyApiModel, entity)

        expect(apiRequestService.put).toHaveBeenCalledWith('api/dummyModel/1', {id: 1, name: 'bob'})
        expect(entityManager.newInstance).toHaveBeenCalledWith(DummyApiModel, props)
        // @ts-ignore
        expect(entityManager.removeTempAfterPersist).toHaveBeenCalledTimes(0)

        expect(result.id).toEqual(1)
        expect(result.name).toEqual('bob')
    })
})


describe('patch', () => {
    test('simple call', async () => {
        const props = {id: 1, name: 'bobby'}

        // @ts-ignore
        apiRequestService.put = vi.fn((url, data) => props)

        // @ts-ignore
        entityManager.newInstance = vi.fn((model, response) => {
            const newEntity = new DummyApiModel(response)
            // @ts-ignore
            newEntity.id = response.id
            // @ts-ignore
            newEntity.name = response.name

            return newEntity
        })

        const result = await entityManager.patch(DummyApiModel, 1, {name: 'bobby'})

        expect(apiRequestService.put).toHaveBeenCalledWith('api/dummyModel/1', '{"name":"bobby"}')
        expect(entityManager.newInstance).toHaveBeenCalledWith(DummyApiModel, {id: 1, name: 'bobby'})

        expect(result.id).toEqual(1)
        expect(result.name).toEqual('bobby')
    })
})


describe('delete', () => {
    test('delete non persisted entity', () => {
        const entity = new DummyApiModel()
        entity.isNew = vi.fn(() => true)
        entity.id = 'tmp123'

        // @ts-ignore
        const repo = vi.fn() as Repository<ApiResource>

        // @ts-ignore
        entityManager.getRepository = vi.fn((model: typeof ApiResource) => {
            return model === DummyApiModel ? repo : null
        })

        apiRequestService.delete = vi.fn()

        // @ts-ignore
        repo.destroy = vi.fn((id: number) => null)

        entityManager.delete(DummyApiModel, entity)

        expect(entityManager.getRepository).toHaveBeenCalledWith(DummyApiModel)
        expect(apiRequestService.delete).toHaveBeenCalledTimes(0)
        expect(repo.destroy).toHaveBeenCalledWith('tmp123')
    })

    test('delete persisted entity', async () => {
        const entity = new DummyApiModel()
        entity.isNew = vi.fn(() => false)
        entity.id = 1

        // @ts-ignore
        const repo = vi.fn() as Repository<ApiResource>

        // @ts-ignore
        entityManager.getRepository = vi.fn((model: typeof ApiResource) => {
            return model === DummyApiModel ? repo : null
        })

        // @ts-ignore
        apiRequestService.delete = vi.fn((id: number) => null)

        // @ts-ignore
        repo.destroy = vi.fn((id: number) => null)

        await entityManager.delete(DummyApiModel, entity)

        expect(entityManager.getRepository).toHaveBeenCalledWith(DummyApiModel)
        expect(apiRequestService.delete).toHaveBeenCalledWith('api/dummyModel/1')
        expect(repo.destroy).toHaveBeenCalledWith(1)
    })
})

describe('reset', () => {
    test('simple call', () => {
        const entity = new DummyApiModel()
        entity.id = 1
        entity.name = 'paul'

        const initialEntity = new DummyApiModel()
        initialEntity.id = 1
        initialEntity.name = 'serges'

        // @ts-ignore
        entityManager.getInitialStateOf = vi.fn((model: typeof ApiResource, id: string | number) => initialEntity)

        // @ts-ignore
        const repo = vi.fn() as Repository<ApiResource>

        // @ts-ignore
        entityManager.getRepository = vi.fn((model: typeof ApiResource) => {
            return model === DummyApiModel ? repo : null
        })

        // @ts-ignore
        repo.save = vi.fn((data: any) => null)

        const result = entityManager.reset(DummyApiModel, entity)

        // @ts-ignore
        expect(entityManager.getInitialStateOf).toHaveBeenCalledWith(DummyApiModel, 1)
        expect(repo.save).toHaveBeenCalledWith(initialEntity)
        expect(result).toEqual(initialEntity)
    })

    test('no initial state stored', () => {
        const entity = new DummyApiModel()
        entity.id = 1

        // @ts-ignore
        entityManager.getInitialStateOf = vi.fn((model: typeof ApiResource, id: string | number) => null)

        expect(() => entityManager.reset(DummyApiModel, entity)).toThrowError(
            'no initial state recorded for this object - abort [dummyModel/1]'
        )
    })
})

describe('flush', () => {
    test('simple call', () => {
        // @ts-ignore
        const repo = vi.fn() as Repository<ApiResource>

        // @ts-ignore
        entityManager.getRepository = vi.fn((model: typeof ApiResource) => {
            return model === DummyApiModel ? repo : null
        })

        repo.flush = vi.fn()

        entityManager.flush(DummyApiModel)

        expect(repo.flush).toHaveBeenCalled()
    })
})

describe('isNewEntity', () => {
    test('with new entity', () => {
        const entity = new DummyApiModel()
        entity.isNew = vi.fn(() => true)

        // @ts-ignore
        const repo = vi.fn() as Repository<ApiResource>

        // @ts-ignore
        entityManager.getRepository = vi.fn((model: typeof ApiResource) => {
            return model === DummyApiModel ? repo : null
        })

        // @ts-ignore
        repo.find = vi.fn((id: number) => entity)

        const result = entityManager.isNewInstance(DummyApiModel, 1)

        expect(result).toBeTruthy()
    })

    test('with existing entity', () => {
        const entity = new DummyApiModel()
        entity.isNew = vi.fn(() => false)

        // @ts-ignore
        const repo = vi.fn() as Repository<ApiResource>

        // @ts-ignore
        entityManager.getRepository = vi.fn((model: typeof ApiResource) => {
            return model === DummyApiModel ? repo : null
        })

        // @ts-ignore
        repo.find = vi.fn((id: number) => entity)

        const result = entityManager.isNewInstance(DummyApiModel, 1)

        expect(result).toBeFalsy()
    })

    test('non-existing entity', () => {
        // @ts-ignore
        const repo = vi.fn() as Repository<ApiResource>

        // @ts-ignore
        entityManager.getRepository = vi.fn((model: typeof ApiResource) => {
            return model === DummyApiModel ? repo : null
        })

        // @ts-ignore
        repo.find = vi.fn((id: number) => null)

        console.error = vi.fn()

        const result = entityManager.isNewInstance(DummyApiModel, 1)

        expect(result).toBeFalsy()

        expect(console.error).toHaveBeenCalledWith('dummyModel/1 does not exist!')
    })
})

describe('saveInitialState', () => {
    test('simple call', () => {
        // @ts-ignore
        const entity = { id: 1, name: 'bob' } as DummyApiResource

        // @ts-ignore
        const repo = vi.fn() as Repository<ApiResource>

        // @ts-ignore
        entityManager.getRepository = vi.fn((model: typeof ApiResource) => {
            return model === DummyApiResource ? repo : null
        })

        // @ts-ignore
        repo.save = vi.fn((record: Element) => null)

        // @ts-ignore
        entityManager.saveInitialState(DummyApiResource, entity)

        expect(repo.save).toHaveBeenCalledWith({ id: '_clone_1', name: 'bob' })
        expect(entity.id).toEqual(1)
    })
})

describe('getInitialStateOf', () => {
    test('with initial state', () => {
        // @ts-ignore
        const entity = { id: 1, name: 'bob' } as DummyApiResource

        // @ts-ignore
        const repo = vi.fn() as Repository<ApiResource>

        // @ts-ignore
        entityManager.getRepository = vi.fn((model: typeof ApiResource) => {
            return model === DummyApiResource ? repo : null
        })

        // @ts-ignore
        repo.find = vi.fn((id: number | string) => {
            // @ts-ignore
            return { id: 1, name: 'robert' } as DummyApiResource
        })

        // @ts-ignore
        const result = entityManager.getInitialStateOf(DummyApiResource, 1) as DummyApiResource

        expect(repo.find).toHaveBeenCalledWith('_clone_1')
        expect(result.id).toEqual(1)
        expect(result.name).toEqual('robert')
    })

    test('without initial state', () => {
        // @ts-ignore
        const repo = vi.fn() as Repository<ApiResource>

        // @ts-ignore
        entityManager.getRepository = vi.fn((model: typeof ApiResource) => {
            return model === DummyApiResource ? repo : null
        })

        // @ts-ignore
        repo.find = vi.fn((id: number | string) => null)

        // @ts-ignore
        const result = entityManager.getInitialStateOf(DummyApiResource, 1) as DummyApiResource

        expect(repo.find).toHaveBeenCalledWith('_clone_1')
        expect(result).toEqual(null)
    })
})

describe('removeTempAfterPersist', () => {
    test('simple call', () => {
        // @ts-ignore
        const entity = new DummyApiResource()
        entity.id = 'tmp123'
        entity.isNew = vi.fn(() => true)

        // @ts-ignore
        const repo = vi.fn() as Repository<ApiResource>

        // @ts-ignore
        entityManager.getRepository = vi.fn((model: typeof ApiResource) => {
            return model === DummyApiResource ? repo : null
        })

        // @ts-ignore
        repo.find = vi.fn((id: number | string) => entity)

        // @ts-ignore
        repo.destroy = vi.fn()

        // @ts-ignore
        entityManager.removeTempAfterPersist(DummyApiResource, 'tmp123')

        expect(repo.destroy).toHaveBeenCalledWith('tmp123')
        expect(repo.destroy).toHaveBeenCalledWith('_clone_tmp123')
    })

    test('entity is not temporary', () => {
        // @ts-ignore
        const entity = new DummyApiResource()
        entity.id = 1
        entity.isNew = vi.fn(() => false)

        // @ts-ignore
        const repo = vi.fn() as Repository<ApiResource>

        // @ts-ignore
        entityManager.getRepository = vi.fn((model: typeof ApiResource) => {
            return model === DummyApiResource ? repo : null
        })

        // @ts-ignore
        repo.find = vi.fn((id: number | string) => entity)

        // @ts-ignore
        repo.destroy = vi.fn()

        // @ts-ignore
        expect(() => entityManager.removeTempAfterPersist(DummyApiResource, 'tmp123')).toThrowError(
            'Error: Can not remove a non-temporary model instance'
        )

        expect(repo.destroy).toHaveBeenCalledTimes(0)
    })

    test('entity does not exist', () => {
        // @ts-ignore
        const repo = vi.fn() as Repository<ApiResource>

        // @ts-ignore
        entityManager.getRepository = vi.fn((model: typeof ApiResource) => {
            return model === DummyApiResource ? repo : null
        })

        // @ts-ignore
        repo.find = vi.fn((id: number | string) => null)

        // @ts-ignore
        repo.destroy = vi.fn()

        console.error = vi.fn()

        // @ts-ignore
        entityManager.removeTempAfterPersist(DummyApiResource, 'tmp123')

        expect(repo.destroy).toHaveBeenCalledTimes(0)
        expect(console.error).toHaveBeenCalledWith('dummyResource/tmp123 does not exist!')
    })
})
