// Handler-Regression mit der ECHTEN gepatchten server.ts (echtes gate(), echte
// PERMISSION_REPLY_RE, echte grammy-Befehlsverarbeitung) in allen drei Modi.
// Bridge: Mock-Bridge als echter Prozess (python3 -I), gesteuert über Textmarkierungen.
import { afterAll, beforeAll, beforeEach, describe, expect, test } from 'bun:test'
import { createHash } from 'node:crypto'
import { existsSync, mkdirSync, readFileSync } from 'node:fs'
import { join } from 'node:path'
import {
  apiCalls, audioUpdate, bootServer, claudeNotifications, defaultAccess, documentUpdate, gatewayLog, GROUP_ID, GROUP_MEMBER,
  HARNESS_ROOT, OWNER, permissionNotifications, photoUpdate, registry, resetRecords, restoreExit, settle,
  stickerUpdate, STRANGER, supabaseBodies, textUpdate, videoNoteUpdate, videoUpdate, voiceUpdate, WORK, type Server,
} from './harness.ts'

type Mode = 'off' | 'shadow' | 'active'
const MODES: Mode[] = ['off', 'shadow', 'active']
const MOCK = join(HARNESS_ROOT, 'test', 'fixtures', 'mock_bridge.py')
const TS = '2027-01-15T08:00:00.000Z'
const ROUTER_DOWN = 'Aria-Gateway nicht erreichbar: Die Nachricht wurde nicht weitergeleitet. Bitte später erneut senden.'
const BLOCKED_DEFAULT = 'Nicht weitergeleitet: Das Aria-Gateway hat diese Nachricht nicht freigegeben.'

let srv: Server
let gateway: any
const seenRequests: Record<string, number> = {}

function bridgeDir(mode: Mode): string {
  return join(WORK, 'modes-bridge', mode)
}

function setMode(mode: Mode, opts: { onFailure?: string; timeoutMs?: number } = {}) {
  process.env.ARIA_GATEWAY_MODE = mode
  process.env.ARIA_GATEWAY_CONFIG = join(bridgeDir(mode), 'gateway.json')
  process.env.ARIA_GATEWAY_ON_FAILURE = opts.onFailure ?? ''
  process.env.ARIA_GATEWAY_TIMEOUT_MS = String(opts.timeoutMs ?? 1200)
  process.env.ARIA_GATEWAY_BRIDGE = MOCK
}

function newRequests(mode: Mode): any[] {
  const file = join(bridgeDir(mode), 'requests.jsonl')
  const all = existsSync(file) ? readFileSync(file, 'utf8').split('\n').filter(Boolean).map(l => JSON.parse(l)) : []
  const fresh = all.slice(seenRequests[mode] ?? 0)
  seenRequests[mode] = all.length
  return fresh
}

type Result = { handlerMs: number; claude: any[]; sent: string[]; sentTo: string[]; requests: any[]; log: any[]; supabase: any[] }

async function deliver(mode: Mode, ...updates: any[]): Promise<Result> {
  resetRecords()
  newRequests(mode)
  const started = Date.now()
  for (const update of updates) await srv.send(update)
  const handlerMs = Date.now() - started
  await settle(20)
  await gateway.settled()
  await settle(5)
  return {
    handlerMs,
    claude: claudeNotifications(),
    sent: apiCalls('sendMessage').map(c => String(c.payload.text)),
    sentTo: apiCalls('sendMessage').map(c => String(c.payload.chat_id)),
    requests: newRequests(mode),
    log: gatewayLog(),
    supabase: supabaseBodies(),
  }
}

function expectForwarded(r: Result, update: any) {
  const m = update.message
  expect(r.claude.length).toBe(1)
  expect(r.claude[0].params.content).toBe(m.text ?? m.caption)
  expect(r.claude[0].params.meta).toEqual({
    chat_id: String(m.chat.id), message_id: String(m.message_id), user: m.from.username ?? String(m.from.id), user_id: String(m.from.id), ts: TS,
  })
}

