"""Tests for the health check reporter module."""

from __future__ import annotations

import threading
import time
from datetime import datetime, timedelta
from unittest.mock import Mock, patch

from oclawma.health import (
    DatabaseHealthCheck,
    DiskSpaceCheck,
    HealthCheck,
    HealthCheckConfig,
    HealthChecker,
    HealthCheckError,
    HealthHistory,
    HealthReporter,
    HealthStatus,
    MemoryUsageCheck,
    QueueOperationsCheck,
    SubsystemHealth,
    WebhookNotifier,
)


class TestHealthStatus:
    """Test HealthStatus enum."""

    def test_enum_values(self):
        """Test that enum values are correct."""
        assert HealthStatus.HEALTHY.value == "healthy"
        assert HealthStatus.DEGRADED.value == "degraded"
        assert HealthStatus.UNHEALTHY.value == "unhealthy"

    def test_priority_ordering(self):
        """Test that statuses can be compared by severity."""
        assert HealthStatus.UNHEALTHY > HealthStatus.DEGRADED > HealthStatus.HEALTHY


class TestSubsystemHealth:
    """Test SubsystemHealth dataclass."""

    def test_creation(self):
        """Test creating a subsystem health instance."""
        health = SubsystemHealth(
            name="test",
            status=HealthStatus.HEALTHY,
            message="All good",
            response_time_ms=10.5,
            details={"key": "value"},
        )

        assert health.name == "test"
        assert health.status == HealthStatus.HEALTHY
        assert health.message == "All good"
        assert health.response_time_ms == 10.5
        assert health.details == {"key": "value"}

    def test_to_dict(self):
        """Test conversion to dictionary."""
        health = SubsystemHealth(
            name="test",
            status=HealthStatus.HEALTHY,
            message="All good",
            response_time_ms=10.5,
        )

        result = health.to_dict()

        assert result["name"] == "test"
        assert result["status"] == "healthy"
        assert result["message"] == "All good"
        assert result["response_time_ms"] == 10.5


class TestHealthCheckConfig:
    """Test HealthCheckConfig dataclass."""

    def test_default_values(self):
        """Test default configuration values."""
        config = HealthCheckConfig()

        assert config.check_interval_seconds == 60
        assert config.history_size == 100
        assert config.disk_warning_percent == 80
        assert config.disk_critical_percent == 95
        assert config.memory_warning_percent == 80
        assert config.memory_critical_percent == 95
        assert config.queue_timeout_seconds == 5
        assert config.webhook_url is None
        assert config.webhook_timeout_seconds == 10

    def test_custom_values(self):
        """Test custom configuration values."""
        config = HealthCheckConfig(
            check_interval_seconds=30,
            history_size=50,
            disk_warning_percent=70,
            disk_critical_percent=90,
            memory_warning_percent=75,
            memory_critical_percent=90,
            queue_timeout_seconds=10,
            webhook_url="https://example.com/webhook",
            webhook_timeout_seconds=5,
        )

        assert config.check_interval_seconds == 30
        assert config.history_size == 50
        assert config.disk_warning_percent == 70
        assert config.disk_critical_percent == 90
        assert config.memory_warning_percent == 75
        assert config.memory_critical_percent == 90
        assert config.queue_timeout_seconds == 10
        assert config.webhook_url == "https://example.com/webhook"
        assert config.webhook_timeout_seconds == 5


