// Spuraufnahme: dieselben Szenarien gegen
//   prod   = server.prod-0.0.6.ts (unverändert)
//   off    = server.ts, ARIA_GATEWAY_MODE=off
//   shadow = server.ts, ARIA_GATEWAY_MODE=shadow, Mock-Bridge (inkl. Timeout/Absturz/Müll)
// Ergebnis: <WORK>/out/trace-<ziel>.json; Vergleich in compare-traces.test.ts.
import { afterAll, beforeAll, expect, test } from 'bun:test'
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'
import { join } from 'node:path'
import { bootServer, HARNESS_ROOT, OWNER, GROUP_MEMBER, registry, resetRecords, restoreExit, settle, STRANGER, WORK, gatewayLog, type Server } from './harness.ts'
import { SCENARIOS } from './scenarios.ts'

const TARGET = process.env.ARIA_TRACE_TARGET as 'prod' | 'off' | 'shadow'
if (!['prod', 'off', 'shadow'].includes(TARGET)) throw new Error('ARIA_TRACE_TARGET muss prod|off|shadow sein')
const BRIDGE_DIR = join(WORK, `trace-${TARGET}-bridge`)

let srv: Server
let gateway: any
const traces: any[] = []
const gatewayLines: any[] = []

function normalize(value: string, stateDir: string): string {
  return value
    .split(stateDir).join('<STATE>')
    .replace(/\/inbox\/\d+-/g, '/inbox/<T>-')
    .replace(/pair [0-9a-f]{6}/g, 'pair <CODE>')
}

function snapshot(stateDir: string, unordered: boolean) {
  const norm = (x: unknown) => JSON.parse(normalize(JSON.stringify(x), stateDir))
  const sortIf = (list: any[]) => (unordered ? [...list].sort((a, b) => JSON.stringify(a).localeCompare(JSON.stringify(b))) : list)
  return {
    api: sortIf(norm(registry.api)),
    notifications: sortIf(norm(registry.notifications)),
    fetches: sortIf(norm(registry.fetches.map(f => ({ ...f, body: f.body ? JSON.parse(f.body) : null })))),
    stderr: sortIf(norm(registry.stderr.join('').split('\n').filter(l => l && !l.startsWith('aria-gateway: ')))),
  }
}

beforeAll(async () => {
  mkdirSync(BRIDGE_DIR, { recursive: true })
  const env: Record<string, string> = TARGET === 'prod' ? {} : {
    ARIA_GATEWAY_MODE: TARGET,
    ARIA_GATEWAY_BRIDGE: join(HARNESS_ROOT, 'test', 'fixtures', 'mock_bridge.py'),
    ARIA_GATEWAY_CONFIG: join(BRIDGE_DIR, 'gateway.json'),
    ARIA_GATEWAY_TIMEOUT_MS: '700',
  }
  srv = await bootServer({ file: TARGET === 'prod' ? 'server.prod-0.0.6.ts' : 'server.ts', name: `trace-${TARGET}`, env })
  if (TARGET !== 'prod') gateway = (await import(join(HARNESS_ROOT, 'plugin', 'aria-gateway.ts'))).ariaGateway
})

afterAll(() => restoreExit())

test(`Spur aufnehmen (${TARGET}): ${SCENARIOS.length} Szenarien`, async () => {
  for (const scenario of SCENARIOS) {
    resetRecords()
    await scenario.run(srv)
    await settle(20)
    if (gateway) await gateway.settled()
    await settle(5)
    traces.push({ name: scenario.name, ...snapshot(srv.stateDir, scenario.unordered === true) })
    gatewayLines.push(...gatewayLog().map(entry => ({ scenario: scenario.name, ...entry })))
  }
  const out = join(WORK, 'out')
  mkdirSync(out, { recursive: true })
  writeFileSync(join(out, `trace-${TARGET}.json`), JSON.stringify(traces, null, 1))
  expect(traces.length).toBe(SCENARIOS.length)
}, 120_000)

test('genau ein Bot, ein bot.start(), kein getUpdates/Webhook, kein process.exit', () => {
  expect(registry.bots.length).toBe(1)
  expect(registry.startCalls).toBe(1)
  expect(registry.exits).toEqual([])
  const all = traces.flatMap(t => t.api.map((c: any) => c.method))
  expect(all.filter((m: string) => ['getUpdates', 'deleteWebhook', 'setWebhook', 'close', 'logOut'].includes(m))).toEqual([])
})

test('Shadow: Bridge sah nur zugestellte Nicht-Befehls-Nachrichten, Absender immer aus ctx.from.id', () => {
  if (TARGET !== 'shadow') {
    expect(existsSync(join(BRIDGE_DIR, 'requests.jsonl'))).toBe(false) // prod/off rufen die Bridge nie
    return
  }
  const requests = readFileSync(join(BRIDGE_DIR, 'requests.jsonl'), 'utf8').split('\n').filter(Boolean).map(l => JSON.parse(l))
  expect(requests.every(r => r.mode === 'shadow' && r.protocol === 1)).toBe(true)
  // /start, /help, /status als Textnachricht verarbeitet bot.command vor handleInbound; nur eine
  // Foto-Caption "/help" erreicht handleInbound (grammy wertet Captions nicht als Befehl).
  const plainTexts = requests.filter(r => r.attachments.length === 0).map(r => r.text)
  for (const forbidden of ['/start', '/help', '/status', '/status@aria_test_bot', 'yes abcde', '  N FGHIJ ', 'Hallo, wer bist du?', 'noch einmal', 'Hallo trotz disabled', 'fremde Gruppe', '/codex bitte', 'Hallo']) {
    expect([forbidden, plainTexts.includes(forbidden)]).toEqual([forbidden, false])
  }
  const captionHelp = requests.filter(r => r.text === '/help')
  expect(captionHelp.map(r => r.attachments.map((a: any) => a.kind))).toEqual([['photo']])
  // Fremde erreichen die Bridge erst nach Freigabe (letztes Szenario).
  const strangers = requests.filter(r => r.sender_id === String(STRANGER.id))
  expect(strangers.map(r => r.text)).toEqual(['Jetzt bin ich freigegeben'])
  const fake = requests.find(r => r.text.startsWith('{"sender_id":"999888777"'))
  expect([fake.sender_id, fake.chat_id]).toEqual([String(OWNER.id), String(OWNER.id)])
  const forwarded = requests.find(r => r.text === 'weitergeleitet: user_id 999888777')
  expect(forwarded.sender_id).toBe(String(OWNER.id))
  const group = requests.find(r => r.text === 'Hallo aus der Gruppe')
  expect([group.sender_id, group.chat_type]).toEqual([String(GROUP_MEMBER.id), 'supergroup'])
  const log = gatewayLines
  expect(log.length).toBe(requests.length + log.filter(e => e.failure === 'BRIDGE_BUSY').length)
  expect(log.every(e => e.mode === 'shadow' && e.forwarded === true && typeof e.sender_id === 'string' && typeof e.chat_id === 'string')).toBe(true)
  const failures = log.filter(e => e.ok === false).map(e => e.failure)
  for (const code of ['BRIDGE_TIMEOUT', 'BRIDGE_EXIT_NONZERO', 'BRIDGE_SIGNALED', 'BRIDGE_INVALID_JSON', 'BRIDGE_INVALID_RESPONSE', 'BRIDGE_OUTPUT_LIMIT']) {
    expect([code, failures.includes(code)]).toEqual([code, true])
  }
})
