use anyhow::{Context, Result};
use serde::{Deserialize, Serialize};
use serde_json::Value;
use std::fs::{self, OpenOptions};
use std::io::Write;
use std::path::{Path, PathBuf};

// ---------------------------------------------------------------------------
// TelemetryEvent -- the canonical on-disk representation of every event
// ---------------------------------------------------------------------------

/// A single telemetry event persisted as one JSON line in the events file.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TelemetryEvent {
    pub user_id: String,
    pub event_type: String,
    pub data: Value,
    pub timestamp: i64,
    pub version: String,
    pub platform: String,
    pub arch: String,
}

// ---------------------------------------------------------------------------
// Specific event structs -- typed builders for the most common event kinds
// ---------------------------------------------------------------------------

/// Emitted when a user creates a new scroll.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ScrollCreatedEvent {
    pub scroll_id: String,
    pub execution_mode: String,
    pub task_count: usize,
}

/// Emitted after a scroll finishes executing.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ScrollExecutedEvent {
    pub scroll_id: String,
    pub duration_secs: u64,
    pub tasks_completed: usize,
    pub tasks_failed: usize,
}

/// Emitted for every search query processed by the tiered search system.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SearchEvent {
    pub query_type: String,
    pub strategy: String,
    pub latency_ms: u64,
    pub result_count: usize,
    pub tier_used: String,
}

/// Emitted after an auto-forge run completes.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AutoForgeEvent {
    pub scroll_id: String,
    pub duration_secs: u64,
    pub test_count: usize,
    pub test_pass_rate: f64,
    pub user_decision: String,
}

// ---------------------------------------------------------------------------
// TelemetryStats -- aggregate statistics derived from the events file
// ---------------------------------------------------------------------------

/// Aggregate statistics computed from the stored telemetry events.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TelemetryStats {
    pub total_events: usize,
    pub scrolls_created: usize,
    pub scrolls_executed: usize,
    pub searches_performed: usize,
    pub auto_forge_runs: usize,
    pub avg_search_latency_ms: f64,
    pub earliest_event: Option<i64>,
    pub latest_event: Option<i64>,
}

// ---------------------------------------------------------------------------
// TelemetryTracker -- the core tracker that writes/reads the JSONL log
// ---------------------------------------------------------------------------

/// Maximum size of the events file before rotation (5 MB).
const MAX_EVENTS_FILE_SIZE: u64 = 5 * 1024 * 1024;

/// Number of rotated archives to keep (recent first).
const MAX_ROTATED_FILES: usize = 3;

/// Local-only, anonymous usage tracker.
///
/// Events are appended as JSON lines to a single file on disk.  No network
/// calls are ever made -- analysis happens entirely offline.  When the file
/// exceeds `MAX_EVENTS_FILE_SIZE`, it is rotated (recent events kept first).
pub struct TelemetryTracker {
    /// Whether telemetry is enabled.
    enabled: bool,
    /// Anonymous user ID (UUID v4, persisted across sessions).
    user_id: String,
    /// Path to the telemetry events JSONL file.
    events_file: PathBuf,
    /// FURNACE version string baked in at compile time.
    version: String,
}

impl TelemetryTracker {
    // -- Construction -------------------------------------------------------

    /// Create or load a `TelemetryTracker`.
    ///
    /// * Ensures `data_dir` exists.
    /// * Reads or generates a persistent anonymous user ID stored in
    ///   `{data_dir}/telemetry_id`.
    /// * Points the events file at `{data_dir}/telemetry.jsonl`.
    /// * Telemetry is **enabled** by default.
    pub fn new(data_dir: &Path) -> Result<Self> {
        fs::create_dir_all(data_dir)
            .with_context(|| format!("failed to create data directory: {}", data_dir.display()))?;

        let id_file = data_dir.join("telemetry_id");
        let user_id = if id_file.exists() {
            let raw = fs::read_to_string(&id_file)
                .with_context(|| format!("failed to read telemetry id: {}", id_file.display()))?;
            let trimmed = raw.trim().to_string();
            if trimmed.is_empty() {
                let new_id = uuid::Uuid::new_v4().to_string();
                fs::write(&id_file, &new_id)
                    .with_context(|| "failed to write fresh telemetry id")?;
                new_id
            } else {
                trimmed
            }
        } else {
            let new_id = uuid::Uuid::new_v4().to_string();
            fs::write(&id_file, &new_id)
                .with_context(|| "failed to persist new telemetry id")?;
            new_id
        };

        let events_file = data_dir.join("telemetry.jsonl");

        Ok(Self {
            enabled: true,
            user_id,
            events_file,
            version: env!("CARGO_PKG_VERSION").to_string(),
        })
    }