class TestHealthHistory:
    """Test HealthHistory class."""

    def test_initial_state(self):
        """Test initial empty history."""
        history = HealthHistory(max_size=10)

        assert len(history) == 0
        assert history.get_recent(5) == []
        assert history.get_all() == []

    def test_add_entry(self):
        """Test adding entries to history."""
        history = HealthHistory(max_size=10)
        check = HealthCheck(
            status=HealthStatus.HEALTHY,
            timestamp=datetime.utcnow(),
            subsystems=[],
        )

        history.add(check)

        assert len(history) == 1
        assert history.get_recent(1)[0] == check

    def test_max_size_limit(self):
        """Test that history respects max size."""
        history = HealthHistory(max_size=3)

        for _i in range(5):
            check = HealthCheck(
                status=HealthStatus.HEALTHY,
                timestamp=datetime.utcnow(),
                subsystems=[],
            )
            history.add(check)

        assert len(history) == 3

    def test_fifo_behavior(self):
        """Test FIFO behavior when max size exceeded."""
        history = HealthHistory(max_size=3)
        checks = []

        for _i in range(5):
            check = HealthCheck(
                status=HealthStatus.HEALTHY,
                timestamp=datetime.utcnow(),
                subsystems=[],
            )
            checks.append(check)
            history.add(check)

        # Should only have the last 3
        recent = history.get_recent(10)
        assert len(recent) == 3
        assert recent[0] == checks[2]
        assert recent[1] == checks[3]
        assert recent[2] == checks[4]

    def test_get_recent(self):
        """Test getting recent entries."""
        history = HealthHistory(max_size=10)

        for _i in range(5):
            check = HealthCheck(
                status=HealthStatus.HEALTHY,
                timestamp=datetime.utcnow(),
                subsystems=[],
            )
            history.add(check)

        recent = history.get_recent(3)
        assert len(recent) == 3

    def test_get_recent_more_than_available(self):
        """Test getting more recent entries than available."""
        history = HealthHistory(max_size=10)

        for _i in range(3):
            check = HealthCheck(
                status=HealthStatus.HEALTHY,
                timestamp=datetime.utcnow(),
                subsystems=[],
            )
            history.add(check)

        recent = history.get_recent(10)
        assert len(recent) == 3

    def test_get_since(self):
        """Test getting entries since a specific time."""
        history = HealthHistory(max_size=10)
        now = datetime.utcnow()

        old_check = HealthCheck(
            status=HealthStatus.HEALTHY,
            timestamp=now - timedelta(hours=2),
            subsystems=[],
        )
        new_check = HealthCheck(
            status=HealthStatus.HEALTHY,
            timestamp=now - timedelta(minutes=30),
            subsystems=[],
        )

        history.add(old_check)
        history.add(new_check)

        since = now - timedelta(hours=1)
        result = history.get_since(since)

        assert len(result) == 1
        assert result[0] == new_check

    def test_clear(self):
        """Test clearing history."""
        history = HealthHistory(max_size=10)

        for _i in range(5):
            check = HealthCheck(
                status=HealthStatus.HEALTHY,
                timestamp=datetime.utcnow(),
                subsystems=[],
            )
            history.add(check)

        history.clear()

        assert len(history) == 0
        assert history.get_all() == []


class TestDatabaseHealthCheck:
    """Test DatabaseHealthCheck class."""

    def test_healthy_database(self, tmp_path):
        """Test check with healthy database."""
        db_path = tmp_path / "test.db"
        check = DatabaseHealthCheck(str(db_path))

        result = check.check()

        assert result.name == "database"
        assert result.status == HealthStatus.HEALTHY
        assert "connected" in result.message.lower()
        assert result.response_time_ms >= 0

    def test_database_error(self, tmp_path):
        """Test check with database error."""
        db_path = tmp_path / "readonly.db"

        # Create a file that looks like a DB but isn't writable
        db_path.touch()
        db_path.chmod(0o444)

        check = DatabaseHealthCheck(str(db_path))
        result = check.check()

        assert result.status == HealthStatus.UNHEALTHY
        assert "error" in result.message.lower() or "failed" in result.message.lower()

        # Cleanup
        db_path.chmod(0o644)


class TestQueueOperationsCheck:
    """Test QueueOperationsCheck class."""

    def test_healthy_queue(self, tmp_path):
        """Test check with healthy queue."""
        db_path = tmp_path / "queue.db"
        check = QueueOperationsCheck(str(db_path), timeout_seconds=5)

        result = check.check()

        assert result.name == "queue"
        assert result.status == HealthStatus.HEALTHY
        assert "operations" in result.message.lower() or "healthy" in result.message.lower()

    def test_queue_error(self, tmp_path):
        """Test check with queue error."""
        db_path = tmp_path / "invalid" / "queue.db"

        check = QueueOperationsCheck(str(db_path), timeout_seconds=5)
        result = check.check()

        assert result.status == HealthStatus.UNHEALTHY


