//! M4 per-message bench for `AllowlistAspect`: end-to-end cost of the
//! pre-dispatch slice of a Discord inbound message — JSON parse +
//! author-id extraction + authorization check. The portion downstream
//! of the auth check (message routing, LLM dispatch, response send)
//! is unchanged by the migration, so it is excluded from this
//! measurement; including it would only add unrelated variance.
//!
//! This is the "headline" zeroclaw-side measurement. It answers: at
//! a realistic per-message work envelope, is the aspect overhead
//! visible in p50/p95/p99 latency?
//!
//! Payload: a fixed, hand-written Discord MESSAGE_CREATE event
//! shaped like the real wire format (channel_id, guild_id, author
//! object with id/username/bot, content, timestamp, etc.). The
//! payload is read at every iteration so `serde_json::from_slice`
//! pays its full cost — *not* parsed once outside the timed loop.
//!
//! Regimes:
//!   - `hit_small`     — N=8 allowlist, author is in it
//!   - `hit_large`     — N=256, author is in it (last position)
//!   - `miss_small`    — N=8, author not in it
//!   - `miss_large`    — N=256, author not in it
//!   - `wildcard`      — allowlist is ["*"], short-circuit path
//!
//! For each regime the baseline (Vec<String> + closure) and the
//! aspect are benched alongside one another. The delta is the
//! per-message aspect overhead under realistic surrounding work.

use aspect_std::AllowlistAspect;
use criterion::{BenchmarkId, Criterion, criterion_group, criterion_main};
use std::hint::black_box;

fn make_entries(n: usize, include_target: bool) -> Vec<String> {
    let mut v: Vec<String> = (0..n).map(|i| format!("{}", 100_000_000 + i as u64)).collect();
    if include_target {
        // The author_id in the fixture payload below is "999999999".
        // Place it last so the linear scan walks the whole list.
        v.pop();
        v.push("999999999".to_string());
    }
    v
}

#[inline]
fn baseline_is_allowed(entries: &[String], identity: &str) -> bool {
    entries.iter().any(|u| u == "*" || u == identity)
}

// Realistic Discord MESSAGE_CREATE event payload (`d` field of a
// gateway frame). Keys and shapes match the real wire format so
// `serde_json::from_slice` has to do real work per iteration.
const DISCORD_PAYLOAD: &[u8] = br#"{
  "id": "1234567890123456789",
  "channel_id": "987654321098765432",
  "guild_id": "555555555555555555",
  "author": {
    "id": "999999999",
    "username": "alice",
    "discriminator": "0001",
    "avatar": "abc123def456",
    "bot": false,
    "system": false,
    "global_name": "Alice"
  },
  "member": {
    "nick": null,
    "roles": ["111111111111111111", "222222222222222222"],
    "joined_at": "2024-01-15T10:00:00.000000+00:00",
    "premium_since": null,
    "deaf": false,
    "mute": false
  },
  "content": "hello bot, please answer this short question",
  "timestamp": "2026-05-13T20:00:00.000000+00:00",
  "edited_timestamp": null,
  "tts": false,
  "mention_everyone": false,
  "mentions": [],
  "mention_roles": [],
  "attachments": [],
  "embeds": [],
  "pinned": false,
  "type": 0
}"#;

#[inline(always)]
fn parse_and_extract_author(payload: &[u8]) -> String {
    let v: serde_json::Value = serde_json::from_slice(payload).expect("parse");
    v.get("author")
        .and_then(|a| a.get("id"))
        .and_then(|i| i.as_str())
        .expect("author.id")
        .to_string()
}

fn bench_per_message(c: &mut Criterion) {
    let mut group = c.benchmark_group("per_message");

    // N=500 samples. 200 was too few for stable p99 (two cells showed
    // single-sample fat-tail outliers in opposite directions); raising
    // to 500 wash out one-shot scheduler interruptions and gives the
    // paper a defensible p99 column.
    group.sample_size(500);

    let cases: &[(&str, usize, bool)] = &[
        ("hit_small", 8, true),
        ("hit_large", 256, true),
        ("miss_small", 8, false),
        ("miss_large", 256, false),
    ];

    for &(name, n, include_target) in cases {
        let entries = make_entries(n, include_target);
        let aspect = AllowlistAspect::new(entries.clone());

        group.bench_with_input(
            BenchmarkId::new("baseline", name),
            &(entries.clone()),
            |b, e| {
                b.iter(|| {
                    let id = parse_and_extract_author(black_box(DISCORD_PAYLOAD));
                    black_box(baseline_is_allowed(e, &id))
                });
            },
        );
        group.bench_with_input(
            BenchmarkId::new("aspect", name),
            &aspect,
            |b, a| {
                b.iter(|| {
                    let id = parse_and_extract_author(black_box(DISCORD_PAYLOAD));
                    black_box(a.is_allowed(&id))
                });
            },
        );
    }

    // Wildcard short-circuit.
    {
        let entries = vec!["*".to_string()];
        let aspect = AllowlistAspect::new(entries.clone());
        group.bench_function("baseline/wildcard", |b| {
            b.iter(|| {
                let id = parse_and_extract_author(black_box(DISCORD_PAYLOAD));
                black_box(baseline_is_allowed(&entries, &id))
            });
        });
        group.bench_function("aspect/wildcard", |b| {
            b.iter(|| {
                let id = parse_and_extract_author(black_box(DISCORD_PAYLOAD));
                black_box(aspect.is_allowed(&id))
            });
        });
    }

    group.finish();
}

criterion_group!(benches, bench_per_message);
criterion_main!(benches);
