//! Prometheus metrics for ngit-grasp relay.
//!
//! This module provides comprehensive monitoring metrics including:
//! - WebSocket connection tracking (with privacy-preserving IP aggregation)
//! - Git operation metrics (clone, fetch, push)
//! - Repository bandwidth tracking (top-N only for cardinality control)
//! - Nostr event metrics
//! - Sync metrics (GRASP-02 proactive sync)
//!
//! # Privacy
//! IP addresses are NEVER exposed in metrics. The `ConnectionTracker` maintains
//! per-IP counts internally only for abuse detection. Only aggregate counts
//! are exposed to Prometheus.

pub mod bandwidth;
pub mod connection;

pub use crate::sync::SyncMetrics;

use std::sync::Arc;
use std::time::Instant;

use lazy_static::lazy_static;
use prometheus::{
    Counter, CounterVec, Encoder, Gauge, GaugeVec, Histogram, HistogramOpts, HistogramVec, Opts,
    Registry, TextEncoder,
};

use bandwidth::BandwidthTracker;
use connection::ConnectionTracker;

lazy_static! {
    /// Global Prometheus registry for ngit-grasp metrics
    pub static ref REGISTRY: Registry = Registry::new();
    static ref BLACKLIST_DELETIONS_TOTAL: CounterVec = {
        let metric = CounterVec::new(
            Opts::new(
                "ngit_blacklist_deletions_total",
                "Blacklist-triggered deletion attempts by phase and result",
            ),
            &["phase", "result"],
        )
        .expect("build blacklist deletion metric");
        REGISTRY
            .register(Box::new(metric.clone()))
            .expect("register blacklist deletion metric");
        metric
    };
    static ref BLACKLIST_STARTUP_RESTORE_TOTAL: CounterVec = {
        let metric = CounterVec::new(
            Opts::new(
                "ngit_blacklist_startup_restore_total",
                "Blacklist startup restore operations by result and reason",
            ),
            &["result", "reason"],
        )
        .expect("build blacklist startup restore metric");
        REGISTRY
            .register(Box::new(metric.clone()))
            .expect("register blacklist startup restore metric");
        metric
    };
    static ref WHITELIST_STARTUP_RESTORE_TOTAL: CounterVec = {
        let metric = CounterVec::new(
            Opts::new(
                "ngit_whitelist_startup_restore_total",
                "Whitelist startup restore operations by result and reason",
            ),
            &["result", "reason"],
        )
        .expect("build whitelist startup restore metric");
        REGISTRY
            .register(Box::new(metric.clone()))
            .expect("register whitelist startup restore metric");
        metric
    };
    static ref HOLDING_CLEANUP_RUNS_TOTAL: Counter = {
        let metric = Counter::with_opts(Opts::new(
            "ngit_holding_cleanup_runs_total",
            "Number of holding cleanup passes run",
        ))
        .expect("build holding cleanup runs metric");
        REGISTRY
            .register(Box::new(metric.clone()))
            .expect("register holding cleanup runs metric");
        metric
    };
    static ref HOLDING_CLEANUP_DELETED_TOTAL: CounterVec = {
        let metric = CounterVec::new(
            Opts::new(
                "ngit_holding_cleanup_deleted_total",
                "Holding cleanup deleted objects by type",
            ),
            &["type"],
        )
        .expect("build holding cleanup deleted metric");
        REGISTRY
            .register(Box::new(metric.clone()))
            .expect("register holding cleanup deleted metric");
        metric
    };
    static ref HOLDING_CLEANUP_LAST_RUN_DELETED: GaugeVec = {
        let metric = GaugeVec::new(
            Opts::new(
                "ngit_holding_cleanup_last_run_deleted",
                "Objects deleted in the most recent holding cleanup pass by type",
            ),
            &["type"],
        )
        .expect("build holding cleanup last-run metric");
        REGISTRY
            .register(Box::new(metric.clone()))
            .expect("register holding cleanup last-run metric");
        metric
    };
    static ref RECOVERY_TOTAL: CounterVec = {
        let metric = CounterVec::new(
            Opts::new(
                "ngit_recovery_total",
                "Recovery attempts by outcome",
            ),
            &["result"],
        )
        .expect("build recovery metric");
        REGISTRY
            .register(Box::new(metric.clone()))
            .expect("register recovery metric");
        metric
    };
    static ref MANUAL_EJECTIONS_TOTAL: Counter = {
        let metric = Counter::with_opts(Opts::new(
            "ngit_manual_ejections_total",
            "Number of manual holding ejections",
        ))
        .expect("build manual ejections metric");
        REGISTRY
            .register(Box::new(metric.clone()))
            .expect("register manual ejections metric");
        metric
    };
    static ref MANUAL_EJECTION_DELETED_TOTAL: CounterVec = {
        let metric = CounterVec::new(
            Opts::new(
                "ngit_manual_ejection_deleted_total",
                "Objects deleted by manual ejection operations",
            ),
            &["type"],
        )
        .expect("build manual ejection deleted metric");
        REGISTRY
            .register(Box::new(metric.clone()))
            .expect("register manual ejection deleted metric");
        metric
    };
}

