import { vi, describe, test, it, expect, afterEach } from 'vitest'
import {AnyJson} from "~/types/data";
import HydraNormalizer from "~/services/data/normalizer/hydraNormalizer";
import {METADATA_TYPE} from "~/types/enum/data";
import ApiModel from "~/models/ApiModel";
import {Str, Uid, Attr} from "pinia-orm/dist/decorators";
import {IriEncoded} from "~/models/decorators";
import UrlUtils from "~/services/utils/urlUtils";
import ApiResource from "~/models/ApiResource";

class DummyApiChild extends ApiModel {
    static entity = 'dummyChild'

    @Uid()
    declare id: number | string
}

class DummyApiModel extends ApiModel {
    static entity = 'dummyModel'

    @Uid()
    declare id: number | string

    @Attr(null)
    @IriEncoded(DummyApiChild)
    declare oneToOneRelation: number | null

    @Attr([])
    @IriEncoded(DummyApiChild)
    declare oneToManyRelation: []

    @Str(null)
    declare name: null
}

afterEach(() => {
    vi.clearAllMocks();
    vi.resetAllMocks();
});

describe('normalize', () => {
    const initialMakeUriMethod = UrlUtils.makeIRI
    //@ts-ignore
    const initialGetIriEncodedFieldsMethod = HydraNormalizer.getIriEncodedFields

    afterEach(() => {
        UrlUtils.makeIRI = initialMakeUriMethod
        //@ts-ignore
        HydraNormalizer.getIriEncodedFields = initialGetIriEncodedFieldsMethod
    })

    test('should normalize an entity into a JSON Object', () => {

        const entity: DummyApiModel = new DummyApiModel({
            id: 7351,
            oneToOneRelation: 99,
            oneToManyRelation: [123, 124, 125]
        })

        //@ts-ignore
        HydraNormalizer.getIriEncodedFields = vi.fn(
            (entity) => {
                return {
                    oneToOneRelation: DummyApiChild,
                    oneToManyRelation: DummyApiChild
                }
            }
        )

        //@ts-ignore
        UrlUtils.makeIRI = vi.fn(
            (targetEntity, id) => {
                return {
                    99: '/api/dummyChild/99',
                    123: '/api/dummyChild/123',
                    124: '/api/dummyChild/124',
                    125: '/api/dummyChild/125',
                }[id]
            }
        )

        const result = HydraNormalizer.normalizeEntity(entity)

        const expected = {
            id: 7351,
            name: null,
            oneToOneRelation: '/api/dummyChild/99',
            oneToManyRelation: ['/api/dummyChild/123', '/api/dummyChild/124', '/api/dummyChild/125']
        }

        expect(result).toStrictEqual(expected)
    })
})


describe('denormalize', () => {
    //@ts-ignore
    const initialGetDataMethod = HydraNormalizer.getData
    //@ts-ignore
    const initialGetMetadataMethod = HydraNormalizer.getMetadata

    afterEach(() => {
        //@ts-ignore
        HydraNormalizer.getData = initialGetDataMethod
        //@ts-ignore
        HydraNormalizer.getMetadata = initialGetMetadataMethod
    })

    test('should parse a API Item response and return a JSON Object', () => {
        const data: AnyJson = { id: 1, name: 'foo' };

        //@ts-ignore
        HydraNormalizer.getData = vi.fn((data, model) => {
            return data
        })

        //@ts-ignore
        HydraNormalizer.getMetadata = vi.fn((data, model) => {
            return { "type": METADATA_TYPE.ITEM }
        })

        const result = HydraNormalizer.denormalize(data, DummyApiModel)

        const expected = {
            "data": {
                id: 1,
                name: 'foo',
            },
            "metadata": {
                "type": METADATA_TYPE.ITEM
            }
        }

        expect(result).toEqual(expected)
    })
})

describe('getData', () => {
    test('With collection', () => {
        const data = {
            "@context": "/api/contexts/Foo",
            "@id": "/api/foo",
            "@type": "hydra:Collection",
            "hydra:member": [ 'foo' ],
        }

        // @ts-ignore
        expect(HydraNormalizer.getData(data)).toEqual([ 'foo' ])
    })

    test('With item', () => {
        const data = {
            "@context": "/api/contexts/Foo",
            "@id": "/api/foo",
            "@type": "Foo",
            "param1": 'a',
        }

        // @ts-ignore
        expect(HydraNormalizer.getData(data)).toEqual(data)
    })
})