class TestDiskSpaceCheck:
    """Test DiskSpaceCheck class."""

    def test_healthy_disk(self):
        """Test check with healthy disk space."""
        check = DiskSpaceCheck(
            warning_percent=95,
            critical_percent=99,
        )

        result = check.check()

        assert result.name == "storage"
        assert result.status == HealthStatus.HEALTHY
        assert "disk" in result.message.lower() or "storage" in result.message.lower()
        assert "percent" in result.details or "total_bytes" in result.details

    def test_degraded_disk_simulation(self):
        """Test degraded status with simulated high usage."""
        check = DiskSpaceCheck(
            warning_percent=1,  # Very low threshold to trigger warning
            critical_percent=99,
        )

        result = check.check()

        assert result.status == HealthStatus.DEGRADED

    def test_critical_disk_simulation(self):
        """Test critical status with simulated very high usage."""
        check = DiskSpaceCheck(
            warning_percent=1,
            critical_percent=2,  # Very low threshold to trigger critical
        )

        result = check.check()

        assert result.status == HealthStatus.UNHEALTHY


class TestMemoryUsageCheck:
    """Test MemoryUsageCheck class."""

    def test_memory_check_runs(self):
        """Test that memory check runs without error."""
        check = MemoryUsageCheck(
            warning_percent=95,
            critical_percent=99,
        )

        result = check.check()

        assert result.name == "memory"
        assert result.status in (
            HealthStatus.HEALTHY,
            HealthStatus.DEGRADED,
            HealthStatus.UNHEALTHY,
        )
        assert "memory" in result.message.lower() or "ram" in result.message.lower()
        assert "percent" in result.details

    def test_degraded_memory(self):
        """Test degraded status with high memory usage using Linux meminfo."""
        # Create a mock check that simulates high memory via threshold settings
        check = MemoryUsageCheck(
            warning_percent=1,  # Very low to trigger degraded
            critical_percent=99,
        )

        result = check.check()

        # Should be at least DEGRADED or HEALTHY depending on actual memory
        assert result.status in (
            HealthStatus.HEALTHY,
            HealthStatus.DEGRADED,
            HealthStatus.UNHEALTHY,
        )
        assert result.name == "memory"
        assert "percent" in result.details

    def test_critical_memory(self):
        """Test critical status with very high memory usage via threshold."""
        check = MemoryUsageCheck(
            warning_percent=1,
            critical_percent=2,  # Very low to potentially trigger critical
        )

        result = check.check()

        # Should return a valid status
        assert result.status in (
            HealthStatus.HEALTHY,
            HealthStatus.DEGRADED,
            HealthStatus.UNHEALTHY,
        )
        assert result.name == "memory"


class TestHealthChecker:
    """Test HealthChecker class."""

    def test_initial_state(self, tmp_path):
        """Test initial state of health checker."""
        db_path = tmp_path / "test.db"
        checker = HealthChecker(db_path=str(db_path))

        assert checker.config is not None
        assert checker.history is not None
        assert len(checker._checks) == 4  # database, queue, storage, memory

    def test_check_all_healthy(self, tmp_path):
        """Test checking all subsystems when healthy."""
        db_path = tmp_path / "test.db"
        checker = HealthChecker(
            db_path=str(db_path),
            config=HealthCheckConfig(
                disk_warning_percent=95,
                disk_critical_percent=99,
                memory_warning_percent=95,
                memory_critical_percent=99,
            ),
        )

        result = checker.check_all()

        assert result.status == HealthStatus.HEALTHY
        assert len(result.subsystems) == 4
        assert all(s.status == HealthStatus.HEALTHY for s in result.subsystems)

    def test_overall_status_degraded(self):
        """Test that overall status is degraded when any subsystem is degraded."""
        # Create a mock check that returns degraded
        mock_check = Mock()
        mock_check.check.return_value = SubsystemHealth(
            name="mock",
            status=HealthStatus.DEGRADED,
            message="Degraded",
            response_time_ms=10,
        )

        checker = HealthChecker()
        checker._checks = [lambda: mock_check.check()]

        result = checker.check_all()

        assert result.status == HealthStatus.DEGRADED

    def test_overall_status_unhealthy(self):
        """Test that overall status is unhealthy when any subsystem is unhealthy."""
        # Create mock checks
        mock_healthy = Mock()
        mock_healthy.check.return_value = SubsystemHealth(
            name="healthy",
            status=HealthStatus.HEALTHY,
            message="OK",
            response_time_ms=10,
        )

        mock_unhealthy = Mock()
        mock_unhealthy.check.return_value = SubsystemHealth(
            name="unhealthy",
            status=HealthStatus.UNHEALTHY,
            message="Failed",
            response_time_ms=10,
        )

        checker = HealthChecker()
        checker._checks = [
            lambda: mock_healthy.check(),
            lambda: mock_unhealthy.check(),
        ]

        result = checker.check_all()

        assert result.status == HealthStatus.UNHEALTHY

    def test_get_health_report(self, tmp_path):
        """Test getting health report."""
        db_path = tmp_path / "test.db"
        checker = HealthChecker(db_path=str(db_path))

        checker.check_all()
        report = checker.get_health_report()

        assert "status" in report
        assert "timestamp" in report
        assert "subsystems" in report
        assert "history_summary" in report

    def test_get_health_report_with_history(self, tmp_path):
        """Test health report includes history summary."""
        db_path = tmp_path / "test.db"
        checker = HealthChecker(db_path=str(db_path))

        # Run a few checks
        for _ in range(3):
            checker.check_all()

        report = checker.get_health_report()

        assert report["history_summary"]["total_checks"] == 3