pub fn record_blacklist_deletion_attempt(phase: &str) {
    BLACKLIST_DELETIONS_TOTAL
        .with_label_values(&[phase, "attempted"])
        .inc();
}

pub fn record_blacklist_deletion_success(phase: &str) {
    BLACKLIST_DELETIONS_TOTAL
        .with_label_values(&[phase, "succeeded"])
        .inc();
}

pub fn record_blacklist_deletion_failure(phase: &str) {
    BLACKLIST_DELETIONS_TOTAL
        .with_label_values(&[phase, "failed"])
        .inc();
}

pub fn record_blacklist_startup_restore_attempt() {
    BLACKLIST_STARTUP_RESTORE_TOTAL
        .with_label_values(&["attempted", "none"])
        .inc();
}

pub fn record_blacklist_startup_restore_success() {
    BLACKLIST_STARTUP_RESTORE_TOTAL
        .with_label_values(&["succeeded", "none"])
        .inc();
}

pub fn record_blacklist_startup_restore_failure() {
    BLACKLIST_STARTUP_RESTORE_TOTAL
        .with_label_values(&["failed", "none"])
        .inc();
}

pub fn record_blacklist_startup_restore_skipped(reason: &str) {
    BLACKLIST_STARTUP_RESTORE_TOTAL
        .with_label_values(&["skipped", reason])
        .inc();
}

pub fn record_whitelist_startup_restore_attempt() {
    WHITELIST_STARTUP_RESTORE_TOTAL
        .with_label_values(&["attempted", "none"])
        .inc();
}

pub fn record_whitelist_startup_restore_success() {
    WHITELIST_STARTUP_RESTORE_TOTAL
        .with_label_values(&["succeeded", "none"])
        .inc();
}

pub fn record_whitelist_startup_restore_failure() {
    WHITELIST_STARTUP_RESTORE_TOTAL
        .with_label_values(&["failed", "none"])
        .inc();
}

pub fn record_whitelist_startup_restore_skipped(reason: &str) {
    WHITELIST_STARTUP_RESTORE_TOTAL
        .with_label_values(&["skipped", reason])
        .inc();
}

pub fn record_holding_cleanup_run(
    metadata_deleted: usize,
    archived_events_deleted: usize,
    archive_files_deleted: usize,
) {
    HOLDING_CLEANUP_RUNS_TOTAL.inc();
    HOLDING_CLEANUP_DELETED_TOTAL
        .with_label_values(&["metadata"])
        .inc_by(metadata_deleted as f64);
    HOLDING_CLEANUP_DELETED_TOTAL
        .with_label_values(&["payload"])
        .inc_by(archived_events_deleted as f64);
    HOLDING_CLEANUP_DELETED_TOTAL
        .with_label_values(&["archive_file"])
        .inc_by(archive_files_deleted as f64);

    HOLDING_CLEANUP_LAST_RUN_DELETED
        .with_label_values(&["metadata"])
        .set(metadata_deleted as f64);
    HOLDING_CLEANUP_LAST_RUN_DELETED
        .with_label_values(&["payload"])
        .set(archived_events_deleted as f64);
    HOLDING_CLEANUP_LAST_RUN_DELETED
        .with_label_values(&["archive_file"])
        .set(archive_files_deleted as f64);
}

