/**
 * Tests for E2E encryption (crypto service + conversations)
 */

import { asyncTest, describe, assertEquals, assertTrue, assertFalse, assertNotEquals } from './test-runner.js'
import { cryptoService } from '../js/services/crypto.js'
import { conversationsService } from '../js/services/conversations.js'
import { keyPinningService } from '../js/services/key-pinning.js'

const TEST_UUID = '00000000-0000-4000-a000-000000000001'
const TEST_UUID_2 = '00000000-0000-4000-a000-000000000002'

// ── Crypto init & keypair management ──

describe('CryptoService', () => {
    asyncTest('loads NaCl library', async () => {
        await cryptoService.ready
        assertTrue(cryptoService.nacl !== null)
        assertTrue(cryptoService.naclUtil !== null)
    })

    asyncTest('getPublicKey returns null when locked', async () => {
        await cryptoService.ready
        cryptoService.lock()
        assertEquals(cryptoService.getPublicKey(), null)
    })

    asyncTest('unlock generates keypair and stores encrypted', async () => {
        localStorage.removeItem('kashilo_keypair')
        localStorage.removeItem('kashilo_keypair_salt')

        await cryptoService.unlock(TEST_UUID)

        assertTrue(cryptoService.keyPair !== null)
        assertTrue(cryptoService.getPublicKey() !== null)
        assertTrue(cryptoService.getPublicKey().length > 10)

        const stored = JSON.parse(localStorage.getItem('kashilo_keypair'))
        assertTrue(stored.ct !== undefined, 'stored keypair should be encrypted (has ct)')
        assertTrue(stored.iv !== undefined, 'stored keypair should be encrypted (has iv)')
        assertFalse(stored.publicKey !== undefined, 'stored keypair should NOT have plaintext publicKey')
    })

    asyncTest('unlock restores same keypair from encrypted storage', async () => {
        await cryptoService.unlock(TEST_UUID)
        const pubKey1 = cryptoService.getPublicKey()

        cryptoService.lock()
        assertEquals(cryptoService.getPublicKey(), null)

        await cryptoService.unlock(TEST_UUID)
        const pubKey2 = cryptoService.getPublicKey()

        assertEquals(pubKey1, pubKey2)
    })

    asyncTest('unlock with wrong UUID generates new keypair', async () => {
        await cryptoService.unlock(TEST_UUID)
        const pubKey1 = cryptoService.getPublicKey()

        cryptoService.lock()

        const origWarn = console.warn
        console.warn = () => {}
        await cryptoService.unlock(TEST_UUID_2)
        console.warn = origWarn
        const pubKey2 = cryptoService.getPublicKey()

        assertNotEquals(pubKey1, pubKey2)
    })

    asyncTest('lock clears keypair from memory', async () => {
        cryptoService.destroyKeyPair()
        await cryptoService.unlock(TEST_UUID)
        assertTrue(cryptoService.keyPair !== null)

        cryptoService.lock()
        assertEquals(cryptoService.keyPair, null)

        assertTrue(localStorage.getItem('kashilo_keypair') !== null)
    })

    asyncTest('destroyKeyPair clears memory and storage', async () => {
        await cryptoService.unlock(TEST_UUID)
        cryptoService.destroyKeyPair()

        assertEquals(cryptoService.keyPair, null)
        assertEquals(localStorage.getItem('kashilo_keypair'), null)
        assertEquals(localStorage.getItem('kashilo_keypair_salt'), null)
    })

    asyncTest('migrates plaintext keypair to encrypted on unlock', async () => {
        await cryptoService.ready
        const kp = cryptoService.nacl.box.keyPair()
        const plaintext = {
            publicKey: cryptoService.naclUtil.encodeBase64(kp.publicKey),
            secretKey: cryptoService.naclUtil.encodeBase64(kp.secretKey)
        }
        localStorage.setItem('kashilo_keypair', JSON.stringify(plaintext))
        localStorage.removeItem('kashilo_keypair_salt')

        await cryptoService.unlock(TEST_UUID)

        assertEquals(cryptoService.getPublicKey(), plaintext.publicKey)

        const stored = JSON.parse(localStorage.getItem('kashilo_keypair'))
        assertTrue(stored.ct !== undefined, 'should be migrated to encrypted format')
        assertTrue(stored.iv !== undefined)
    })
})

// ── Encrypt / Decrypt roundtrip ──

