import { Hex, PublicKey, WebCryptoP256 } from 'ox'
import { describe, expect, test } from 'vp/test'

const { privateKey, publicKey } = await WebCryptoP256.createKeyPair()

describe('createKeyPair', () => {
  test('default', async () => {
    const key = await WebCryptoP256.createKeyPair()
    expect(key.privateKey).toBeDefined()
    expect(key.publicKey.prefix).toBeDefined()
    expect(key.publicKey.x).toBeDefined()
    expect(key.publicKey.y).toBeDefined()
  })
})

describe('createKeyPairECDH', () => {
  test('default', async () => {
    const key = await WebCryptoP256.createKeyPairECDH()
    expect(key.privateKey).toBeDefined()
    expect(key.privateKey.algorithm.name).toBe('ECDH')
    expect((key.privateKey.algorithm as EcKeyAlgorithm).namedCurve).toBe(
      'P-256',
    )
    expect(key.privateKey.usages).toContain('deriveBits')
    expect(key.publicKey.prefix).toBeDefined()
    expect(key.publicKey.x).toBeDefined()
    expect(key.publicKey.y).toBeDefined()
  })

  test('options: extractable', async () => {
    const keyExtractable = await WebCryptoP256.createKeyPairECDH({
      extractable: true,
    })
    const keyNonExtractable = await WebCryptoP256.createKeyPairECDH({
      extractable: false,
    })

    expect(keyExtractable.privateKey.extractable).toBe(true)
    expect(keyNonExtractable.privateKey.extractable).toBe(false)
  })
})

describe('getSharedSecret', () => {
  test('default', async () => {
    const { privateKey: privateKeyA, publicKey: publicKeyA } =
      await WebCryptoP256.createKeyPairECDH()
    const { privateKey: privateKeyB, publicKey: publicKeyB } =
      await WebCryptoP256.createKeyPairECDH()

    const sharedSecretA = await WebCryptoP256.getSharedSecret({
      privateKey: privateKeyA,
      publicKey: publicKeyB,
    })

    const sharedSecretB = await WebCryptoP256.getSharedSecret({
      privateKey: privateKeyB,
      publicKey: publicKeyA,
    })

    expect(sharedSecretA).toEqual(sharedSecretB)
  })

  test('behavior: same key pairs produce same secret', async () => {
    const { privateKey: privateKeyA } = await WebCryptoP256.createKeyPairECDH()
    const { publicKey: publicKeyB } = await WebCryptoP256.createKeyPairECDH()

    const sharedSecret1 = await WebCryptoP256.getSharedSecret({
      privateKey: privateKeyA,
      publicKey: publicKeyB,
    })

    const sharedSecret2 = await WebCryptoP256.getSharedSecret({
      privateKey: privateKeyA,
      publicKey: publicKeyB,
    })

    // Should be deterministic - same inputs produce same output
    expect(sharedSecret1).toEqual(sharedSecret2)
  })

  test('behavior: different key pairs produce different secrets', async () => {
    const { privateKey: privateKeyA } = await WebCryptoP256.createKeyPairECDH()
    const { publicKey: publicKeyB } = await WebCryptoP256.createKeyPairECDH()
    const { publicKey: publicKeyC } = await WebCryptoP256.createKeyPairECDH()

    const sharedSecretAB = await WebCryptoP256.getSharedSecret({
      privateKey: privateKeyA,
      publicKey: publicKeyB,
    })

    const sharedSecretAC = await WebCryptoP256.getSharedSecret({
      privateKey: privateKeyA,
      publicKey: publicKeyC,
    })

    // Different key pairs should produce different shared secrets
    expect(sharedSecretAB).not.toEqual(sharedSecretAC)
  })

  test('options: as', async () => {
    const { privateKey: privateKeyA } = await WebCryptoP256.createKeyPairECDH()
    const { publicKey: publicKeyB } = await WebCryptoP256.createKeyPairECDH()

    // Test Hex output (default)
    const sharedSecretHex = await WebCryptoP256.getSharedSecret({
      privateKey: privateKeyA,
      publicKey: publicKeyB,
    })

    // Test Bytes output
    const sharedSecretBytes = await WebCryptoP256.getSharedSecret({
      privateKey: privateKeyA,
      publicKey: publicKeyB,
      as: 'Bytes',
    })

    // Verify formats
    expect(typeof sharedSecretHex).toBe('string')
    expect(sharedSecretHex).toMatch(/^0x[0-9a-f]{64}$/)
    expect(sharedSecretBytes).toBeInstanceOf(Uint8Array)
    expect(sharedSecretBytes.length).toBe(32) // 32 bytes for raw shared secret

    // Verify they represent the same data
    expect(Hex.fromBytes(sharedSecretBytes)).toEqual(sharedSecretHex)
  })

  test('behavior: compressed public key', async () => {
    const { privateKey: privateKeyA, ...A } =
      await WebCryptoP256.createKeyPairECDH()
    const { privateKey: privateKeyB, ...B } =
      await WebCryptoP256.createKeyPairECDH()

    const publicKeyA = PublicKey.compress(A.publicKey)
    const publicKeyB = PublicKey.compress(B.publicKey)

    const sharedSecret = await WebCryptoP256.getSharedSecret({
      privateKey: privateKeyA,
      publicKey: publicKeyB,
    })
    const sharedSecret2 = await WebCryptoP256.getSharedSecret({
      privateKey: privateKeyB,
      publicKey: publicKeyA,
    })

    expect(sharedSecret).toEqual(sharedSecret2)
  })

  test('error: private key not for ECDH', async () => {
    const { privateKey: ecdsaPrivateKey } = await WebCryptoP256.createKeyPair() // ECDSA key
    const { publicKey: publicKeyB } = await WebCryptoP256.createKeyPairECDH()

    await expect(
      WebCryptoP256.getSharedSecret({
        privateKey: ecdsaPrivateKey,
        publicKey: publicKeyB,
      }),
    ).rejects.toThrowErrorMatchingInlineSnapshot(
      '[WebCryptoP256.InvalidPrivateKeyAlgorithmError: privateKey is not compatible with ECDH. Please use `createKeyPairECDH` to create an ECDH key.]',
    )
  })

  test('error: invalid public key', async () => {
    const { privateKey: privateKeyA } = await WebCryptoP256.createKeyPairECDH()
    const invalidPublicKey = {
      prefix: 4,
      x: '0x0000000000000000000000000000000000000000000000000000000000000000',
      y: '0x0000000000000000000000000000000000000000000000000000000000000000',
    } as const

    await expect(
      WebCryptoP256.getSharedSecret({
        privateKey: privateKeyA,
        publicKey: invalidPublicKey,
      }),
    ).rejects.toThrowErrorMatchingInlineSnapshot('[DataError: Invalid keyData]')
  })
})

