import { Hono } from 'hono'
import { beforeEach, describe, expect, it, vi } from 'vitest'

const { idempotencyKeyMock } = vi.hoisted(() => ({
  idempotencyKeyMock: vi.fn((parts: readonly string[]) => parts.join(':')),
}))

vi.mock('@platform-modules/billing', () => ({
  idempotencyKey: idempotencyKeyMock,
}))

import { AppError } from '../errors.js'
import { onError } from '../http.js'
import { createStaffRoute, type StaffRouteDeps } from './staff.js'

type Principal = {
  account: string
  userId: string
  scopes: string[]
}

async function readJson(response: Response): Promise<unknown> {
  return response.json()
}

function createDeps(): StaffRouteDeps {
  return {
    adjustCredits: vi.fn(async () => ({ balance: 15n })),
    audit: vi.fn(async () => undefined),
    allowedTiers: ['pro', 'enterprise'],
    reserveRefund: vi.fn(async () => ({ status: 'reserved' as const })),
    releaseRefundReservation: vi.fn(async () => undefined),
    recordRefundCredit: vi.fn(async () => ({
      balance: 20n,
      creditNote: {
        documentId: 'credit-note-1',
        documentNumber: 'CN-1',
        documentUrl: 'https://billing.example.test/documents/CN-1',
      },
    })),
    listAudit: vi.fn(async () => ({
      items: [],
      nextCursor: null,
    })),
    refundProvider: {
      refund: vi.fn(async () => ({
        kind: 'refunded' as const,
        refundKey: 'refund-key-1',
        chargeKey: 'charge-1',
        providerRef: 'paypal-refund-1',
        amount: 30,
        currency: 'USD',
      })),
    },
    setTier: vi.fn(async () => undefined),
  }
}

function createApp(input: { capabilities: string[]; deps?: StaffRouteDeps }) {
  const deps = input.deps ?? createDeps()
  const app = new Hono<{
    Variables: {
      capabilities: string[]
      principal: Principal
    }
  }>()

  app.onError(onError)
  app.use('*', async (context, next) => {
    context.set('capabilities', input.capabilities)
    context.set('principal', {
      account: 'staff-account',
      userId: 'staff-user',
      scopes: input.capabilities,
    })
    await next()
  })
  app.route('/staff', createStaffRoute(deps))

  return { app, deps }
}