beforeAll(async () => {
  for (const mode of MODES) mkdirSync(bridgeDir(mode), { recursive: true })
  srv = await bootServer({ file: 'server.ts', name: 'modes', env: { ARIA_GATEWAY_MODE: 'off', ARIA_GATEWAY_BRIDGE: MOCK } })
  gateway = (await import(join(HARNESS_ROOT, 'plugin', 'aria-gateway.ts'))).ariaGateway
})

beforeEach(() => srv.writeAccess(defaultAccess()))

afterAll(() => restoreExit())

for (const mode of MODES) {
  describe(`Modus ${mode}`, () => {
    beforeEach(() => setMode(mode))

    test('Claude-Route: Weiterleitung mit Prod-Payload', async () => {
      const update = textUpdate(OWNER, 'Hallo Aria')
      const r = await deliver(mode, update)
      expectForwarded(r, update)
      expect(r.supabase.length).toBe(1)
      if (mode === 'off') expect([r.requests.length, r.log.length]).toEqual([0, 0])
      if (mode === 'shadow') {
        expect(r.requests.map(q => q.mode)).toEqual(['shadow'])
        expect([r.log[0].handling, r.log[0].planned_handling, r.log[0].forwarded]).toEqual(['shadow_decision', 'forward_to_claude_channel', true])
      }
      if (mode === 'active') {
        expect(r.requests.map(q => q.mode)).toEqual(['execute'])
        expect([r.log[0].outcome, r.log[0].forwarded]).toEqual(['FORWARD', true])
      }
    })

    test('Codex-Route (Mock-Bridge)', async () => {
      const update = textUpdate(OWNER, '[codex] schreib eine Funktion')
      const r = await deliver(mode, update)
      if (mode === 'active') {
        expect(r.claude).toEqual([])
        expect(r.sent).toEqual([`MOCK_CODEX_RESPONSE für Nachricht ${update.message.message_id}`])
        expect(r.sentTo).toEqual([String(OWNER.id)])
        expect([r.log[0].outcome, r.log[0].backend]).toEqual(['REPLY', 'codex'])
        expect(r.supabase.length).toBe(1) // Eingangsprotokoll bleibt wie heute
      } else {
        expectForwarded(r, update)
        expect(r.sent).toEqual([])
        if (mode === 'shadow') expect([r.log[0].planned_handling, r.log[0].backend]).toEqual(['reply', 'codex'])
        if (mode === 'off') expect(r.requests).toEqual([])
      }
    })

    test('Blocked: in active Erklärung und KEINE Weiterleitung', async () => {
      const update = textUpdate(OWNER, '[blocked] Kundendaten Firma X')
      const r = await deliver(mode, update)
      if (mode === 'active') {
        expect(r.claude).toEqual([])
        expect(r.sent).toEqual(['Nicht gesendet: Testsperre (Datenschutz).'])
        expect([r.log[0].outcome, r.log[0].forwarded]).toEqual(['BLOCKED', false])
      } else {
        expectForwarded(r, update)
        if (mode === 'shadow') expect(r.log[0].planned_handling).toBe('blocked')
      }
    })

    test('Router-Timeout', async () => {
      const update = textUpdate(OWNER, '[timeout] Hallo')
      const r = await deliver(mode, update)
      expectForwarded(r, update)
      if (mode === 'off') expect(r.requests).toEqual([])
      if (mode === 'shadow') {
        expect(r.handlerMs).toBeLessThan(400) // wartet nicht auf die Bridge (Timeout 1200 ms)
        expect([r.log[0].ok, r.log[0].failure]).toEqual([false, 'BRIDGE_TIMEOUT'])
      }
      if (mode === 'active') {
        expect(r.handlerMs).toBeGreaterThanOrEqual(1100)
        expect([r.log[0].outcome, r.log[0].policy]).toEqual(['ROUTER_FAILURE:BRIDGE_TIMEOUT', 'claude_status_quo'])
        setMode('active', { onFailure: 'block' })
        const blocked = await deliver(mode, textUpdate(OWNER, '[timeout] Hallo mit block'))
        expect([blocked.claude.length, blocked.sent]).toEqual([0, [ROUTER_DOWN]])
      }
    })

    test('Router-Absturz (Exit-Code und Signal)', async () => {
      for (const marker of ['[crash]', '[kill]']) {
        const update = textUpdate(OWNER, `${marker} Hallo`)
        const r = await deliver(mode, update)
        expectForwarded(r, update)
        const code = marker === '[crash]' ? 'BRIDGE_EXIT_NONZERO' : 'BRIDGE_SIGNALED'
        if (mode === 'shadow') expect(r.log[0].failure).toBe(code)
        if (mode === 'active') expect(r.log[0].outcome).toBe(`ROUTER_FAILURE:${code}`)
      }
      if (mode === 'active') {
        setMode('active', { onFailure: 'block' })
        const r = await deliver(mode, textUpdate(OWNER, '[crash] mit block'))
        expect([r.claude.length, r.sent]).toEqual([0, [ROUTER_DOWN]])
      }
    })

    test('ungültige Bridge-Antwort (kein JSON, zwei Zeilen, Schemafehler, falsche Bindung, Shadow-Antwort)', async () => {
      const cases: Array<[string, string]> = [
        ['[garbage]', 'BRIDGE_INVALID_JSON'], ['[twolines]', 'BRIDGE_INVALID_JSON'], ['[invalid]', 'BRIDGE_INVALID_RESPONSE'],
        ['[mismatch]', 'FORWARD_CONTENT_MISMATCH'], ['[meta-mismatch]', 'FORWARD_META_MISMATCH'],
        ['[foreign-subject]', 'FORWARD_SUBJECT_MISMATCH'], ['[shadow-in-execute]', 'BRIDGE_MODE_MISMATCH'],
      ]
      for (const [marker, code] of cases) {
        const update = textUpdate(OWNER, `${marker} Hallo`)
        const r = await deliver(mode, update)
        expectForwarded(r, update) // off/shadow immer, active nach claude_status_quo
        if (mode === 'active') expect([marker, r.log[0].outcome]).toEqual([marker, `ROUTER_FAILURE:${code}`])
      }
      if (mode === 'active') {
        setMode('active', { onFailure: 'block' })
        for (const [marker] of cases) {
          const r = await deliver(mode, textUpdate(OWNER, `${marker} mit block`))
          expect([marker, r.claude.length, r.sent]).toEqual([marker, 0, [ROUTER_DOWN]])
        }
      }
    })

    test('doppelte message_id', async () => {
      const update = textUpdate(OWNER, 'Diese Nachricht kommt zweimal')
      const r = await deliver(mode, update, { ...update, update_id: update.update_id + 100000 })
      if (mode === 'active') {
        expect(r.claude.length).toBe(1)
        expect(r.requests.length).toBe(1)
        expect(r.log.map(e => e.outcome)).toEqual(['FORWARD', 'DUPLICATE_LOCAL'])
      } else {
        expect(r.claude.length).toBe(2) // Prod-Verhalten
        expect(r.requests.length).toBe(mode === 'shadow' ? 2 : 0)
      }
    })

    test('10 parallele Nachrichten', async () => {
      if (mode === 'active') setMode('active', { timeoutMs: 8000 })
      const updates = Array.from({ length: 10 }, (_, i) => textUpdate(OWNER, `[slow] parallel ${i}${i % 3 === 0 ? ' [codex]' : ''}`))
      const eventsFile = join(bridgeDir(mode), 'events.jsonl')
      const readEvents = () => (existsSync(eventsFile) ? readFileSync(eventsFile, 'utf8').split('\n').filter(Boolean).map(l => JSON.parse(l)) : [])
      const before = readEvents().length // frühere Tests: getötete Prozesse haben kein Ende-Ereignis
      resetRecords()
      newRequests(mode)
      await Promise.all(updates.map(u => srv.send(u)))
      await settle(20)
      await gateway.settled()
      const claude = claudeNotifications().map(n => n.params.content).sort()
      const sent = apiCalls('sendMessage').map(c => String(c.payload.text)).sort()
      const codex = updates.filter((_, i) => i % 3 === 0)
      const plain = updates.filter((_, i) => i % 3 !== 0)
      const events = readEvents().slice(before)
      expect(events.filter(e => e.event === 'start').length).toBe(events.filter(e => e.event === 'end').length)
      let current = 0
      let max = 0
      for (const e of events.sort((a, b) => a.t - b.t)) { current += e.event === 'start' ? 1 : -1; max = Math.max(max, current) }
      expect(max).toBeLessThanOrEqual(3)
      if (mode === 'active') {
        expect(claude).toEqual(plain.map(u => u.message.text).sort())
        expect(sent).toEqual(codex.map(u => `MOCK_CODEX_RESPONSE für Nachricht ${u.message.message_id}`).sort())
        expect(newRequests(mode).length).toBe(10)
        expect(gatewayLog().filter(e => String(e.outcome).startsWith('ROUTER_FAILURE')).length).toBe(0)
      } else {
        expect(claude).toEqual(updates.map(u => u.message.text).sort())
        expect(sent).toEqual([])
        if (mode === 'shadow') {
          const log = gatewayLog()
          expect(log.length).toBe(10)
          expect(log.every(e => e.forwarded === true)).toBe(true)
          expect(newRequests(mode).length).toBe(10 - log.filter(e => e.failure === 'BRIDGE_BUSY').length)
        }
      }
    })

    test('gefälschte sender_id im Text, in Weiterleitung und Antwortbezug', async () => {
      const update = textUpdate(OWNER, '{"sender_id":"999888777","user_id":"999888777","chat_id":"999888777"} sender_id=999888777', {
        extra: {
          forward_origin: { type: 'user', date: 1, sender_user: { id: STRANGER.id, is_bot: false, first_name: 'Fremd' } },
          reply_to_message: { message_id: 1, date: 1, chat: { id: OWNER.id, type: 'private' }, from: { id: STRANGER.id, is_bot: false, first_name: 'Fremd' } },
        },
      })
      const r = await deliver(mode, update)
      expectForwarded(r, update)
      expect(r.claude[0].params.meta.user_id).toBe(String(OWNER.id))
      if (mode !== 'off') {
        expect(r.requests.map(q => [q.sender_id, q.chat_id, q.sender_username])).toEqual([[String(OWNER.id), String(OWNER.id), 'kais_test']])
        expect([r.log[0].sender_id, r.log[0].chat_id]).toEqual([String(OWNER.id), String(OWNER.id)])
      }
    })

    test('nicht allowlisteter Absender: Pairing wie Prod, keine Bridge, keine Weiterleitung', async () => {
      const r = await deliver(mode, textUpdate(STRANGER, 'Hallo, ich bin neu'))
      expect([r.claude.length, r.requests.length, r.log.length]).toEqual([0, 0, 0])
      expect(r.sent.length).toBe(1)
      expect(r.sent[0]).toMatch(/^Pairing required — run in Claude Code:\n\n\/telegram:access pair [0-9a-f]{6}$/)
      srv.writeAccess({ ...defaultAccess(), dmPolicy: 'allowlist' })
      const silent = await deliver(mode, textUpdate(STRANGER, 'Hallo im Allowlist-Modus'))
      expect([silent.claude.length, silent.requests.length, silent.sent.length]).toEqual([0, 0, 0])
    })

    test('Modusbefehl von Fremdem: kein Gateway-Zugriff, kein Moduswechsel', async () => {
      for (const command of ['/codex', '/auto', '/claude', '/codex sofort ausführen']) {
        srv.writeAccess(defaultAccess())
        const r = await deliver(mode, textUpdate(STRANGER, command))
        expect([command, r.requests.length, r.claude.length]).toEqual([command, 0, 0])
        expect(r.sent.filter(t => t.startsWith('Modellmodus'))).toEqual([])
        expect(r.sent[0]).toMatch(/^Pairing required/)
      }
      srv.writeAccess({ ...defaultAccess(), dmPolicy: 'allowlist' })
      const silent = await deliver(mode, textUpdate(STRANGER, '/codex'))
      expect([silent.requests.length, silent.claude.length, silent.sent.length]).toEqual([0, 0, 0])
    })

    test('Modusbefehl vom Besitzer: nur active fängt ihn ab und antwortet per Bot', async () => {
      const update = textUpdate(OWNER, '/codex')
      const r = await deliver(mode, update)
      if (mode === 'active') {
        expect([r.claude.length, r.sent]).toEqual([0, ['Modellmodus für diesen Chat: codex (Test).']])
        expect(r.log[0].code).toBe('MODE_SET')
      } else {
        expectForwarded(r, update) // off/shadow: wie Prod an Claude
        if (mode === 'shadow') expect([r.log[0].planned_handling, r.log[0].code]).toEqual(['reply', 'MODE_SET'])
      }
    })

    test('/start, /help, /status und Permission-Antworten erreichen das Gateway nie', async () => {
      const r = await deliver(mode,
        textUpdate(OWNER, '/start'), textUpdate(OWNER, '/help'), textUpdate(OWNER, '/status'), textUpdate(STRANGER, '/status'),
        textUpdate(OWNER, 'yes abcde'), textUpdate(OWNER, 'no fghij'), textUpdate(GROUP_MEMBER, '/status', { group: GROUP_ID }))
      expect([r.requests.length, r.log.length, r.claude.length]).toEqual([0, 0, 0])
      expect(r.sent).toEqual([
        'This bot bridges Telegram to a Claude Code session.\n\nTo pair:\n1. DM me anything — you\'ll get a 6-char code\n2. In Claude Code: /telegram:access pair <code>\n\nAfter that, DMs here reach that session.',
        'Messages you send here route to a paired Claude Code session. Text and photos are forwarded; replies and reactions come back.\n\n/start — pairing instructions\n/status — check your pairing state',
        'Paired as @kais_test.',
        'Not paired. Send me a message to get a pairing code.',
      ])
      expect(permissionNotifications().map(n => n.params)).toEqual([{ request_id: 'abcde', behavior: 'allow' }, { request_id: 'fghij', behavior: 'deny' }])
      expect(apiCalls('setMessageReaction').map(c => (c.payload.reaction as any)[0].emoji)).toEqual(['✅', '❌'])
    })
  })
}