describe('sign', () => {
  test('default', async () => {
    const signature = await WebCryptoP256.sign({
      payload:
        '0xd9eba16ed0ecae432b71fe008c98cc872bb4cc214d3220a36f365326cf807d68',
      privateKey,
    })
    expect(signature.r).toBeDefined()
    expect(signature.s).toBeDefined()
  })
})

describe('verify', () => {
  test('default', async () => {
    const payload = '0xdeadbeef'
    const { r, s } = await WebCryptoP256.sign({ payload, privateKey })
    expect(
      await WebCryptoP256.verify({ publicKey, payload, signature: { r, s } }),
    ).toBe(true)
  })

  test('behavior: signature with leading-zero r/s is verified', async () => {
    // Sign repeatedly until we hit a signature whose r or s value has its
    // most-significant byte equal to zero. With variable-width number
    // encoding such signatures fail to verify; with fixed-width 32-byte
    // encoding (the regression fix) they verify correctly.
    const payload = '0xdeadbeef'
    for (let i = 0; i < 256; i++) {
      const signature = await WebCryptoP256.sign({ payload, privateKey })
      if (signature.r.startsWith('0x00') || signature.s.startsWith('0x00')) {
        expect(
          await WebCryptoP256.verify({ publicKey, payload, signature }),
        ).toBe(true)
        return
      }
    }
  })
})

test('exports', () => {
  expect(Object.keys(WebCryptoP256)).toMatchInlineSnapshot(`
    [
      "createKeyPair",
      "createKeyPairECDH",
      "getSharedSecret",
      "sign",
      "verify",
      "InvalidPrivateKeyAlgorithmError",
    ]
  `)
})

describe('as option / serialized inputs', () => {
  test("sign: as 'Hex' returns hex string", async () => {
    const { privateKey } = await WebCryptoP256.createKeyPair()
    const sigHex = await WebCryptoP256.sign({
      payload: '0xdeadbeef',
      privateKey,
      as: 'Hex',
    })
    expect(typeof sigHex).toBe('string')
    expect((sigHex as Hex.Hex).startsWith('0x')).toBe(true)
  })

  test("sign: as 'Bytes' returns Uint8Array", async () => {
    const { privateKey } = await WebCryptoP256.createKeyPair()
    const sigBytes = await WebCryptoP256.sign({
      payload: '0xdeadbeef',
      privateKey,
      as: 'Bytes',
    })
    expect(sigBytes).toBeInstanceOf(Uint8Array)
  })

  test('default as remains Object', async () => {
    const { privateKey } = await WebCryptoP256.createKeyPair()
    const sig = await WebCryptoP256.sign({
      payload: '0xdeadbeef',
      privateKey,
    })
    expect(typeof sig).toBe('object')
    expect('r' in sig).toBe(true)
  })

  test('verify accepts Hex publicKey + Hex signature', async () => {
    const { privateKey, publicKey } = await WebCryptoP256.createKeyPair()
    const sigHex = await WebCryptoP256.sign({
      payload: '0xdeadbeef',
      privateKey,
      as: 'Hex',
    })
    const pkHex = PublicKey.toHex(publicKey)
    expect(
      await WebCryptoP256.verify({
        payload: '0xdeadbeef',
        publicKey: pkHex,
        signature: sigHex,
      }),
    ).toBe(true)
  })
})