describe('E2E encryption roundtrip', () => {
    asyncTest('encrypt and decrypt between two keypairs', async () => {
        await cryptoService.ready
        const nacl = cryptoService.nacl
        const naclUtil = cryptoService.naclUtil

        const alice = nacl.box.keyPair()
        const bob = nacl.box.keyPair()
        const alicePub = naclUtil.encodeBase64(alice.publicKey)
        const bobPub = naclUtil.encodeBase64(bob.publicKey)

        cryptoService.keyPair = alice
        const { nonce, ciphertext } = cryptoService.encrypt('Hello Bob!', bobPub)

        assertTrue(nonce.length > 0)
        assertTrue(ciphertext.length > 0)
        assertNotEquals(ciphertext, 'Hello Bob!')

        cryptoService.keyPair = bob
        const plaintext = cryptoService.decrypt(ciphertext, nonce, alicePub)
        assertEquals(plaintext, 'Hello Bob!')
    })

    asyncTest('decrypt with wrong key returns null', async () => {
        await cryptoService.ready
        const nacl = cryptoService.nacl
        const naclUtil = cryptoService.naclUtil

        const alice = nacl.box.keyPair()
        const bob = nacl.box.keyPair()
        const eve = nacl.box.keyPair()
        const bobPub = naclUtil.encodeBase64(bob.publicKey)
        const alicePub = naclUtil.encodeBase64(alice.publicKey)

        cryptoService.keyPair = alice
        const { nonce, ciphertext } = cryptoService.encrypt('Secret message', bobPub)

        cryptoService.keyPair = eve
        const result = cryptoService.decrypt(ciphertext, nonce, alicePub)
        assertEquals(result, null)
    })

    asyncTest('handles empty string message', async () => {
        await cryptoService.ready
        const nacl = cryptoService.nacl
        const naclUtil = cryptoService.naclUtil

        const alice = nacl.box.keyPair()
        const bob = nacl.box.keyPair()

        cryptoService.keyPair = alice
        const { nonce, ciphertext } = cryptoService.encrypt('', naclUtil.encodeBase64(bob.publicKey))

        cryptoService.keyPair = bob
        const plaintext = cryptoService.decrypt(ciphertext, nonce, naclUtil.encodeBase64(alice.publicKey))
        assertEquals(plaintext, '')
    })

    asyncTest('handles unicode and emoji', async () => {
        await cryptoService.ready
        const nacl = cryptoService.nacl
        const naclUtil = cryptoService.naclUtil

        const alice = nacl.box.keyPair()
        const bob = nacl.box.keyPair()

        const message = 'Preis: 50€ 🎉 日本語テスト'

        cryptoService.keyPair = alice
        const { nonce, ciphertext } = cryptoService.encrypt(message, naclUtil.encodeBase64(bob.publicKey))

        cryptoService.keyPair = bob
        const plaintext = cryptoService.decrypt(ciphertext, nonce, naclUtil.encodeBase64(alice.publicKey))
        assertEquals(plaintext, message)
    })

    asyncTest('each encryption produces different ciphertext (random nonce)', async () => {
        await cryptoService.ready
        const nacl = cryptoService.nacl
        const naclUtil = cryptoService.naclUtil

        const alice = nacl.box.keyPair()
        const bob = nacl.box.keyPair()
        const bobPub = naclUtil.encodeBase64(bob.publicKey)

        cryptoService.keyPair = alice
        const enc1 = cryptoService.encrypt('Same message', bobPub)
        const enc2 = cryptoService.encrypt('Same message', bobPub)

        assertNotEquals(enc1.nonce, enc2.nonce)
        assertNotEquals(enc1.ciphertext, enc2.ciphertext)
    })

    asyncTest('bidirectional communication works', async () => {
        await cryptoService.ready
        const nacl = cryptoService.nacl
        const naclUtil = cryptoService.naclUtil

        const alice = nacl.box.keyPair()
        const bob = nacl.box.keyPair()
        const alicePub = naclUtil.encodeBase64(alice.publicKey)
        const bobPub = naclUtil.encodeBase64(bob.publicKey)

        // Alice → Bob
        cryptoService.keyPair = alice
        const msg1 = cryptoService.encrypt('Hi Bob', bobPub)

        cryptoService.keyPair = bob
        assertEquals(cryptoService.decrypt(msg1.ciphertext, msg1.nonce, alicePub), 'Hi Bob')

        // Bob → Alice
        const msg2 = cryptoService.encrypt('Hi Alice', alicePub)

        cryptoService.keyPair = alice
        assertEquals(cryptoService.decrypt(msg2.ciphertext, msg2.nonce, bobPub), 'Hi Alice')
    })
})

// ── Participant hash (conversations) ──

describe('ConversationsService.hashPublicKey', () => {
    asyncTest('produces deterministic SHA-256 hash', async () => {
        const hash1 = await conversationsService.hashPublicKey('test-key-123')
        const hash2 = await conversationsService.hashPublicKey('test-key-123')
        assertEquals(hash1, hash2)
    })

    asyncTest('produces 64-char hex string', async () => {
        const hash = await conversationsService.hashPublicKey('any-key')
        assertEquals(hash.length, 64)
        assertTrue(/^[0-9a-f]+$/.test(hash))
    })

    asyncTest('different keys produce different hashes', async () => {
        const hash1 = await conversationsService.hashPublicKey('key-a')
        const hash2 = await conversationsService.hashPublicKey('key-b')
        assertNotEquals(hash1, hash2)
    })
})

// ── Per-listing keypair management ──

