import type { ICryptoProvider } from '@mtcute/core/utils.js'

import type { MockInstance } from 'vitest'
import { gzipSync, inflateSync } from 'node:zlib'
import { hex, typed, utf8 } from '@fuman/utils'
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest'

import { defaultCryptoProvider } from './platform.js'

// some random 1024 bytes of entropy
const DEFAULT_ENTROPY = `
29afd26df40fb8ed10b6b4ad6d56ef5df9453f88e6ee6adb6e0544ba635dc6a8a990c9b8b980c343936b33fa7f97bae025102532233abb269c489920ef99021b
259ce3a2c964c5c8972b4a84ff96f3375a94b535a9468f2896e2080ac7a32ed58e910474a4b02415e07671cbb5bdd58a5dd26fd137c4c98b8c346571fae6ead3
9dfd612bd6b480b6723433f5218e9d6e271591153fb3ffefc089f7e848d3f4633459fff66b33cf939e5655813149fa34be8625f9bc4814d1ee6cf40e4d0de229
1aa22e68c8ad8cc698103734f9aaf79f2bdc052a787a7a9b3629d1ed38750f88cb0481c0ba30a9c611672f9a4d1dc02637abb4e98913ee810a3b152d3d75f25d
7efdc263c08833569968b1771ebbe843d187e2c917d9ad8e8865e44b69f7b74d72ab86a4ef1891dce196ee11a7c9d7d8074fc0450e745bd3a827d77bb0820b90
3055dc15f0abd897ea740a99606b64d28968d770b5e43492ddbf07a7c75104d3e522be9b72050c0fdae8412cdf49014be21105b87a06cb7202dd580387adc007
6280d98b015a1a413819d817f007939d1490467a1ef85a345584c7e594bb729c12a1233f806e515e7088360219dfa109264310ba84777b93eb1ad3c40727a25a
a5d9cdd6748c6ab2ca0bd4daa2ba8225bce2b066a163bcacf05609fc84055bb86a4742c28addd7d7ab8d87b64cfde0b3f4b3bc8e05f3d0a1a2fadb294860e099
a10b3721b0d5b28918b8fb49a18a82a0fde6680a64ed915637805e35ffe8b2c1d4177ec10d10eaaf24425e0351b6a89e794944e1aa82eb5c0210a37da66cccac
895398cf915a8aa141f611521fc258514a99c02721113942c66f2c9a8f9601ff0044a953d17a47b07ad1b5f8725cc020a1a5239be65db0a43d42c206903740f0
27c3f749ecfff2e646570118cd54db2fec392b44d8eb8377309f3e4d164dbc0530914b117b9d278b06db8359d97442d4dcbcaff93cd9a08a6b06a5ba8725d0d7
06b313a5d792be254d33e087b7a4fafcdf819941b9bec4c6057d4c050bd01eb243efd4e6b707281b127820a2b734c6d8f6b2131bf0b5b215c7a798ff3fe90ceb
da91539fcc7b03d2b8b1381bd6023fff20278344ad944d364ba684842db3901c346335f0d455eda414f99c1e794a86aa3a90bcc6e085eecb0b4bf61198d16ed3
89cfa495f977a37a51502b2f60649f2efd7d89c757b6366776ba4c0612017bf1fbfc682dd62e9960d39cbea854d2dcc708b1db5d268192954d13ee72c0bb1bd8
558a3cf3b02b1cd795b40f7a57780391bb8724883d3f7764846c3823e165b3f8c025f59d896905f9a955478586ce57f820d958a01aa59a4cace7ecdf125df334
fa3de8e50aac96c1275591a1221c32a60a1513370a33a228e00894341b10cf44a6ae6ac250d17a364e956ab1a17b068df3fb2d5b5a672d8a409eeb8b6ca1ade6
`.replace(/\s/g, '')

export function withFakeRandom(provider: ICryptoProvider, source: string = DEFAULT_ENTROPY): ICryptoProvider {
    const sourceBytes = hex.decode(source)
    let offset = 0

    function getRandomValues(buf: Uint8Array) {
        if (offset + buf.length > sourceBytes.length) {
            throw new Error('not enough entropy')
        }

        buf.set(sourceBytes.subarray(offset, offset + buf.length))
        offset += buf.length
    }

    return new Proxy<ICryptoProvider>(provider, {
        get(target, prop, receiver) {
            if (prop === 'randomFill') return getRandomValues

            // eslint-disable-next-line ts/no-unsafe-return
            return Reflect.get(target, prop, receiver)
        },
    })
}

