//! Privacy invariant integration test (DEC-027 / SDD-090 / SAD-077).
//!
//! Every `tracing` event emitted while exercising the public-API
//! surface of the PTT subsystem must carry only field names from a
//! known allow-list, and never any of the banned key-data field
//! names. The test installs a cross-platform recording `Layer`,
//! drives the backends and the keymap through realistic scenarios,
//! and asserts the captured records honour both invariants.
//!
//! This test is platform-agnostic to compile — the recording layer
//! is generic, the assertions are generic — but the
//! Windows-specific dispatcher / backend exercise lives under a
//! `#[cfg(target_os = "windows")]` block. On Linux and macOS the
//! test exercises only the cross-platform pieces (`AudioTransmitGate`,
//! `MissedKeyUpWatchdog`, `PttBinding`, `PttInputClass`) which are
//! sufficient to verify the privacy invariant on those hosts.

use std::sync::{Arc, Mutex};

use tracing::subscriber::with_default;
use tracing_subscriber::layer::{Context, SubscriberExt};
use tracing_subscriber::registry::LookupSpan;
use tracing_subscriber::Layer;

/// Banned field names per DEC-027 / SDD-090. The test fails fast
/// if any emitted record carries one of these names.
const BANNED_FIELDS: &[&str] = &[
    "vk",
    "scan_code",
    "scancode",
    "keysym",
    "keysym_string",
    "key_label",
    "bound_key",
    "binding",
    "platform_key",
    "VKey",
    "wVk",
    "wScan",
    "kbflags",
    "mouseflags",
    "key_code",
    "virtual_key",
    "key_sequence",
    "key_press_history",
    "key_timing",
];

/// Allow-list of field names that may appear in a PTT-subsystem
/// tracing record. Anything outside this set is a privacy
/// regression even if it is not on the banned list — the spec
/// uses an allow-list precisely to catch new field additions
/// before they can leak.
const ALLOWED_FIELDS: &[&str] = &[
    "backend_id",
    "bound_input_class",
    "capability_level",
    "host_id",
    "timeout_secs",
    "error",
    "event",
    "message",
    "target",
    "level",
];

/// Captured shape of a single tracing event. We hold only field
/// names — values would defeat the privacy intent of the test.
#[derive(Debug, Clone)]
struct Captured {
    target: String,
    field_names: Vec<String>,
}

/// `tracing_subscriber::Layer` that records every emitted event's
/// target + field-name set. The records live in a `Mutex<Vec>`
/// shared with the test body.
#[derive(Clone, Default)]
struct RecordingLayer {
    records: Arc<Mutex<Vec<Captured>>>,
}

impl RecordingLayer {
    fn snapshot(&self) -> Vec<Captured> {
        self.records.lock().unwrap().clone()
    }
}

#[derive(Default)]
struct NameCollector(Vec<String>);

impl tracing::field::Visit for NameCollector {
    fn record_debug(&mut self, field: &tracing::field::Field, _value: &dyn std::fmt::Debug) {
        self.0.push(field.name().to_string());
    }
    fn record_str(&mut self, field: &tracing::field::Field, _value: &str) {
        self.0.push(field.name().to_string());
    }
    fn record_i64(&mut self, field: &tracing::field::Field, _value: i64) {
        self.0.push(field.name().to_string());
    }
    fn record_u64(&mut self, field: &tracing::field::Field, _value: u64) {
        self.0.push(field.name().to_string());
    }
    fn record_bool(&mut self, field: &tracing::field::Field, _value: bool) {
        self.0.push(field.name().to_string());
    }
}

impl<S> Layer<S> for RecordingLayer
where
    S: tracing::Subscriber + for<'a> LookupSpan<'a>,
{
    fn on_event(&self, event: &tracing::Event<'_>, _ctx: Context<'_, S>) {
        let mut names = NameCollector::default();
        event.record(&mut names);
        let captured = Captured {
            target: event.metadata().target().to_string(),
            field_names: names.0,
        };
        self.records.lock().unwrap().push(captured);
    }
}

/// Assert the captured records honour the DEC-027 banned-field
/// rule and the allow-list. Only records from the chanora targets
/// participate — host-side tracing-subscriber chatter (e.g. the
/// tokio runtime) is ignored.
fn assert_privacy_invariants(records: &[Captured]) {
    let relevant: Vec<&Captured> = records
        .iter()
        .filter(|r| r.target.starts_with("chanora_"))
        .collect();
    for r in &relevant {
        for name in &r.field_names {
            assert!(
                !BANNED_FIELDS.contains(&name.as_str()),
                "banned field {name:?} emitted by target {:?}",
                r.target
            );
            assert!(
                ALLOWED_FIELDS.contains(&name.as_str()),
                "field {name:?} from target {:?} is not in the allow-list",
                r.target
            );
        }
    }
}

#[test]
fn ptt_subsystem_never_emits_banned_or_unknown_fields() {
    let layer = RecordingLayer::default();
    let subscriber = tracing_subscriber::registry().with(layer.clone());

    with_default(subscriber, || {
        // Cross-platform paths: AudioTransmitGate transitions
        // (these emit no tracing themselves but exercise the
        // public surface); PttBinding / PttInputClass construction.
        use chanora_audio::ptt_backends::{PttBinding, PttInputClass};
        use chanora_audio::AudioTransmitGate;

        let gate = AudioTransmitGate::new(false);
        gate.set(true);
        gate.set(false);
        let _ = gate.load();
        let _ = PttBinding::none();
        let _ = PttBinding {
            input_class: PttInputClass::Keyboard,
            platform_key: "Space".to_string(),
        };
        let _ = PttInputClass::Keyboard.as_str();
        let _ = PttInputClass::MouseSideButton.as_str();

        // Platform-specific dispatcher paths. These live behind
        // the cfg gate; cross-platform we still get coverage of
        // the cross-platform surface above which is enough to
        // catch any accidental info!/warn! that names a banned
        // field at module init time.
        #[cfg(target_os = "windows")]
        windows_exercise();
    });

    let records = layer.snapshot();
    assert_privacy_invariants(&records);
}

#[cfg(target_os = "windows")]
fn windows_exercise() {
    // The Windows backend types are intentionally not part of
    // chanora_audio's public API (they live in
    // `crate::ptt_backends::windows` as `pub(crate)`). The privacy
    // invariant for those code paths is enforced by the dispatcher
    // unit tests inside `windows.rs` itself, where the same
    // recording layer pattern is used. From here we simply
    // exercise the `select_ptt_backend` factory + descriptor +
    // start/stop lifecycle through the public trait surface so
    // any info!/warn! the factory or the backend's `start` path
    // emits is captured by the layer.
    use chanora_audio::ptt_backends::{select_ptt_backend, PttBinding, PttInputClass};
    use chanora_audio::AudioTransmitGate;

    let mut backend = select_ptt_backend();
    let gate = AudioTransmitGate::new(false);
    let binding = PttBinding {
        input_class: PttInputClass::Keyboard,
        platform_key: "Space".to_string(),
    };
    // Best-effort: in the CI sandbox the actual Raw Input /
    // hook registration may fail. The privacy invariant is
    // about field names, not about whether the start succeeds.
    let _ = backend.start(gate, binding);
    let _ = backend.descriptor();
    let bad_binding = PttBinding {
        input_class: PttInputClass::Keyboard,
        platform_key: "Banana".to_string(),
    };
    let _ = backend.rebind(bad_binding);
    backend.stop();
}