pub fn record_recovery_attempt() {
    RECOVERY_TOTAL.with_label_values(&["attempted"]).inc();
}

pub fn record_recovery_success() {
    RECOVERY_TOTAL.with_label_values(&["succeeded"]).inc();
}

pub fn record_recovery_failed() {
    RECOVERY_TOTAL.with_label_values(&["failed"]).inc();
}

pub fn record_recovery_partial() {
    RECOVERY_TOTAL.with_label_values(&["partial"]).inc();
}

pub fn record_manual_ejection(
    metadata_deleted: usize,
    archived_events_deleted: usize,
    archive_files_deleted: usize,
) {
    MANUAL_EJECTIONS_TOTAL.inc();
    MANUAL_EJECTION_DELETED_TOTAL
        .with_label_values(&["metadata"])
        .inc_by(metadata_deleted as f64);
    MANUAL_EJECTION_DELETED_TOTAL
        .with_label_values(&["payload"])
        .inc_by(archived_events_deleted as f64);
    MANUAL_EJECTION_DELETED_TOTAL
        .with_label_values(&["archive_file"])
        .inc_by(archive_files_deleted as f64);
}

/// Central metrics collection for ngit-grasp relay.
///
/// Thread-safe and designed for concurrent access from multiple tokio tasks.
#[derive(Clone)]
pub struct Metrics {
    inner: Arc<MetricsInner>,
}

struct MetricsInner {
    /// Connection tracking with abuse detection
    pub connection_tracker: ConnectionTracker,

    /// Repository bandwidth tracking (top-N only)
    pub bandwidth_tracker: BandwidthTracker,

    /// Sync metrics (GRASP-02 proactive sync)
    pub sync_metrics: Option<crate::sync::SyncMetrics>,

    // === WebSocket Metrics ===
    /// Total WebSocket connections since startup
    pub websocket_connections_total: Counter,
    /// Connection duration histogram
    pub websocket_connection_duration: Histogram,
    /// Messages received by type (REQ, EVENT, CLOSE)
    pub websocket_messages_received: CounterVec,
    /// Messages sent by type (EVENT, EOSE, OK, NOTICE)
    pub websocket_messages_sent: CounterVec,

    // === Git Operation Metrics ===
    /// Git operations by type and status
    pub git_operations_total: CounterVec,
    /// Git operation duration histogram
    pub git_operation_duration: HistogramVec,
    /// Total bytes transferred
    pub git_bytes_total: CounterVec,
    /// Push authorization results
    pub git_push_authorization: CounterVec,

    // === Nostr Event Metrics ===
    /// Events received by kind
    pub events_received_total: CounterVec,
    /// Events successfully stored by kind
    pub events_stored_total: CounterVec,
    /// Events rejected by kind and reason
    pub events_rejected_total: CounterVec,

    // === Repository Metrics ===
    /// Total repositories hosted (counted from disk on each metrics request)
    pub repositories_total: Gauge,
    /// Git data directory path for counting repositories on disk
    pub git_data_path: Option<String>,

    // === System Health Metrics ===
    /// Server start time for uptime calculation
    pub start_time: Instant,
    /// Build information gauge (stored to prevent unregistration from Prometheus)
    #[allow(dead_code)]
    pub build_info: GaugeVec,
}

impl Metrics {
    /// Creates a new Metrics instance and registers all metrics with Prometheus.
    ///
    /// # Arguments
    /// * `abuse_threshold` - Number of connections from a single IP before flagging as abuse
    /// * `git_data_path` - Optional path to git data directory for counting repositories
    pub fn new(abuse_threshold: u32, git_data_path: Option<String>) -> Self {
        let inner = MetricsInner::new(abuse_threshold, git_data_path);
        Self {
            inner: Arc::new(inner),
        }
    }

    /// Returns the sync metrics if registered.
    pub fn sync_metrics(&self) -> Option<&crate::sync::SyncMetrics> {
        self.inner.sync_metrics.as_ref()
    }

