import { afterAll, beforeAll, describe, expect, it } from 'vitest'
import type { Actor } from '@platform-modules/commerce-orders'
import type { TransactionalDatabase } from '@platform-modules/db'
import {
  DownloadNotFoundError,
  FulfillmentValidationError,
  isDownloadNotFoundError,
  isFulfillmentValidationError,
} from '../errors.js'
import { startPg } from '../pg-harness.js'
import { createMemoryStorageAdapter } from '../testing.js'
import type { FulfillmentSchema } from '../schema.js'
import { normalizeEmail, ownerKeyOf } from '../types.js'
import { grantDigitalAccess } from './grant.js'
import { DOWNLOAD_TTL_SECONDS, issueDownloadToken } from './token.js'

const ORDER_ID = 'cccccccc-cccc-4ccc-8ccc-cccccccccccc'
const USER_ID = '33333333-3333-4333-8333-333333333333'
const OTHER_USER_ID = '44444444-4444-4444-8444-444444444444'
const MISSING_GRANT_ID = 'dddddddd-dddd-4ddd-8ddd-dddddddddddd'

describe('issueDownloadToken', () => {
  let db: TransactionalDatabase<FulfillmentSchema>
  let stop: (() => Promise<void>) | undefined

  beforeAll(async () => {
    const pg = await startPg()
    db = pg.db
    stop = pg.stop
  }, 120_000)

  afterAll(async () => {
    await stop?.()
  }, 30_000)

  async function insertUserGrant() {
    return db.transaction((tx) =>
      grantDigitalAccess(tx, {
        orderId: ORDER_ID,
        itemId: 'variant-user',
        ownerKey: ownerKeyOf({ userId: USER_ID }),
        blobKey: 'blob/digital/user-track.zip',
      }),
    )
  }

  async function insertGuestGrant(ownerEmail = 'Guest@X.com ') {
    return db.transaction((tx) =>
      grantDigitalAccess(tx, {
        orderId: ORDER_ID,
        itemId: 'variant-guest',
        ownerKey: ownerKeyOf({ guestEmail: ownerEmail }),
        blobKey: 'blob/digital/guest-track.zip',
      }),
    )
  }

  it('user grant: owner Actor receives a signed URL', async () => {
    const grant = await insertUserGrant()
    const storage = createMemoryStorageAdapter()
    const requester: Actor = { userId: USER_ID }

    const result = await issueDownloadToken(db, grant.id, requester, storage)

    expect(result.url).toBe(
      `https://memory-storage.test/blob%2Fdigital%2Fuser-track.zip?ttl=${DOWNLOAD_TTL_SECONDS}`,
    )
    expect(result.expiresAt).toBeInstanceOf(Date)
  })

  it('user grant: different userId throws DownloadNotFoundError (404, not 403)', async () => {
    const grant = await insertUserGrant()
    const storage = createMemoryStorageAdapter()
    const requester: Actor = { userId: OTHER_USER_ID }

    await expect(issueDownloadToken(db, grant.id, requester, storage)).rejects.toSatisfy(
      (e) => isDownloadNotFoundError(e) && (e as DownloadNotFoundError).httpStatus === 404,
    )
  })

  it('guest grant: owner guestEmail receives a signed URL', async () => {
    const grant = await insertGuestGrant('buyer@example.com')
    const storage = createMemoryStorageAdapter()

    const result = await issueDownloadToken(db, grant.id, { guestEmail: 'buyer@example.com' }, storage)

    expect(result.url).toContain('guest-track.zip')
    expect(result.expiresAt).toBeInstanceOf(Date)
  })

  it('guest grant: different guestEmail throws DownloadNotFoundError', async () => {
    const grant = await insertGuestGrant('buyer@example.com')
    const storage = createMemoryStorageAdapter()

    await expect(
      issueDownloadToken(db, grant.id, { guestEmail: 'other@example.com' }, storage),
    ).rejects.toSatisfy((e) => isDownloadNotFoundError(e))
  })

  it('guest grant: Actor with userId against guest grant throws DownloadNotFoundError (cross-namespace)', async () => {
    const grant = await insertGuestGrant('buyer@example.com')
    const storage = createMemoryStorageAdapter()
    const requester: Actor = { userId: USER_ID }

    await expect(issueDownloadToken(db, grant.id, requester, storage)).rejects.toSatisfy(
      (e) => isDownloadNotFoundError(e),
    )
  })

  it('missing grant id throws the same DownloadNotFoundError (no enumeration oracle)', async () => {
    const storage = createMemoryStorageAdapter()
    const requester: Actor = { userId: USER_ID }

    await expect(
      issueDownloadToken(db, MISSING_GRANT_ID, requester, storage),
    ).rejects.toSatisfy((e) => isDownloadNotFoundError(e))
  })

  it('email normalization: grant written with messy casing matches normalized guest re-check', async () => {
    const grant = await insertGuestGrant('Guest@X.com ')
    const storage = createMemoryStorageAdapter()

    expect(grant.ownerKey).toBe(`email:${normalizeEmail('Guest@X.com ')}`)

    const result = await issueDownloadToken(db, grant.id, { guestEmail: 'guest@x.com' }, storage)
    expect(result.url).toContain('guest-track.zip')
  })

  it('requester with neither userId nor guestEmail fails closed', async () => {
    const grant = await insertUserGrant()
    const storage = createMemoryStorageAdapter()

    await expect(
      issueDownloadToken(db, grant.id, {} as Actor, storage),
    ).rejects.toSatisfy(
      (e) => isFulfillmentValidationError(e) || isDownloadNotFoundError(e),
    )

    try {
      await issueDownloadToken(db, grant.id, {} as Actor, storage)
    } catch (e) {
      expect(
        isFulfillmentValidationError(e) || isDownloadNotFoundError(e),
      ).toBe(true)
      if (isFulfillmentValidationError(e)) {
        expect((e as FulfillmentValidationError).field).toBe('owner')
      }
    }
  })
})
