import type { TlBinaryReader, TlReaderMap } from '@mtcute/tl-runtime'
import { hex, utf8 } from '@fuman/utils'
import { defaultPlatform, defaultTestCryptoProvider } from '@mtcute/test'
import Long from 'long'
import { describe, expect, it, vi } from 'vitest'

import { LogManager } from '../utils/index.js'

import { AuthKey } from './auth-key.js'

const authKey = new Uint8Array(256)

for (let i = 0; i < 256; i += 32) {
    authKey.subarray(i, i + 32).set(hex.decode('98cb29c6ffa89e79da695a54f572e6cb101e81c688b63a4bf73c3622dec230e0'))
}

describe('AuthKey', () => {
    async function create() {
        const logger = new LogManager(undefined, defaultPlatform)
        const readerMap: TlReaderMap = {}
        const crypto = await defaultTestCryptoProvider()

        const key = new AuthKey(crypto, logger, readerMap)
        key.setup(authKey)

        return key
    }

    const msgId = Long.fromBits(0xBEEF1234, 0x1234BEEF, true)
    const seqNo = 777
    const serverSalt = Long.fromBits(0xDEADBEEF, 0xBEEFDEAD)
    const sessionId = Long.fromBits(0xFEEDBEEF, 0xBEEFFEED)

    function writeMessage(body: Uint8Array) {
        const buf = new Uint8Array(16 + body.length)
        const dv = new DataView(buf.buffer)

        dv.setInt32(0, msgId.low, true)
        dv.setInt32(4, msgId.high, true)
        dv.setInt32(8, seqNo, true)
        dv.setInt32(12, body.length, true)
        buf.set(body, 16)

        return buf
    }

    it('should calculate derivatives', async () => {
        const key = await create()

        expect(hex.encode(key.key)).toEqual(hex.encode(authKey))
        expect(hex.encode(key.clientSalt)).toEqual('f73c3622dec230e098cb29c6ffa89e79da695a54f572e6cb101e81c688b63a4b')
        expect(hex.encode(key.serverSalt)).toEqual('98cb29c6ffa89e79da695a54f572e6cb101e81c688b63a4bf73c3622dec230e0')
        expect(hex.encode(key.id)).toEqual('40fa5bb7cb56a895')
    })

    it('should encrypt a message', async () => {
        const message = writeMessage(utf8.encoder.encode('hello, world!!!!'))

        const key = await create()
        const msg = key.encryptMessage(message, serverSalt, sessionId)

        expect(hex.encode(msg)).toEqual(
            '40fa5bb7cb56a895f6f5a88914892aadf87c68031cc953ba29d68e118021f329'
            + 'be386a620d49f3ad3a50c60dcef3733f214e8cefa3e403c11d193637d4971dc1'
            + '5db7f74b26fd16cb0e8fee30bf7e3f68858fe82927e2cd06',
        )
    })

    describe('decrypt', () => {
        async function decrypt(message: Uint8Array) {
            const key = await create()

            return new Promise<[Long, number, TlBinaryReader]>((resolve, reject) => {
                // in this method, errors are not thrown but rather logged
                vi.spyOn(key.log, 'warn').mockImplementation((msg, ...fmt) =>
                    // eslint-disable-next-line prefer-promise-reject-errors
                    reject(`${msg} : ${fmt.map(it => JSON.stringify(it)).join(' ')}`),
                )

                key.decryptMessage(message, sessionId, (...args) => resolve(args))
            })
        }

        it('should decrypt a message', async () => {
            const message = hex.decode(
                '40fa5bb7cb56a8950c394b884f1529efc42fea22d972fea650a714ce6d2d1bdb'
                + '3d98ff5929b8768c401771a69795f36a7e720dcafac2efbccd0ba368e8a7f48b'
                + '07362cac1a32ffcabe188b51a36cc4d54e1d0633cf9eaf35',
            )

            const [decMsgId, decSeqNo, data] = await decrypt(message)

            expect(decMsgId).toEqual(msgId)
            expect(decSeqNo).toEqual(seqNo)
            expect(utf8.decoder.decode(data.raw(16))).toEqual('hello, world!!!!')
        })

        it('should decrypt a message with padding', async () => {
            const message = hex.decode(
                '40fa5bb7cb56a8950c394b884f1529efc42fea22d972fea650a714ce6d2d1bdb'
                + '3d98ff5929b8768c401771a69795f36a7e720dcafac2efbccd0ba368e8a7f48b'
                + '07362cac1a32ffcabe188b51a36cc4d54e1d0633cf9eaf35'
                + 'deadbeef', // some padding (e.g. from padded transport),
            )

            const [decMsgId, decSeqNo, data] = await decrypt(message)

            expect(decMsgId).toEqual(msgId)
            expect(decSeqNo).toEqual(seqNo)
            expect(utf8.decoder.decode(data.raw(16))).toEqual('hello, world!!!!')
        })

        it('should ignore messages with invalid message key', async () => {
            const message = hex.decode(
                '40fa5bb7cb56a8950000000000000000000000000000000050a714ce6d2d1bdb'
                + '3d98ff5929b8768c401771a69795f36a7e720dcafac2efbccd0ba368e8a7f48b'
                + '07362cac1a32ffcabe188b51a36cc4d54e1d0633cf9eaf35',
            )

            await expect(() => decrypt(message)).rejects.toThrow('message with invalid messageKey')
        })

        it('should ignore messages with invalid session_id', async () => {
            const message = hex.decode(
                '40fa5bb7cb56a895a986a7e97f4e90aa2769b5e702c6e86f5e1e82c6ff0c6829'
                + '2521a2ba9704fa37fb341d895cf32662c6cf47ba31cbf27c30d5c03f6c2930f4'
                + '30fd8858b836b73fe32d4a95b8ebcdbc9ca8908f7964c40a',
            )

            await expect(() => decrypt(message)).rejects.toThrow('message with invalid sessionId')
        })

        it('should ignore messages with invalid length', async () => {
            const messageTooLong = hex.decode(
                '40fa5bb7cb56a8950d19412233dd5d24be697c73274e08fbe515cf65e0c5f70c'
                + 'ad75fd2badc18c9f999f287351144eeb1cfcaa9bea33ef5058999ad96a498306'
                + '08d2859425685a55b21fab413bfabc42ec5da283853b28c0',
            )
            const messageUnaligned = hex.decode(
                '40fa5bb7cb56a8957b4e4bec561eee4a5a1025bc8a35d3d0c79a3685d2b90ff0'
                + '5f638e9c42c9fd9448b0ce8e7d49e7ea1ce458e47b825b5c7fd8ddf5b4fded46'
                + '2a4bcc02f3ff2e89de6764d6d219f575e457fdcf8c163cdf',
            )

            await expect(() => decrypt(messageTooLong)).rejects.toThrow('message with invalid length: %d > %d')
            await expect(() => decrypt(messageUnaligned)).rejects.toThrow(
                'message with invalid length: %d is not a multiple of 4',
            )
        })

        it('should ignore messages with invalid padding', async () => {
            const message = hex.decode(
                '40fa5bb7cb56a895133671d1c637a9836e2c64b4d1a0521d8a25a6416fd4dc9e'
                + '79f9478fb837703cc9efa0a19d12143c2a26e57cb4bc64d7bc972dd8f19c53c590cc258162f44afc',
            )

            await expect(() => decrypt(message)).rejects.toThrow('message with invalid padding size')
        })
    })
})