describe('active: Zusatzfälle', () => {
  beforeEach(() => setMode('active'))
  const norm = (list: any[]) => JSON.parse(JSON.stringify(list).replace(/\/inbox\/\d+-/g, '/inbox/<T>-'))

  test('Claude-Weg mit identischem Payload wie Prod für alle Nachrichtentypen', async () => {
    const makers = [
      () => textUpdate(OWNER, 'Text'), () => photoUpdate(OWNER, 'Bild'), () => photoUpdate(OWNER), () => documentUpdate(OWNER),
      () => voiceUpdate(OWNER), () => audioUpdate(OWNER), () => videoUpdate(OWNER, 'Video'), () => videoNoteUpdate(OWNER), () => stickerUpdate(OWNER),
      () => textUpdate(GROUP_MEMBER, 'Gruppe', { group: GROUP_ID }),
    ]
    for (const make of makers) {
      const update = make()
      setMode('off')
      const prod = await deliver('off', update)
      setMode('active')
      const act = await deliver('active', { ...update, update_id: update.update_id + 500000 })
      expect(prod.claude.length).toBe(1)
      expect(norm(act.claude)).toEqual(norm(prod.claude))
      expect(act.supabase.map(b => JSON.stringify(b).replace(/\/inbox\/\d+-/g, ''))).toEqual(prod.supabase.map(b => JSON.stringify(b).replace(/\/inbox\/\d+-/g, '')))
      expect([act.log[0].outcome, act.log[0].forwarded]).toEqual(['FORWARD', true])
    }
  })

  test('verweigerte Freigaben kippen nie in eine Weiterleitung, auch nicht bei claude_status_quo oder Sendefehler', async () => {
    const cases: Array<[string, string | null]> = [
      ['[blocked] a', 'Nicht gesendet: Testsperre (Datenschutz).'], ['[blocked-silent] b', BLOCKED_DEFAULT],
      ['[failed] c', 'Auftrag an codex fehlgeschlagen (TIMEOUT).'], ['[drop] d', null],
      ['[not-intercepted] e', 'Nicht weitergeleitet: /start, /help und /status bitte als eigene Textnachricht senden.'],
      ['[unexpected-forward] f', BLOCKED_DEFAULT], ['[extra-field] [blocked] g', BLOCKED_DEFAULT], ['[empty-reply] h', 'Auftrag fehlgeschlagen. Es gibt keinen automatischen Wechsel auf ein anderes Modell.'],
    ]
    for (const policy of ['claude_status_quo', 'block']) {
      for (const [text, reply] of cases) {
        setMode('active', { onFailure: policy })
        const r = await deliver('active', textUpdate(OWNER, text))
        expect([policy, text, r.claude.length, r.sent]).toEqual([policy, text, 0, reply === null ? [] : [reply]])
        expect(r.log[0].forwarded).toBe(false)
      }
    }
    registry.apiBehavior = undefined
    setMode('active')
    resetRecords()
    newRequests('active')
    registry.apiBehavior = method => (method === 'sendMessage' ? { ok: false, error_code: 403, description: 'Forbidden: test' } : undefined)
    await srv.send(textUpdate(OWNER, '[blocked] Sendefehler'))
    await gateway.settled()
    expect(claudeNotifications()).toEqual([])
    expect(gatewayLog().map(e => [e.outcome, e.reply_sent, e.forwarded])).toEqual([['BLOCKED', false, false]])
    registry.apiBehavior = undefined
  })

  test('Gruppe: Modusbefehl eines Mitglieds wird gesperrt (generische Erklärung), Text eines Mitglieds normal', async () => {
    const r = await deliver('active', textUpdate(GROUP_MEMBER, '/codex', { group: GROUP_ID }))
    expect([r.claude.length, r.sent, r.sentTo]).toEqual([0, [BLOCKED_DEFAULT], [String(GROUP_ID)]])
  })

  test('Einmal-Modus /claude <Text>: Claude erhält GENAU den freigegebenen Rest, nicht die Rohnachricht (Review #11)', async () => {
    const update = textUpdate(OWNER, '/claude bitte prüfen')
    const r = await deliver('active', update)
    expect(r.claude.length).toBe(1)
    // Vorher wurde die ganze Nachricht ("/claude bitte prüfen") gesendet; jetzt nur der klassifizierte Rest.
    expect(r.claude[0].params.content).toBe('bitte prüfen')
    expect(r.claude[0].params.meta.chat_id).toBe(String(update.message.chat.id))
    expect(r.log[0].outcome).toBe('FORWARD')
  })

  test('Hinweis bei geprüftem Fallback wird gesendet, danach Weiterleitung', async () => {
    const update = textUpdate(OWNER, '[forward-notice] Kundendaten')
    const r = await deliver('active', update)
    expectForwarded(r, update)
    expect(r.sent).toEqual(['Hinweis: geprüfter Fallback (Test).'])
  })

  test('Bridge nicht konfiguriert: Ausfallregel greift (status quo leitet weiter, block nicht)', async () => {
    delete process.env.ARIA_GATEWAY_BRIDGE
    const update = textUpdate(OWNER, 'ohne Bridge')
    const r = await deliver('active', update)
    expectForwarded(r, update)
    expect(r.log.find(e => e.phase === 'decision').outcome).toBe('ROUTER_FAILURE:BRIDGE_NOT_CONFIGURED')
    process.env.ARIA_GATEWAY_ON_FAILURE = 'block'
    const b = await deliver('active', textUpdate(OWNER, 'ohne Bridge, block'))
    expect([b.claude.length, b.sent]).toEqual([0, [ROUTER_DOWN]])
  })

  test('Foto: Anfrage trägt local_path und sha256 der heruntergeladenen Datei', async () => {
    const update = photoUpdate(OWNER, 'Bild prüfen')
    const r = await deliver('active', update)
    expect(r.requests.length).toBe(1)
    const att = r.requests[0].attachments
    expect(att.length).toBe(1)
    expect([att[0].kind, att[0].file_id, att[0].size]).toEqual(['photo', 'AgAC-large', 98765])
    expect(att[0].local_path).toBe(r.claude[0].params.meta.image_path)
    // Mock-Download liefert die Bytes ff d8 ff (harness.ts); der Hash muss genau darüber gebildet sein.
    expect(att[0].sha256).toBe(createHash('sha256').update(Buffer.from([0xff, 0xd8, 0xff])).digest('hex'))
  })
})

