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

// KAR-778 / R22: queueEmail must be idempotent for link-bearing templates
// (account_created, password_reset) and must NOT dedupe informational mails.

interface QueueRow {
  id: string
  to_email: string
  template: string
  status: string
}

let queue: QueueRow[] = []
let idSeq = 0
// KAR-789: when set, the next insert simulates the DB partial-unique index
// rejecting a row that slipped past the check-then-insert (the TOCTOU race).
let forceInsertConflict = false

// Minimal stateful fake of the Supabase admin client for the email_queue table.
// Each .from() returns a fresh chain so filters don't leak between calls.
function makeAdminClient() {
  return {
    from(_table: string) {
      const filters: Record<string, unknown> = {}
      const chain = {
        select() { return chain },
        eq(col: string, val: unknown) { filters[col] = val; return chain },
        limit() { return chain },
        async maybeSingle() {
          const match = queue.find(
            (r) =>
              r.to_email === filters.to_email &&
              r.template === filters.template &&
              r.status === filters.status
          )
          return { data: match ?? null, error: null }
        },
        async insert(row: { to_email: string; template: string }) {
          if (forceInsertConflict) {
            forceInsertConflict = false
            return { error: { code: '23505', message: 'duplicate key value violates unique constraint' } }
          }
          queue.push({ id: `row-${++idSeq}`, status: 'pending', ...row })
          return { error: null }
        },
      }
      return chain
    },
  }
}

vi.mock('@/lib/supabase/admin', () => ({
  createAdminClient: () => makeAdminClient(),
}))
vi.mock('@/lib/logger', () => ({
  logger: { debug: vi.fn(), error: vi.fn(), info: vi.fn(), warn: vi.fn() },
}))

import {
  notifyInvitation,
  notifyPasswordReset,
  notifyPasswordChanged,
  notifyEmailChanged,
} from '../notifications'

beforeEach(() => {
  queue = []
  idSeq = 0
  forceInsertConflict = false
})

describe('queueEmail idempotency (R22)', () => {
  it('notifyInvitation twice for the same email enqueues only one pending row', async () => {
    await notifyInvitation('user@example.de', 'https://app/confirm?a=1')
    await notifyInvitation('user@example.de', 'https://app/confirm?a=2')
    const rows = queue.filter((r) => r.template === 'account_created' && r.to_email === 'user@example.de')
    expect(rows).toHaveLength(1)
  })

  it('notifyPasswordReset twice for the same email enqueues only one pending row', async () => {
    await notifyPasswordReset('user@example.de', 'https://app/reset?a=1')
    await notifyPasswordReset('user@example.de', 'https://app/reset?a=2')
    const rows = queue.filter((r) => r.template === 'password_reset' && r.to_email === 'user@example.de')
    expect(rows).toHaveLength(1)
  })

  it('different recipients each get their own pending invite row', async () => {
    await notifyInvitation('a@example.de', 'https://app/confirm?a=1')
    await notifyInvitation('b@example.de', 'https://app/confirm?b=1')
    expect(queue.filter((r) => r.template === 'account_created')).toHaveLength(2)
  })

  it('a sent invite no longer blocks a fresh one (only pending rows dedupe)', async () => {
    await notifyInvitation('user@example.de', 'https://app/confirm?a=1')
    // Simulate the worker having sent the first invite.
    queue[0].status = 'sent'
    await notifyInvitation('user@example.de', 'https://app/confirm?a=2')
    expect(queue.filter((r) => r.template === 'account_created')).toHaveLength(2)
  })

  it('informational mails are NOT deduped — each event notifies', async () => {
    await notifyPasswordChanged('user@example.de')
    await notifyPasswordChanged('user@example.de')
    expect(queue.filter((r) => r.template === 'password_changed')).toHaveLength(2)

    await notifyEmailChanged('user@example.de', 'old@example.de', 'new@example.de')
    await notifyEmailChanged('user@example.de', 'old@example.de', 'new@example.de')
    expect(queue.filter((r) => r.template === 'email_changed')).toHaveLength(2)
  })

  // KAR-789: race backstop — a concurrent enqueue that wins makes our insert hit
  // the partial-unique index (23505); for an idempotent template that resolves
  // to a no-op, not a thrown error.
  it('treats a 23505 unique-violation on an invite as an idempotent no-op', async () => {
    forceInsertConflict = true
    await expect(notifyInvitation('race@example.de', 'https://app/confirm')).resolves.toBeUndefined()
    // the conflicting row was not added (the concurrent winner already has it)
    expect(queue.filter((r) => r.template === 'account_created')).toHaveLength(0)
  })
})