    /// Returns the connection tracker for WebSocket connection management.
    pub fn connection_tracker(&self) -> &ConnectionTracker {
        &self.inner.connection_tracker
    }

    /// Returns the bandwidth tracker for repository bandwidth tracking.
    pub fn bandwidth_tracker(&self) -> &BandwidthTracker {
        &self.inner.bandwidth_tracker
    }

    // === WebSocket Recording Methods ===

    /// Record a new WebSocket connection
    pub fn record_websocket_connection(&self) {
        self.inner.websocket_connections_total.inc();
    }

    /// Start timing a WebSocket connection, returns timer that records on drop
    pub fn start_connection_timer(&self) -> HistogramTimer {
        HistogramTimer::new(self.inner.websocket_connection_duration.clone())
    }

    /// Record a received WebSocket message
    pub fn record_message_received(&self, msg_type: &str) {
        self.inner
            .websocket_messages_received
            .with_label_values(&[msg_type])
            .inc();
    }

    /// Record a sent WebSocket message
    pub fn record_message_sent(&self, msg_type: &str) {
        self.inner
            .websocket_messages_sent
            .with_label_values(&[msg_type])
            .inc();
    }

    // === Git Operation Recording Methods ===

    /// Record a git operation event.
    ///
    /// Buffered handlers record completion from the synchronous request path.
    /// Streaming handlers record the final `success`/`error` from the background
    /// task that owns the Git subprocess; their synchronous `Ok(Response)` only
    /// means stream setup succeeded.
    pub fn record_git_operation(&self, operation: &str, status: &str) {
        self.inner
            .git_operations_total
            .with_label_values(&[operation, status])
            .inc();
    }

    /// Start timing a git operation, returns a timer
    pub fn start_git_operation_timer(&self, operation: &str) -> GitOperationTimer {
        GitOperationTimer::new(
            self.inner.git_operation_duration.clone(),
            operation.to_string(),
        )
    }

    /// Record bytes transferred for a git operation
    pub fn record_git_bytes(&self, direction: &str, bytes: u64) {
        self.inner
            .git_bytes_total
            .with_label_values(&[direction])
            .inc_by(bytes as f64);
    }

    /// Record a push authorization result
    pub fn record_push_authorization(&self, result: &str) {
        self.inner
            .git_push_authorization
            .with_label_values(&[result])
            .inc();
    }

    // === Nostr Event Recording Methods ===

    /// Record a received Nostr event
    pub fn record_event_received(&self, kind: u64) {
        self.inner
            .events_received_total
            .with_label_values(&[&kind.to_string()])
            .inc();
    }

    /// Record a stored Nostr event
    pub fn record_event_stored(&self, kind: u64) {
        self.inner
            .events_stored_total
            .with_label_values(&[&kind.to_string()])
            .inc();
    }

    /// Record a rejected Nostr event
    pub fn record_event_rejected(&self, kind: u64, reason: &str) {
        let kind = kind.to_string();
        self.inner
            .events_rejected_total
            .with_label_values(&[kind.as_str(), reason])
            .inc();
    }

    // === Repository Metrics ===

    /// Set the total number of repositories
    pub fn set_repositories_total(&self, count: u64) {
        self.inner.repositories_total.set(count as f64);
    }

    /// Count all git repositories on disk.
    ///
    /// This scans the git data directory for all `*.git` directories.
    ///
    /// # Arguments
    /// * `git_data_path` - Path to the git data directory (e.g., "./data/git")
    ///
    /// # Returns
    /// The number of repositories found on disk
    pub fn count_repositories_on_disk(git_data_path: &str) -> u64 {
        use std::fs;
        use std::path::Path;

        let git_dir = Path::new(git_data_path);
        if !git_dir.exists() {
            return 0;
        }

        let mut count = 0u64;
        if let Ok(entries) = fs::read_dir(git_dir) {
            for npub_entry in entries.flatten() {
                if let Ok(npub_meta) = npub_entry.metadata() {
                    if npub_meta.is_dir() {
                        // This is a npub directory, scan for *.git repos inside
                        if let Ok(repo_entries) = fs::read_dir(npub_entry.path()) {
                            for repo_entry in repo_entries.flatten() {
                                if let Some(name) = repo_entry.file_name().to_str() {
                                    if name.ends_with(".git") {
                                        if let Ok(repo_meta) = repo_entry.metadata() {
                                            if repo_meta.is_dir() {
                                                count += 1;
                                            }
                                        }
                                    }
                                }
                            }
                        }
                    }
                }
            }
        }

        count
    }