describe('createStaffRoute', () => {
  beforeEach(() => {
    vi.clearAllMocks()
  })

  it('returns 403 FORBIDDEN when the caller lacks the staff capability', async () => {
    const { app, deps } = createApp({ capabilities: [] })

    const response = await app.request('http://press-zone.test/staff/credits/adjust', {
      method: 'POST',
      headers: { 'content-type': 'application/json' },
      body: JSON.stringify({
        walletKey: 'acct-1:translate:2026-07',
        amount: '5',
        idempotencyKey: 'credit-adjust-1',
        reason: 'manual adjustment',
      }),
    })

    expect(response.status).toBe(403)
    await expect(readJson(response)).resolves.toEqual({
      error: {
        code: 'FORBIDDEN',
        message: 'Forbidden',
      },
    })
    expect(deps.adjustCredits).not.toHaveBeenCalled()
    expect(deps.audit).not.toHaveBeenCalled()
  })

  it('adjusts credits and writes an audit record', async () => {
    const { app, deps } = createApp({ capabilities: ['staff'] })

    const response = await app.request('http://press-zone.test/staff/credits/adjust', {
      method: 'POST',
      headers: { 'content-type': 'application/json' },
      body: JSON.stringify({
        walletKey: 'acct-1:translate:2026-07',
        amount: '5',
        idempotencyKey: 'credit-adjust-1',
        reason: 'manual adjustment',
        mode: 'delta',
      }),
    })

    expect(response.status).toBe(200)
    await expect(readJson(response)).resolves.toEqual({
      data: {
        walletKey: 'acct-1:translate:2026-07',
        balance: '15',
      },
    })
    expect(deps.adjustCredits).toHaveBeenCalledWith({
      walletKey: 'acct-1:translate:2026-07',
      amount: 5n,
      idempotencyKey: 'credit-adjust-1',
      reason: 'manual adjustment',
      mode: 'delta',
    })
    expect(deps.audit).toHaveBeenCalledWith(
      expect.objectContaining({
        actorId: 'staff-user',
        action: 'staff.credit_adjusted',
        entityType: 'wallet',
        entityId: 'acct-1:translate:2026-07',
      }),
    )
  })

  it('refunds through PayPal, issues a credit note, applies the balance reversal, and audits it', async () => {
    const { app, deps } = createApp({ capabilities: ['staff'] })

    const response = await app.request('http://press-zone.test/staff/refund', {
      method: 'POST',
      headers: { 'content-type': 'application/json' },
      body: JSON.stringify({
        walletKey: 'acct-1:translate:2026-07',
        chargeKey: 'charge-1',
        refundId: 'refund-request-1',
        amountMinor: '30',
        currency: 'ILS',
        customer: {
          name: 'Acme Ltd',
          email: 'billing@example.test',
          taxId: '514000000',
        },
      }),
    })

    expect(response.status).toBe(200)
    await expect(readJson(response)).resolves.toEqual({
      data: {
        status: 'refunded',
        balance: '20',
        creditNote: {
          documentId: 'credit-note-1',
          documentNumber: 'CN-1',
          documentUrl: 'https://billing.example.test/documents/CN-1',
        },
      },
    })
    expect(deps.refundProvider.refund).toHaveBeenCalledWith({
      refundKey: 'staff-refund:charge-1:refund-request-1',
      chargeKey: 'charge-1',
      refundId: 'refund-request-1',
      amount: 30,
    })
    expect(deps.reserveRefund).toHaveBeenCalledWith({
      chargeKey: 'charge-1',
      refundKey: 'staff-refund:charge-1:refund-request-1',
      walletKey: 'acct-1:translate:2026-07',
      amountMinor: 30n,
      currency: 'ILS',
    })
    expect(deps.recordRefundCredit).toHaveBeenCalledWith({
      chargeKey: 'charge-1',
      refundId: 'refund-request-1',
      refundKey: 'staff-refund:charge-1:refund-request-1',
      providerRefundKey: 'refund-key-1',
      walletKey: 'acct-1:translate:2026-07',
      amountMinor: 30n,
      currency: 'ILS',
      customer: {
        name: 'Acme Ltd',
        email: 'billing@example.test',
        taxId: '514000000',
      },
    })
    expect(deps.audit).toHaveBeenCalledWith(
      expect.objectContaining({
        actorId: 'staff-user',
        action: 'staff.refund_issued',
        entityType: 'refund',
        entityId: 'charge-1',
      }),
    )
  })

  it('short-circuits an already recorded refund without re-running provider or credit effects', async () => {
    const deps = createDeps()
    deps.reserveRefund = vi.fn(async () => ({
      status: 'completed' as const,
      balance: 20n,
      creditNote: {
        documentId: 'credit-note-1',
        documentNumber: 'CN-1',
        documentUrl: 'https://billing.example.test/documents/CN-1',
      },
    }))
    const { app } = createApp({ capabilities: ['staff'], deps })

    const response = await app.request('http://press-zone.test/staff/refund', {
      method: 'POST',
      headers: { 'content-type': 'application/json' },
      body: JSON.stringify({
        walletKey: 'acct-1:translate:2026-07',
        chargeKey: 'charge-1',
        refundId: 'refund-request-1',
        amountMinor: '30',
        currency: 'ILS',
        customer: { name: 'Acme Ltd' },
      }),
    })

    expect(response.status).toBe(200)
    expect(deps.refundProvider.refund).not.toHaveBeenCalled()
    expect(deps.recordRefundCredit).not.toHaveBeenCalled()
  })

  it('rejects cumulative refunds that exceed the captured charge amount', async () => {
    const deps = createDeps()
    deps.reserveRefund = vi.fn(async () => {
      throw new AppError('BAD_REQUEST', 'refund exceeds captured charge amount', 400)
    })
    const { app } = createApp({ capabilities: ['staff'], deps })

    const response = await app.request('http://press-zone.test/staff/refund', {
      method: 'POST',
      headers: { 'content-type': 'application/json' },
      body: JSON.stringify({
        walletKey: 'acct-1:translate:2026-07',
        chargeKey: 'charge-1',
        refundId: 'refund-request-2',
        amountMinor: '30',
        currency: 'ILS',
        customer: { name: 'Acme Ltd' },
      }),
    })

    expect(response.status).toBe(400)
    expect(deps.refundProvider.refund).not.toHaveBeenCalled()
    expect(deps.recordRefundCredit).not.toHaveBeenCalled()
  })

  it('rejects refund currency mismatches before provider calls', async () => {
    const deps = createDeps()
    deps.reserveRefund = vi.fn(async () => {
      throw new AppError('BAD_REQUEST', 'currency does not match the original charge', 400)
    })
    const { app } = createApp({ capabilities: ['staff'], deps })

    const response = await app.request('http://press-zone.test/staff/refund', {
      method: 'POST',
      headers: { 'content-type': 'application/json' },
      body: JSON.stringify({
        walletKey: 'acct-1:translate:2026-07',
        chargeKey: 'charge-1',
        refundId: 'refund-request-1',
        amountMinor: '30',
        currency: 'USD',
        customer: { name: 'Acme Ltd' },
      }),
    })

    expect(response.status).toBe(400)
    expect(deps.refundProvider.refund).not.toHaveBeenCalled()
  })

  it('releases the reservation when the provider refund call throws', async () => {
    const deps = createDeps()
    deps.refundProvider.refund = vi.fn(async () => {
      throw new Error('provider refund failed')
    })
    const { app } = createApp({ capabilities: ['staff'], deps })

    const response = await app.request('http://press-zone.test/staff/refund', {
      method: 'POST',
      headers: { 'content-type': 'application/json' },
      body: JSON.stringify({
        walletKey: 'acct-1:translate:2026-07',
        chargeKey: 'charge-1',
        refundId: 'refund-request-3',
        amountMinor: '30',
        currency: 'ILS',
        customer: { name: 'Acme Ltd' },
      }),
    })

    expect(response.status).toBe(500)
    expect(deps.releaseRefundReservation).toHaveBeenCalledWith({
      refundKey: 'staff-refund:charge-1:refund-request-3',
    })
    expect(deps.recordRefundCredit).not.toHaveBeenCalled()
  })

  it('rejects credit adjustments without positive amount and idempotency key', async () => {
    const { app, deps } = createApp({ capabilities: ['staff'] })

    const missingKey = await app.request('http://press-zone.test/staff/credits/adjust', {
      method: 'POST',
      headers: { 'content-type': 'application/json' },
      body: JSON.stringify({
        walletKey: 'acct-1:translate:2026-07',
        amount: '5',
        reason: 'manual adjustment',
      }),
    })
    const negative = await app.request('http://press-zone.test/staff/credits/adjust', {
      method: 'POST',
      headers: { 'content-type': 'application/json' },
      body: JSON.stringify({
        walletKey: 'acct-1:translate:2026-07',
        amount: '-1',
        idempotencyKey: 'credit-adjust-2',
        reason: 'manual adjustment',
      }),
    })

    expect(missingKey.status).toBe(400)
    expect(negative.status).toBe(400)
    expect(deps.adjustCredits).not.toHaveBeenCalled()
  })

  it('overrides a tier and audits the mutation', async () => {
    const { app, deps } = createApp({ capabilities: ['staff'] })

    const response = await app.request('http://press-zone.test/staff/tier', {
      method: 'POST',
      headers: { 'content-type': 'application/json' },
      body: JSON.stringify({
        account: 'acct-1',
        tier: 'enterprise',
      }),
    })

    expect(response.status).toBe(200)
    await expect(readJson(response)).resolves.toEqual({
      data: {
        account: 'acct-1',
        tier: 'enterprise',
      },
    })
    expect(deps.setTier).toHaveBeenCalledWith('acct-1', 'enterprise')
    expect(deps.audit).toHaveBeenCalledWith(
      expect.objectContaining({
        actorId: 'staff-user',
        action: 'staff.tier_overridden',
        entityType: 'account',
        entityId: 'acct-1',
      }),
    )
  })

  it('lists audit entries with parsed filters', async () => {
    const deps = createDeps()
    deps.listAudit = vi.fn(async () => ({
      items: [
        {
          id: 'audit-1',
          actorId: 'staff-user',
          actorType: 'staff',
          actorLabel: 'staff-user',
          tenantId: 'acct-1',
          action: 'staff.credit_adjusted',
          entityType: 'wallet',
          entityId: 'acct-1:translate:2026-07',
          metadata: { reason: 'manual adjustment' },
          ip: null,
          createdAt: new Date('2026-07-03T10:00:00.000Z'),
        },
      ],
      nextCursor: 'cursor-1',
    }))
    const { app } = createApp({ capabilities: ['staff'], deps })

    const response = await app.request(
      'http://press-zone.test/staff/audit?tenant=staff-account&action=staff.credit_adjusted&limit=2&from=2026-07-01T00:00:00.000Z&to=2026-07-31T23:59:59.999Z',
    )

    expect(response.status).toBe(200)
    await expect(readJson(response)).resolves.toEqual({
      data: {
        items: [
          {
            id: 'audit-1',
            actorId: 'staff-user',
            actorType: 'staff',
            actorLabel: 'staff-user',
            tenantId: 'acct-1',
            action: 'staff.credit_adjusted',
            entityType: 'wallet',
            entityId: 'acct-1:translate:2026-07',
            metadata: { reason: 'manual adjustment' },
            ip: null,
            createdAt: '2026-07-03T10:00:00.000Z',
          },
        ],
        nextCursor: 'cursor-1',
      },
    })
    expect(deps.listAudit).toHaveBeenCalledWith({
      tenant: 'staff-account',
      actor: undefined,
      entityType: undefined,
      entityId: undefined,
      action: 'staff.credit_adjusted',
      q: undefined,
      cursor: undefined,
      limit: 2,
      from: new Date('2026-07-01T00:00:00.000Z'),
      to: new Date('2026-07-31T23:59:59.999Z'),
    })
  })

  it('rejects cross-tenant audit queries and over-large limits', async () => {
    const { app, deps } = createApp({ capabilities: ['staff'] })

    const crossTenant = await app.request('http://press-zone.test/staff/audit?tenant=acct-2')
    const tooLarge = await app.request('http://press-zone.test/staff/audit?limit=101')

    expect(crossTenant.status).toBe(403)
    expect(tooLarge.status).toBe(400)
    expect(deps.listAudit).not.toHaveBeenCalled()
  })

  it('rejects tier overrides outside the configured allowlist', async () => {
    const { app, deps } = createApp({ capabilities: ['staff'] })

    const response = await app.request('http://press-zone.test/staff/tier', {
      method: 'POST',
      headers: { 'content-type': 'application/json' },
      body: JSON.stringify({
        account: 'acct-1',
        tier: 'root',
      }),
    })

    expect(response.status).toBe(400)
    expect(deps.setTier).not.toHaveBeenCalled()
  })

  it('rejects a non-integer audit limit', async () => {
    const { app, deps } = createApp({ capabilities: ['staff'] })

    const response = await app.request(
      'http://press-zone.test/staff/audit?limit=2x',
    )

    expect(response.status).toBe(400)
    await expect(readJson(response)).resolves.toEqual({
      error: {
        code: 'BAD_REQUEST',
        message: 'limit must be an integer',
      },
    })
    expect(deps.listAudit).not.toHaveBeenCalled()
  })
})
