# Copyright (c) 2023-present Pi Dash Software, Inc. and contributors
# SPDX-License-Identifier: AGPL-3.0-only
# See the LICENSE file for details.

"""
Unit tests for ReadReplicaRoutingMiddleware.
This module contains comprehensive tests for the ReadReplicaRoutingMiddleware
that handles intelligent database routing to read replicas based on HTTP methods
and view configuration.
Test Organization:
- TestReadReplicaRoutingMiddleware: Core middleware functionality
- TestProcessView: process_view method behavior
- TestReplicaDecisionLogic: Decision logic for replica usage
- TestAttributeDetection: View attribute detection methods
- TestExceptionHandling: Exception handling and cleanup
- TestRealViewIntegration: Real Django/DRF view integration
- TestEdgeCases: Edge cases and error conditions
"""

import pytest
from unittest.mock import Mock, patch

from django.http import HttpResponse
from django.test import RequestFactory
from django.views import View
from rest_framework.views import APIView
from rest_framework.viewsets import ViewSet

from pi_dash.middleware.db_routing import ReadReplicaRoutingMiddleware


# Pytest fixtures
@pytest.fixture
def mock_get_response():
    """Fixture for mocked get_response callable."""
    return Mock(return_value=HttpResponse())


@pytest.fixture
def middleware(mock_get_response):
    """Fixture for ReadReplicaRoutingMiddleware instance."""
    return ReadReplicaRoutingMiddleware(mock_get_response)


@pytest.fixture
def request_factory():
    """Fixture for Django RequestFactory."""
    return RequestFactory()


@pytest.fixture
def mock_view_func():
    """Fixture for a basic mocked view function."""
    view = Mock()
    view.use_read_replica = True
    return view


@pytest.fixture
def get_request(request_factory):
    """Fixture for a GET request."""
    return request_factory.get("/api/test/")


@pytest.fixture
def post_request(request_factory):
    """Fixture for a POST request."""
    return request_factory.post("/api/test/")


@pytest.mark.unit
class TestReadReplicaRoutingMiddleware:
    """Test cases for ReadReplicaRoutingMiddleware core functionality."""

    def test_middleware_initialization(self, middleware, mock_get_response):
        """Test middleware initializes correctly with expected attributes."""
        assert middleware.get_response == mock_get_response
        assert hasattr(middleware, "READ_ONLY_METHODS")
        assert "GET" in middleware.READ_ONLY_METHODS
        assert "HEAD" in middleware.READ_ONLY_METHODS
        assert "OPTIONS" in middleware.READ_ONLY_METHODS

    def test_read_only_methods_constant(self, middleware):
        """Test READ_ONLY_METHODS contains expected HTTP methods."""
        expected_methods = {"GET", "HEAD", "OPTIONS"}
        assert middleware.READ_ONLY_METHODS == expected_methods

    @patch("pi_dash.middleware.db_routing.set_use_read_replica")
    @patch("pi_dash.middleware.db_routing.clear_read_replica_context")
    def test_call_routes_write_methods_to_primary(
        self, mock_clear, mock_set, middleware, post_request, mock_get_response
    ):
        """Test __call__ routes write methods to primary database."""
        response = middleware(post_request)

        mock_set.assert_called_once_with(False)  # Primary database
        mock_clear.assert_called_once()
        assert response == mock_get_response.return_value

    @patch("pi_dash.middleware.db_routing.clear_read_replica_context")
    def test_call_with_read_methods_waits_for_process_view(
        self, mock_clear, middleware, get_request, mock_get_response
    ):
        """Test __call__ with read methods waits for process_view."""
        response = middleware(get_request)

        mock_clear.assert_called_once()
        assert response == mock_get_response.return_value

    @patch("pi_dash.middleware.db_routing.clear_read_replica_context")
    def test_call_always_cleans_up_context(self, mock_clear, middleware, get_request):
        """Test __call__ always cleans up context."""
        middleware(get_request)

        mock_clear.assert_called_once()

    @patch("pi_dash.middleware.db_routing.clear_read_replica_context")
    def test_call_cleans_up_context_on_exception(self, mock_clear, middleware, get_request, mock_get_response):
        """Test __call__ cleans up context even if get_response raises."""
        mock_get_response.side_effect = Exception("Test exception")

        with pytest.raises(Exception, match="Test exception"):
            middleware(get_request)

        mock_clear.assert_called_once()


