"""
Tests for custom middleware implementations:
- CustomBrokenLinkEmailsMiddleware
- BlockBlackListedIPMiddleware
"""
import logging
from unittest.mock import patch

import pytest
from django.core.cache import cache
from django.http import HttpResponse, HttpResponseForbidden
from django.test import RequestFactory

from iam_service.learngual.middleware import (
    BlockBlackListedIPMiddleware,
    CustomBrokenLinkEmailsMiddleware,
    _block_ip,
    _check_threshold_exceeded,
    _get_allowed_patterns,
    _get_broken_link_config,
    _get_exempt_ips,
    _increment_ip_counter,
    _log_blocked_request,
    _log_malicious_request,
    _send_block_alert_email,
    _send_to_sentry,
    _should_ignore_broken_link,
    get_client_ip,
)

factory = RequestFactory()
logger = logging.getLogger("Malicious Tracker")


class TestGetClientIp:
    """Test IP extraction from request."""

    def test_get_client_ip_from_x_forwarded_for(self):
        """Test extracting IP from X-Forwarded-For header."""
        request = factory.get("/test/")
        request.META["HTTP_X_FORWARDED_FOR"] = "192.168.1.100, 10.0.0.1"
        assert get_client_ip(request) == "192.168.1.100"

    def test_get_client_ip_from_remote_addr(self):
        """Test extracting IP from REMOTE_ADDR."""
        request = factory.get("/test/")
        request.META["REMOTE_ADDR"] = "10.0.0.50"
        assert get_client_ip(request) == "10.0.0.50"

    def test_get_client_ip_x_forwarded_for_priority(self):
        """Test X-Forwarded-For takes priority over REMOTE_ADDR."""
        request = factory.get("/test/")
        request.META["HTTP_X_FORWARDED_FOR"] = "192.168.1.100"
        request.META["REMOTE_ADDR"] = "10.0.0.50"
        assert get_client_ip(request) == "192.168.1.100"


class TestBrokenLinkConfig:
    """Test configuration helper functions."""

    def test_get_broken_link_config_from_settings(self, settings):
        """Test getting config from Django settings."""
        settings.BROKEN_LINK_CONFIG = {"THRESHOLD": 20}
        assert _get_broken_link_config("THRESHOLD", 10) == 20

    def test_get_broken_link_config_default(self, settings):
        """Test getting default config value."""
        settings.BROKEN_LINK_CONFIG = {}
        assert _get_broken_link_config("THRESHOLD", 10) == 10

    @patch.dict("os.environ", {"BROKEN_LINK_THRESHOLD": "15"})
    def test_get_broken_link_config_from_env(self, settings):
        """Test getting config from environment variables."""
        settings.BROKEN_LINK_CONFIG = {}
        assert _get_broken_link_config("THRESHOLD") == 15

    @patch.dict("os.environ", {"BROKEN_LINK_AUTO_BLOCK_ENABLED": "false"})
    def test_get_broken_link_config_boolean_env(self, settings):
        """Test boolean environment variable parsing."""
        settings.BROKEN_LINK_CONFIG = {}
        assert _get_broken_link_config("AUTO_BLOCK_ENABLED") is False

    def test_get_allowed_patterns(self, settings):
        """Test getting compiled regex patterns."""
        settings.BROKEN_LINK_CONFIG = {"ALLOW_PATTERNS": [r"^/static/.*", r"\.js$"]}
        patterns = _get_allowed_patterns()
        assert len(patterns) == 2
        assert patterns[0].search("/static/file.css")
        assert patterns[1].search("script.js")

    def test_get_exempt_ips(self, settings):
        """Test getting exempt IPs."""
        settings.BROKEN_LINK_CONFIG = {"EXEMPT_IPS": ["192.168.1.1", "10.0.0.1"]}
        assert _get_exempt_ips() == ["192.168.1.1", "10.0.0.1"]