    // -- Enable / disable ---------------------------------------------------

    /// Toggle telemetry collection on or off.
    pub fn set_enabled(&mut self, enabled: bool) {
        self.enabled = enabled;
    }

    /// Returns `true` when telemetry collection is active.
    pub fn is_enabled(&self) -> bool {
        self.enabled
    }

    // -- Typed track helpers ------------------------------------------------

    /// Record a *scroll created* event.
    pub fn track_scroll_created(&self, event: ScrollCreatedEvent) -> Result<()> {
        let data = serde_json::to_value(&event)
            .context("failed to serialise ScrollCreatedEvent")?;
        self.track_custom("scroll_created", data)
    }

    /// Record a *scroll executed* event.
    pub fn track_scroll_executed(&self, event: ScrollExecutedEvent) -> Result<()> {
        let data = serde_json::to_value(&event)
            .context("failed to serialise ScrollExecutedEvent")?;
        self.track_custom("scroll_executed", data)
    }

    /// Record a *search* event.
    pub fn track_search(&self, event: SearchEvent) -> Result<()> {
        let data = serde_json::to_value(&event)
            .context("failed to serialise SearchEvent")?;
        self.track_custom("search", data)
    }

    /// Record an *auto-forge* event.
    pub fn track_auto_forge(&self, event: AutoForgeEvent) -> Result<()> {
        let data = serde_json::to_value(&event)
            .context("failed to serialise AutoForgeEvent")?;
        self.track_custom("auto_forge", data)
    }

    /// Record an arbitrary custom event.
    pub fn track_custom(&self, event_type: &str, data: Value) -> Result<()> {
        if !self.enabled {
            return Ok(());
        }

        let event = self.build_event(event_type, data);
        self.record_event(event)
    }

    // -- Internal: event construction & persistence -------------------------

    /// Build a fully-populated `TelemetryEvent` from an event type and payload.
    fn build_event(&self, event_type: &str, data: Value) -> TelemetryEvent {
        TelemetryEvent {
            user_id: self.user_id.clone(),
            event_type: event_type.to_string(),
            data,
            timestamp: chrono::Utc::now().timestamp(),
            version: self.version.clone(),
            platform: std::env::consts::OS.to_string(),
            arch: std::env::consts::ARCH.to_string(),
        }
    }

    /// Append a single `TelemetryEvent` as a JSON line to the events file.
    /// Rotates the file when it exceeds `MAX_EVENTS_FILE_SIZE`.
    fn record_event(&self, event: TelemetryEvent) -> Result<()> {
        // Rotate if the events file exceeds the size limit.
        self.maybe_rotate();

        let mut line = serde_json::to_string(&event)
            .context("failed to serialise TelemetryEvent")?;
        line.push('\n');

        let mut file = OpenOptions::new()
            .append(true)
            .create(true)
            .open(&self.events_file)
            .with_context(|| {
                format!(
                    "failed to open events file for appending: {}",
                    self.events_file.display()
                )
            })?;

        file.write_all(line.as_bytes())
            .context("failed to write event to events file")?;

        Ok(())
    }

    /// Rotate the events file if it exceeds the size limit.
    /// Archives are named `.1`, `.2`, etc. (`.1` = most recent).
    fn maybe_rotate(&self) {
        let size = fs::metadata(&self.events_file)
            .map(|m| m.len())
            .unwrap_or(0);

        if size < MAX_EVENTS_FILE_SIZE {
            return;
        }

        // Shift existing archives: .2 → .3, .1 → .2
        for i in (1..MAX_ROTATED_FILES).rev() {
            let from = format!("{}.{}", self.events_file.display(), i);
            let to = format!("{}.{}", self.events_file.display(), i + 1);
            let _ = fs::rename(&from, &to);
        }

        // Delete the oldest archive if it exceeds the limit.
        let oldest = format!("{}.{}", self.events_file.display(), MAX_ROTATED_FILES + 1);
        let _ = fs::remove_file(&oldest);

        // Move current file to .1
        let archive = format!("{}.1", self.events_file.display());
        let _ = fs::rename(&self.events_file, &archive);
    }

