"""Tests for the metrics module."""

from __future__ import annotations

import json
import tempfile
import threading
import time
from datetime import datetime
from pathlib import Path
from unittest.mock import MagicMock, patch

import pytest

from gaol.metrics.alerts import AlertManager
from gaol.metrics.dashboard import GrafanaDashboardGenerator, generate_grafana_dashboard
from gaol.metrics.exceptions import (
    AlertRuleExistsError,
    AlertRuleNotFoundError,
    MetricsServerAlreadyRunningError,
    MetricsServerNotRunningError,
)
from gaol.metrics.exporter import PrometheusExporter
from gaol.metrics.models import (
    AlertRule,
    AlertSeverity,
    AlertState,
    ContainerMetrics,
    MetricsServerConfig,
    MetricsSnapshot,
    MetricType,
)
from gaol.metrics.server import MetricsServer
from gaol.metrics.store import MetricsStore


# -------------------------------------------------------------------------
# Model tests
# -------------------------------------------------------------------------


class TestMetricType:
    """Tests for MetricType enum."""

    def test_all_metric_types(self) -> None:
        """Test all metric types exist."""
        assert MetricType.CPU.value == "cpu"
        assert MetricType.MEMORY.value == "memory"
        assert MetricType.MEMORY_PERCENT.value == "memory_percent"
        assert MetricType.NETWORK_RX.value == "network_rx"
        assert MetricType.NETWORK_TX.value == "network_tx"
        assert MetricType.BLOCK_READ.value == "block_read"
        assert MetricType.BLOCK_WRITE.value == "block_write"
        assert MetricType.PIDS.value == "pids"


class TestAlertSeverity:
    """Tests for AlertSeverity enum."""

    def test_all_severities(self) -> None:
        """Test all severity levels exist."""
        assert AlertSeverity.INFO.value == "info"
        assert AlertSeverity.WARNING.value == "warning"
        assert AlertSeverity.CRITICAL.value == "critical"


class TestAlertRule:
    """Tests for AlertRule model."""

    def test_create_rule(self) -> None:
        """Test creating an alert rule."""
        rule = AlertRule(
            name="high-cpu",
            container_pattern="*",
            metric=MetricType.CPU,
            threshold=90.0,
            comparison="gt",
            severity=AlertSeverity.WARNING,
        )
        assert rule.name == "high-cpu"
        assert rule.container_pattern == "*"
        assert rule.metric == MetricType.CPU
        assert rule.threshold == 90.0
        assert rule.comparison == "gt"
        assert rule.severity == AlertSeverity.WARNING
        assert rule.enabled is True

    def test_evaluate_gt(self) -> None:
        """Test greater than comparison."""
        rule = AlertRule(
            name="test", metric=MetricType.CPU, threshold=50.0, comparison="gt"
        )
        assert rule.evaluate(51.0) is True
        assert rule.evaluate(50.0) is False
        assert rule.evaluate(49.0) is False

    def test_evaluate_lt(self) -> None:
        """Test less than comparison."""
        rule = AlertRule(
            name="test", metric=MetricType.CPU, threshold=50.0, comparison="lt"
        )
        assert rule.evaluate(49.0) is True
        assert rule.evaluate(50.0) is False
        assert rule.evaluate(51.0) is False

    def test_evaluate_gte(self) -> None:
        """Test greater than or equal comparison."""
        rule = AlertRule(
            name="test", metric=MetricType.CPU, threshold=50.0, comparison="gte"
        )
        assert rule.evaluate(51.0) is True
        assert rule.evaluate(50.0) is True
        assert rule.evaluate(49.0) is False

    def test_evaluate_lte(self) -> None:
        """Test less than or equal comparison."""
        rule = AlertRule(
            name="test", metric=MetricType.CPU, threshold=50.0, comparison="lte"
        )
        assert rule.evaluate(49.0) is True
        assert rule.evaluate(50.0) is True
        assert rule.evaluate(51.0) is False

    def test_format_message(self) -> None:
        """Test alert message formatting."""
        rule = AlertRule(
            name="high-cpu",
            metric=MetricType.CPU,
            threshold=90.0,
            comparison="gt",
            severity=AlertSeverity.WARNING,
        )
        message = rule.format_message("mycontainer", 95.5)
        assert "[WARNING]" in message
        assert "high-cpu" in message
        assert "mycontainer" in message
        assert "95.50" in message
        assert "> 90" in message


