import { describe, expect, test } from 'vp/test'
import * as Bytes from '../../core/Bytes.js'
import * as Hash from '../../core/Hash.js'
import * as Hex from '../../core/Hex.js'
import * as P256 from '../../core/P256.js'
import {
  Authentication,
  Authenticator,
  Credential,
  Registration,
} from '../index.js'

const rpId = 'localhost'
const rpName = 'Ox Test'

describe('sign', () => {
  test('signs with credentialId', async () => {
    const { credential } = await createCredential()
    const challenge = Hex.random(32)

    const result = await Authentication.sign({
      credentialId: credential.id,
      challenge,
      rpId,
    })

    expect(result.metadata.authenticatorData).toBeTypeOf('string')
    expect(result.metadata.clientDataJSON).toBeTypeOf('string')
    expect(result.metadata.challengeIndex).toBeTypeOf('number')
    expect(result.metadata.typeIndex).toBeTypeOf('number')
    expect(result.metadata.userVerificationRequired).toBe(true)
    expect(result.signature.r).toBeTypeOf('string')
    expect(result.signature.s).toBeTypeOf('string')
    expect(result.raw).toBeDefined()
  })

  test('signs with credentialId array', async () => {
    const { credential } = await createCredential()
    const challenge = Hex.random(32)

    const result = await Authentication.sign({
      credentialId: [credential.id],
      challenge,
      rpId,
    })

    expect(result.signature.r).toBeTypeOf('string')
    expect(result.signature.s).toBeTypeOf('string')
  })

  test('signs with CredentialRequestOptions', async () => {
    const { credential } = await createCredential()
    const challenge = Hex.random(32)

    const options = Authentication.getOptions({
      credentialId: credential.id,
      challenge,
      rpId,
    })

    const result = await Authentication.sign(options)

    expect(result.signature.r).toBeTypeOf('string')
    expect(result.signature.s).toBeTypeOf('string')

    const verified = Authentication.verify({
      metadata: result.metadata,
      challenge,
      publicKey: credential.publicKey,
      signature: result.signature,
    })

    expect(verified).toBe(true)
  })
})

describe('sign + verify', () => {
  test('full ceremony', async () => {
    const { credential } = await createCredential()
    const challenge = Hex.random(32)

    const { metadata, signature } = await Authentication.sign({
      credentialId: credential.id,
      challenge,
      rpId,
    })

    const verified = Authentication.verify({
      metadata,
      challenge,
      publicKey: credential.publicKey,
      signature,
    })

    expect(verified).toBe(true)
  })

  test('verify with origin validation', async () => {
    const { credential } = await createCredential()
    const challenge = Hex.random(32)

    const { metadata, signature } = await Authentication.sign({
      credentialId: credential.id,
      challenge,
      rpId,
    })

    const verified = Authentication.verify({
      metadata,
      challenge,
      publicKey: credential.publicKey,
      signature,
      origin: window.location.origin,
    })

    expect(verified).toBe(true)
  })

  test('verify with origin array', async () => {
    const { credential } = await createCredential()
    const challenge = Hex.random(32)

    const { metadata, signature } = await Authentication.sign({
      credentialId: credential.id,
      challenge,
      rpId,
    })

    const verified = Authentication.verify({
      metadata,
      challenge,
      publicKey: credential.publicKey,
      signature,
      origin: ['https://other.com', window.location.origin],
    })

    expect(verified).toBe(true)
  })

  test('verify with rpId validation', async () => {
    const { credential } = await createCredential()
    const challenge = Hex.random(32)

    const { metadata, signature } = await Authentication.sign({
      credentialId: credential.id,
      challenge,
      rpId,
    })

    const verified = Authentication.verify({
      metadata,
      challenge,
      publicKey: credential.publicKey,
      signature,
      rpId,
    })

    expect(verified).toBe(true)
  })

  test('rejects wrong challenge', async () => {
    const { credential } = await createCredential()
    const challenge = Hex.random(32)

    const { metadata, signature } = await Authentication.sign({
      credentialId: credential.id,
      challenge,
      rpId,
    })

    const verified = Authentication.verify({
      metadata,
      challenge: Hex.random(32),
      publicKey: credential.publicKey,
      signature,
    })

    expect(verified).toBe(false)
  })

  test('rejects wrong origin', async () => {
    const { credential } = await createCredential()
    const challenge = Hex.random(32)

    const { metadata, signature } = await Authentication.sign({
      credentialId: credential.id,
      challenge,
      rpId,
    })

    const verified = Authentication.verify({
      metadata,
      challenge,
      publicKey: credential.publicKey,
      signature,
      origin: 'https://evil.com',
    })

    expect(verified).toBe(false)
  })

  test('rejects wrong rpId', async () => {
    const { credential } = await createCredential()
    const challenge = Hex.random(32)

    const { metadata, signature } = await Authentication.sign({
      credentialId: credential.id,
      challenge,
      rpId,
    })

    const verified = Authentication.verify({
      metadata,
      challenge,
      publicKey: credential.publicKey,
      signature,
      rpId: 'evil.com',
    })

    expect(verified).toBe(false)
  })
})

