/**
 * Inbound webhook / intake auth regression tests — S7-001..004.
 */
import { describe, it, expect, vi, beforeEach } from 'vitest'

// Node test runtime lacks Web Crypto timingSafeEqual — polyfill for route tests.
if (!crypto.subtle.timingSafeEqual) {
  crypto.subtle.timingSafeEqual = (a: BufferSource, b: BufferSource): boolean => {
    const bufA = new Uint8Array(a as ArrayBuffer)
    const bufB = new Uint8Array(b as ArrayBuffer)
    if (bufA.byteLength !== bufB.byteLength) return false
    let diff = 0
    for (let i = 0; i < bufA.byteLength; i++) diff |= bufA[i] ^ bufB[i]
    return diff === 0
  }
}
import { Hono } from 'hono'
import type { AppEnv } from '../src/types'

async function hmacSha256Hex(secret: string, data: string): Promise<string> {
  const keyBytes = new TextEncoder().encode(secret)
  const dataBytes = new TextEncoder().encode(data)
  const key = await crypto.subtle.importKey(
    'raw',
    keyBytes,
    { name: 'HMAC', hash: 'SHA-256' },
    false,
    ['sign'],
  )
  const sigBuf = await crypto.subtle.sign('HMAC', key, dataBytes)
  return Array.from(new Uint8Array(sigBuf))
    .map((b) => b.toString(16).padStart(2, '0'))
    .join('')
}

// ── S7-001 WhatsApp ───────────────────────────────────────────────────────────

vi.mock('@zync/db/queries', () => ({
  createDb: vi.fn(() => ({})),
  loadAdapterCredentialRow: vi.fn(),
  getTenantById: vi.fn(),
  getActiveMemberEmailsForTenant: vi.fn(),
}))

vi.mock('@zync/auth', async (importOriginal) => {
  const actual = await importOriginal<typeof import('@zync/auth')>()
  return {
    ...actual,
    decryptCredential: vi.fn(),
    meetsMinimumTier: vi.fn(() => true),
  }
})

import { whatsappRouter } from '../src/routes/webhooks/whatsapp'
import {
  loadAdapterCredentialRow,
  getTenantById,
} from '@zync/db/queries'
import { decryptCredential } from '@zync/auth'

const TENANT_ID = '11111111-1111-4111-8111-111111111111'
const mockCredRow = {
  ciphertext: 'ct',
  iv: 'iv',
  authTag: 'tag',
}

function whatsappApp() {
  const app = new Hono<AppEnv>()
  app.route('/api/webhooks/whatsapp', whatsappRouter)
  return app
}

function whatsappEnv(queueSend = vi.fn()) {
  return {
    DB: { connectionString: 'postgresql://test/test' },
    INTEGRATION_ENCRYPTION_KEY: 'test-key',
    QUEUE: { send: queueSend },
  } as unknown as AppEnv['Bindings']
}

describe('WhatsApp webhook auth (S7-001)', () => {
  beforeEach(() => {
    vi.clearAllMocks()
    vi.mocked(getTenantById).mockResolvedValue({
      id: TENANT_ID,
      tier: 'enterprise',
    } as Awaited<ReturnType<typeof getTenantById>>)
    vi.mocked(loadAdapterCredentialRow).mockResolvedValue(mockCredRow as never)
  })

  it('returns 503 when app_secret is missing but signature header is present', async () => {
    const diagnostic = vi.spyOn(console, 'error').mockImplementation(() => undefined)
    vi.mocked(decryptCredential).mockResolvedValue(JSON.stringify({ app_secret: '' }))
    const body = '{"entry":[]}'
    const res = await whatsappApp().request(`/api/webhooks/whatsapp/${TENANT_ID}`, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'X-Hub-Signature-256': 'sha256=deadbeef',
      },
      body,
    }, whatsappEnv())
    expect(res.status).toBe(503)
    expect(diagnostic).toHaveBeenCalledWith(
      '[whatsapp-webhook] active integration missing app_secret',
      { tenantId: TENANT_ID },
    )
    diagnostic.mockRestore()
  })

  it('returns 401 when signature header is missing', async () => {
    vi.mocked(decryptCredential).mockResolvedValue(JSON.stringify({ app_secret: 'secret' }))
    const res = await whatsappApp().request(`/api/webhooks/whatsapp/${TENANT_ID}`, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: '{}',
    }, whatsappEnv())
    expect(res.status).toBe(401)
  })

  it('returns 401 when signature is invalid', async () => {
    vi.mocked(decryptCredential).mockResolvedValue(JSON.stringify({ app_secret: 'secret' }))
    const res = await whatsappApp().request(`/api/webhooks/whatsapp/${TENANT_ID}`, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'X-Hub-Signature-256': 'sha256=invalid',
      },
      body: '{}',
    }, whatsappEnv())
    expect(res.status).toBe(401)
  })

  it('accepts a valid signed payload', async () => {
    const secret = 'meta-app-secret'
    const body = '{}'
    const hex = await hmacSha256Hex(secret, body)
    vi.mocked(decryptCredential).mockResolvedValue(JSON.stringify({ app_secret: secret }))
    const queueSend = vi.fn().mockResolvedValue(undefined)
    const res = await whatsappApp().request(`/api/webhooks/whatsapp/${TENANT_ID}`, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'X-Hub-Signature-256': `sha256=${hex}`,
      },
      body,
    }, whatsappEnv(queueSend))
    expect(res.status).toBe(200)
    expect(queueSend).not.toHaveBeenCalled()
  })
})