class TestContainerMetrics:
    """Tests for ContainerMetrics model."""

    def test_create_metrics(self) -> None:
        """Test creating container metrics."""
        metrics = ContainerMetrics(
            container_name="test",
            runtime="docker",
            status="running",
            cpu_percent=50.0,
            memory_used_bytes=1024 * 1024 * 100,
            pids=10,
        )
        assert metrics.container_name == "test"
        assert metrics.runtime == "docker"
        assert metrics.cpu_percent == 50.0
        assert metrics.pids == 10

    def test_get_metric_value(self) -> None:
        """Test getting specific metric values."""
        metrics = ContainerMetrics(
            container_name="test",
            runtime="docker",
            status="running",
            cpu_percent=50.0,
            memory_used_bytes=1024 * 1024 * 100,
            memory_percent=25.0,
            network_rx_bytes=1000,
            network_tx_bytes=2000,
            block_read_bytes=3000,
            block_write_bytes=4000,
            pids=10,
        )
        assert metrics.get_metric_value(MetricType.CPU) == 50.0
        assert metrics.get_metric_value(MetricType.MEMORY) == 1024 * 1024 * 100
        assert metrics.get_metric_value(MetricType.MEMORY_PERCENT) == 25.0
        assert metrics.get_metric_value(MetricType.NETWORK_RX) == 1000
        assert metrics.get_metric_value(MetricType.NETWORK_TX) == 2000
        assert metrics.get_metric_value(MetricType.BLOCK_READ) == 3000
        assert metrics.get_metric_value(MetricType.BLOCK_WRITE) == 4000
        assert metrics.get_metric_value(MetricType.PIDS) == 10


class TestMetricsSnapshot:
    """Tests for MetricsSnapshot model."""

    def test_empty_snapshot(self) -> None:
        """Test creating empty snapshot."""
        snapshot = MetricsSnapshot()
        assert snapshot.container_count == 0
        assert snapshot.get_container("test") is None

    def test_snapshot_with_containers(self) -> None:
        """Test snapshot with containers."""
        metrics1 = ContainerMetrics(
            container_name="container1", runtime="docker", status="running"
        )
        metrics2 = ContainerMetrics(
            container_name="container2", runtime="nspawn", status="running"
        )
        snapshot = MetricsSnapshot(containers=[metrics1, metrics2])

        assert snapshot.container_count == 2
        assert snapshot.get_container("container1") is metrics1
        assert snapshot.get_container("container2") is metrics2
        assert snapshot.get_container("container3") is None


class TestMetricsServerConfig:
    """Tests for MetricsServerConfig model."""

    def test_default_config(self) -> None:
        """Test default configuration."""
        config = MetricsServerConfig()
        assert config.host == "127.0.0.1"
        assert config.port == 9100
        assert config.refresh_interval == 15.0
        assert config.containers is None
        assert config.enabled is True

    def test_custom_config(self) -> None:
        """Test custom configuration."""
        config = MetricsServerConfig(
            host="0.0.0.0",
            port=9200,
            refresh_interval=30.0,
            containers=["web", "db"],
        )
        assert config.host == "0.0.0.0"
        assert config.port == 9200
        assert config.refresh_interval == 30.0
        assert config.containers == ["web", "db"]


# -------------------------------------------------------------------------
# Store tests
# -------------------------------------------------------------------------