    // -- Reading & querying -------------------------------------------------

    /// Read **all** events from the JSONL file.
    ///
    /// Returns an empty `Vec` when the file does not exist yet.
    pub fn get_events(&self) -> Result<Vec<TelemetryEvent>> {
        if !self.events_file.exists() {
            return Ok(Vec::new());
        }

        let content = fs::read_to_string(&self.events_file)
            .with_context(|| {
                format!(
                    "failed to read events file: {}",
                    self.events_file.display()
                )
            })?;

        let mut events = Vec::new();
        for line in content.lines() {
            let trimmed = line.trim();
            if trimmed.is_empty() {
                continue;
            }
            let event: TelemetryEvent = serde_json::from_str(trimmed)
                .with_context(|| format!("failed to parse event line: {}", trimmed))?;
            events.push(event);
        }

        Ok(events)
    }

    /// Return only events whose `timestamp` is >= `since_timestamp`.
    pub fn get_events_since(&self, since_timestamp: i64) -> Result<Vec<TelemetryEvent>> {
        let all = self.get_events()?;
        Ok(all
            .into_iter()
            .filter(|e| e.timestamp >= since_timestamp)
            .collect())
    }

    // -- Aggregate statistics -----------------------------------------------

    /// Compute aggregate statistics from all stored events.
    pub fn get_statistics(&self) -> Result<TelemetryStats> {
        let events = self.get_events()?;

        let total_events = events.len();
        let mut scrolls_created: usize = 0;
        let mut scrolls_executed: usize = 0;
        let mut searches_performed: usize = 0;
        let mut auto_forge_runs: usize = 0;
        let mut search_latency_sum: u64 = 0;
        let mut search_latency_count: usize = 0;
        let mut earliest_event: Option<i64> = None;
        let mut latest_event: Option<i64> = None;

        for event in &events {
            // Track earliest / latest
            match earliest_event {
                None => earliest_event = Some(event.timestamp),
                Some(e) if event.timestamp < e => earliest_event = Some(event.timestamp),
                _ => {}
            }
            match latest_event {
                None => latest_event = Some(event.timestamp),
                Some(l) if event.timestamp > l => latest_event = Some(event.timestamp),
                _ => {}
            }

            // Count by type
            match event.event_type.as_str() {
                "scroll_created" => scrolls_created += 1,
                "scroll_executed" => scrolls_executed += 1,
                "search" => {
                    searches_performed += 1;
                    if let Some(latency) = event.data.get("latency_ms").and_then(|v| v.as_u64()) {
                        search_latency_sum += latency;
                        search_latency_count += 1;
                    }
                }
                "auto_forge" => auto_forge_runs += 1,
                _ => {}
            }
        }

        let avg_search_latency_ms = if search_latency_count > 0 {
            search_latency_sum as f64 / search_latency_count as f64
        } else {
            0.0
        };

        Ok(TelemetryStats {
            total_events,
            scrolls_created,
            scrolls_executed,
            searches_performed,
            auto_forge_runs,
            avg_search_latency_ms,
            earliest_event,
            latest_event,
        })
    }

    // -- Maintenance --------------------------------------------------------

    /// Truncate (clear) the events file.
    pub fn clear_events(&self) -> Result<()> {
        // Truncate by opening with create + truncate (default write mode).
        OpenOptions::new()
            .write(true)
            .create(true)
            .truncate(true)
            .open(&self.events_file)
            .with_context(|| {
                format!(
                    "failed to truncate events file: {}",
                    self.events_file.display()
                )
            })?;

        Ok(())
    }

    /// Export all events to `path` and return the number of events written.
    pub fn export_events(&self, path: &Path) -> Result<usize> {
        let events = self.get_events()?;
        let count = events.len();

        let mut file = OpenOptions::new()
            .write(true)
            .create(true)
            .truncate(true)
            .open(path)
            .with_context(|| {
                format!("failed to open export file: {}", path.display())
            })?;

        for event in &events {
            let mut line = serde_json::to_string(event)
                .context("failed to serialise event for export")?;
            line.push('\n');
            file.write_all(line.as_bytes())
                .context("failed to write event to export file")?;
        }

        Ok(count)
    }

    // -- Accessors (useful in tests & diagnostics) --------------------------

    /// The anonymous user ID for this tracker instance.
    pub fn user_id(&self) -> &str {
        &self.user_id
    }