describe('end-to-end: registration → authentication', () => {
  test('Registration.create → Registration.verify → Authentication.sign → Authentication.verify', async () => {
    const regChallenge = Hex.random(32)
    const credential = await Registration.create({
      name: `e2e-${Date.now()}`,
      challenge: regChallenge,
      rp: { id: rpId, name: rpName },
    })

    const regResult = Registration.verify({
      credential,
      challenge: regChallenge,
      origin: window.location.origin,
      rpId,
    })

    const authChallenge = Hex.random(32)
    const { metadata, signature } = await Authentication.sign({
      credentialId: credential.id,
      challenge: authChallenge,
      rpId,
    })

    const verified = Authentication.verify({
      metadata,
      challenge: authChallenge,
      publicKey: regResult.credential.publicKey,
      signature,
      origin: window.location.origin,
      rpId,
    })

    expect(verified).toBe(true)
  })

  test('multiple authentications with same credential', async () => {
    const { credential } = await createCredential()

    for (let i = 0; i < 3; i++) {
      const challenge = Hex.random(32)
      const { metadata, signature } = await Authentication.sign({
        credentialId: credential.id,
        challenge,
        rpId,
      })

      const verified = Authentication.verify({
        metadata,
        challenge,
        publicKey: credential.publicKey,
        signature,
      })

      expect(verified).toBe(true)
    }
  })
})

describe('serialize/deserialize round-trip', () => {
  test('options round-trip: getOptions → serialize → JSON → deserialize → sign', async () => {
    const { credential } = await createCredential()
    const challenge = Hex.random(32)

    const options = Authentication.getOptions({
      credentialId: credential.id,
      challenge,
      rpId,
    })
    const serialized = Authentication.serializeOptions(options)
    const json = JSON.stringify(serialized)
    const deserialized = Authentication.deserializeOptions(JSON.parse(json))

    const { metadata, signature } = await Authentication.sign(deserialized)

    const verified = Authentication.verify({
      metadata,
      challenge,
      publicKey: credential.publicKey,
      signature,
    })

    expect(verified).toBe(true)
  })

  test('response round-trip: serializeResponse → JSON → deserializeResponse → verify', async () => {
    const { credential } = await createCredential()
    const challenge = Hex.random(32)

    const response = await Authentication.sign({
      credentialId: credential.id,
      challenge,
      rpId,
    })

    const serialized = Authentication.serializeResponse(response)
    const json = JSON.stringify(serialized)
    const deserialized = Authentication.deserializeResponse(JSON.parse(json))

    const verified = Authentication.verify({
      ...deserialized,
      challenge,
      publicKey: credential.publicKey,
    })

    expect(verified).toBe(true)
  })

  test('credential serialize → deserialize → authenticate', async () => {
    const { credential } = await createCredential()

    const serialized = Credential.serialize(credential)
    const json = JSON.stringify(serialized)
    const deserialized = Credential.deserialize(JSON.parse(json))

    const challenge = Hex.random(32)
    const { metadata, signature } = await Authentication.sign({
      credentialId: deserialized.id,
      challenge,
      rpId,
    })

    const verified = Authentication.verify({
      metadata,
      challenge,
      publicKey: deserialized.publicKey,
      signature,
    })

    expect(verified).toBe(true)
  })
})

describe('behavior: type validation', () => {
  test('rejects webauthn.create type in authentication', () => {
    const { metadata, signature, publicKey, challenge } = buildSignedMetadata({
      type: 'webauthn.create',
    })

    const verified = Authentication.verify({
      metadata,
      challenge,
      publicKey,
      signature,
    })

    expect(verified).toBe(false)
  })
})