class TestMetricsStore:
    """Tests for MetricsStore."""

    @pytest.fixture
    def store(self, tmp_path: Path) -> MetricsStore:
        """Create a temporary store."""
        return MetricsStore(store_dir=tmp_path / "metrics")

    def test_save_and_get_config(self, store: MetricsStore) -> None:
        """Test saving and retrieving configuration."""
        config = MetricsServerConfig(host="0.0.0.0", port=9200)
        store.save_config(config)

        loaded = store.get_config()
        assert loaded.host == "0.0.0.0"
        assert loaded.port == 9200

    def test_get_default_config(self, store: MetricsStore) -> None:
        """Test getting default config when none saved."""
        config = store.get_config()
        assert config.host == "127.0.0.1"
        assert config.port == 9100

    def test_delete_config(self, store: MetricsStore) -> None:
        """Test deleting configuration."""
        config = MetricsServerConfig(host="0.0.0.0", port=9200)
        store.save_config(config)
        assert store.delete_config() is True
        assert store.delete_config() is False

    def test_add_and_get_rule(self, store: MetricsStore) -> None:
        """Test adding and retrieving alert rules."""
        rule = AlertRule(
            name="high-cpu",
            metric=MetricType.CPU,
            threshold=90.0,
        )
        store.add_rule(rule)

        loaded = store.get_rule("high-cpu")
        assert loaded is not None
        assert loaded.name == "high-cpu"
        assert loaded.metric == MetricType.CPU
        assert loaded.threshold == 90.0

    def test_add_duplicate_rule(self, store: MetricsStore) -> None:
        """Test adding duplicate rule raises error."""
        rule = AlertRule(name="test", metric=MetricType.CPU, threshold=90.0)
        store.add_rule(rule)

        with pytest.raises(AlertRuleExistsError):
            store.add_rule(rule)

    def test_update_rule(self, store: MetricsStore) -> None:
        """Test updating an existing rule."""
        rule = AlertRule(name="test", metric=MetricType.CPU, threshold=90.0)
        store.add_rule(rule)

        rule.threshold = 80.0
        store.update_rule(rule)

        loaded = store.get_rule("test")
        assert loaded is not None
        assert loaded.threshold == 80.0

    def test_update_nonexistent_rule(self, store: MetricsStore) -> None:
        """Test updating nonexistent rule raises error."""
        rule = AlertRule(name="test", metric=MetricType.CPU, threshold=90.0)

        with pytest.raises(AlertRuleNotFoundError):
            store.update_rule(rule)

    def test_delete_rule(self, store: MetricsStore) -> None:
        """Test deleting a rule."""
        rule = AlertRule(name="test", metric=MetricType.CPU, threshold=90.0)
        store.add_rule(rule)

        assert store.delete_rule("test") is True
        assert store.delete_rule("test") is False
        assert store.get_rule("test") is None

    def test_list_rules(self, store: MetricsStore) -> None:
        """Test listing all rules."""
        rule1 = AlertRule(name="rule1", metric=MetricType.CPU, threshold=90.0)
        rule2 = AlertRule(name="rule2", metric=MetricType.MEMORY, threshold=1e9)
        store.add_rule(rule1)
        store.add_rule(rule2)

        rules = store.get_rules()
        assert len(rules) == 2
        names = {r.name for r in rules}
        assert names == {"rule1", "rule2"}

    def test_enable_disable_rule(self, store: MetricsStore) -> None:
        """Test enabling and disabling rules."""
        rule = AlertRule(name="test", metric=MetricType.CPU, threshold=90.0)
        store.add_rule(rule)

        store.disable_rule("test")
        loaded = store.get_rule("test")
        assert loaded is not None
        assert loaded.enabled is False

        store.enable_rule("test")
        loaded = store.get_rule("test")
        assert loaded is not None
        assert loaded.enabled is True

    def test_get_enabled_rules(self, store: MetricsStore) -> None:
        """Test getting only enabled rules."""
        rule1 = AlertRule(name="enabled", metric=MetricType.CPU, threshold=90.0)
        rule2 = AlertRule(
            name="disabled", metric=MetricType.MEMORY, threshold=1e9, enabled=False
        )
        store.add_rule(rule1)
        store.add_rule(rule2)

        enabled = store.get_enabled_rules()
        assert len(enabled) == 1
        assert enabled[0].name == "enabled"