describe('dokumentierte Einschränkung: grammy verarbeitet Updates sequenziell', () => {
  // bot.handleUpdates() ist der Pfad der Polling-Schleife (bot.start -> loop -> handleUpdates).
  async function permissionDelay(mode: Mode): Promise<number> {
    setMode(mode, { timeoutMs: 5000 })
    resetRecords()
    const started = Date.now()
    let at = -1
    const watcher = (async () => {
      while (permissionNotifications().length === 0 && Date.now() - started < 5000) await Bun.sleep(5)
      at = Date.now() - started
    })()
    await srv.bot.handleUpdates([textUpdate(OWNER, '[slow] [codex] langer Auftrag'), textUpdate(OWNER, 'yes kmnop')])
    await watcher
    await gateway.settled()
    expect(permissionNotifications().map(n => n.params)).toEqual([{ request_id: 'kmnop', behavior: 'allow' }])
    return at
  }

  test('off/shadow: Permission-Antwort sofort; active: wartet auf den laufenden Gateway-Aufruf (~400 ms Mock-Bridge)', async () => {
    expect(await permissionDelay('off')).toBeLessThan(150)
    expect(await permissionDelay('shadow')).toBeLessThan(150)
    expect(await permissionDelay('active')).toBeGreaterThanOrEqual(350)
  })
})