@pytest.mark.unit
class TestProcessView:
    """Test cases for process_view method functionality."""

    @patch("pi_dash.middleware.db_routing.set_use_read_replica")
    def test_with_read_method_and_replica_true(self, mock_set, middleware, get_request):
        """Test process_view with GET request and use_read_replica=True."""
        view_func = Mock()
        view_func.use_read_replica = True

        result = middleware.process_view(get_request, view_func, (), {})

        mock_set.assert_called_once_with(True)
        assert result is None

    @patch("pi_dash.middleware.db_routing.set_use_read_replica")
    def test_with_read_method_and_replica_false(self, mock_set, middleware, get_request):
        """Test process_view with GET request and use_read_replica=False."""
        view_func = Mock()
        view_func.use_read_replica = False

        result = middleware.process_view(get_request, view_func, (), {})

        mock_set.assert_called_once_with(False)
        assert result is None

    @patch("pi_dash.middleware.db_routing.set_use_read_replica")
    def test_with_read_method_and_no_replica_attribute(self, mock_set, middleware, get_request):
        """Test process_view with GET request and no use_read_replica attr."""
        view_func = Mock(spec=[])  # No use_read_replica attribute

        result = middleware.process_view(get_request, view_func, (), {})

        mock_set.assert_called_once_with(False)  # Default to primary
        assert result is None

    def test_with_write_method_ignores_view_attributes(self, middleware, post_request):
        """Test process_view with write methods ignores view attributes."""
        view_func = Mock()
        view_func.use_read_replica = True  # This should be ignored for POST

        result = middleware.process_view(post_request, view_func, (), {})

        assert result is None  # Should not process for write methods


@pytest.mark.unit
class TestReplicaDecisionLogic:
    """Test cases for replica decision logic methods."""

    def test_should_use_read_replica_with_true_attribute(self, middleware):
        """Test _should_use_read_replica returns True for True attribute."""
        view_func = Mock()
        view_func.use_read_replica = True

        result = middleware._should_use_read_replica(view_func)

        assert result is True

    def test_should_use_read_replica_with_false_attribute(self, middleware):
        """Test _should_use_read_replica returns False for False attribute."""
        view_func = Mock()
        view_func.use_read_replica = False

        result = middleware._should_use_read_replica(view_func)

        assert result is False

    def test_should_use_read_replica_with_no_attribute_defaults_false(self, middleware):
        """Test _should_use_read_replica defaults to False for missing attr."""
        view_func = Mock(spec=[])  # No use_read_replica attribute

        result = middleware._should_use_read_replica(view_func)

        assert result is False


@pytest.mark.unit
class TestAttributeDetection:
    """Test cases for view attribute detection methods."""

    def test_get_use_replica_attribute_function_based_view(self, middleware):
        """Test _get_use_replica_attribute with function-based view."""
        # Test with True
        view_func = Mock()
        view_func.use_read_replica = True
        result = middleware._get_use_replica_attribute(view_func)
        assert result is True

        # Test with False
        view_func.use_read_replica = False
        result = middleware._get_use_replica_attribute(view_func)
        assert result is False

        # Test with no attribute
        view_func = Mock(spec=[])
        result = middleware._get_use_replica_attribute(view_func)
        assert result is None

    def test_get_use_replica_attribute_django_cbv(self, middleware):
        """Test _get_use_replica_attribute with Django CBV wrapper."""
        view_class = Mock()
        view_class.use_read_replica = True
        view_func = Mock()
        view_func.view_class = view_class
        # Remove use_read_replica from view_func to ensure it checks view_class
        del view_func.use_read_replica

        result = middleware._get_use_replica_attribute(view_func)

        assert result is True

    def test_get_use_replica_attribute_drf_wrapper(self, middleware):
        """Test _get_use_replica_attribute with DRF wrapper."""

        # Create a real object to avoid Mock issues
        class ViewClass:
            use_read_replica = True

        class ViewFunc:
            cls = ViewClass()

        view_func = ViewFunc()

        result = middleware._get_use_replica_attribute(view_func)

        assert result is True

    def test_get_use_replica_attribute_priority_order(self, middleware):
        """Test attribute priority: direct > view_class > cls."""
        view_func = Mock()
        view_func.use_read_replica = True  # Direct attribute (highest priority)

        # Add conflicting attributes with lower priority
        view_class = Mock()
        view_class.use_read_replica = False
        view_func.view_class = view_class

        cls = Mock()
        cls.use_read_replica = False
        view_func.cls = cls

        result = middleware._get_use_replica_attribute(view_func)

        assert result is True  # Should use direct attribute

    @pytest.mark.parametrize(
        "value,expected",
        [
            (True, True),
            (False, False),
            (1, True),
            (0, False),
            ("yes", True),
            ("", False),
            ([], False),
            ([1], True),
            (None, False),
        ],
    )
    def test_should_use_read_replica_truthy_falsy_values(self, middleware, value, expected):
        """Test _should_use_read_replica with various truthy/falsy values."""

        # Create a real object to test the attribute handling
        class TestView:
            pass

        view_func = TestView()
        view_func.use_read_replica = value

        result = middleware._should_use_read_replica(view_func)

        assert result == expected