    /// The path to the backing JSONL events file.
    pub fn events_file(&self) -> &Path {
        &self.events_file
    }
}

// ===========================================================================
// Tests
// ===========================================================================

#[cfg(test)]
mod tests {
    use super::*;
    use tempfile::TempDir;

    /// Helper: create a tracker backed by a fresh temp directory.
    fn make_tracker() -> (TelemetryTracker, TempDir) {
        let dir = TempDir::new().expect("failed to create temp dir");
        let tracker = TelemetryTracker::new(dir.path()).expect("failed to create tracker");
        (tracker, dir)
    }

    // -- Construction & identity persistence --------------------------------

    #[test]
    fn test_new_creates_user_id_file() {
        let (tracker, dir) = make_tracker();
        let id_path = dir.path().join("telemetry_id");
        assert!(id_path.exists(), "telemetry_id file should be created");

        let stored = fs::read_to_string(&id_path).unwrap();
        assert_eq!(stored.trim(), tracker.user_id());
        // Should look like a UUID v4
        assert_eq!(tracker.user_id().len(), 36);
    }

    #[test]
    fn test_user_id_persisted_across_instances() {
        let dir = TempDir::new().unwrap();
        let id1 = {
            let t = TelemetryTracker::new(dir.path()).unwrap();
            t.user_id().to_string()
        };
        let id2 = {
            let t = TelemetryTracker::new(dir.path()).unwrap();
            t.user_id().to_string()
        };
        assert_eq!(id1, id2, "user_id must be stable across restarts");
    }

    #[test]
    fn test_regenerates_empty_id_file() {
        let dir = TempDir::new().unwrap();
        let id_path = dir.path().join("telemetry_id");
        fs::write(&id_path, "   ").unwrap(); // blank file
        let tracker = TelemetryTracker::new(dir.path()).unwrap();
        assert_eq!(tracker.user_id().len(), 36, "should have generated a new UUID");
    }

    // -- Enable / disable ---------------------------------------------------

    #[test]
    fn test_enabled_by_default() {
        let (tracker, _dir) = make_tracker();
        assert!(tracker.is_enabled());
    }

    #[test]
    fn test_disable_prevents_recording() {
        let (mut tracker, _dir) = make_tracker();
        tracker.set_enabled(false);
        assert!(!tracker.is_enabled());

        tracker
            .track_custom("test_event", serde_json::json!({"key": "value"}))
            .unwrap();

        let events = tracker.get_events().unwrap();
        assert!(events.is_empty(), "no events should be recorded when disabled");
    }

    // -- Recording & reading ------------------------------------------------

    #[test]
    fn test_track_custom_event() {
        let (tracker, _dir) = make_tracker();
        tracker
            .track_custom("my_event", serde_json::json!({"foo": 42}))
            .unwrap();

        let events = tracker.get_events().unwrap();
        assert_eq!(events.len(), 1);
        assert_eq!(events[0].event_type, "my_event");
        assert_eq!(events[0].data["foo"], 42);
        assert_eq!(events[0].user_id, tracker.user_id());
        assert!(!events[0].platform.is_empty());
        assert!(!events[0].arch.is_empty());
    }

    #[test]
    fn test_track_scroll_created() {
        let (tracker, _dir) = make_tracker();
        tracker
            .track_scroll_created(ScrollCreatedEvent {
                scroll_id: "scroll-abc".into(),
                execution_mode: "parallel".into(),
                task_count: 5,
            })
            .unwrap();

        let events = tracker.get_events().unwrap();
        assert_eq!(events.len(), 1);
        assert_eq!(events[0].event_type, "scroll_created");
        assert_eq!(events[0].data["scroll_id"], "scroll-abc");
        assert_eq!(events[0].data["execution_mode"], "parallel");
        assert_eq!(events[0].data["task_count"], 5);
    }

    #[test]
    fn test_track_scroll_executed() {
        let (tracker, _dir) = make_tracker();
        tracker
            .track_scroll_executed(ScrollExecutedEvent {
                scroll_id: "scroll-xyz".into(),
                duration_secs: 120,
                tasks_completed: 8,
                tasks_failed: 2,
            })
            .unwrap();

        let events = tracker.get_events().unwrap();
        assert_eq!(events.len(), 1);
        assert_eq!(events[0].event_type, "scroll_executed");
        assert_eq!(events[0].data["duration_secs"], 120);
        assert_eq!(events[0].data["tasks_completed"], 8);
        assert_eq!(events[0].data["tasks_failed"], 2);
    }