describe('getMetadata', () => {
    test('With valid collection metadata', () => {
        const data = {
            "@context": "/api/contexts/Foo",
            "@id": "/api/foo",
            "@type": "hydra:Collection",
            "hydra:member": [ 'foo' ],
            "hydra:totalItems": 10,
            "hydra:view": {
                "@id": "/api/foo?page=3",
                "@type": "hydra:PartialCollectionView",
                "hydra:first": "/api/foo?page=1",
                "hydra:last": "/api/foo?page=5",
                "hydra:next": "/api/foo?page=4",
                "hydra:previous": "/api/foo?page=2"
            }
        }

        // @ts-ignore
        const metadata = HydraNormalizer.getMetadata(data)

        expect(metadata.totalItems).toEqual(10)
        expect(metadata.firstPage).toEqual(1)
        expect(metadata.lastPage).toEqual(5)
        expect(metadata.nextPage).toEqual(4)
        expect(metadata.previousPage).toEqual(2)
        expect(metadata.type).toEqual(METADATA_TYPE.COLLECTION)
    })
    test('With partial collection metadata', () => {
        const data = {
            "@context": "/api/contexts/Foo",
            "@id": "/api/foo",
            "@type": "hydra:Collection",
            "hydra:member": [ 'foo' ],
            "hydra:totalItems": 10,
            "hydra:view": {
                "@id": "/api/foo?page=3",
                "@type": "hydra:PartialCollectionView",
                "hydra:first": "/api/foo?page=1",
            }
        }

        // @ts-ignore
        const metadata = HydraNormalizer.getMetadata(data)

        expect(metadata.totalItems).toEqual(10)
        expect(metadata.firstPage).toEqual(1)
        expect(metadata.lastPage).toEqual(1)
        expect(metadata.nextPage).toEqual(undefined)
        expect(metadata.previousPage).toEqual(undefined)
    })

    test('With item metadata', () => {
        // @ts-ignore
        const metadata = HydraNormalizer.getMetadata({})
        expect(metadata.type).toEqual(METADATA_TYPE.ITEM)
    })
})

describe('denormalizeItem', () => {
    //@ts-ignore
    const initialDenormalizeEntity = HydraNormalizer.denormalizeEntity
    //@ts-ignore
    const initialDenormalizeEnum = HydraNormalizer.denormalizeEnum
    //@ts-ignore
    const initialParseEntityIRI = HydraNormalizer.parseEntityIRI
    const initialConsoleError = console.error

    afterEach(() => {
        //@ts-ignore
        HydraNormalizer.denormalizeEntity = initialDenormalizeEntity
        //@ts-ignore
        HydraNormalizer.denormalizeEnum = initialDenormalizeEnum
        //@ts-ignore
        HydraNormalizer.parseEntityIRI = initialParseEntityIRI

        console.error = initialConsoleError
    })

    test('With provided model', () => {

        const item = {
            '@id': '/api/dummyModel/1',
            id: 1,
            name: 'foo'
        }

        const model = DummyApiModel
        const expected = new DummyApiModel(item)

        //@ts-ignore
        HydraNormalizer.denormalizeEntity = vi.fn((model, item) => {
            return expected
        })

        //@ts-ignore
        const result = HydraNormalizer.denormalizeItem(item, model)

        expect(result).toEqual(expected)

        //@ts-ignore
        expect(HydraNormalizer.denormalizeEntity).toHaveBeenCalledWith(model, item)
    })

    test('with no @id prop', () => {
        const item = {
            id: 1,
            name: 'foo'
        }

        console.error = vi.fn((msg) => {})

        //@ts-ignore
        const result = HydraNormalizer.denormalizeItem(item)

        expect(result).toEqual(item)

        expect(console.error).toHaveBeenCalledWith(
            'De-normalization error : the item is not hydra formatted',
            item
        )
    })

    test('with enum', () => {
        const item = {
            '@id': '/api/enum/abc',
            A: 1,
            B: 2,
            C: 3
        }

        //@ts-ignore
        HydraNormalizer.denormalizeEnum = vi.fn((item) => {
            return item
        })

        //@ts-ignore
        const result = HydraNormalizer.denormalizeItem(item)

        expect(result).toEqual(item)

        //@ts-ignore
        expect(HydraNormalizer.denormalizeEnum).toHaveBeenCalledWith(item)
    })

    test('with unparsable iri', () => {
        const item = {
            '@id': 'some_invalid_iri',
            id: 1,
            name: 'foo'
        }

        //@ts-ignore
        HydraNormalizer.parseEntityIRI = vi.fn((iri) => {
            throw('parsing error')
        })

        console.error = vi.fn((msg) => {})

        //@ts-ignore
        const result = HydraNormalizer.denormalizeItem(item)

        expect(result).toEqual(item)

        //@ts-ignore
        expect(console.error).toHaveBeenCalledWith(
            'De-normalization error : could not parse the IRI',
            item
        )
    })

    test('With valid iri and existing model', () => {

        HydraNormalizer.models = {
            'dummyModel': DummyApiModel
        }

        const item = {
            '@id': '/api/dummyModel/1',
            id: 1,
            name: 'foo'
        }

        //@ts-ignore
        HydraNormalizer.parseEntityIRI = vi.fn((iri) => {
            return { entity: 'dummyModel' }
        })

        const expected = new DummyApiModel(item)

        //@ts-ignore
        HydraNormalizer.denormalizeEntity = vi.fn((model, item) => {
            return expected
        })

        //@ts-ignore
        const result = HydraNormalizer.denormalizeItem(item)

        expect(result).toEqual(expected)

        //@ts-ignore
        expect(HydraNormalizer.denormalizeEntity).toHaveBeenCalledWith(DummyApiModel, item)
    })

    test('With valid iri and un-existing model', () => {

        HydraNormalizer.models = {
            'dummyModel': DummyApiModel
        }

        const item = {
            '@id': '/api/unknownModel/1',
            id: 1,
            name: 'foo'
        }

        //@ts-ignore
        HydraNormalizer.parseEntityIRI = vi.fn((iri) => {
            return 'unknownModel'
        })

        expect(
            //@ts-ignore
            () => HydraNormalizer.denormalizeItem(item)
        ).toThrowError()
    })
})