@pytest.mark.unit
class TestExceptionHandling:
    """Test cases for exception handling and cleanup."""

    @patch("pi_dash.middleware.db_routing.clear_read_replica_context")
    def test_process_exception_cleans_up_context(self, mock_clear, middleware, request_factory):
        """Test process_exception cleans up context."""
        request = request_factory.get("/api/test/")
        exception = Exception("Test exception")

        result = middleware.process_exception(request, exception)

        mock_clear.assert_called_once()
        assert result is None  # Don't handle the exception

    @patch("pi_dash.middleware.db_routing.set_use_read_replica")
    @patch("pi_dash.middleware.db_routing.clear_read_replica_context")
    def test_integration_full_request_cycle(self, mock_clear, mock_set, middleware, request_factory, mock_get_response):
        """Test complete request cycle from __call__ through process_view."""
        request = request_factory.get("/api/test/")
        view_func = Mock()
        view_func.use_read_replica = True

        # Call middleware and process_view manually
        response = middleware(request)
        middleware.process_view(request, view_func, (), {})

        mock_set.assert_called_once_with(True)
        mock_clear.assert_called_once()
        assert response == mock_get_response.return_value


@pytest.mark.unit
class TestRealViewIntegration:
    """Test middleware with real Django/DRF view classes."""

    @patch("pi_dash.middleware.db_routing.set_use_read_replica")
    def test_with_django_class_based_view(self, mock_set, middleware, request_factory):
        """Test middleware with actual Django CBV."""

        class TestView(View):
            use_read_replica = True

        # Simulate Django's URL resolver creating a view wrapper
        view_func = TestView.as_view()
        request = request_factory.get("/api/test/")

        middleware.process_view(request, view_func, (), {})

        mock_set.assert_called_once_with(True)

    @patch("pi_dash.middleware.db_routing.set_use_read_replica")
    def test_with_drf_api_view(self, mock_set, middleware, request_factory):
        """Test middleware with DRF APIView."""

        class TestAPIView(APIView):
            use_read_replica = True

        # Simulate DRF's URL pattern creating a view wrapper
        view_func = TestAPIView.as_view()
        request = request_factory.get("/api/test/")

        middleware.process_view(request, view_func, (), {})

        mock_set.assert_called_once_with(True)

    @patch("pi_dash.middleware.db_routing.set_use_read_replica")
    def test_with_drf_viewset(self, mock_set, middleware, request_factory):
        """Test middleware with DRF ViewSet."""

        class TestViewSet(ViewSet):
            use_read_replica = True

        # Simulate DRF router creating viewset action
        view_func = TestViewSet.as_view({"get": "list"})
        request = request_factory.get("/api/test/")

        middleware.process_view(request, view_func, (), {})

        mock_set.assert_called_once_with(True)


@pytest.mark.unit
class TestEdgeCases:
    """Test edge cases and error conditions."""

    def test_process_view_with_none_view_func(self, middleware, request_factory):
        """Test process_view handles None view_func gracefully."""
        request = request_factory.get("/api/test/")

        result = middleware.process_view(request, None, (), {})

        assert result is None  # Should not crash

    def test_get_use_replica_attribute_with_attribute_error(self, middleware):
        """Test _get_use_replica_attribute with view that raises AttributeError."""

        # Create a view class that raises AttributeError on access
        class ProblematicView:
            def __getattr__(self, name):
                if name == "use_read_replica":
                    raise AttributeError("Simulated attribute error")
                raise AttributeError(f"'{type(self).__name__}' object has no attribute '{name}'")

        view_func = ProblematicView()

        result = middleware._get_use_replica_attribute(view_func)

        assert result is None  # Should handle gracefully

    def test_multiple_exception_calls_are_safe(self, middleware, request_factory):
        """Test that multiple calls to process_exception don't cause issues."""
        request = request_factory.get("/api/test/")
        exception = Exception("Test exception")

        # Call multiple times
        result1 = middleware.process_exception(request, exception)
        result2 = middleware.process_exception(request, exception)

        assert result1 is None  # Both should return None safely
        assert result2 is None