class TestBrokenLinkHelpers:
    """Test broken link tracking helper functions."""

    def setUp(self):
        """Clear cache before each test."""
        cache.clear()

    def test_should_ignore_broken_link_match(self, settings):
        """Test URL matching against allow patterns."""
        settings.BROKEN_LINK_CONFIG = {"ALLOW_PATTERNS": [r"^/static/.*", r"\.js$"]}
        assert _should_ignore_broken_link("/static/style.css")
        assert _should_ignore_broken_link("script.js")
        assert not _should_ignore_broken_link("/api/users")

    def test_increment_ip_counter(self, settings):
        """Test incrementing IP counter in cache."""
        cache.clear()
        settings.BROKEN_LINK_CONFIG = {"THRESHOLD_PERIOD": 60}

        ip = "192.168.1.100"
        counter1 = _increment_ip_counter(ip)
        counter2 = _increment_ip_counter(ip)
        counter3 = _increment_ip_counter(ip)

        assert counter1 == 1
        assert counter2 == 2
        assert counter3 == 3

    def test_check_threshold_exceeded(self, settings):
        """Test threshold checking."""
        cache.clear()
        settings.BROKEN_LINK_CONFIG = {
            "THRESHOLD": 5,
            "THRESHOLD_PERIOD": 60,
        }

        ip = "192.168.1.100"
        # Fill cache to threshold
        for _ in range(5):
            _increment_ip_counter(ip)

        assert _check_threshold_exceeded(ip)

    def test_block_ip(self, settings):
        """Test blocking an IP in cache."""
        cache.clear()
        settings.BROKEN_LINK_CONFIG = {"BLOCK_DURATION": 3600}

        ip = "192.168.1.100"
        _block_ip(ip, "TEST_REASON", 10)

        block_info = cache.get(f"broken_links:blocked_ips:{ip}")
        assert block_info is not None
        assert block_info["reason"] == "TEST_REASON"
        assert block_info["counter"] == 10
        assert block_info["duration"] == 3600

    def test_increment_ip_counter_expires(self, settings):
        """Test IP counter expires after threshold period."""
        cache.clear()
        settings.BROKEN_LINK_CONFIG = {"THRESHOLD_PERIOD": 1}

        ip = "192.168.1.100"
        _increment_ip_counter(ip)

        # Wait for expiration (mocking by clearing cache)
        counter_key = f"broken_links:counter:{ip}"
        assert cache.get(counter_key) == 1

        cache.delete(counter_key)
        assert cache.get(counter_key) is None


class TestLoggingFunctions:
    """Test logging helper functions."""

    def test_log_malicious_request(self, caplog, settings):
        """Test logging malicious requests."""
        settings.BROKEN_LINK_CONFIG = {"LOGGER_NAME": "Malicious Tracker"}

        with caplog.at_level(logging.WARNING):
            _log_malicious_request(
                ip="192.168.1.100",
                path="/fake-page",
                referer="https://example.com",
                user_agent="Mozilla/5.0",
                counter=5,
                threshold="10/60",
            )

        assert "ip=192.168.1.100" in caplog.text
        assert "path=/fake-page" in caplog.text
        assert "counter=5" in caplog.text

    def test_log_blocked_request(self, caplog, settings):
        """Test logging blocked requests."""
        settings.BROKEN_LINK_CONFIG = {"LOGGER_NAME": "Malicious Tracker"}

        with caplog.at_level(logging.ERROR):
            _log_blocked_request(
                ip="192.168.1.100",
                reason="THRESHOLD_EXCEEDED",
                counter=10,
                block_duration=3600,
            )

        assert "BLOCKED" in caplog.text
        assert "ip=192.168.1.100" in caplog.text
        assert "THRESHOLD_EXCEEDED" in caplog.text

    @patch("iam_service.learngual.middleware.mail_managers")
    def test_send_block_alert_email(self, mock_mail, settings):
        """Test sending block alert email."""
        settings.MANAGERS = [("Admin", "admin@example.com")]
        settings.BROKEN_LINK_CONFIG = {}

        _send_block_alert_email(
            ip="192.168.1.100",
            reason="THRESHOLD_EXCEEDED",
            counter=10,
            block_duration=3600,
        )

        mock_mail.assert_called_once()
        args = mock_mail.call_args
        assert "192.168.1.100" in args[0][1]  # Check message contains IP
        assert "THRESHOLD_EXCEEDED" in args[0][1]

    @patch("iam_service.learngual.middleware.sentry_sdk.capture_message")
    def test_send_to_sentry(self, mock_sentry, settings):
        """Test sending alert to Sentry."""
        settings.BROKEN_LINK_CONFIG = {"SENTRY_ENABLED": True}

        _send_to_sentry(ip="192.168.1.100", reason="THRESHOLD_EXCEEDED", counter=10)

        mock_sentry.assert_called_once()
        assert "192.168.1.100" in mock_sentry.call_args[0][0]

    @patch("iam_service.learngual.middleware.sentry_sdk.capture_message")
    def test_send_to_sentry_disabled(self, mock_sentry, settings):
        """Test Sentry is not called when disabled."""
        settings.BROKEN_LINK_CONFIG = {"SENTRY_ENABLED": False}

        _send_to_sentry(ip="192.168.1.100", reason="THRESHOLD_EXCEEDED", counter=10)

        mock_sentry.assert_not_called()