    // === Rendering ===

    /// Render all metrics in Prometheus text format.
    ///
    /// This method:
    /// 1. Refreshes the top-N bandwidth metrics if needed
    /// 2. Counts repositories on disk (if git_data_path configured)
    /// 3. Updates uptime
    /// 4. Gathers all metrics from the registry
    /// 5. Encodes them in Prometheus text format
    pub fn render(&self) -> String {
        // Refresh top-N bandwidth repos if needed
        self.inner.bandwidth_tracker.maybe_refresh_top_n();

        // Count repositories on disk and update metric
        if let Some(git_data_path) = &self.inner.git_data_path {
            let count = Self::count_repositories_on_disk(git_data_path);
            self.inner.repositories_total.set(count as f64);
        }

        // Gather and encode metrics
        let encoder = TextEncoder::new();
        let metric_families = REGISTRY.gather();
        let mut buffer = Vec::new();
        encoder.encode(&metric_families, &mut buffer).unwrap();

        // Add uptime as a comment (it's derived, not a registered metric)
        let uptime = self.inner.start_time.elapsed().as_secs();
        let mut output = String::from_utf8(buffer).unwrap();
        output.push_str(&format!(
            "\n# HELP ngit_uptime_seconds Seconds since server startup\n# TYPE ngit_uptime_seconds counter\nngit_uptime_seconds {}\n",
            uptime
        ));

        output
    }

    /// Check if the system is under high load (for sync scheduling)
    pub fn is_high_load(&self, threshold: u64) -> bool {
        self.inner.connection_tracker.active_connections() > threshold
    }
}