describe('Drift-Schutz', () => {
  test('channelParams() in aria-gateway.ts entspricht dem tatsächlichen Payload aus server.ts (alle Typen)', async () => {
    const mod = await import(join(HARNESS_ROOT, 'plugin', 'aria-gateway.ts'))
    setMode('shadow')
    const original = gateway.inbound
    const captured: any[] = []
    gateway.inbound = (input: any) => {
      captured.push(input)
      return original.call(gateway, input)
    }
    try {
      const makers = [
        () => textUpdate(OWNER, 'Text'), () => photoUpdate(OWNER, 'Bild'), () => documentUpdate(OWNER), () => voiceUpdate(OWNER),
        () => audioUpdate(OWNER), () => videoUpdate(OWNER), () => videoNoteUpdate(OWNER), () => stickerUpdate(OWNER),
        () => textUpdate({ id: 123123, first_name: 'OhneName' }, 'ohne username'),
      ]
      srv.writeAccess({ ...defaultAccess(), allowFrom: [String(OWNER.id), '123123'] })
      for (const make of makers) {
        captured.length = 0
        const r = await deliver('shadow', make())
        expect(captured.length).toBe(1)
        const c = captured[0]
        expect(mod.channelParams(c.ctx, c.text, c.imagePath, c.attachment)).toEqual(r.claude[0].params)
      }
    } finally {
      gateway.inbound = original
    }
  })
})

describe('Ein Poller (Laufzeit)', () => {
  test('genau ein Bot-Objekt, ein bot.start(), keine getUpdates/Webhook-Aufrufe, kein process.exit', () => {
    expect(registry.bots.length).toBe(1)
    expect(registry.startCalls).toBe(1)
    expect(registry.stopCalls).toBe(0)
    expect(registry.exits).toEqual([])
    expect([...registry.methodsSeen].filter(m => ['getUpdates', 'deleteWebhook', 'setWebhook', 'close', 'logOut', 'getMe'].includes(m))).toEqual([])
    expect(registry.methodsSeen.has('sendMessage')).toBe(true) // die Aufzeichnung hat tatsächlich gearbeitet
  })
})