@pytest.mark.django_db
class TestCustomBrokenLinkEmailsMiddleware:
    """Test CustomBrokenLinkEmailsMiddleware."""

    def setup_method(self):
        """Clear cache before each test."""
        cache.clear()

    def get_response(self, request):
        """Dummy view that returns a response."""
        return HttpResponse("OK", status=200)

    def test_middleware_ignores_200_response(self, settings):
        """Test middleware ignores non-404 responses."""
        settings.DEBUG = False
        settings.BROKEN_LINK_CONFIG = {"ENABLE_RATE_LIMITING": True}

        middleware = CustomBrokenLinkEmailsMiddleware(self.get_response)
        request = factory.get("/test/")
        request.META["REMOTE_ADDR"] = "192.168.1.100"

        response = HttpResponse("OK", status=200)
        result = middleware.process_response(request, response)

        assert result.status_code == 200
        # Check that counter was not incremented
        assert cache.get("broken_links:counter:192.168.1.100") is None

    def test_middleware_ignores_404_in_debug(self, settings):
        """Test middleware ignores 404 in DEBUG mode."""
        settings.DEBUG = True
        settings.BROKEN_LINK_CONFIG = {"ENABLE_RATE_LIMITING": True}

        middleware = CustomBrokenLinkEmailsMiddleware(self.get_response)
        request = factory.get("/fake/")
        request.META["REMOTE_ADDR"] = "192.168.1.100"
        request.META["HTTP_REFERER"] = "https://example.com"

        response = HttpResponse("Not Found", status=404)
        result = middleware.process_response(request, response)

        assert result.status_code == 404
        # Check that counter was not incremented
        assert cache.get("broken_links:counter:192.168.1.100") is None

    def test_middleware_logs_broken_link(self, settings, caplog):
        """Test middleware logs broken link attempts."""
        cache.clear()
        settings.DEBUG = False
        settings.BROKEN_LINK_CONFIG = {
            "ENABLE_RATE_LIMITING": True,
            "THRESHOLD": 10,
            "THRESHOLD_PERIOD": 60,
            "LOGGER_NAME": "Malicious Tracker",
            "ALLOW_PATTERNS": [],
            "EXEMPT_IPS": [],
        }

        middleware = CustomBrokenLinkEmailsMiddleware(self.get_response)
        request = factory.get("/fake-page/")
        request.META["REMOTE_ADDR"] = "192.168.1.100"
        request.META["HTTP_REFERER"] = "https://example.com"
        request.META["HTTP_USER_AGENT"] = "Mozilla/5.0"

        with caplog.at_level(logging.WARNING):
            response = HttpResponse("Not Found", status=404)
            result = middleware.process_response(request, response)

        assert result.status_code == 404
        assert "ip=192.168.1.100" in caplog.text

    def test_middleware_ignores_allowed_patterns(self, settings):
        """Test middleware ignores URLs matching allow patterns."""
        cache.clear()
        settings.DEBUG = False
        settings.BROKEN_LINK_CONFIG = {
            "ENABLE_RATE_LIMITING": True,
            "ALLOW_PATTERNS": [r"^/static/.*", r"\.js$"],
        }

        middleware = CustomBrokenLinkEmailsMiddleware(self.get_response)
        request = factory.get("/static/missing.css")
        request.META["REMOTE_ADDR"] = "192.168.1.100"
        request.META["HTTP_REFERER"] = "https://example.com"

        response = HttpResponse("Not Found", status=404)
        middleware.process_response(request, response)

        # Counter should not be incremented
        assert cache.get("broken_links:counter:192.168.1.100") is None

    def test_middleware_blocks_ip_on_threshold(self, settings):
        """Test middleware blocks IP after exceeding threshold."""
        cache.clear()
        settings.DEBUG = False
        settings.MANAGERS = []
        settings.BROKEN_LINK_CONFIG = {
            "ENABLE_RATE_LIMITING": True,
            "THRESHOLD": 3,
            "THRESHOLD_PERIOD": 60,
            "AUTO_BLOCK_ENABLED": True,
            "BLOCK_DURATION": 3600,
            "SEND_EMAIL_ON_THRESHOLD_EXCEEDED": False,
            "SENTRY_ENABLED": False,
            "ALLOW_PATTERNS": [],
            "EXEMPT_IPS": [],
        }

        middleware = CustomBrokenLinkEmailsMiddleware(self.get_response)
        ip = "192.168.1.100"

        # Make 3 requests (threshold)
        for i in range(3):
            request = factory.get(f"/fake-{i}/")
            request.META["REMOTE_ADDR"] = ip
            request.META["HTTP_REFERER"] = "https://example.com"
            response = HttpResponse("Not Found", status=404)
            middleware.process_response(request, response)

        # Check that IP is blocked
        block_info = cache.get(f"broken_links:blocked_ips:{ip}")
        assert block_info is not None
        assert block_info["reason"] == "THRESHOLD_EXCEEDED"

    def test_middleware_exempt_ips(self, settings):
        """Test middleware respects exempt IPs."""
        cache.clear()
        settings.DEBUG = False
        settings.BROKEN_LINK_CONFIG = {
            "ENABLE_RATE_LIMITING": True,
            "EXEMPT_IPS": ["192.168.1.100"],
        }

        middleware = CustomBrokenLinkEmailsMiddleware(self.get_response)
        ip = "192.168.1.100"

        # Make multiple requests
        for i in range(5):
            request = factory.get(f"/fake-{i}/")
            request.META["REMOTE_ADDR"] = ip
            request.META["HTTP_REFERER"] = "https://example.com"
            response = HttpResponse("Not Found", status=404)
            middleware.process_response(request, response)

        # Check that IP is NOT blocked
        block_info = cache.get(f"broken_links:blocked_ips:{ip}")
        assert block_info is None

    def test_middleware_ignores_requests_without_referer(self, settings):
        """Test middleware ignores 404s without referer (direct access)."""
        cache.clear()
        settings.DEBUG = False
        settings.BROKEN_LINK_CONFIG = {
            "ENABLE_RATE_LIMITING": True,
            "ALLOW_PATTERNS": [],
            "EXEMPT_IPS": [],
        }

        middleware = CustomBrokenLinkEmailsMiddleware(self.get_response)
        request = factory.get("/fake/")
        request.META["REMOTE_ADDR"] = "192.168.1.100"
        # No HTTP_REFERER

        response = HttpResponse("Not Found", status=404)
        middleware.process_response(request, response)

        # Counter should not be incremented (ignorable request)
        assert cache.get("broken_links:counter:192.168.1.100") is None