    #[test]
    fn test_track_search() {
        let (tracker, _dir) = make_tracker();
        tracker
            .track_search(SearchEvent {
                query_type: "code".into(),
                strategy: "semantic".into(),
                latency_ms: 45,
                result_count: 12,
                tier_used: "tier2".into(),
            })
            .unwrap();

        let events = tracker.get_events().unwrap();
        assert_eq!(events.len(), 1);
        assert_eq!(events[0].event_type, "search");
        assert_eq!(events[0].data["latency_ms"], 45);
        assert_eq!(events[0].data["tier_used"], "tier2");
    }

    #[test]
    fn test_track_auto_forge() {
        let (tracker, _dir) = make_tracker();
        tracker
            .track_auto_forge(AutoForgeEvent {
                scroll_id: "scroll-forge-1".into(),
                duration_secs: 300,
                test_count: 20,
                test_pass_rate: 0.95,
                user_decision: "accept".into(),
            })
            .unwrap();

        let events = tracker.get_events().unwrap();
        assert_eq!(events.len(), 1);
        assert_eq!(events[0].event_type, "auto_forge");
        assert_eq!(events[0].data["test_pass_rate"], 0.95);
        assert_eq!(events[0].data["user_decision"], "accept");
    }

    #[test]
    fn test_multiple_events_append() {
        let (tracker, _dir) = make_tracker();

        for i in 0..5 {
            tracker
                .track_custom("event", serde_json::json!({"i": i}))
                .unwrap();
        }

        let events = tracker.get_events().unwrap();
        assert_eq!(events.len(), 5);
        for (idx, event) in events.iter().enumerate() {
            assert_eq!(event.data["i"], idx);
        }
    }

    // -- Filtering ----------------------------------------------------------

    #[test]
    fn test_get_events_since() {
        let (tracker, _dir) = make_tracker();

        // Manually insert events with known timestamps
        let now = chrono::Utc::now().timestamp();

        for offset in &[-100i64, -50, 0, 50, 100] {
            let event = TelemetryEvent {
                user_id: tracker.user_id().to_string(),
                event_type: "tick".into(),
                data: serde_json::json!({"offset": offset}),
                timestamp: now + offset,
                version: "0.1.0".into(),
                platform: std::env::consts::OS.into(),
                arch: std::env::consts::ARCH.into(),
            };
            tracker.record_event(event).unwrap();
        }

        let recent = tracker.get_events_since(now).unwrap();
        assert_eq!(recent.len(), 3, "should include offset 0, 50, 100");
        for e in &recent {
            assert!(e.timestamp >= now);
        }
    }

    // -- Statistics ---------------------------------------------------------

    #[test]
    fn test_get_statistics() {
        let (tracker, _dir) = make_tracker();

        // Seed a mix of events
        tracker
            .track_scroll_created(ScrollCreatedEvent {
                scroll_id: "s1".into(),
                execution_mode: "sequential".into(),
                task_count: 3,
            })
            .unwrap();

        tracker
            .track_scroll_created(ScrollCreatedEvent {
                scroll_id: "s2".into(),
                execution_mode: "parallel".into(),
                task_count: 7,
            })
            .unwrap();

        tracker
            .track_scroll_executed(ScrollExecutedEvent {
                scroll_id: "s1".into(),
                duration_secs: 10,
                tasks_completed: 3,
                tasks_failed: 0,
            })
            .unwrap();

        tracker
            .track_search(SearchEvent {
                query_type: "code".into(),
                strategy: "lexical".into(),
                latency_ms: 20,
                result_count: 5,
                tier_used: "tier1".into(),
            })
            .unwrap();

        tracker
            .track_search(SearchEvent {
                query_type: "docs".into(),
                strategy: "semantic".into(),
                latency_ms: 80,
                result_count: 3,
                tier_used: "tier2".into(),
            })
            .unwrap();

        tracker
            .track_auto_forge(AutoForgeEvent {
                scroll_id: "s2".into(),
                duration_secs: 200,
                test_count: 10,
                test_pass_rate: 1.0,
                user_decision: "accept".into(),
            })
            .unwrap();

        let stats = tracker.get_statistics().unwrap();
        assert_eq!(stats.total_events, 6);
        assert_eq!(stats.scrolls_created, 2);
        assert_eq!(stats.scrolls_executed, 1);
        assert_eq!(stats.searches_performed, 2);
        assert_eq!(stats.auto_forge_runs, 1);
        assert!(
            (stats.avg_search_latency_ms - 50.0).abs() < f64::EPSILON,
            "avg latency should be (20+80)/2 = 50"
        );
        assert!(stats.earliest_event.is_some());
        assert!(stats.latest_event.is_some());
    }

