"""Tests for sluice._safety — memory guard and PII redaction."""

from __future__ import annotations

import logging
import time
from typing import TYPE_CHECKING
from unittest.mock import MagicMock, patch

from sluice._safety import (
    _REDACTED,
    MemoryGuard,
    _get_rss_bytes,
    redact_task_args,
)

if TYPE_CHECKING:
    import pytest


class TestMemoryGuard:
    """Tests for the MemoryGuard class."""

    def test_starts_with_capture_enabled(self) -> None:
        """New guard should have capture enabled."""
        guard = MemoryGuard()
        assert guard.is_disabled is False

    def test_records_baseline_rss(self) -> None:
        """start() should record a non-zero baseline RSS."""
        guard = MemoryGuard()
        guard.start()
        try:
            # On any real OS, RSS should be > 0.
            assert guard.baseline_rss > 0
        finally:
            guard.stop()

    def test_check_once_allows_normal_growth(self) -> None:
        """check_once() should allow capture when growth is below threshold."""
        guard = MemoryGuard(threshold_bytes=500 * 1024 * 1024)  # 500 MB
        guard.start()
        try:
            assert guard.check_once() is True
            assert guard.is_disabled is False
        finally:
            guard.stop()

    def test_check_once_disables_on_excessive_growth(self) -> None:
        """check_once() should disable capture when growth exceeds threshold."""
        guard = MemoryGuard(threshold_bytes=1)  # 1 byte — any growth triggers
        # Set a very low baseline to guarantee threshold is exceeded.
        guard._baseline_rss = 1
        guard._disabled_event.clear()

        result = guard.check_once()

        assert result is False
        assert guard.is_disabled is True

    def test_disabled_stays_disabled(self) -> None:
        """Once disabled, check_once() should remain disabled."""
        guard = MemoryGuard()
        guard._disabled_event.set()

        result = guard.check_once()

        assert result is False
        assert guard.is_disabled is True

    def test_stop_terminates_thread(self) -> None:
        """stop() should cleanly terminate the monitoring thread."""
        guard = MemoryGuard(check_interval_s=0.1)
        guard.start()
        assert guard._thread is not None
        assert guard._thread.is_alive()

        guard.stop()

        assert guard._thread is None

    def test_thread_is_daemon(self) -> None:
        """Monitoring thread must be a daemon (not block process exit)."""
        guard = MemoryGuard(check_interval_s=60.0)
        guard.start()
        try:
            assert guard._thread is not None
            assert guard._thread.daemon is True
        finally:
            guard.stop()

    def test_check_loop_catches_exceptions(
        self, caplog: pytest.LogCaptureFixture
    ) -> None:
        """_check_loop should never crash, even if check_once raises."""
        guard = MemoryGuard(check_interval_s=0.05)
        guard.start()

        # Break check_once to raise.
        original = guard.check_once
        call_count = 0

        def _broken_check() -> bool:
            nonlocal call_count
            call_count += 1
            if call_count == 1:
                raise RuntimeError("simulated check failure")
            return original()

        guard.check_once = _broken_check  # type: ignore[method-assign]

        time.sleep(0.2)
        guard.stop()

        # Thread should have survived the exception.
        assert call_count >= 1

    def test_logs_warning_on_disable(self, caplog: pytest.LogCaptureFixture) -> None:
        """Disabling capture should log a warning with actionable message."""
        guard = MemoryGuard(threshold_bytes=1)
        guard._baseline_rss = 1

        with caplog.at_level(logging.WARNING, logger="sluice"):
            guard.check_once()

        assert "Memory growth" in caplog.text
        assert "threshold" in caplog.text
        assert "docs.sluice.sh/troubleshooting/memory" in caplog.text

    def test_rss_zero_skips_monitoring(self, caplog: pytest.LogCaptureFixture) -> None:
        """If RSS can't be read, start() should skip monitoring."""
        with patch("sluice._safety._get_rss_bytes", return_value=0):
            guard = MemoryGuard()

            with caplog.at_level(logging.DEBUG, logger="sluice"):
                guard.start()

            assert guard._thread is None
            assert "Could not read process RSS" in caplog.text


class TestGetRssBytes:
    """Tests for the _get_rss_bytes helper."""

    def test_returns_positive_on_real_system(self) -> None:
        """On any real system, _get_rss_bytes should return > 0."""
        rss = _get_rss_bytes()
        assert rss > 0

    def test_returns_reasonable_value(self) -> None:
        """RSS should be reasonable (>1 MB for a Python process)."""
        rss = _get_rss_bytes()
        assert rss > 1024 * 1024  # At least 1 MB

    def test_handles_proc_not_available(self) -> None:
        """Should fall back gracefully if /proc is not available."""
        with patch("os.path.exists", return_value=False):
            rss = _get_rss_bytes()
            # Should still work via resource module fallback.
            assert rss > 0


class TestRedactTaskArgs:
    """Tests for the redact_task_args function."""

    def test_redacts_args(self) -> None:
        """Task positional args should be replaced with [redacted]."""
        result = redact_task_args(args=(1, 2, "password123"))
        assert result["args"] == _REDACTED

    def test_redacts_kwargs(self) -> None:
        """Task keyword args should be replaced with [redacted]."""
        result = redact_task_args(kwargs={"password": "secret", "token": "abc"})
        assert result["kwargs"] == _REDACTED

    def test_redacts_both(self) -> None:
        """Both args and kwargs should be redacted."""
        result = redact_task_args(args=(1,), kwargs={"key": "val"})
        assert result["args"] == _REDACTED
        assert result["kwargs"] == _REDACTED

    def test_none_args_excluded(self) -> None:
        """None values should not produce keys in the result."""
        result = redact_task_args(args=None, kwargs=None)
        assert "args" not in result
        assert "kwargs" not in result

    def test_original_values_never_in_output(self) -> None:
        """Original values should never appear in redacted output."""
        secret_args = ("api_key_12345", "ssn_123-45-6789")
        secret_kwargs = {"credit_card": "4111-1111-1111-1111"}
        result = redact_task_args(args=secret_args, kwargs=secret_kwargs)

        output_str = str(result)
        assert "api_key_12345" not in output_str
        assert "ssn_123-45-6789" not in output_str
        assert "4111-1111-1111-1111" not in output_str


class TestMemoryGuardIntegration:
    """Integration tests for MemoryGuard with EventBuffer."""

    def test_buffer_skips_push_when_guard_disabled(self) -> None:
        """EventBuffer.push() should silently drop events when guard is disabled."""
        from sluice._buffer import EventBuffer

        mock_transport = MagicMock()
        guard = MemoryGuard()
        guard._disabled_event.set()

        buf = EventBuffer(mock_transport, memory_guard=guard)
        buf.start()

        try:
            buf.push({"type": "test"})
            assert buf.pending_count == 0
        finally:
            buf.stop()

    def test_buffer_accepts_push_when_guard_enabled(self) -> None:
        """EventBuffer.push() should accept events when guard allows capture."""
        from sluice._buffer import EventBuffer

        mock_transport = MagicMock()
        guard = MemoryGuard()
        guard._disabled_event.clear()

        buf = EventBuffer(mock_transport, memory_guard=guard)

        buf.push({"type": "test"})
        assert buf.pending_count == 1

    def test_buffer_works_without_guard(self) -> None:
        """EventBuffer should work normally without a memory guard."""
        from sluice._buffer import EventBuffer

        mock_transport = MagicMock()
        buf = EventBuffer(mock_transport)

        buf.push({"type": "test"})
        assert buf.pending_count == 1