@pytest.mark.django_db
class TestBlockBlackListedIPMiddleware:
    """Test BlockBlackListedIPMiddleware."""

    def setup_method(self):
        """Clear cache before each test."""
        cache.clear()

    def get_response(self, request):
        """Dummy view that returns a response."""
        return HttpResponse("OK", status=200)

    def test_middleware_allows_normal_ip(self):
        """Test middleware allows normal IP addresses."""
        middleware = BlockBlackListedIPMiddleware(self.get_response)
        request = factory.get("/test/")
        request.META["REMOTE_ADDR"] = "192.168.1.100"

        response = middleware(request)

        assert response.status_code == 200

    def test_middleware_blocks_static_ip(self, monkeypatch):
        """Test middleware blocks static blocked IPs."""
        monkeypatch.setattr(
            "iam_service.learngual.middleware.BLOCKED_IPS",
            ["192.168.1.100", "10.0.0.1"],
        )
        monkeypatch.setattr("iam_service.learngual.middleware.BLOCKED_NETWORKS", [])

        middleware = BlockBlackListedIPMiddleware(self.get_response)
        request = factory.get("/test/")
        request.META["REMOTE_ADDR"] = "192.168.1.100"

        response = middleware(request)

        assert isinstance(response, HttpResponseForbidden)
        assert response.status_code == 403

    def test_middleware_blocks_network_range(self, monkeypatch):
        """Test middleware blocks IPs in blocked network ranges."""
        monkeypatch.setattr("iam_service.learngual.middleware.BLOCKED_IPS", [])
        monkeypatch.setattr(
            "iam_service.learngual.middleware.BLOCKED_NETWORKS", ["192.168.0.0/16"]
        )

        middleware = BlockBlackListedIPMiddleware(self.get_response)
        request = factory.get("/test/")
        request.META["REMOTE_ADDR"] = "192.168.1.100"

        response = middleware(request)

        assert isinstance(response, HttpResponseForbidden)
        assert response.status_code == 403

    def test_middleware_blocks_dynamic_ip(self, monkeypatch):
        """Test middleware blocks dynamically blocked IPs from cache."""
        cache.clear()
        monkeypatch.setattr("iam_service.learngual.middleware.BLOCKED_IPS", [])
        monkeypatch.setattr("iam_service.learngual.middleware.BLOCKED_NETWORKS", [])

        middleware = BlockBlackListedIPMiddleware(self.get_response)

        ip = "192.168.1.100"
        # Add IP to dynamic block list
        cache.set(
            f"broken_links:blocked_ips:{ip}",
            {"reason": "THRESHOLD_EXCEEDED", "counter": 10},
            3600,
        )

        request = factory.get("/test/")
        request.META["REMOTE_ADDR"] = ip

        response = middleware(request)

        assert isinstance(response, HttpResponseForbidden)
        assert response.status_code == 403

    def test_middleware_x_forwarded_for_priority(self, monkeypatch):
        """Test middleware uses X-Forwarded-For over REMOTE_ADDR."""
        cache.clear()
        monkeypatch.setattr("iam_service.learngual.middleware.BLOCKED_IPS", [])
        monkeypatch.setattr("iam_service.learngual.middleware.BLOCKED_NETWORKS", [])

        middleware = BlockBlackListedIPMiddleware(self.get_response)

        ip = "192.168.1.100"
        cache.set(
            f"broken_links:blocked_ips:{ip}", {"reason": "THRESHOLD_EXCEEDED"}, 3600
        )

        request = factory.get("/test/")
        request.META["HTTP_X_FORWARDED_FOR"] = ip
        request.META["REMOTE_ADDR"] = "10.0.0.1"

        response = middleware(request)

        assert isinstance(response, HttpResponseForbidden)