class TestWebhookNotifier:
    """Test WebhookNotifier class."""

    @patch("httpx.post")
    def test_send_notification_success(self, mock_post):
        """Test successful webhook notification."""
        mock_response = Mock()
        mock_response.status_code = 200
        mock_post.return_value = mock_response

        notifier = WebhookNotifier(
            webhook_url="https://example.com/webhook",
            timeout_seconds=10,
        )

        health_check = HealthCheck(
            status=HealthStatus.HEALTHY,
            timestamp=datetime.utcnow(),
            subsystems=[],
        )

        result = notifier.notify(health_check)

        assert result is True
        mock_post.assert_called_once()

    @patch("httpx.post")
    def test_send_notification_failure(self, mock_post):
        """Test webhook notification failure."""
        mock_post.side_effect = Exception("Connection failed")

        notifier = WebhookNotifier(
            webhook_url="https://example.com/webhook",
            timeout_seconds=10,
        )

        health_check = HealthCheck(
            status=HealthStatus.HEALTHY,
            timestamp=datetime.utcnow(),
            subsystems=[],
        )

        result = notifier.notify(health_check)

        assert result is False

    def test_no_webhook_url(self):
        """Test that notification is skipped when no webhook URL."""
        notifier = WebhookNotifier(webhook_url=None, timeout_seconds=10)

        health_check = HealthCheck(
            status=HealthStatus.HEALTHY,
            timestamp=datetime.utcnow(),
            subsystems=[],
        )

        result = notifier.notify(health_check)

        assert result is False

    @patch("httpx.post")
    def test_payload_structure(self, mock_post):
        """Test that webhook payload has correct structure."""
        mock_response = Mock()
        mock_response.status_code = 200
        mock_post.return_value = mock_response

        notifier = WebhookNotifier(
            webhook_url="https://example.com/webhook",
            timeout_seconds=10,
        )

        subsystem = SubsystemHealth(
            name="test",
            status=HealthStatus.HEALTHY,
            message="Test message",
            response_time_ms=10.5,
            details={"key": "value"},
        )

        health_check = HealthCheck(
            status=HealthStatus.HEALTHY,
            timestamp=datetime.utcnow(),
            subsystems=[subsystem],
        )

        notifier.notify(health_check)

        call_args = mock_post.call_args
        payload = call_args.kwargs["json"]

        assert payload["status"] == "healthy"
        assert "timestamp" in payload
        assert len(payload["subsystems"]) == 1
        assert payload["subsystems"][0]["name"] == "test"


