// Ende-zu-Ende: echte gepatchte server.ts -> echter Bridge-Prozess -> echtes Python-Gateway
// (python/bin/aria_bridge.py aus dem Repo, Kopie im Test-Arbeitsverzeichnis). Codex nur über
// den MockRunner (real_execution=false); es wird nie ein echtes codex/claude gestartet.
// Gateway und Plugin lesen dieselbe Test-access.json (Allowlist bleibt maßgeblich).
import { afterAll, beforeAll, beforeEach, describe, expect, test } from 'bun:test'
import { randomBytes } from 'node:crypto'
import { chmodSync, existsSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from 'node:fs'
import { join } from 'node:path'
import {
  apiCalls, bootServer, claudeNotifications, defaultAccess, documentUpdate, gatewayLog, GROUP_ID, GROUP_MEMBER, HARNESS_ROOT,
  OWNER, photoUpdate, registry, resetRecords, restoreExit, settle, STRANGER, textUpdate, voiceUpdate, WORK, type Server,
} from './harness.ts'

const ROOT = join(WORK, 'e2e-gw')
const CONFIG = join(ROOT, 'gateway.json')
const KEY = join(ROOT, 'keys', 'clearance.key')
const STATE = join(ROOT, 'state')
const BRIDGE = join(HARNESS_ROOT, 'python', 'bin', 'aria_bridge.py')
const FAKE_CODEX = join(HARNESS_ROOT, 'python', 'tests', 'fixtures', 'fake_codex.py')
const ROUTER_DOWN = 'Aria-Gateway nicht erreichbar: Die Nachricht wurde nicht weitergeleitet. Bitte später erneut senden.'

let srv: Server
let gateway: any
let mod: any

function writeConfig(stateDir: string, accessFile: string) {
  mkdirSync(join(ROOT, 'keys'), { recursive: true, mode: 0o700 })
  mkdirSync(join(ROOT, 'work'), { recursive: true })
  writeFileSync(KEY, randomBytes(32), { mode: 0o600 }) // generierter Testschlüssel, kein Secret
  chmodSync(KEY, 0o600)
  const config = {
    version: 1,
    allowed_modes: ['shadow', 'execute'],
    state_dir: stateDir,
    clearance: { key_file: KEY, ttl_seconds: 120 },
    auth: { telegram_access_file: accessFile },
    privacy: {
      matrix: { codex: ['public'], claude: ['public', 'internal', 'unknown'] },
      claude_status_quo_confidential: true,
      public_marker_enabled: true,
    },
    routing: { min_quality: 0, manual_fallback: 'block', default_task_class: 'analysis' },
    backends: {
      codex: { enabled: true, capabilities: ['text', 'code'], tasks: ['analysis', 'coding', 'review', 'writing', 'planning'], quality: 90, max_context_bytes: 131072 },
      claude: { enabled: true, capabilities: ['text', 'code', 'vision', 'files'], tasks: ['analysis', 'coding', 'review', 'writing', 'planning', 'vision'], quality: 90, max_context_bytes: 1048576 },
    },
    budget: { codex: { daily_jobs: 100 }, claude: { daily_jobs: 100 } },
    health: { failure_threshold: 3, open_seconds: 300 },
    limits: { max_text_bytes: 65536, max_reply_chars: 12000, max_attachments: 1 },
    codex: {
      real_execution: false, // MockRunner; das Template wird nie ausgeführt und zeigt nur auf das Fake-Skript
      command_template: ['/usr/bin/python3', '-I', FAKE_CODEX, 'success'],
      timeout_seconds: 30,
      workspace_root: join(ROOT, 'work'),
      env: { PATH: '/usr/bin:/bin', LANG: 'C.UTF-8' },
    },
  }
  writeFileSync(CONFIG, JSON.stringify(config, null, 2))
}

function decisions(): any[] {
  const file = join(STATE, 'decisions.jsonl')
  return existsSync(file) ? readFileSync(file, 'utf8').split('\n').filter(Boolean).map(l => JSON.parse(l)) : []
}

function setMode(mode: 'shadow' | 'active', onFailure = '') {
  process.env.ARIA_GATEWAY_MODE = mode
  process.env.ARIA_GATEWAY_ON_FAILURE = onFailure
  process.env.ARIA_GATEWAY_CONFIG = CONFIG
}

const allLogs: any[] = []

async function deliver(update: any) {
  resetRecords()
  await srv.send(update)
  await settle(20)
  await gateway.settled()
  const log = gatewayLog()
  allLogs.push(...log)
  return { claude: claudeNotifications(), sent: apiCalls('sendMessage').map(c => String(c.payload.text)), log }
}

beforeAll(async () => {
  srv = await bootServer({
    file: 'server.ts', name: 'e2e',
    env: { ARIA_GATEWAY_MODE: 'shadow', ARIA_GATEWAY_BRIDGE: BRIDGE, ARIA_GATEWAY_CONFIG: CONFIG, ARIA_GATEWAY_TIMEOUT_MS: '20000' },
  })
  writeConfig(STATE, join(srv.stateDir, 'access.json'))
  gateway = (await import(join(HARNESS_ROOT, 'plugin', 'aria-gateway.ts'))).ariaGateway
  mod = await import(join(HARNESS_ROOT, 'plugin', 'aria-gateway.ts'))
})

beforeEach(() => {
  srv.writeAccess(defaultAccess())
  // Testisolation: Chat-Modus im Gateway-Zustand zurücksetzen (sonst erbt ein Test den Modus eines fehlgeschlagenen)
  rmSync(join(STATE, 'modes.json'), { force: true })
  rmSync(join(STATE, 'shadow-modes.json'), { force: true })
})
afterAll(() => restoreExit())

describe('shadow mit echtem Python-Gateway', () => {
  beforeEach(() => setMode('shadow'))

  test('Entscheidung mit echter sender_id/chat_id protokolliert, Weiterleitung unverändert', async () => {
    const update = textUpdate(OWNER, 'Hallo Aria, echte Bridge im Shadow')
    const r = await deliver(update)
    expect(r.claude.length).toBe(1)
    expect(r.claude[0].params.content).toBe('Hallo Aria, echte Bridge im Shadow')
    expect([r.log[0].ok, r.log[0].handling, r.log[0].planned_handling, r.log[0].sender_id, r.log[0].chat_id])
      .toEqual([true, 'shadow_decision', 'forward_to_claude_channel', String(OWNER.id), String(OWNER.id)])
    const record = decisions().filter(d => d.phase === 'decision' && d.message_id === update.message.message_id)
    expect(record.length).toBe(1)
    expect([record[0].shadow, record[0].sender_id, record[0].chat_id, record[0].authentication.principal])
      .toEqual([true, String(OWNER.id), String(OWNER.id), 'owner'])
  })

  test('#public-Codingauftrag: Shadow plant Codex, Claude bekommt die Nachricht trotzdem', async () => {
    const r = await deliver(textUpdate(OWNER, '#public schreib eine Python-Funktion, die Primzahlen prüft'))
    expect(r.claude.length).toBe(1)
    expect([r.log[0].planned_handling, r.log[0].backend, r.log[0].privacy]).toEqual(['reply', 'codex', 'public'])
  })

  test('Fremder erreicht das Python-Gateway nie (gate() davor)', async () => {
    const before = decisions().length
    const r = await deliver(textUpdate(STRANGER, '/codex'))
    expect([r.claude.length, r.log.length]).toEqual([0, 0])
    expect(decisions().length).toBe(before)
    expect(decisions().some(d => d.sender_id === String(STRANGER.id))).toBe(false)
  })
})

describe('active mit echtem Python-Gateway (Codex = MockRunner)', () => {
  beforeEach(() => setMode('active'))

  test('Claude-Weg: Python-forward.params == Payload aus server.ts (Text, Foto, Dokument, Sprache, Gruppe)', async () => {
    for (const update of [textUpdate(OWNER, 'Hallo echte Bridge'), photoUpdate(OWNER, 'Bild an Claude'), documentUpdate(OWNER),
      voiceUpdate(OWNER), textUpdate(GROUP_MEMBER, 'Gruppe an Claude', { group: GROUP_ID })]) {
      const r = await deliver(update)
      expect(r.claude.length).toBe(1)
      expect([r.log[0].outcome, r.log[0].handling, r.log[0].backend]).toEqual(['FORWARD', 'forward_to_claude_channel', 'claude'])
    }
  })

  test('Modusbefehle: /codex setzt den Chat-Modus im Gateway-Zustand, Antwort per Bot, keine Weiterleitung', async () => {
    const r = await deliver(textUpdate(OWNER, '/codex'))
    expect(r.claude).toEqual([])
    expect(r.sent).toEqual(['Modellmodus für diesen Chat: codex (Datenschutz-, Budget- und Verfügbarkeitsregeln gelten weiter).'])
    const modes = JSON.parse(readFileSync(join(STATE, 'modes.json'), 'utf8'))
    expect(JSON.stringify(modes)).toContain(String(OWNER.id))
    expect(JSON.stringify(modes)).toContain('codex')

    const codex = await deliver(textUpdate(OWNER, '#public schreib eine Funktion für Fakultät'))
    expect([codex.claude.length, codex.sent, codex.log[0].backend]).toEqual([0, ['MOCK_CODEX_RESPONSE'], 'codex'])

    const denied = await deliver(textUpdate(OWNER, 'Bitte prüfe den Vertrag von Kunde Müller GmbH'))
    expect(denied.claude).toEqual([])
    expect(denied.sent.length).toBe(1)
    expect(denied.sent[0].startsWith('Nicht gesendet: Modus /codex ist gesetzt')).toBe(true)
    expect(denied.log[0].outcome).toBe('BLOCKED')

    const back = await deliver(textUpdate(OWNER, '/auto'))
    expect(back.sent).toEqual(['Modellmodus für diesen Chat: auto (automatische Auswahl).'])
    const normal = await deliver(textUpdate(OWNER, 'Bitte prüfe den Vertrag von Kunde Müller GmbH'))
    expect([normal.claude.length, normal.log[0].outcome]).toEqual([1, 'FORWARD']) // Status-quo-Matrix: vertraulich an Claude
  })

  test('Einmal-Modus /claude <Text> und Gruppen-Modusbefehl', async () => {
    const once = await deliver(textUpdate(OWNER, '/claude Hallo zusammen'))
    // Review #11: Claude erhaelt GENAU den vom Gateway klassifizierten Rest ("Hallo zusammen"),
    // nicht die Rohnachricht mit dem /claude-Praefix.
    expect([once.claude.length, once.claude[0]?.params.content, once.log[0].outcome]).toEqual([1, 'Hallo zusammen', 'FORWARD'])
    const group = await deliver(textUpdate(GROUP_MEMBER, '/codex', { group: GROUP_ID }))
    expect([group.claude.length, group.log[0].code, group.sent]).toEqual([0, 'MODE_COMMAND_NOT_ALLOWED', ['Nicht weitergeleitet: Das Aria-Gateway hat diese Nachricht nicht freigegeben.']])
  })

  test('Review #11: ein Secret im /claude@<...>-Praefix gelangt NICHT zu Claude (nur der klassifizierte Rest)', async () => {
    // Fake-Token (kein echtes Secret). Es steht im gestrippten @-Teil des Befehls.
    const once = await deliver(textUpdate(OWNER, '/claude@sk-ant-TESTCANARY00000000000000 #public Hallo'))
    expect(once.claude.length).toBe(1)
    const content = once.claude[0].params.content
    expect(content).toBe('#public Hallo')          // genau der klassifizierte Rest
    expect(content.includes('sk-ant')).toBe(false)  // der Token-Teil wurde nie zugestellt
  })

  test('Randfälle Caption: "/help" -> not_intercepted (Hinweis, keine Weiterleitung), "/codex" setzt den Modus', async () => {
    const help = await deliver(photoUpdate(OWNER, '/help'))
    expect([help.claude.length, help.log[0].handling, help.log[0].outcome]).toEqual([0, 'not_intercepted', 'NOT_INTERCEPTED'])
    expect(help.sent).toEqual(['Nicht weitergeleitet: /start, /help und /status bitte als eigene Textnachricht senden.'])
    const other = await deliver(textUpdate(OWNER, '/status@anderer_bot'))
    expect([other.claude.length, other.log[0].handling]).toEqual([0, 'not_intercepted'])
    const mode = await deliver(photoUpdate(OWNER, '/codex'))
    expect([mode.claude.length, mode.log[0].code, mode.sent]).toEqual([0, 'MODE_SET', ['Modellmodus für diesen Chat: codex (Datenschutz-, Budget- und Verfügbarkeitsregeln gelten weiter).']])
  })

  test('Python-Idempotenz über den echten Bridge-Prozess: zweite Anfrage gleicher message_id ist drop', async () => {
    const settings = mod.readSettings({ ...process.env, ARIA_GATEWAY_MODE: 'active' })
    const ctx = { from: { id: OWNER.id, username: 'kais_test' }, chat: { id: OWNER.id, type: 'private' }, message: { message_id: 990001, date: 1_800_000_000 } }
    const request = await mod.buildRequest('execute', { bot: null, ctx, text: 'Idempotenz', imagePath: undefined, attachment: undefined })
    const first = await mod.runBridgeProcess(settings, JSON.stringify(request), 20000)
    const second = await mod.runBridgeProcess(settings, JSON.stringify(request), 20000)
    expect([first.ok, first.response?.handling]).toEqual([true, 'forward_to_claude_channel'])
    expect([second.ok, second.response?.handling, second.response?.code]).toEqual([true, 'drop', 'DUPLICATE_MESSAGE'])
    const expected = mod.channelParams(ctx, 'Idempotenz', undefined, undefined)
    expect(mod.decideActive(request, expected, second, 'claude_status_quo').forward).toBe(false)
  })

  test('Gateway-Fehler (Schlüssel fehlt, Konfiguration fehlt): Ausfallregel', async () => {
    renameSync(KEY, `${KEY}.weg`)
    try {
      const quo = await deliver(textUpdate(OWNER, 'ohne Schlüssel'))
      expect([quo.claude.length, quo.log[0].outcome]).toEqual([1, 'ROUTER_FAILURE:GATEWAY_KEY_UNAVAILABLE'])
      setMode('active', 'block')
      const blocked = await deliver(textUpdate(OWNER, 'ohne Schlüssel, block'))
      expect([blocked.claude.length, blocked.sent]).toEqual([0, [ROUTER_DOWN]])
    } finally {
      renameSync(`${KEY}.weg`, KEY)
    }
    setMode('active')
    process.env.ARIA_GATEWAY_CONFIG = join(ROOT, 'fehlt.json')
    const missing = await deliver(textUpdate(OWNER, 'ohne Konfiguration'))
    expect([missing.claude.length, missing.log[0].outcome]).toEqual([1, 'ROUTER_FAILURE:GATEWAY_CONFIG_UNAVAILABLE'])
    process.env.ARIA_GATEWAY_CONFIG = CONFIG
  })

  test('jede echte Python-Antwort bestand die strikte Schemaprüfung; Fremde nie im Entscheidungsprotokoll', () => {
    const decisionsLogged = allLogs.filter(e => e.phase === 'decision')
    expect(decisionsLogged.length).toBeGreaterThanOrEqual(15)
    expect(decisionsLogged.filter(e => e.ok === false).map(e => e.failure)).toEqual([]) // kein Prozess-/Schemafehler
    expect(decisionsLogged.some(e => String(e.outcome).includes('BRIDGE_'))).toBe(false)
    expect(decisions().some(d => d.sender_id === String(STRANGER.id))).toBe(false)
    expect(registry.bots.length).toBe(1)
    expect(registry.startCalls).toBe(1)
  })
})