# -------------------------------------------------------------------------
# Alert Manager tests
# -------------------------------------------------------------------------


class TestAlertManager:
    """Tests for AlertManager."""

    @pytest.fixture
    def manager(self, tmp_path: Path) -> AlertManager:
        """Create a manager with temporary store."""
        store = MetricsStore(store_dir=tmp_path / "metrics")
        return AlertManager(store=store)

    def test_add_rule(self, manager: AlertManager) -> None:
        """Test adding an alert rule."""
        rule = AlertRule(name="test", metric=MetricType.CPU, threshold=90.0)
        manager.add_rule(rule)

        loaded = manager.get_rule("test")
        assert loaded.name == "test"

    def test_delete_rule(self, manager: AlertManager) -> None:
        """Test deleting a rule."""
        rule = AlertRule(name="test", metric=MetricType.CPU, threshold=90.0)
        manager.add_rule(rule)

        assert manager.delete_rule("test") is True
        assert manager.delete_rule("test") is False

    def test_evaluate_firing_alert(self, manager: AlertManager) -> None:
        """Test evaluating alerts that should fire."""
        rule = AlertRule(
            name="high-cpu",
            metric=MetricType.CPU,
            threshold=90.0,
            comparison="gt",
        )
        manager.add_rule(rule)

        metrics = ContainerMetrics(
            container_name="test",
            runtime="docker",
            status="running",
            cpu_percent=95.0,
        )
        snapshot = MetricsSnapshot(containers=[metrics])

        firing = manager.evaluate(snapshot)
        assert len(firing) == 1
        assert firing[0].rule_name == "high-cpu"
        assert firing[0].container_name == "test"
        assert firing[0].firing is True

    def test_evaluate_non_firing_alert(self, manager: AlertManager) -> None:
        """Test evaluating alerts that should not fire."""
        rule = AlertRule(
            name="high-cpu",
            metric=MetricType.CPU,
            threshold=90.0,
            comparison="gt",
        )
        manager.add_rule(rule)

        metrics = ContainerMetrics(
            container_name="test",
            runtime="docker",
            status="running",
            cpu_percent=50.0,
        )
        snapshot = MetricsSnapshot(containers=[metrics])

        firing = manager.evaluate(snapshot)
        assert len(firing) == 0

    def test_evaluate_container_pattern(self, manager: AlertManager) -> None:
        """Test alert rule container pattern matching."""
        rule = AlertRule(
            name="web-cpu",
            container_pattern="web-*",
            metric=MetricType.CPU,
            threshold=90.0,
        )
        manager.add_rule(rule)

        web_metrics = ContainerMetrics(
            container_name="web-1",
            runtime="docker",
            status="running",
            cpu_percent=95.0,
        )
        db_metrics = ContainerMetrics(
            container_name="db-1",
            runtime="docker",
            status="running",
            cpu_percent=95.0,
        )
        snapshot = MetricsSnapshot(containers=[web_metrics, db_metrics])

        firing = manager.evaluate(snapshot)
        assert len(firing) == 1
        assert firing[0].container_name == "web-1"

    def test_evaluate_duration(self, manager: AlertManager) -> None:
        """Test alert duration requirement."""
        rule = AlertRule(
            name="high-cpu",
            metric=MetricType.CPU,
            threshold=90.0,
            duration_seconds=5,
        )
        manager.add_rule(rule)

        metrics = ContainerMetrics(
            container_name="test",
            runtime="docker",
            status="running",
            cpu_percent=95.0,
        )
        snapshot = MetricsSnapshot(containers=[metrics])

        # First evaluation - should not fire yet (duration not met)
        firing = manager.evaluate(snapshot)
        assert len(firing) == 0

        # Sleep to meet duration
        time.sleep(0.1)  # Use a shorter time for tests

        # Override the duration check for testing
        rule.duration_seconds = 0
        manager._store.update_rule(rule)

        firing = manager.evaluate(snapshot)
        assert len(firing) == 1

    def test_get_firing_alerts(self, manager: AlertManager) -> None:
        """Test getting currently firing alerts."""
        rule = AlertRule(
            name="high-cpu",
            metric=MetricType.CPU,
            threshold=90.0,
        )
        manager.add_rule(rule)

        metrics = ContainerMetrics(
            container_name="test",
            runtime="docker",
            status="running",
            cpu_percent=95.0,
        )
        snapshot = MetricsSnapshot(containers=[metrics])

        manager.evaluate(snapshot)
        firing = manager.get_firing_alerts()
        assert len(firing) == 1

    def test_clear_states(self, manager: AlertManager) -> None:
        """Test clearing alert states."""
        rule = AlertRule(
            name="high-cpu",
            metric=MetricType.CPU,
            threshold=90.0,
        )
        manager.add_rule(rule)

        metrics = ContainerMetrics(
            container_name="test",
            runtime="docker",
            status="running",
            cpu_percent=95.0,
        )
        snapshot = MetricsSnapshot(containers=[metrics])

        manager.evaluate(snapshot)
        assert len(manager.get_firing_alerts()) == 1

        manager.clear_states()
        assert len(manager.get_firing_alerts()) == 0