describe('behavior: BE/BS flag consistency', () => {
  test('rejects BS set without BE', () => {
    // flag 0x15 = UP (0x01) + UV (0x04) + BS (0x10) but no BE (0x08)
    const { metadata, signature, publicKey, challenge } = buildSignedMetadata({
      flag: 0x15,
    })

    const verified = Authentication.verify({
      metadata,
      challenge,
      publicKey,
      signature,
    })

    expect(verified).toBe(false)
  })

  test('accepts BE + BS both set', () => {
    // flag 0x1d = UP (0x01) + UV (0x04) + BE (0x08) + BS (0x10)
    const { metadata, signature, publicKey, challenge } = buildSignedMetadata({
      flag: 0x1d,
    })

    const verified = Authentication.verify({
      metadata,
      challenge,
      publicKey,
      signature,
    })

    expect(verified).toBe(true)
  })

  test('accepts BE set + BS not set', () => {
    // flag 0x0d = UP (0x01) + UV (0x04) + BE (0x08)
    const { metadata, signature, publicKey, challenge } = buildSignedMetadata({
      flag: 0x0d,
    })

    const verified = Authentication.verify({
      metadata,
      challenge,
      publicKey,
      signature,
    })

    expect(verified).toBe(true)
  })
})

describe('behavior: UP flag', () => {
  test('rejects when UP flag is not set', () => {
    // flag 0x04 = UV only, no UP
    const { metadata, signature, publicKey, challenge } = buildSignedMetadata({
      flag: 0x04,
    })

    const verified = Authentication.verify({
      metadata,
      challenge,
      publicKey,
      signature,
    })

    expect(verified).toBe(false)
  })
})

describe('behavior: UV flag', () => {
  test('rejects when UV not set but userVerificationRequired is true', () => {
    // flag 0x01 = UP only, no UV
    const { metadata, signature, publicKey, challenge } = buildSignedMetadata({
      flag: 0x01,
      userVerification: 'required',
    })

    const verified = Authentication.verify({
      metadata,
      challenge,
      publicKey,
      signature,
    })

    expect(verified).toBe(false)
  })

  test('accepts when UV not set and userVerificationRequired is false', () => {
    // flag 0x01 = UP only
    const { metadata, signature, publicKey, challenge } = buildSignedMetadata({
      flag: 0x01,
      userVerification: 'preferred',
    })

    const verified = Authentication.verify({
      metadata,
      challenge,
      publicKey,
      signature,
    })

    expect(verified).toBe(true)
  })
})

describe('behavior: authenticatorData length', () => {
  test('rejects authenticatorData shorter than 37 bytes', () => {
    const { metadata, signature, publicKey, challenge } = buildSignedMetadata()

    metadata.authenticatorData = Hex.fromBytes(new Uint8Array(36))

    const verified = Authentication.verify({
      metadata,
      challenge,
      publicKey,
      signature,
    })

    expect(verified).toBe(false)
  })
})

describe('behavior: origin validation', () => {
  test('rejects wrong origin when origin is provided', () => {
    const { metadata, signature, publicKey, challenge } = buildSignedMetadata({
      origin: 'https://legitimate.com',
    })

    const verified = Authentication.verify({
      metadata,
      challenge,
      publicKey,
      signature,
      origin: 'https://evil.com',
    })

    expect(verified).toBe(false)
  })

  test('accepts matching origin from array', () => {
    const { metadata, signature, publicKey, challenge } = buildSignedMetadata({
      origin: 'https://app.example.com',
    })

    const verified = Authentication.verify({
      metadata,
      challenge,
      publicKey,
      signature,
      origin: ['https://other.com', 'https://app.example.com'],
    })

    expect(verified).toBe(true)
  })

  test('rejects when origin not in array', () => {
    const { metadata, signature, publicKey, challenge } = buildSignedMetadata({
      origin: 'https://evil.com',
    })

    const verified = Authentication.verify({
      metadata,
      challenge,
      publicKey,
      signature,
      origin: ['https://app.example.com', 'https://other.com'],
    })

    expect(verified).toBe(false)
  })
})

describe('behavior: rpId validation', () => {
  test('rejects wrong rpId', () => {
    const { metadata, signature, publicKey, challenge } = buildSignedMetadata({
      rpId: 'legitimate.com',
    })

    const verified = Authentication.verify({
      metadata,
      challenge,
      publicKey,
      signature,
      rpId: 'evil.com',
    })

    expect(verified).toBe(false)
  })

  test('accepts matching rpId', () => {
    const { metadata, signature, publicKey, challenge } = buildSignedMetadata({
      rpId: 'example.com',
    })

    const verified = Authentication.verify({
      metadata,
      challenge,
      publicKey,
      signature,
      rpId: 'example.com',
    })

    expect(verified).toBe(true)
  })
})