// ── S7-002 Telegram bot webhook ───────────────────────────────────────────────

import { telegramRoutes } from '../src/routes/telegram/index'

function telegramApp() {
  const app = new Hono<AppEnv>()
  app.route('/api/telegram', telegramRoutes)
  return app
}

describe('Telegram bot webhook auth (S7-002)', () => {
  it('returns 503 when TELEGRAM_WEBHOOK_SECRET is unset', async () => {
    const diagnostic = vi.spyOn(console, 'error').mockImplementation(() => undefined)
    const res = await telegramApp().request('/api/telegram/webhook', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ update_id: 1 }),
    }, { DB: { connectionString: 'postgresql://test/test' } } as AppEnv['Bindings'])
    expect(res.status).toBe(503)
    expect(diagnostic).toHaveBeenCalledWith(
      '[telegram-webhook] TELEGRAM_WEBHOOK_SECRET is not configured',
    )
    diagnostic.mockRestore()
  })

  it('returns 403 when secret token does not match', async () => {
    const res = await telegramApp().request('/api/telegram/webhook', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'X-Telegram-Bot-Api-Secret-Token': 'wrong',
      },
      body: JSON.stringify({ update_id: 1 }),
    }, {
      DB: { connectionString: 'postgresql://test/test' },
      TELEGRAM_WEBHOOK_SECRET: 'expected-secret',
    } as AppEnv['Bindings'])
    expect(res.status).toBe(403)
  })

  it('accepts webhook when secret token matches', async () => {
    const res = await telegramApp().request('/api/telegram/webhook', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'X-Telegram-Bot-Api-Secret-Token': 'expected-secret',
      },
      body: JSON.stringify({ update_id: 1 }),
    }, {
      DB: { connectionString: 'postgresql://test/test' },
      TELEGRAM_WEBHOOK_SECRET: 'expected-secret',
    } as AppEnv['Bindings'])
    expect(res.status).toBe(200)
  })
})

// ── S7-003 Outlook clientState ────────────────────────────────────────────────

import {
  parseOutlookNotifications,
  outlookNotificationsAuthorized,
  outlookClientStateKey,
} from '../src/lib/outlook-calendar-webhook'

describe('Outlook webhook clientState (S7-003)', () => {
  it('outlookClientStateKey is stable per connection', () => {
    expect(outlookClientStateKey('conn-1')).toBe('outlook_client_state:conn-1')
  })

  it('rejects empty notification batches', () => {
    expect(outlookNotificationsAuthorized([], 'stored')).toBe(false)
  })

  it('rejects when stored clientState is missing', () => {
    const notifications = [{ clientState: 'abc' }]
    expect(outlookNotificationsAuthorized(notifications, null)).toBe(false)
  })

  it('rejects mismatched clientState', () => {
    const notifications = [{ clientState: 'attacker' }]
    expect(outlookNotificationsAuthorized(notifications, 'stored-secret')).toBe(false)
  })

  it('accepts matching clientState on every notification', () => {
    const secret = 'stored-secret'
    const notifications = [{ clientState: secret }, { clientState: secret }]
    expect(outlookNotificationsAuthorized(notifications, secret)).toBe(true)
  })

  it('parseOutlookNotifications returns null for invalid JSON', () => {
    expect(parseOutlookNotifications('{not-json')).toBe(null)
  })
})

// ── S7-004 Expense email sender ─────────────────────────────────────────────

import {
  normalizeEmailAddress,
  isExpenseEmailSenderAllowed,
} from '../src/lib/expense-email-sender'

describe('Expense email sender allowlist (S7-004)', () => {
  it('normalizes RFC5322 From addresses', () => {
    expect(normalizeEmailAddress('Staff <staff@acme.com>')).toBe('staff@acme.com')
    expect(normalizeEmailAddress('staff@acme.com')).toBe('staff@acme.com')
  })

  it('isExpenseEmailSenderAllowed matches active member emails only', async () => {
    const { getActiveMemberEmailsForTenant } = await import('@zync/db/queries')
    vi.mocked(getActiveMemberEmailsForTenant).mockResolvedValue([
      'member@tenant.com',
      'owner@tenant.com',
    ])

    const db = {} as Parameters<typeof isExpenseEmailSenderAllowed>[0]
    expect(await isExpenseEmailSenderAllowed(db, 'tenant-1', 'member@tenant.com')).toBe(true)
    expect(await isExpenseEmailSenderAllowed(db, 'tenant-1', 'Member <member@tenant.com>')).toBe(true)
    expect(await isExpenseEmailSenderAllowed(db, 'tenant-1', 'stranger@evil.com')).toBe(false)
  })
})