export function useFakeMathRandom(source: string = DEFAULT_ENTROPY): void {
    const sourceBytes = hex.decode(source)
    const dv = typed.toDataView(sourceBytes)

    let spy: MockInstance<() => number>

    beforeEach(() => {
        let offset = 0

        spy = vi.spyOn(globalThis.Math, 'random').mockImplementation(() => {
            const ret = dv.getUint32(offset, true) / 0xFFFFFFFF
            offset += 4

            return ret
        })
    })
    afterEach(() => {
        spy.mockRestore()
    })
}

export async function defaultTestCryptoProvider(source: string = DEFAULT_ENTROPY): Promise<ICryptoProvider> {
    const prov = withFakeRandom(defaultCryptoProvider, source)
    await prov.initialize?.()

    return prov
}

export function testCryptoProvider(c: ICryptoProvider): void {
    beforeAll(() => c.initialize?.())

    function gzipSyncWrap(data: Uint8Array) {
        if (import.meta.env.TEST_ENV === 'browser') {
            // @ts-expect-error fucking crutch because @jspm/core uses Buffer.isBuffer for some reason
            data._isBuffer = true

            return new Uint8Array(gzipSync(data))
        }

        return gzipSync(data)
    }

    function inflateSyncWrap(data: Uint8Array) {
        if (import.meta.env.TEST_ENV === 'browser') {
            // @ts-expect-error fucking crutch because @jspm/core uses Buffer.isBuffer for some reason
            data._isBuffer = true

            return new Uint8Array(inflateSync(data))
        }

        return inflateSync(data)
    }

    it('should calculate sha1', () => {
        expect(hex.encode(c.sha1(utf8.encoder.encode('')))).to.eq('da39a3ee5e6b4b0d3255bfef95601890afd80709')
        expect(hex.encode(c.sha1(utf8.encoder.encode('hello')))).to.eq('aaf4c61ddcc5e8a2dabede0f3b482cd9aea9434d')
        expect(hex.encode(c.sha1(hex.decode('aebb1f')))).to.eq('62849d15c5dea495916c5eea8dba5f9551288850')
    })

    it('should calculate sha256', () => {
        expect(hex.encode(c.sha256(utf8.encoder.encode('')))).to.eq(
            'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855',
        )
        expect(hex.encode(c.sha256(utf8.encoder.encode('hello')))).to.eq(
            '2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824',
        )
        expect(hex.encode(c.sha256(hex.decode('aebb1f')))).to.eq(
            '2d29658aba48f2b286fe8bbddb931b7ad297e5adb5b9a6fc3aab67ef7fbf4e80',
        )
    })

    it('should calculate hmac-sha256', async () => {
        const key = hex.decode('aaeeff')

        expect(hex.encode(await c.hmacSha256(utf8.encoder.encode(''), key))).to.eq(
            '642711307c9e4437df09d6ebaa6bdc1b3a810c7f15c50fd1d0f8d7d5490f44dd',
        )
        expect(hex.encode(await c.hmacSha256(utf8.encoder.encode('hello'), key))).to.eq(
            '39b00bab151f9868e6501655c580b5542954711181243474d46b894703b1c1c2',
        )
        expect(hex.encode(await c.hmacSha256(hex.decode('aebb1f'), key))).to.eq(
            'a3a7273871808711cab17aba14f58e96f63f3ccfc5097d206f0f00ead2c3dd35',
        )
    })

    it('should derive pbkdf2 key', async () => {
        expect(hex.encode(await c.pbkdf2(utf8.encoder.encode('pbkdf2 test'), utf8.encoder.encode('some salt'), 10))).to.eq(
            'e43276cfa27f135f261cec8ddcf593fd74ec251038e459c165461f2308f3a7235e0744ee1aed9710b00db28d1a2112e20fea3601c60e770ac57ffe6b33ca8be1',
        )
    })

    it('should encrypt and decrypt aes-ctr', () => {
        let aes = c.createAesCtr(
            hex.decode('d450aae0bf0060a4af1044886b42a13f7c506b35255d134a7e87ab3f23a9493b'),
            hex.decode('0182de2bd789c295c3c6c875c5e9e190'),
            true,
        )

        const data = hex.decode('7baae571e4c2f4cfadb1931d5923aca7')
        expect(hex.encode(aes.process(data))).eq('df5647dbb70bc393f2fb05b72f42286f')
        expect(hex.encode(aes.process(data))).eq('3917147082672516b3177150129bc579')
        expect(hex.encode(aes.process(data))).eq('2a7a9089270a5de45d5e3dd399cac725')
        expect(hex.encode(aes.process(data))).eq('56d085217771398ac13583de4d677dd8')
        expect(hex.encode(aes.process(data))).eq('cc639b488126cf36e79c4515e8012b92')
        expect(hex.encode(aes.process(data))).eq('01384d100646cd562cc5586ec3f8f8c4')

        aes.close?.()
        aes = c.createAesCtr(
            hex.decode('d450aae0bf0060a4af1044886b42a13f7c506b35255d134a7e87ab3f23a9493b'),
            hex.decode('0182de2bd789c295c3c6c875c5e9e190'),
            false,
        )

        expect(hex.encode(aes.process(hex.decode('df5647dbb70bc393f2fb05b72f42286f')))).eq(hex.encode(data))
        expect(hex.encode(aes.process(hex.decode('3917147082672516b3177150129bc579')))).eq(hex.encode(data))
        expect(hex.encode(aes.process(hex.decode('2a7a9089270a5de45d5e3dd399cac725')))).eq(hex.encode(data))
        expect(hex.encode(aes.process(hex.decode('56d085217771398ac13583de4d677dd8')))).eq(hex.encode(data))
        expect(hex.encode(aes.process(hex.decode('cc639b488126cf36e79c4515e8012b92')))).eq(hex.encode(data))
        expect(hex.encode(aes.process(hex.decode('01384d100646cd562cc5586ec3f8f8c4')))).eq(hex.encode(data))

        aes.close?.()
    })

    it('should encrypt and decrypt aes-ige', () => {
        const aes = c.createAesIge(
            hex.decode('5468697320697320616E20696D706C655468697320697320616E20696D706C65'),
            hex.decode('6D656E746174696F6E206F6620494745206D6F646520666F72204F70656E5353'),
        )
        expect(
            hex.encode(aes.encrypt(hex.decode('99706487a1cde613bc6de0b6f24b1c7aa448c8b9c3403e3467a8cad89340f53b'))),
        ).to.eq('792ea8ae577b1a66cb3bd92679b8030ca54ee631976bd3a04547fdcb4639fa69')
        expect(
            hex.encode(aes.decrypt(hex.decode('792ea8ae577b1a66cb3bd92679b8030ca54ee631976bd3a04547fdcb4639fa69'))),
        ).to.eq('99706487a1cde613bc6de0b6f24b1c7aa448c8b9c3403e3467a8cad89340f53b')
    })

    it(
        'should decompose PQ to prime factors P and Q',
        async () => {
            const testFactorization = async (pq: string, p_: string, q: string) => {
                const [p1, q1] = await c.factorizePQ(hex.decode(pq))
                expect(hex.encode(p1)).eq(p_.toLowerCase())
                expect(hex.encode(q1)).eq(q.toLowerCase())
            }

            // from samples at https://core.telegram.org/mtproto/samples-auth_key
            await testFactorization('17ED48941A08F981', '494C553B', '53911073')
            // random example
            await testFactorization('14fcab4dfc861f45', '494c5c99', '494c778d')
        },
        // since PQ factorization relies on RNG, it may take a while (or may not!)
        { timeout: 10000 },
    )

    it('should correctly gzip', () => {
        const data = new Uint8Array(1000).fill(0x42)

        const compressed = c.gzip(data, 100)

        expect(compressed).not.toBeNull()

        const decompressed = inflateSyncWrap(compressed!)

        expect(compressed!.length).toBeLessThan(data.length)
        expect(hex.encode(decompressed)).toEqual(hex.encode(data))
    })

    it('should correctly gunzip', () => {
        const data = new Uint8Array(1000).fill(0x42)

        const compressed = gzipSyncWrap(data)
        const decompressed = c.gunzip(compressed)

        expect(hex.encode(decompressed)).toEqual(hex.encode(data))
    })

    describe('randomBytes', () => {
        it('should return exactly N bytes', () => {
            expect(c.randomBytes(0).length).eq(0)
            expect(c.randomBytes(5).length).eq(5)
            expect(c.randomBytes(10).length).eq(10)
            expect(c.randomBytes(256).length).eq(256)
        })

        it('should not be deterministic', () => {
            expect([...c.randomBytes(8)]).not.eql([...c.randomBytes(8)])
        })

        it('should use randomFill', () => {
            const spy = vi.spyOn(c, 'randomFill')
            c.randomBytes(8)

            expect(spy).toHaveBeenCalled()
        })
    })
}