    #[test]
    fn test_statistics_empty() {
        let (tracker, _dir) = make_tracker();
        let stats = tracker.get_statistics().unwrap();
        assert_eq!(stats.total_events, 0);
        assert_eq!(stats.avg_search_latency_ms, 0.0);
        assert!(stats.earliest_event.is_none());
        assert!(stats.latest_event.is_none());
    }

    // -- Maintenance --------------------------------------------------------

    #[test]
    fn test_clear_events() {
        let (tracker, _dir) = make_tracker();
        tracker
            .track_custom("a", serde_json::json!({}))
            .unwrap();
        tracker
            .track_custom("b", serde_json::json!({}))
            .unwrap();

        assert_eq!(tracker.get_events().unwrap().len(), 2);

        tracker.clear_events().unwrap();
        assert_eq!(tracker.get_events().unwrap().len(), 0);
    }

    #[test]
    fn test_export_events() {
        let (tracker, dir) = make_tracker();
        tracker
            .track_custom("x", serde_json::json!({"val": 1}))
            .unwrap();
        tracker
            .track_custom("y", serde_json::json!({"val": 2}))
            .unwrap();

        let export_path = dir.path().join("exported.jsonl");
        let count = tracker.export_events(&export_path).unwrap();
        assert_eq!(count, 2);

        // Verify the exported file is valid JSONL
        let content = fs::read_to_string(&export_path).unwrap();
        let lines: Vec<&str> = content.lines().filter(|l| !l.trim().is_empty()).collect();
        assert_eq!(lines.len(), 2);

        let first: TelemetryEvent = serde_json::from_str(lines[0]).unwrap();
        assert_eq!(first.event_type, "x");
        let second: TelemetryEvent = serde_json::from_str(lines[1]).unwrap();
        assert_eq!(second.event_type, "y");
    }

    #[test]
    fn test_export_empty() {
        let (tracker, dir) = make_tracker();
        let export_path = dir.path().join("empty_export.jsonl");
        let count = tracker.export_events(&export_path).unwrap();
        assert_eq!(count, 0);
        assert!(export_path.exists());
    }

    // -- Edge cases ---------------------------------------------------------

    #[test]
    fn test_get_events_no_file() {
        let dir = TempDir::new().unwrap();
        let tracker = TelemetryTracker::new(dir.path()).unwrap();
        // events file doesn't exist yet
        let events = tracker.get_events().unwrap();
        assert!(events.is_empty());
    }

    #[test]
    fn test_events_file_path() {
        let dir = TempDir::new().unwrap();
        let tracker = TelemetryTracker::new(dir.path()).unwrap();
        assert_eq!(
            tracker.events_file(),
            dir.path().join("telemetry.jsonl")
        );
    }

    #[test]
    fn test_event_contains_platform_info() {
        let (tracker, _dir) = make_tracker();
        tracker
            .track_custom("probe", serde_json::json!({}))
            .unwrap();

        let events = tracker.get_events().unwrap();
        assert_eq!(events[0].platform, std::env::consts::OS);
        assert_eq!(events[0].arch, std::env::consts::ARCH);
    }

    #[test]
    fn test_event_timestamp_is_recent() {
        let before = chrono::Utc::now().timestamp();
        let (tracker, _dir) = make_tracker();
        tracker
            .track_custom("time_check", serde_json::json!({}))
            .unwrap();
        let after = chrono::Utc::now().timestamp();

        let events = tracker.get_events().unwrap();
        let ts = events[0].timestamp;
        assert!(ts >= before && ts <= after, "timestamp should be ~now");
    }

    #[test]
    fn test_re_enable_after_disable() {
        let (mut tracker, _dir) = make_tracker();

        tracker.set_enabled(false);
        tracker.track_custom("ignored", serde_json::json!({})).unwrap();
        assert_eq!(tracker.get_events().unwrap().len(), 0);

        tracker.set_enabled(true);
        tracker.track_custom("recorded", serde_json::json!({})).unwrap();
        assert_eq!(tracker.get_events().unwrap().len(), 1);
    }
}