impl MetricsInner {
    fn new(abuse_threshold: u32, git_data_path: Option<String>) -> Self {
        // Create connection tracker
        let connection_tracker = ConnectionTracker::new(abuse_threshold, &REGISTRY);

        // Create bandwidth tracker
        let bandwidth_tracker = BandwidthTracker::new(&REGISTRY);

        // Create sync metrics (may fail if already registered in tests)
        let sync_metrics = crate::sync::SyncMetrics::register(&REGISTRY).ok();
        if sync_metrics.is_some() {
            tracing::info!("Sync metrics registered with Prometheus");
        }

        // WebSocket metrics
        let websocket_connections_total = Counter::with_opts(Opts::new(
            "ngit_websocket_connections_total",
            "Total WebSocket connections since startup",
        ))
        .unwrap();
        REGISTRY
            .register(Box::new(websocket_connections_total.clone()))
            .unwrap();

        let websocket_connection_duration = Histogram::with_opts(
            HistogramOpts::new(
                "ngit_websocket_connection_duration_seconds",
                "Duration of WebSocket connections",
            )
            .buckets(vec![1.0, 5.0, 15.0, 30.0, 60.0, 300.0, 900.0, 3600.0]),
        )
        .unwrap();
        REGISTRY
            .register(Box::new(websocket_connection_duration.clone()))
            .unwrap();

        let websocket_messages_received = CounterVec::new(
            Opts::new(
                "ngit_websocket_messages_received_total",
                "WebSocket messages received by type",
            ),
            &["type"],
        )
        .unwrap();
        REGISTRY
            .register(Box::new(websocket_messages_received.clone()))
            .unwrap();

        let websocket_messages_sent = CounterVec::new(
            Opts::new(
                "ngit_websocket_messages_sent_total",
                "WebSocket messages sent by type",
            ),
            &["type"],
        )
        .unwrap();
        REGISTRY
            .register(Box::new(websocket_messages_sent.clone()))
            .unwrap();

        // Git operation metrics
        let git_operations_total = CounterVec::new(
            Opts::new(
                "ngit_git_operations_total",
                "Git operations by type and status",
            ),
            &["operation", "status"],
        )
        .unwrap();
        REGISTRY
            .register(Box::new(git_operations_total.clone()))
            .unwrap();

        let git_operation_duration = HistogramVec::new(
            HistogramOpts::new(
                "ngit_git_operation_duration_seconds",
                "Duration of git operations",
            )
            .buckets(vec![0.1, 0.5, 1.0, 2.5, 5.0, 10.0, 30.0, 60.0]),
            &["operation"],
        )
        .unwrap();
        REGISTRY
            .register(Box::new(git_operation_duration.clone()))
            .unwrap();

        let git_bytes_total = CounterVec::new(
            Opts::new(
                "ngit_git_bytes_total",
                "Total bytes transferred for git operations",
            ),
            &["direction"],
        )
        .unwrap();
        REGISTRY
            .register(Box::new(git_bytes_total.clone()))
            .unwrap();

        let git_push_authorization = CounterVec::new(
            Opts::new(
                "ngit_git_push_authorization_total",
                "Push authorization results",
            ),
            &["result"],
        )
        .unwrap();
        REGISTRY
            .register(Box::new(git_push_authorization.clone()))
            .unwrap();

        // Nostr event metrics
        let events_received_total = CounterVec::new(
            Opts::new(
                "ngit_events_received_total",
                "Nostr events received by kind",
            ),
            &["kind"],
        )
        .unwrap();
        REGISTRY
            .register(Box::new(events_received_total.clone()))
            .unwrap();

        let events_stored_total = CounterVec::new(
            Opts::new(
                "ngit_events_stored_total",
                "Nostr events successfully stored by kind",
            ),
            &["kind"],
        )
        .unwrap();
        REGISTRY
            .register(Box::new(events_stored_total.clone()))
            .unwrap();

        let events_rejected_total = CounterVec::new(
            Opts::new(
                "ngit_events_rejected_total",
                "Nostr events rejected by kind and reason",
            ),
            &["kind", "reason"],
        )
        .unwrap();
        REGISTRY
            .register(Box::new(events_rejected_total.clone()))
            .unwrap();

        // Repository metrics
        let repositories_total = Gauge::with_opts(Opts::new(
            "ngit_repositories_total",
            "Total repositories hosted",
        ))
        .unwrap();
        REGISTRY
            .register(Box::new(repositories_total.clone()))
            .unwrap();

        // Build info
        let build_info = GaugeVec::new(
            Opts::new("ngit_build_info", "Build information"),
            &["version", "commit"],
        )
        .unwrap();
        REGISTRY.register(Box::new(build_info.clone())).unwrap();

        // Set build info gauge to 1 (it's just for labels)
        build_info
            .with_label_values(&[
                env!("CARGO_PKG_VERSION"),
                option_env!("GIT_HASH").unwrap_or("unknown"),
            ])
            .set(1.0);

        Self {
            connection_tracker,
            bandwidth_tracker,
            sync_metrics,
            websocket_connections_total,
            websocket_connection_duration,
            websocket_messages_received,
            websocket_messages_sent,
            git_operations_total,
            git_operation_duration,
            git_bytes_total,
            git_push_authorization,
            events_received_total,
            events_stored_total,
            events_rejected_total,
            repositories_total,
            git_data_path,
            start_time: Instant::now(),
            build_info,
        }
    }
}

/// Timer for tracking WebSocket connection duration.
/// Records the elapsed time when dropped.
pub struct HistogramTimer {
    histogram: Histogram,
    start: Instant,
}

impl HistogramTimer {
    fn new(histogram: Histogram) -> Self {
        Self {
            histogram,
            start: Instant::now(),
        }
    }
}

impl Drop for HistogramTimer {
    fn drop(&mut self) {
        let elapsed = self.start.elapsed().as_secs_f64();
        self.histogram.observe(elapsed);
    }
}

/// Timer for tracking Git operation duration.
/// Records the elapsed time when dropped.
pub struct GitOperationTimer {
    histogram_vec: HistogramVec,
    operation: String,
    start: Instant,
}