describe('behavior: challenge validation', () => {
  test('rejects tampered challenge in clientDataJSON', () => {
    const { metadata, signature, publicKey } = buildSignedMetadata()

    const verified = Authentication.verify({
      metadata,
      challenge: Hex.random(32),
      publicKey,
      signature,
    })

    expect(verified).toBe(false)
  })

  test('rejects missing challenge in clientDataJSON', () => {
    const { metadata, signature, publicKey, challenge } = buildSignedMetadata()

    const clientData = JSON.parse(metadata.clientDataJSON)
    delete clientData.challenge
    metadata.clientDataJSON = JSON.stringify(clientData)

    const verified = Authentication.verify({
      metadata,
      challenge,
      publicKey,
      signature,
    })

    expect(verified).toBe(false)
  })
})

describe('behavior: signature validation', () => {
  test('rejects signature from wrong key', () => {
    const { metadata, challenge } = buildSignedMetadata()

    const { publicKey: wrongPublicKey } = P256.createKeyPair()

    const verified = Authentication.verify({
      metadata,
      challenge,
      publicKey: wrongPublicKey,
      signature: P256.sign({
        payload: Bytes.fromHex('0xdeadbeef'),
        privateKey: P256.createKeyPair().privateKey,
        hash: true,
      }),
    })

    expect(verified).toBe(false)
  })

  test('rejects tampered authenticatorData (invalidates signature)', async () => {
    const { credential } = await createCredential()
    const challenge = Hex.random(32)

    const { metadata, signature } = await Authentication.sign({
      credentialId: credential.id,
      challenge,
      rpId,
    })

    // Flip a byte in the authenticatorData
    const authDataBytes = Bytes.fromHex(metadata.authenticatorData)
    authDataBytes[33] = authDataBytes[33]! ^ 0xff
    metadata.authenticatorData = Hex.fromBytes(authDataBytes)

    const verified = Authentication.verify({
      metadata,
      challenge,
      publicKey: credential.publicKey,
      signature,
    })

    expect(verified).toBe(false)
  })

  test('rejects tampered clientDataJSON (invalidates signature)', async () => {
    const { credential } = await createCredential()
    const challenge = Hex.random(32)

    const { metadata, signature } = await Authentication.sign({
      credentialId: credential.id,
      challenge,
      rpId,
    })

    // Inject extra field into clientDataJSON
    const clientData = JSON.parse(metadata.clientDataJSON)
    clientData.injected = 'evil'
    metadata.clientDataJSON = JSON.stringify(clientData)

    const verified = Authentication.verify({
      metadata,
      challenge,
      publicKey: credential.publicKey,
      signature,
    })

    expect(verified).toBe(false)
  })

  test('rejects high-S signature (malleability)', () => {
    const { metadata, signature, publicKey, challenge } = buildSignedMetadata()

    const n =
      0xffffffff00000000ffffffffffffffffbce6faada7179e84f3b9cac2fc632551n
    const highS = {
      r: signature.r,
      s: Hex.fromNumber(n - Hex.toBigInt(signature.s), { size: 32 }),
    }

    const verified = Authentication.verify({
      metadata,
      challenge,
      publicKey,
      signature: highS,
    })

    expect(verified).toBe(false)
  })
})

async function createCredential(challenge?: Hex.Hex) {
  const ch = challenge ?? Hex.random(32)
  const credential = await Registration.create({
    name: `user-${Date.now()}`,
    challenge: ch,
    rp: { id: rpId, name: rpName },
  })
  return { credential, challenge: ch }
}

/** @internal */
function buildSignedMetadata(options?: {
  challenge?: Hex.Hex
  flag?: number
  origin?: string
  rpId?: string
  userVerification?: 'required' | 'preferred' | 'discouraged'
  type?: 'webauthn.get' | 'webauthn.create'
}) {
  const challenge = options?.challenge ?? Hex.random(32)
  const flag = options?.flag ?? 0x05 // UP + UV
  const origin = options?.origin ?? window.location.origin
  const rp = options?.rpId ?? rpId
  const userVerification = options?.userVerification ?? 'required'
  const type = options?.type ?? 'webauthn.get'

  const { privateKey, publicKey } = P256.createKeyPair()

  const authenticatorData = Authenticator.getAuthenticatorData({
    rpId: rp,
    flag,
  })
  const clientDataJSON = Authenticator.getClientDataJSON({
    challenge,
    origin,
    type,
  })
  const clientDataJSONHash = Hash.sha256(Hex.fromString(clientDataJSON))
  const payload = Hex.concat(authenticatorData, clientDataJSONHash)

  const signature = P256.sign({ payload, privateKey, hash: true })

  const challengeIndex = clientDataJSON.indexOf('"challenge"')
  const typeIndex = clientDataJSON.indexOf('"type"')

  const metadata = {
    authenticatorData,
    clientDataJSON,
    challengeIndex,
    typeIndex,
    userVerificationRequired: userVerification === 'required',
  }

  return { metadata, signature, publicKey, challenge }
}