# -------------------------------------------------------------------------
# Exporter tests
# -------------------------------------------------------------------------


class TestPrometheusExporter:
    """Tests for PrometheusExporter."""

    @pytest.fixture
    def mock_container_store(self) -> MagicMock:
        """Create a mock container store."""
        store = MagicMock()
        store.list.return_value = []
        return store

    @pytest.fixture
    def exporter(self, mock_container_store: MagicMock) -> PrometheusExporter:
        """Create an exporter with mock store."""
        return PrometheusExporter(container_store=mock_container_store)

    def test_collect_empty(self, exporter: PrometheusExporter) -> None:
        """Test collecting with no containers."""
        snapshot = exporter.collect()
        assert snapshot.container_count == 0

    def test_generate_metrics_empty(self, exporter: PrometheusExporter) -> None:
        """Test generating metrics with no containers."""
        exporter.collect()
        metrics = exporter.generate_metrics_text()

        # Should have at least the up metric
        assert "gaol_up" in metrics

    def test_generate_metrics_bytes(self, exporter: PrometheusExporter) -> None:
        """Test generating metrics as bytes."""
        exporter.collect()
        metrics = exporter.generate_metrics()

        assert isinstance(metrics, bytes)
        assert b"gaol_up" in metrics


# -------------------------------------------------------------------------
# Server tests
# -------------------------------------------------------------------------


class TestMetricsServer:
    """Tests for MetricsServer."""

    @pytest.fixture
    def mock_exporter(self) -> MagicMock:
        """Create a mock exporter."""
        exporter = MagicMock(spec=PrometheusExporter)
        exporter.collect.return_value = MetricsSnapshot()
        exporter.generate_metrics.return_value = b"# metrics"
        return exporter

    @pytest.fixture
    def mock_store(self, tmp_path: Path) -> MetricsStore:
        """Create a temporary store."""
        return MetricsStore(store_dir=tmp_path / "metrics")

    def test_start_stop(self, mock_exporter: MagicMock, mock_store: MetricsStore) -> None:
        """Test starting and stopping the server."""
        config = MetricsServerConfig(host="127.0.0.1", port=19100)
        server = MetricsServer(
            config=config,
            exporter=mock_exporter,
            store=mock_store,
        )

        assert server.is_running is False

        server.start()
        assert server.is_running is True

        server.stop()
        assert server.is_running is False

    def test_start_already_running(
        self, mock_exporter: MagicMock, mock_store: MetricsStore
    ) -> None:
        """Test starting already running server raises error."""
        config = MetricsServerConfig(host="127.0.0.1", port=19101)
        server = MetricsServer(
            config=config,
            exporter=mock_exporter,
            store=mock_store,
        )

        server.start()
        try:
            with pytest.raises(MetricsServerAlreadyRunningError):
                server.start()
        finally:
            server.stop()

    def test_stop_not_running(
        self, mock_exporter: MagicMock, mock_store: MetricsStore
    ) -> None:
        """Test stopping non-running server raises error."""
        config = MetricsServerConfig(host="127.0.0.1", port=19102)
        server = MetricsServer(
            config=config,
            exporter=mock_exporter,
            store=mock_store,
        )

        with pytest.raises(MetricsServerNotRunningError):
            server.stop()

    def test_collect_once(
        self, mock_exporter: MagicMock, mock_store: MetricsStore
    ) -> None:
        """Test one-shot collection."""
        config = MetricsServerConfig(host="127.0.0.1", port=19103)
        server = MetricsServer(
            config=config,
            exporter=mock_exporter,
            store=mock_store,
        )

        server.collect_once()
        mock_exporter.collect.assert_called_once()