describe('Per-listing keypair management', () => {
    asyncTest('generateListingKeyPair creates and stores keypair', async () => {
        localStorage.removeItem('kashilo_listing_keys')
        await cryptoService.unlock(TEST_UUID)
        
        const publicKey = await cryptoService.generateListingKeyPair('test-listing-1')
        assertTrue(publicKey !== null)
        assertTrue(publicKey.length > 10)
    })

    asyncTest('getListingSecretKey retrieves stored key', async () => {
        const secretKey = await cryptoService.getListingSecretKey('test-listing-1')
        assertTrue(secretKey !== null)
        assertTrue(secretKey.length > 10)
    })

    asyncTest('getListingSecretKey returns null for unknown listing', async () => {
        const secretKey = await cryptoService.getListingSecretKey('nonexistent')
        assertEquals(secretKey, null)
    })

    asyncTest('encrypt/decrypt with listing-specific secret key', async () => {
        await cryptoService.ready
        const nacl = cryptoService.nacl
        const naclUtil = cryptoService.naclUtil

        // Simulate: seller creates listing key, buyer uses account key
        const buyerKp = nacl.box.keyPair()
        const buyerPub = naclUtil.encodeBase64(buyerKp.publicKey)

        // Seller generates listing keypair
        await cryptoService.unlock(TEST_UUID)
        const sellerListingPub = await cryptoService.generateListingKeyPair('test-listing-2')
        const sellerListingSk = await cryptoService.getListingSecretKey('test-listing-2')

        // Buyer encrypts to seller listing key
        cryptoService.keyPair = buyerKp
        const { nonce, ciphertext } = cryptoService.encrypt('Hello seller!', sellerListingPub)

        // Seller decrypts using listing secret key (not account key)
        const plaintext = cryptoService.decrypt(ciphertext, nonce, buyerPub, sellerListingSk)
        assertEquals(plaintext, 'Hello seller!')
    })

    asyncTest('seller can encrypt back to buyer using listing key', async () => {
        await cryptoService.ready
        const nacl = cryptoService.nacl
        const naclUtil = cryptoService.naclUtil

        const buyerKp = nacl.box.keyPair()
        const buyerPub = naclUtil.encodeBase64(buyerKp.publicKey)

        const sellerListingSk = await cryptoService.getListingSecretKey('test-listing-2')
        assertTrue(sellerListingSk !== null)

        // Seller encrypts with listing key
        const { nonce, ciphertext } = cryptoService.encrypt('Hi buyer!', buyerPub, sellerListingSk)

        // Buyer decrypts
        const sk = naclUtil.decodeBase64(sellerListingSk)
        const kp = nacl.box.keyPair.fromSecretKey(sk)
        const sellerListingPub = naclUtil.encodeBase64(kp.publicKey)

        cryptoService.keyPair = buyerKp
        const plaintext = cryptoService.decrypt(ciphertext, nonce, sellerListingPub)
        assertEquals(plaintext, 'Hi buyer!')
    })

    asyncTest('destroyKeyPair also clears listing keys', async () => {
        await cryptoService.unlock(TEST_UUID)
        await cryptoService.generateListingKeyPair('temp-listing')
        
        cryptoService.destroyKeyPair()
        assertEquals(localStorage.getItem('kashilo_listing_keys'), null)
    })
})

// ── Key pinning (TOFU) ──

describe('KeyPinningService (TOFU)', () => {
    asyncTest('check returns "new" for unknown listing and pins it', async () => {
        keyPinningService.clear()
        const result = keyPinningService.check('listing-a', 'pubkey-123')
        assertEquals(result, 'new')
    })

    asyncTest('check returns "ok" for matching pinned key', async () => {
        const result = keyPinningService.check('listing-a', 'pubkey-123')
        assertEquals(result, 'ok')
    })

    asyncTest('check returns "changed" when key differs from pinned', async () => {
        const result = keyPinningService.check('listing-a', 'pubkey-different')
        assertEquals(result, 'changed')
    })

    asyncTest('acceptChange updates pinned key', async () => {
        keyPinningService.acceptChange('listing-a', 'pubkey-different')
        const result = keyPinningService.check('listing-a', 'pubkey-different')
        assertEquals(result, 'ok')
    })

    asyncTest('unpin removes pin for listing', async () => {
        keyPinningService.unpin('listing-a')
        const result = keyPinningService.check('listing-a', 'pubkey-new')
        assertEquals(result, 'new')
    })

    asyncTest('clear removes all pinned keys', async () => {
        keyPinningService.check('listing-x', 'key-x')
        keyPinningService.check('listing-y', 'key-y')
        keyPinningService.clear()
        assertEquals(keyPinningService.check('listing-x', 'key-x'), 'new')
        assertEquals(keyPinningService.check('listing-y', 'key-y'), 'new')
        keyPinningService.clear()
    })
})

// Cleanup after all tests
describe('Crypto test cleanup', () => {
    asyncTest('cleanup test data from storage', async () => {
        cryptoService.destroyKeyPair()
        assertTrue(true)
    })
})