class TestHealthReporter:
    """Test HealthReporter class."""

    def test_initial_state(self, tmp_path):
        """Test initial state of health reporter."""
        db_path = tmp_path / "test.db"
        reporter = HealthReporter(db_path=str(db_path))

        assert reporter.checker is not None
        assert reporter.config is not None
        assert reporter.running is False

    def test_get_status(self, tmp_path):
        """Test getting current status."""
        db_path = tmp_path / "test.db"
        reporter = HealthReporter(db_path=str(db_path))

        status = reporter.get_status()

        assert status["running"] is False
        assert status["last_check"] is None
        assert status["check_count"] == 0

    def test_manual_check(self, tmp_path):
        """Test running a manual health check."""
        db_path = tmp_path / "test.db"
        reporter = HealthReporter(db_path=str(db_path))

        result = reporter.check_now()

        assert result.status in (
            HealthStatus.HEALTHY,
            HealthStatus.DEGRADED,
            HealthStatus.UNHEALTHY,
        )
        assert len(result.subsystems) == 4

    @patch("oclawma.health.WebhookNotifier.notify")
    def test_check_with_webhook(self, mock_notify):
        """Test that check sends webhook notification."""
        mock_notify.return_value = True

        reporter = HealthReporter(
            config=HealthCheckConfig(
                webhook_url="https://example.com/webhook",
            ),
        )

        reporter.check_now()

        mock_notify.assert_called_once()

    def test_start_stop_background_checks(self, tmp_path):
        """Test starting and stopping background health checks."""
        db_path = tmp_path / "test.db"
        reporter = HealthReporter(
            db_path=str(db_path),
            config=HealthCheckConfig(
                check_interval_seconds=0.1,  # Fast interval for testing
            ),
        )

        reporter.start()
        assert reporter.running is True

        # Wait for at least one check to run
        time.sleep(0.2)

        reporter.stop()
        assert reporter.running is False

        # Check that at least one check was recorded
        status = reporter.get_status()
        assert status["check_count"] >= 1

    def test_get_endpoint_response(self, tmp_path):
        """Test getting endpoint response."""
        db_path = tmp_path / "test.db"
        reporter = HealthReporter(db_path=str(db_path))

        # Run a check first
        reporter.check_now()

        response = reporter.get_endpoint_response()

        assert response["status"] in ("healthy", "degraded", "unhealthy")
        assert "timestamp" in response
        assert "subsystems" in response

    def test_context_manager(self, tmp_path):
        """Test using reporter as context manager."""
        db_path = tmp_path / "test.db"

        with HealthReporter(db_path=str(db_path)) as reporter:
            assert reporter.checker is not None
            result = reporter.check_now()
            assert result is not None


class TestHealthCheckError:
    """Test HealthCheckError exception."""

    def test_exception_creation(self):
        """Test creating a health check error."""
        error = HealthCheckError("Test error", subsystem="test")

        assert str(error) == "Test error"
        assert error.subsystem == "test"

    def test_exception_without_subsystem(self):
        """Test creating a health check error without subsystem."""
        error = HealthCheckError("Test error")

        assert str(error) == "Test error"
        assert error.subsystem is None


class TestIntegration:
    """Integration tests for the health module."""

    def test_full_health_check_workflow(self, tmp_path):
        """Test complete health check workflow."""
        db_path = tmp_path / "test.db"

        # Create reporter
        reporter = HealthReporter(
            db_path=str(db_path),
            config=HealthCheckConfig(
                check_interval_seconds=0.1,
                history_size=10,
            ),
        )

        # Run manual checks
        for _ in range(3):
            result = reporter.check_now()
            assert result.timestamp is not None
            assert len(result.subsystems) == 4

        # Check history
        assert len(reporter.checker.history) == 3

        # Get report
        report = reporter.get_endpoint_response()
        assert report["status"] in ("healthy", "degraded", "unhealthy")

    def test_concurrent_access(self, tmp_path):
        """Test thread safety of health checks."""
        db_path = tmp_path / "test.db"
        reporter = HealthReporter(db_path=str(db_path))

        results = []
        errors = []

        def run_check():
            try:
                result = reporter.check_now()
                results.append(result)
            except Exception as e:
                errors.append(e)

        # Run multiple checks concurrently
        threads = [threading.Thread(target=run_check) for _ in range(10)]
        for t in threads:
            t.start()
        for t in threads:
            t.join()

        assert len(errors) == 0
        assert len(results) == 10

    def test_health_status_transitions(self, tmp_path):
        """Test health status transitions."""
        db_path = tmp_path / "test.db"
        reporter = HealthReporter(db_path=str(db_path))

        # Initial check
        result1 = reporter.check_now()

        # All checks should return valid statuses
        assert result1.status in (
            HealthStatus.HEALTHY,
            HealthStatus.DEGRADED,
            HealthStatus.UNHEALTHY,
        )

        # Get report - should be consistent
        report = reporter.get_endpoint_response()
        assert report["status"] == result1.status.value