# -------------------------------------------------------------------------
# Dashboard tests
# -------------------------------------------------------------------------


class TestGrafanaDashboardGenerator:
    """Tests for GrafanaDashboardGenerator."""

    def test_generate_dashboard(self) -> None:
        """Test generating a dashboard."""
        generator = GrafanaDashboardGenerator()
        dashboard = generator.generate()

        # Should be valid JSON
        parsed = json.loads(dashboard)
        assert "title" in parsed
        assert "panels" in parsed
        assert parsed["title"] == "Gaol Container Metrics"

    def test_generate_with_custom_title(self) -> None:
        """Test generating with custom title."""
        generator = GrafanaDashboardGenerator(title="My Dashboard")
        dashboard = generator.generate()

        parsed = json.loads(dashboard)
        assert parsed["title"] == "My Dashboard"

    def test_generate_dict(self) -> None:
        """Test generating as dictionary."""
        generator = GrafanaDashboardGenerator()
        dashboard = generator.generate_dict()

        assert isinstance(dashboard, dict)
        assert "title" in dashboard
        assert "panels" in dashboard

    def test_generate_grafana_dashboard_function(self) -> None:
        """Test the convenience function."""
        dashboard = generate_grafana_dashboard(
            title="Test Dashboard",
            datasource="my-prometheus",
        )

        parsed = json.loads(dashboard)
        assert parsed["title"] == "Test Dashboard"

    def test_dashboard_has_variables(self) -> None:
        """Test dashboard has template variables."""
        generator = GrafanaDashboardGenerator()
        dashboard = generator.generate_dict()

        variables = dashboard["templating"]["list"]
        assert len(variables) == 2

        var_names = {v["name"] for v in variables}
        assert "container" in var_names
        assert "runtime" in var_names

    def test_dashboard_has_panels(self) -> None:
        """Test dashboard has expected panels."""
        generator = GrafanaDashboardGenerator()
        dashboard = generator.generate_dict()

        panels = dashboard["panels"]
        assert len(panels) > 0

        # Check for key panel types
        panel_types = {p.get("type") for p in panels}
        assert "stat" in panel_types
        assert "timeseries" in panel_types
        assert "table" in panel_types


# -------------------------------------------------------------------------
# Exception tests
# -------------------------------------------------------------------------


class TestExceptions:
    """Tests for exception classes."""

    def test_alert_rule_exists_error(self) -> None:
        """Test AlertRuleExistsError."""
        error = AlertRuleExistsError("my-rule")
        assert error.rule_name == "my-rule"
        assert "my-rule" in str(error)

    def test_alert_rule_not_found_error(self) -> None:
        """Test AlertRuleNotFoundError."""
        error = AlertRuleNotFoundError("my-rule")
        assert error.rule_name == "my-rule"
        assert "my-rule" in str(error)

    def test_metrics_server_already_running_error(self) -> None:
        """Test MetricsServerAlreadyRunningError."""
        error = MetricsServerAlreadyRunningError("localhost", 9100)
        assert error.host == "localhost"
        assert error.port == 9100
        assert "localhost:9100" in str(error)

    def test_metrics_server_not_running_error(self) -> None:
        """Test MetricsServerNotRunningError."""
        error = MetricsServerNotRunningError()
        assert "not running" in str(error)
