import { assert, describe, it } from "@effect/vitest"
import { Effect } from "effect"
import * as EventLogSessionAuth from "effect/unstable/eventlog/EventLogSessionAuth"

const makeSigningKeyPair = Effect.promise(async () => {
  const keyPair = await globalThis.crypto.subtle.generateKey("Ed25519", true, ["sign", "verify"])

  if (!("privateKey" in keyPair) || !("publicKey" in keyPair)) {
    throw new Error("Expected Ed25519 CryptoKeyPair")
  }

  return {
    signingPublicKey: new Uint8Array(await globalThis.crypto.subtle.exportKey("raw", keyPair.publicKey)),
    signingPrivateKey: new Uint8Array(await globalThis.crypto.subtle.exportKey("pkcs8", keyPair.privateKey))
  }
})

const makePayload = (signingPublicKey: Uint8Array): EventLogSessionAuth.SessionAuthPayload => ({
  remoteId: "remote-123",
  challenge: Uint8Array.from([1, 2, 3, 4, 5, 6, 7, 8]),
  publicKey: "client-public-key",
  signingPublicKey
})

describe("EventLogSessionAuth", () => {
  it.effect("encodes and decodes canonical auth payload deterministically", () =>
    Effect.gen(function*() {
      const signingPublicKey = new Uint8Array(EventLogSessionAuth.Ed25519PublicKeyLength)
      for (let i = 0; i < signingPublicKey.byteLength; i++) {
        signingPublicKey[i] = i
      }

      const payload = makePayload(signingPublicKey)
      const encodedA = yield* EventLogSessionAuth.encodeSessionAuthPayload(payload)
      const encodedB = yield* EventLogSessionAuth.encodeSessionAuthPayload(payload)

      assert.deepStrictEqual(encodedA, encodedB)

      const decoded = yield* EventLogSessionAuth.decodeSessionAuthPayload(encodedA)
      assert.strictEqual(decoded.remoteId, payload.remoteId)
      assert.strictEqual(decoded.publicKey, payload.publicKey)
      assert.deepStrictEqual(decoded.challenge, payload.challenge)
      assert.deepStrictEqual(decoded.signingPublicKey, payload.signingPublicKey)
    }))

  it.effect("signs and verifies canonical auth payload with Ed25519", () =>
    Effect.gen(function*() {
      const keyPair = yield* makeSigningKeyPair
      const payload = makePayload(keyPair.signingPublicKey)

      const signature = yield* EventLogSessionAuth.signSessionAuthPayload({
        ...payload,
        signingPrivateKey: keyPair.signingPrivateKey
      })

      assert.strictEqual(signature.byteLength, EventLogSessionAuth.Ed25519SignatureLength)

      const verified = yield* EventLogSessionAuth.verifySessionAuthPayload({
        ...payload,
        signature
      })

      assert.strictEqual(verified, true)
    }))

  it.effect("returns false for mismatched payload/signature", () =>
    Effect.gen(function*() {
      const keyPair = yield* makeSigningKeyPair
      const payload = makePayload(keyPair.signingPublicKey)

      const signature = yield* EventLogSessionAuth.signSessionAuthPayload({
        ...payload,
        signingPrivateKey: keyPair.signingPrivateKey
      })

      const challenge = payload.challenge.slice()
      challenge[0] = challenge[0] ^ 0xff

      const verified = yield* EventLogSessionAuth.verifySessionAuthPayload({
        ...payload,
        challenge,
        signature
      })

      assert.strictEqual(verified, false)
    }))

  it.effect("rejects malformed signature length", () =>
    Effect.gen(function*() {
      const keyPair = yield* makeSigningKeyPair
      const payload = makePayload(keyPair.signingPublicKey)

      const error = yield* EventLogSessionAuth.verifySessionAuthPayload({
        ...payload,
        signature: new Uint8Array(EventLogSessionAuth.Ed25519SignatureLength - 1)
      }).pipe(Effect.flip)

      assert.strictEqual(error._tag, "EventLogSessionAuthError")
      assert.strictEqual(error.reason, "InvalidSignatureLength")
    }))

  it.effect("rejects malformed signing public key length", () =>
    Effect.gen(function*() {
      const error = yield* EventLogSessionAuth.encodeSessionAuthPayload({
        remoteId: "remote-123",
        challenge: new Uint8Array(32),
        publicKey: "client-public-key",
        signingPublicKey: new Uint8Array(EventLogSessionAuth.Ed25519PublicKeyLength - 1)
      }).pipe(Effect.flip)
      assert.strictEqual(error._tag, "EventLogSessionAuthError")
      assert.strictEqual(error.reason, "InvalidSigningPublicKeyLength")
    }))

  it.effect("rejects invalid canonical payload bytes", () =>
    Effect.gen(function*() {
      const keyPair = yield* makeSigningKeyPair
      const payload = makePayload(keyPair.signingPublicKey)
      const encoded = yield* EventLogSessionAuth.encodeSessionAuthPayload(payload)
      const malformed = encoded.slice(0, encoded.byteLength - 1)

      const error = yield* EventLogSessionAuth.verifySessionAuthPayloadBytes({
        payload: malformed,
        signingPublicKey: payload.signingPublicKey,
        signature: new Uint8Array(EventLogSessionAuth.Ed25519SignatureLength)
      }).pipe(Effect.flip)

      assert.strictEqual(error._tag, "EventLogSessionAuthError")
      assert.strictEqual(error.reason, "InvalidPayload")
    }))
})