impl GitOperationTimer {
    fn new(histogram_vec: HistogramVec, operation: String) -> Self {
        Self {
            histogram_vec,
            operation,
            start: Instant::now(),
        }
    }
}

impl Drop for GitOperationTimer {
    fn drop(&mut self) {
        let elapsed = self.start.elapsed().as_secs_f64();
        self.histogram_vec
            .with_label_values(&[&self.operation])
            .observe(elapsed);
    }
}

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

    #[test]
    fn test_count_repositories_on_disk() {
        use std::fs;
        use tempfile::TempDir;

        // Create temporary directory structure
        let temp_dir = TempDir::new().unwrap();
        let git_data_path = temp_dir.path();

        // Initially should be 0
        let count = Metrics::count_repositories_on_disk(git_data_path.to_str().unwrap());
        assert_eq!(count, 0);

        // Create some fake repositories
        let npub1 = git_data_path.join("npub1test");
        fs::create_dir_all(&npub1).unwrap();
        fs::create_dir_all(npub1.join("repo1.git")).unwrap();
        fs::create_dir_all(npub1.join("repo2.git")).unwrap();

        let npub2 = git_data_path.join("npub2test");
        fs::create_dir_all(&npub2).unwrap();
        fs::create_dir_all(npub2.join("repo3.git")).unwrap();

        // Should count 3 repositories
        let count = Metrics::count_repositories_on_disk(git_data_path.to_str().unwrap());
        assert_eq!(count, 3);

        // Create a non-.git directory (should be ignored)
        fs::create_dir_all(npub1.join("not-a-repo")).unwrap();
        let count = Metrics::count_repositories_on_disk(git_data_path.to_str().unwrap());
        assert_eq!(count, 3);

        // Create a file with .git suffix (should be ignored, not a directory)
        fs::write(npub1.join("file.git"), "content").unwrap();
        let count = Metrics::count_repositories_on_disk(git_data_path.to_str().unwrap());
        assert_eq!(count, 3);
    }

    /// Comprehensive test for Metrics functionality including repository counting.
    ///
    /// NOTE: This test creates a Metrics instance which registers with the global
    /// Prometheus REGISTRY. Due to this global state, we cannot have multiple tests
    /// that create Metrics instances - they would conflict. Therefore, this single
    /// test covers:
    /// 1. Metrics creation and basic operations
    /// 2. Repository counting on disk via render()
    ///
    /// If additional Metrics tests are needed, they should either be added to this
    /// test or use a separate test-specific Prometheus registry.
    #[test]
    fn test_metrics_with_repository_counting() {
        use std::fs;
        use tempfile::TempDir;

        // Create temporary directory structure for repository counting
        let temp_dir = TempDir::new().unwrap();
        let git_data_path = temp_dir.path();

        // Create Metrics with git_data_path for repository counting
        let metrics = Metrics::new(10, Some(git_data_path.to_str().unwrap().to_string()));

        // Test basic metrics operations
        metrics.record_websocket_connection();
        metrics.record_message_received("REQ");
        metrics.record_message_sent("EVENT");
        metrics.record_git_operation("clone", "success");
        metrics.record_git_bytes("in", 1024);
        metrics.record_event_received(1);
        metrics.record_event_stored(1);
        metrics.record_event_rejected(1, "invalid_signature");
        metrics.set_repositories_total(5);

        // Test repository counting via render()
        // Render should count 0 repos initially (even though we set it to 5 above,
        // render() recounts from disk)
        let output = metrics.render();
        assert!(output.contains("ngit_repositories_total 0"));

        // Create some repositories
        let npub1 = git_data_path.join("npub1test");
        fs::create_dir_all(&npub1).unwrap();
        fs::create_dir_all(npub1.join("repo1.git")).unwrap();
        fs::create_dir_all(npub1.join("repo2.git")).unwrap();

        // Render should count 2 repos now
        let output = metrics.render();
        assert!(output.contains("ngit_repositories_total 2"));

        // Add another repo
        fs::create_dir_all(npub1.join("repo3.git")).unwrap();

        // Render should count 3 repos
        let output = metrics.render();
        assert!(output.contains("ngit_repositories_total 3"));
    }
}