describe('denormalizeEntity', () => {
    //@ts-ignore
    const initialGetIriEncodedFieldsMethod = HydraNormalizer.getIriEncodedFields
    //@ts-ignore
    const initialGetIdFromEntityIriMethod = HydraNormalizer.getIdFromEntityIri

    afterEach(() => {
        //@ts-ignore
        HydraNormalizer.getIriEncodedFields = initialGetIriEncodedFieldsMethod
        //@ts-ignore
        HydraNormalizer.getIdFromEntityIri = initialGetIdFromEntityIriMethod
    })

    test('should denormalize a Json object into an entity', () => {
        const data = {
            id: 7351,
            name: null,
            oneToOneRelation: '/api/dummyChild/99',
            oneToManyRelation: ['/api/dummyChild/123', '/api/dummyChild/124', '/api/dummyChild/125']
        }

        //@ts-ignore
        HydraNormalizer.getIriEncodedFields = vi.fn(
            (entity) => {
                return {
                    oneToOneRelation: DummyApiChild,
                    oneToManyRelation: DummyApiChild
                }
            }
        )

        //@ts-ignore
        HydraNormalizer.getIdFromEntityIri = vi.fn((iri) => {
            return {
                '/api/dummyChild/99': 99,
                '/api/dummyChild/123': 123,
                '/api/dummyChild/124': 124,
                '/api/dummyChild/125': 125
            }[iri]
        })

        //@ts-ignore
        const result = HydraNormalizer.denormalizeEntity(DummyApiModel, data)

        expect(result).toStrictEqual(new DummyApiModel({
            id: 7351,
            name: null,
            oneToOneRelation: 99,
            oneToManyRelation: [123, 124, 125]
        }))

    })
})

describe('denormalizeEnum', () => {
    test('does nothing', () => {
        const item = {
            '@id': '/api/enum/abc',
            A: 1,
            B: 2,
            C: 3
        }

        //@ts-ignore
        expect(HydraNormalizer.denormalizeEnum(item)).toStrictEqual(item)
    })
})

describe('parseEntityIRI', () => {
    test('valid iri', () => {

        const iri = '/api/someEntity/456'
        const expected = {
            entity: 'someEntity',
            id: 456
        }

        //@ts-ignore
        expect(HydraNormalizer.parseEntityIRI(iri)).toStrictEqual(expected)
    })

    test('invalid iri', () => {
        const iri = 'some_invalid_iri'

        //@ts-ignore
        expect(() => HydraNormalizer.parseEntityIRI(iri)).toThrowError('could not parse the IRI : "some_invalid_iri"')
    })
})

describe('getIriEncodedFields', () => {
    const initialRelations = DummyApiModel.relations

    afterEach(() => {
        DummyApiModel.relations = initialRelations
    })

    test('get relations', () => {
        //@ts-ignore
        const relations:  Record<string, ApiResource> = {'someField': DummyApiChild}

        DummyApiModel.relations = relations

        const entity = new DummyApiModel()

        //@ts-ignore
        const results = HydraNormalizer.getIriEncodedFields(entity)

        expect(results).toEqual(relations)
    })
})

describe('getIdFromEntityIri', () => {
    //@ts-ignore
    const initialParseEntityIRI = HydraNormalizer.parseEntityIRI

    afterEach(() => {
        //@ts-ignore
        HydraNormalizer.parseEntityIRI = initialParseEntityIRI
    })

    test('valid iri', () => {
        const iri = '/api/someEntity/456'

        //@ts-ignore
        const result = HydraNormalizer.getIdFromEntityIri(iri, 'someEntity')

        expect(result).toEqual(456)
    })

    test('entity not matching', () => {
        const iri = '/api/someEntity/456'

        //@ts-ignore
        expect(
            //@ts-ignore
            () => HydraNormalizer.getIdFromEntityIri(iri, 'otherEntity')
        ).toThrowError("IRI entity does not match the field's target entity (someEntity != otherEntity)")
    })


})
