from unittest.mock import MagicMock, patch


class TestLibreNMSPermissionMixin:
    """Tests for permission mixin functionality."""

    def test_has_write_permission_granted(self):
        """User with change permission has write access."""
        from netbox_librenms_plugin.views.mixins import LibreNMSPermissionMixin

        mixin = LibreNMSPermissionMixin()
        mixin.request = MagicMock()
        mixin.request.user.has_perm.return_value = True

        assert mixin.has_write_permission() is True

    def test_has_write_permission_denied(self):
        """User without change permission lacks write access."""
        from netbox_librenms_plugin.views.mixins import LibreNMSPermissionMixin

        mixin = LibreNMSPermissionMixin()
        mixin.request = MagicMock()
        mixin.request.user.has_perm.return_value = False

        assert mixin.has_write_permission() is False

    def test_require_write_permission_allowed(self):
        """User with write permission gets None (allowed to proceed)."""
        from netbox_librenms_plugin.views.mixins import LibreNMSPermissionMixin

        mixin = LibreNMSPermissionMixin()
        mixin.request = MagicMock()
        mixin.request.user.has_perm.return_value = True

        result = mixin.require_write_permission()
        assert result is None

    def test_require_write_permission_denied(self):
        """User without write permission gets redirect response to referrer."""
        from netbox_librenms_plugin.views.mixins import LibreNMSPermissionMixin

        mixin = LibreNMSPermissionMixin()
        mixin.request = MagicMock()
        mixin.request.user.has_perm.return_value = False
        mixin.request.path = "/some/path/"
        mixin.request.META = {"HTTP_REFERER": "/original/page/"}
        mixin.request.get_host.return_value = "testserver"
        mixin.request.is_secure.return_value = False
        mixin.request.headers = {}  # Not an HTMX request

        with patch("netbox_librenms_plugin.views.mixins.redirect") as mock_redirect:
            with patch("netbox_librenms_plugin.views.mixins.messages"):
                result = mixin.require_write_permission()

        mock_redirect.assert_called_once_with("/original/page/")
        assert result is not None

    def test_require_write_permission_denied_htmx(self):
        """HTMX request without write permission gets HX-Redirect response."""
        from netbox_librenms_plugin.views.mixins import LibreNMSPermissionMixin

        mixin = LibreNMSPermissionMixin()
        mixin.request = MagicMock()
        mixin.request.user.has_perm.return_value = False
        mixin.request.path = "/some/path/"
        mixin.request.META = {"HTTP_REFERER": "/original/page/"}
        mixin.request.get_host.return_value = "testserver"
        mixin.request.is_secure.return_value = False
        mixin.request.headers = {"HX-Request": "true"}

        with patch("netbox_librenms_plugin.views.mixins.messages"):
            result = mixin.require_write_permission()

        # Should return HttpResponse with HX-Redirect header
        assert result is not None
        assert result["HX-Redirect"] == "/original/page/"

    def test_require_write_permission_json_allowed(self):
        """User with write permission gets None (allowed to proceed)."""
        from netbox_librenms_plugin.views.mixins import LibreNMSPermissionMixin

        mixin = LibreNMSPermissionMixin()
        mixin.request = MagicMock()
        mixin.request.user.has_perm.return_value = True

        result = mixin.require_write_permission_json()
        assert result is None

    def test_require_write_permission_json_denied(self):
        """User without write permission gets JsonResponse with 403."""
        import json
        from netbox_librenms_plugin.views.mixins import LibreNMSPermissionMixin

        mixin = LibreNMSPermissionMixin()
        mixin.request = MagicMock()
        mixin.request.user.has_perm.return_value = False

        result = mixin.require_write_permission_json()

        assert result is not None
        assert result.status_code == 403
        content = json.loads(result.content)
        assert content["error"] == "You do not have permission to perform this action."

    def test_require_write_permission_json_custom_message(self):
        """Custom error message is returned in JsonResponse."""
        import json
        from netbox_librenms_plugin.views.mixins import LibreNMSPermissionMixin

        mixin = LibreNMSPermissionMixin()
        mixin.request = MagicMock()
        mixin.request.user.has_perm.return_value = False

        result = mixin.require_write_permission_json(error_message="Custom denied message")

        assert result is not None
        assert result.status_code == 403
        content = json.loads(result.content)
        assert content["error"] == "Custom denied message"


class TestAPIPermissions:
    """Tests for API permission class."""

    def test_get_requires_view_permission(self):
        """GET requests require view permission."""
        from netbox_librenms_plugin.api.views import LibreNMSPluginPermission
        from netbox_librenms_plugin.constants import PERM_VIEW_PLUGIN

        permission = LibreNMSPluginPermission()
        request = MagicMock()
        request.method = "GET"
        request.user.has_perm.return_value = True

        assert permission.has_permission(request, None) is True
        request.user.has_perm.assert_called_with(PERM_VIEW_PLUGIN)

    def test_post_requires_change_permission(self):
        """POST requests require change permission."""
        from netbox_librenms_plugin.api.views import LibreNMSPluginPermission
        from netbox_librenms_plugin.constants import PERM_CHANGE_PLUGIN

        permission = LibreNMSPluginPermission()
        request = MagicMock()
        request.method = "POST"
        request.user.has_perm.return_value = True

        assert permission.has_permission(request, None) is True
        request.user.has_perm.assert_called_with(PERM_CHANGE_PLUGIN)

    def test_put_requires_change_permission(self):
        """PUT requests require change permission."""
        from netbox_librenms_plugin.api.views import LibreNMSPluginPermission
        from netbox_librenms_plugin.constants import PERM_CHANGE_PLUGIN

        permission = LibreNMSPluginPermission()
        request = MagicMock()
        request.method = "PUT"
        request.user.has_perm.return_value = True

        assert permission.has_permission(request, None) is True
        request.user.has_perm.assert_called_with(PERM_CHANGE_PLUGIN)

    def test_delete_requires_change_permission(self):
        """DELETE requests require change permission."""
        from netbox_librenms_plugin.api.views import LibreNMSPluginPermission
        from netbox_librenms_plugin.constants import PERM_CHANGE_PLUGIN

        permission = LibreNMSPluginPermission()
        request = MagicMock()
        request.method = "DELETE"
        request.user.has_perm.return_value = True

        assert permission.has_permission(request, None) is True
        request.user.has_perm.assert_called_with(PERM_CHANGE_PLUGIN)

    def test_get_denied_without_view_permission(self):
        """GET requests denied without view permission."""
        from netbox_librenms_plugin.api.views import LibreNMSPluginPermission

        permission = LibreNMSPluginPermission()
        request = MagicMock()
        request.method = "GET"
        request.user.has_perm.return_value = False

        assert permission.has_permission(request, None) is False

    def test_post_denied_without_change_permission(self):
        """POST requests denied without change permission."""
        from netbox_librenms_plugin.api.views import LibreNMSPluginPermission

        permission = LibreNMSPluginPermission()
        request = MagicMock()
        request.method = "POST"
        request.user.has_perm.return_value = False

        assert permission.has_permission(request, None) is False


class TestPermissionConstants:
    """Tests for permission constants."""

    def test_view_permission_constant(self):
        """View permission constant is correct."""
        from netbox_librenms_plugin.constants import PERM_VIEW_PLUGIN

        assert PERM_VIEW_PLUGIN == "netbox_librenms_plugin.view_librenmssettings"

    def test_change_permission_constant(self):
        """Change permission constant is correct."""
        from netbox_librenms_plugin.constants import PERM_CHANGE_PLUGIN

        assert PERM_CHANGE_PLUGIN == "netbox_librenms_plugin.change_librenmssettings"


# =============================================================================
# Phase 2: Object Permission Tests
# =============================================================================


class TestObjectPermissionHelpers:
    """Tests for Phase 2 object permission helper functions."""

    def test_check_user_permissions_all_granted(self):
        """Returns True when user has all permissions."""
        from netbox_librenms_plugin.import_utils import check_user_permissions

        user = MagicMock()
        user.has_perm.return_value = True

        has_all, missing = check_user_permissions(user, ["dcim.add_device", "dcim.add_interface"])

        assert has_all is True
        assert missing == []
        assert user.has_perm.call_count == 2

    def test_check_user_permissions_some_missing(self):
        """Returns False with list of missing permissions."""
        from netbox_librenms_plugin.import_utils import check_user_permissions

        user = MagicMock()
        user.has_perm.side_effect = lambda p: p != "dcim.add_interface"

        has_all, missing = check_user_permissions(user, ["dcim.add_device", "dcim.add_interface"])

        assert has_all is False
        assert missing == ["dcim.add_interface"]

    def test_check_user_permissions_all_missing(self):
        """Returns False with all permissions listed as missing."""
        from netbox_librenms_plugin.import_utils import check_user_permissions

        user = MagicMock()
        user.has_perm.return_value = False

        has_all, missing = check_user_permissions(user, ["dcim.add_device", "dcim.add_interface"])

        assert has_all is False
        assert "dcim.add_device" in missing
        assert "dcim.add_interface" in missing

    def test_check_user_permissions_no_user(self):
        """Raises PermissionDenied when user is None."""
        import pytest
        from django.core.exceptions import PermissionDenied

        from netbox_librenms_plugin.import_utils import check_user_permissions

        with pytest.raises(PermissionDenied, match="No user context"):
            check_user_permissions(None, ["dcim.add_device"])

    def test_require_permissions_passes_when_granted(self):
        """Does not raise when user has all permissions."""
        from netbox_librenms_plugin.import_utils import require_permissions

        user = MagicMock()
        user.has_perm.return_value = True

        # Should not raise
        require_permissions(user, ["dcim.add_device", "dcim.add_interface"], "import devices")

    def test_require_permissions_raises_on_missing(self):
        """Raises PermissionDenied with descriptive message."""
        import pytest
        from django.core.exceptions import PermissionDenied

        from netbox_librenms_plugin.import_utils import require_permissions

        user = MagicMock()
        user.has_perm.return_value = False

        with pytest.raises(PermissionDenied) as exc_info:
            require_permissions(user, ["dcim.add_device"], "import devices")

        # Check error message contains action description and missing permission
        assert "import devices" in str(exc_info.value)
        assert "dcim.add_device" in str(exc_info.value)

    def test_require_permissions_lists_multiple_missing(self):
        """Error message includes all missing permissions."""
        import pytest
        from django.core.exceptions import PermissionDenied

        from netbox_librenms_plugin.import_utils import require_permissions

        user = MagicMock()
        user.has_perm.return_value = False

        with pytest.raises(PermissionDenied) as exc_info:
            require_permissions(
                user,
                ["dcim.add_device", "dcim.add_interface"],
                "import devices",
            )

        error_msg = str(exc_info.value)
        assert "dcim.add_device" in error_msg
        assert "dcim.add_interface" in error_msg


class TestNetBoxObjectPermissionMixin:
    """Tests for the NetBoxObjectPermissionMixin class."""

    def test_check_object_permissions_all_granted(self):
        """Returns True when user has all object permissions."""
        from netbox_librenms_plugin.views.mixins import NetBoxObjectPermissionMixin

        mixin = NetBoxObjectPermissionMixin()
        mixin.request = MagicMock()
        mixin.request.user.has_perm.return_value = True

        mock_model = MagicMock()
        mixin.required_object_permissions = {
            "POST": [("add", mock_model), ("change", mock_model)],
        }

        with patch("netbox_librenms_plugin.views.mixins.get_permission_for_model") as mock_get:
            mock_get.side_effect = ["dcim.add_interface", "dcim.change_interface"]
            has_all, missing = mixin.check_object_permissions("POST")

        assert has_all is True
        assert missing == []

    def test_check_object_permissions_some_missing(self):
        """Returns False with missing permission strings."""
        from netbox_librenms_plugin.views.mixins import NetBoxObjectPermissionMixin

        mixin = NetBoxObjectPermissionMixin()
        mixin.request = MagicMock()
        mixin.request.user.has_perm.side_effect = lambda p: p != "dcim.add_interface"

        mock_model = MagicMock()
        mixin.required_object_permissions = {
            "POST": [("add", mock_model)],
        }

        with patch("netbox_librenms_plugin.views.mixins.get_permission_for_model") as mock_get:
            mock_get.return_value = "dcim.add_interface"
            has_all, missing = mixin.check_object_permissions("POST")

        assert has_all is False
        assert "dcim.add_interface" in missing

    def test_check_object_permissions_no_requirements(self):
        """Returns True when no permissions required for method."""
        from netbox_librenms_plugin.views.mixins import NetBoxObjectPermissionMixin

        mixin = NetBoxObjectPermissionMixin()
        mixin.request = MagicMock()
        mixin.required_object_permissions = {}  # No requirements

        has_all, missing = mixin.check_object_permissions("POST")

        assert has_all is True
        assert missing == []

    def test_require_object_permissions_returns_none_when_granted(self):
        """Returns None when all permissions are granted."""
        from netbox_librenms_plugin.views.mixins import NetBoxObjectPermissionMixin

        mixin = NetBoxObjectPermissionMixin()
        mixin.request = MagicMock()
        mixin.request.user.has_perm.return_value = True

        mock_model = MagicMock()
        mixin.required_object_permissions = {
            "POST": [("add", mock_model)],
        }

        with patch("netbox_librenms_plugin.views.mixins.get_permission_for_model") as mock_get:
            mock_get.return_value = "dcim.add_cable"
            response = mixin.require_object_permissions("POST")

        assert response is None

    def test_require_object_permissions_returns_redirect_response(self):
        """Returns redirect response with message when permissions missing."""
        from netbox_librenms_plugin.views.mixins import NetBoxObjectPermissionMixin

        mixin = NetBoxObjectPermissionMixin()
        mixin.request = MagicMock()
        mixin.request.user.has_perm.return_value = False
        mixin.request.path = "/original/page/"
        mixin.request.META = {"HTTP_REFERER": "/original/page/"}
        mixin.request.get_host.return_value = "testserver"
        mixin.request.is_secure.return_value = False
        mixin.request.headers = {}  # Not an HTMX request

        mock_model = MagicMock()
        mixin.required_object_permissions = {
            "POST": [("add", mock_model)],
        }

        with patch("netbox_librenms_plugin.views.mixins.get_permission_for_model") as mock_get:
            with patch("netbox_librenms_plugin.views.mixins.messages") as mock_messages:
                with patch("netbox_librenms_plugin.views.mixins.redirect") as mock_redirect:
                    mock_get.return_value = "dcim.add_cable"
                    response = mixin.require_object_permissions("POST")

        assert response is not None
        # Verify error message was added
        mock_messages.error.assert_called_once()
        error_msg = mock_messages.error.call_args[0][1]
        assert "dcim.add_cable" in error_msg
        # Verify redirect was called
        mock_redirect.assert_called_once_with("/original/page/")

    def test_require_object_permissions_htmx_returns_hx_redirect(self):
        """HTMX request returns HX-Redirect header when permissions missing."""
        from netbox_librenms_plugin.views.mixins import NetBoxObjectPermissionMixin

        mixin = NetBoxObjectPermissionMixin()
        mixin.request = MagicMock()
        mixin.request.user.has_perm.return_value = False
        mixin.request.path = "/original/page/"
        mixin.request.META = {"HTTP_REFERER": "/original/page/"}
        mixin.request.get_host.return_value = "testserver"
        mixin.request.is_secure.return_value = False
        mixin.request.headers = {"HX-Request": "true"}

        mock_model = MagicMock()
        mixin.required_object_permissions = {
            "POST": [("add", mock_model)],
        }

        with patch("netbox_librenms_plugin.views.mixins.get_permission_for_model") as mock_get:
            with patch("netbox_librenms_plugin.views.mixins.messages"):
                mock_get.return_value = "dcim.add_cable"
                response = mixin.require_object_permissions("POST")

        assert response is not None
        assert response["HX-Redirect"] == "/original/page/"

    def test_require_object_permissions_json_allowed(self):
        """Returns None when all object permissions are granted."""
        from netbox_librenms_plugin.views.mixins import NetBoxObjectPermissionMixin

        mixin = NetBoxObjectPermissionMixin()
        mixin.request = MagicMock()
        mixin.request.user.has_perm.return_value = True

        mock_model = MagicMock()
        mixin.required_object_permissions = {
            "POST": [("delete", mock_model)],
        }

        with patch("netbox_librenms_plugin.views.mixins.get_permission_for_model") as mock_get:
            mock_get.return_value = "dcim.delete_interface"
            response = mixin.require_object_permissions_json("POST")

        assert response is None

    def test_require_object_permissions_json_denied(self):
        """Returns JsonResponse with 403 when object permissions missing."""
        import json

        from netbox_librenms_plugin.views.mixins import NetBoxObjectPermissionMixin

        mixin = NetBoxObjectPermissionMixin()
        mixin.request = MagicMock()
        mixin.request.user.has_perm.return_value = False

        mock_model = MagicMock()
        mixin.required_object_permissions = {
            "POST": [("delete", mock_model)],
        }

        with patch("netbox_librenms_plugin.views.mixins.get_permission_for_model") as mock_get:
            mock_get.return_value = "dcim.delete_interface"
            response = mixin.require_object_permissions_json("POST")

        assert response is not None
        assert response.status_code == 403
        content = json.loads(response.content)
        assert "dcim.delete_interface" in content["error"]

    def test_require_all_permissions_allowed(self):
        """Returns None when both write and object permissions granted."""
        from netbox_librenms_plugin.views.mixins import (
            LibreNMSPermissionMixin,
            NetBoxObjectPermissionMixin,
        )

        class TestView(LibreNMSPermissionMixin, NetBoxObjectPermissionMixin):
            pass

        mixin = TestView()
        mixin.request = MagicMock()
        mixin.request.user.has_perm.return_value = True

        mock_model = MagicMock()
        mixin.required_object_permissions = {
            "POST": [("change", mock_model)],
        }

        with patch("netbox_librenms_plugin.views.mixins.get_permission_for_model") as mock_get:
            mock_get.return_value = "dcim.change_device"
            response = mixin.require_all_permissions("POST")

        assert response is None

    def test_require_all_permissions_denied_write(self):
        """Returns error when write permission denied (doesn't check object perms)."""
        from netbox_librenms_plugin.views.mixins import (
            LibreNMSPermissionMixin,
            NetBoxObjectPermissionMixin,
        )

        class TestView(LibreNMSPermissionMixin, NetBoxObjectPermissionMixin):
            pass

        mixin = TestView()
        mixin.request = MagicMock()
        mixin.request.user.has_perm.return_value = False
        mixin.request.path = "/original/page/"
        mixin.request.META = {"HTTP_REFERER": "/original/page/"}
        mixin.request.get_host.return_value = "testserver"
        mixin.request.is_secure.return_value = False
        mixin.request.headers = {}

        mixin.required_object_permissions = {"POST": []}

        with patch("netbox_librenms_plugin.views.mixins.redirect") as mock_redirect:
            with patch("netbox_librenms_plugin.views.mixins.messages"):
                response = mixin.require_all_permissions("POST")

        assert response is not None
        mock_redirect.assert_called_once_with("/original/page/")

    def test_require_all_permissions_denied_object(self):
        """Returns error when object permissions denied (write passes)."""
        from netbox_librenms_plugin.views.mixins import (
            LibreNMSPermissionMixin,
            NetBoxObjectPermissionMixin,
        )

        class TestView(LibreNMSPermissionMixin, NetBoxObjectPermissionMixin):
            pass

        mixin = TestView()
        mixin.request = MagicMock()
        # has_write_permission passes, but object perms fail
        mixin.request.user.has_perm.side_effect = lambda p: p == "netbox_librenms_plugin.change_librenmssettings"
        mixin.request.path = "/original/page/"
        mixin.request.META = {"HTTP_REFERER": "/original/page/"}
        mixin.request.get_host.return_value = "testserver"
        mixin.request.is_secure.return_value = False
        mixin.request.headers = {}

        mock_model = MagicMock()
        mixin.required_object_permissions = {
            "POST": [("add", mock_model)],
        }

        with patch("netbox_librenms_plugin.views.mixins.get_permission_for_model") as mock_get:
            with patch("netbox_librenms_plugin.views.mixins.messages"):
                with patch("netbox_librenms_plugin.views.mixins.redirect") as mock_redirect:
                    mock_get.return_value = "dcim.add_device"
                    response = mixin.require_all_permissions("POST")

        assert response is not None
        mock_redirect.assert_called_once()

    def test_require_all_permissions_json_allowed(self):
        """Returns None when both write and object permissions granted (JSON variant)."""
        from netbox_librenms_plugin.views.mixins import (
            LibreNMSPermissionMixin,
            NetBoxObjectPermissionMixin,
        )

        class TestView(LibreNMSPermissionMixin, NetBoxObjectPermissionMixin):
            pass

        mixin = TestView()
        mixin.request = MagicMock()
        mixin.request.user.has_perm.return_value = True

        mock_model = MagicMock()
        mixin.required_object_permissions = {
            "POST": [("delete", mock_model)],
        }

        with patch("netbox_librenms_plugin.views.mixins.get_permission_for_model") as mock_get:
            mock_get.return_value = "dcim.delete_interface"
            response = mixin.require_all_permissions_json("POST")

        assert response is None

    def test_require_all_permissions_json_denied_write(self):
        """Returns JSON 403 when write permission denied (JSON variant)."""
        import json

        from netbox_librenms_plugin.views.mixins import (
            LibreNMSPermissionMixin,
            NetBoxObjectPermissionMixin,
        )

        class TestView(LibreNMSPermissionMixin, NetBoxObjectPermissionMixin):
            pass

        mixin = TestView()
        mixin.request = MagicMock()
        mixin.request.user.has_perm.return_value = False

        response = mixin.require_all_permissions_json("POST")

        assert response is not None
        assert response.status_code == 403
        content = json.loads(response.content)
        assert "error" in content


class TestBulkImportPermissions:
    """Tests for permission checks in bulk import functions."""

    @patch("netbox_librenms_plugin.import_utils.bulk_import.require_permissions")
    @patch("netbox_librenms_plugin.import_utils.bulk_import.LibreNMSAPI")
    def test_bulk_import_devices_checks_permissions(self, mock_api_class, mock_require):
        """bulk_import_devices_shared calls require_permissions."""
        from netbox_librenms_plugin.import_utils import bulk_import_devices_shared

        user = MagicMock()
        mock_api = MagicMock()
        mock_api_class.return_value = mock_api

        # Set up API to return empty device so loop completes quickly
        mock_api.get_device_info.return_value = (False, None)

        bulk_import_devices_shared(
            device_ids=[1],
            user=user,
            server_key="default",
        )

        mock_require.assert_called_once()
        call_args = mock_require.call_args
        assert user == call_args[0][0]
        assert "dcim.add_device" in call_args[0][1]
        assert "dcim.change_device" in call_args[0][1]
        assert "dcim.add_interface" not in call_args[0][1]

    @patch("netbox_librenms_plugin.import_utils.bulk_import.require_permissions")
    @patch("netbox_librenms_plugin.import_utils.bulk_import.LibreNMSAPI")
    def test_bulk_import_devices_extracts_user_from_job(self, mock_api_class, mock_require):
        """bulk_import_devices_shared extracts user from job if not provided."""
        from netbox_librenms_plugin.import_utils import bulk_import_devices_shared

        job_user = MagicMock()
        job = MagicMock()
        job.job.user = job_user

        mock_api = MagicMock()
        mock_api_class.return_value = mock_api
        mock_api.get_device_info.return_value = (False, None)

        bulk_import_devices_shared(
            device_ids=[1],
            job=job,
            server_key="default",
        )

        mock_require.assert_called_once()
        call_args = mock_require.call_args
        assert job_user == call_args[0][0]

    @patch("netbox_librenms_plugin.import_utils.vm_operations.require_permissions")
    def test_bulk_import_vms_checks_permissions(self, mock_require):
        """bulk_import_vms calls require_permissions."""
        from netbox_librenms_plugin.import_utils import bulk_import_vms

        user = MagicMock()
        api = MagicMock()
        api.server_key = "default"

        # Empty vm_imports to complete quickly
        bulk_import_vms(
            vm_imports={},
            api=api,
            user=user,
        )

        mock_require.assert_called_once()
        call_args = mock_require.call_args
        assert user == call_args[0][0]
        assert "virtualization.add_virtualmachine" in call_args[0][1]

    @patch("netbox_librenms_plugin.import_utils.vm_operations.require_permissions")
    def test_bulk_import_vms_extracts_user_from_job(self, mock_require):
        """bulk_import_vms extracts user from job if not provided."""
        from netbox_librenms_plugin.import_utils import bulk_import_vms

        job_user = MagicMock()
        job = MagicMock()
        job.job.user = job_user

        api = MagicMock()
        api.server_key = "default"

        bulk_import_vms(
            vm_imports={},
            api=api,
            job=job,
        )

        mock_require.assert_called_once()
        call_args = mock_require.call_args
        assert job_user == call_args[0][0]


class TestBulkImportPermissionDenied:
    """Tests for permission denied behavior in bulk import."""

    @patch("netbox_librenms_plugin.import_utils.permissions.check_user_permissions")
    def test_bulk_import_devices_raises_on_missing_permissions(self, mock_check):
        """bulk_import_devices_shared raises PermissionDenied when permissions missing."""
        import pytest
        from django.core.exceptions import PermissionDenied

        from netbox_librenms_plugin.import_utils import bulk_import_devices_shared

        mock_check.return_value = (False, ["dcim.add_device"])

        user = MagicMock()

        with pytest.raises(PermissionDenied):
            bulk_import_devices_shared(
                device_ids=[1],
                user=user,
                server_key="default",
            )

    @patch("netbox_librenms_plugin.import_utils.permissions.check_user_permissions")
    def test_bulk_import_vms_raises_on_missing_permissions(self, mock_check):
        """bulk_import_vms raises PermissionDenied when permissions missing."""
        import pytest
        from django.core.exceptions import PermissionDenied

        from netbox_librenms_plugin.import_utils import bulk_import_vms

        mock_check.return_value = (False, ["virtualization.add_virtualmachine"])

        user = MagicMock()
        api = MagicMock()

        with pytest.raises(PermissionDenied):
            bulk_import_vms(
                vm_imports={1: {"cluster_id": 1}},
                api=api,
                user=user,
            )


class TestSafeRedirectUrl:
    """Tests for the _get_safe_redirect_url helper."""

    def test_internal_referrer_is_accepted(self):
        """Internal referrer URL is returned when host matches."""
        from netbox_librenms_plugin.views.mixins import _get_safe_redirect_url

        request = MagicMock()
        request.META = {"HTTP_REFERER": "http://testserver/some/page/"}
        request.get_host.return_value = "testserver"
        request.is_secure.return_value = False
        request.path = "/fallback/"

        result = _get_safe_redirect_url(request)
        assert result == "http://testserver/some/page/"

    def test_external_referrer_is_rejected(self):
        """External referrer URL is rejected, falls back to request.path."""
        from netbox_librenms_plugin.views.mixins import _get_safe_redirect_url

        request = MagicMock()
        request.META = {"HTTP_REFERER": "http://evil.com/attack"}
        request.get_host.return_value = "testserver"
        request.is_secure.return_value = False
        request.path = "/safe/fallback/"

        result = _get_safe_redirect_url(request)
        assert result == "/safe/fallback/"

    def test_no_referrer_falls_back_to_path(self):
        """Missing referrer falls back to request.path."""
        from netbox_librenms_plugin.views.mixins import _get_safe_redirect_url

        request = MagicMock()
        request.META = {}
        request.path = "/current/page/"

        result = _get_safe_redirect_url(request)
        assert result == "/current/page/"

    def test_no_referrer_no_path_falls_back_to_slash(self):
        """Missing referrer and no path attribute falls back to '/'."""
        from netbox_librenms_plugin.views.mixins import _get_safe_redirect_url

        request = MagicMock(spec=[])  # No attributes at all
        request.META = {}

        result = _get_safe_redirect_url(request)
        assert result == "/"

    def test_relative_referrer_is_accepted(self):
        """Relative referrer path is accepted (no host to mismatch)."""
        from netbox_librenms_plugin.views.mixins import _get_safe_redirect_url

        request = MagicMock()
        request.META = {"HTTP_REFERER": "/original/page/"}
        request.get_host.return_value = "testserver"
        request.is_secure.return_value = False
        request.path = "/fallback/"

        result = _get_safe_redirect_url(request)
        assert result == "/original/page/"

    def test_write_permission_denied_rejects_external_referrer(self):
        """Write permission denial with external referrer falls back to request.path."""
        from netbox_librenms_plugin.views.mixins import LibreNMSPermissionMixin

        mixin = LibreNMSPermissionMixin()
        mixin.request = MagicMock()
        mixin.request.user.has_perm.return_value = False
        mixin.request.path = "/safe/page/"
        mixin.request.META = {"HTTP_REFERER": "http://evil.com/steal"}
        mixin.request.get_host.return_value = "testserver"
        mixin.request.is_secure.return_value = False
        mixin.request.headers = {}

        with patch("netbox_librenms_plugin.views.mixins.redirect") as mock_redirect:
            with patch("netbox_librenms_plugin.views.mixins.messages"):
                mixin.require_write_permission()

        mock_redirect.assert_called_once_with("/safe/page/")

    def test_htmx_rejects_external_referrer(self):
        """HTMX request with external referrer uses fallback in HX-Redirect."""
        from netbox_librenms_plugin.views.mixins import LibreNMSPermissionMixin

        mixin = LibreNMSPermissionMixin()
        mixin.request = MagicMock()
        mixin.request.user.has_perm.return_value = False
        mixin.request.path = "/safe/page/"
        mixin.request.META = {"HTTP_REFERER": "http://evil.com/steal"}
        mixin.request.get_host.return_value = "testserver"
        mixin.request.is_secure.return_value = False
        mixin.request.headers = {"HX-Request": "true"}

        with patch("netbox_librenms_plugin.views.mixins.messages"):
            result = mixin.require_write_permission()

        assert result["HX-Redirect"] == "/safe/page/"


class TestBulkImportVCPermission:
    """Tests that bulk import checks virtualchassis permission."""

    @patch("netbox_librenms_plugin.import_utils.bulk_import.require_permissions")
    @patch("netbox_librenms_plugin.import_utils.bulk_import.LibreNMSAPI")
    def test_bulk_import_devices_checks_vc_permission(self, mock_api_class, mock_require):
        """bulk_import_devices_shared includes dcim.add_virtualchassis in required perms."""
        from netbox_librenms_plugin.import_utils import bulk_import_devices_shared

        user = MagicMock()
        mock_api = MagicMock()
        mock_api_class.return_value = mock_api
        mock_api.get_device_info.return_value = (False, None)

        bulk_import_devices_shared(
            device_ids=[1],
            user=user,
            server_key="default",
        )

        mock_require.assert_called_once()
        call_args = mock_require.call_args
        # After Fix 6: interface/VC permissions removed from initial check
        assert "dcim.add_virtualchassis" not in call_args[0][1]
        assert "dcim.add_device" in call_args[0][1]


class TestObjectTypeValidation:
    """Tests that get_required_permissions_for_object_type validates object_type."""

    def test_sync_interfaces_device_type(self):
        """SyncInterfacesView returns correct perms for device type."""
        from netbox_librenms_plugin.views.sync.interfaces import SyncInterfacesView

        view = SyncInterfacesView()
        perms = view.get_required_permissions_for_object_type("device")
        assert len(perms) == 2

    def test_sync_interfaces_vm_type(self):
        """SyncInterfacesView returns correct perms for virtualmachine type."""
        from netbox_librenms_plugin.views.sync.interfaces import SyncInterfacesView

        view = SyncInterfacesView()
        perms = view.get_required_permissions_for_object_type("virtualmachine")
        assert len(perms) == 2

    def test_sync_interfaces_invalid_type_raises_404(self):
        """SyncInterfacesView raises Http404 for invalid object type."""
        import pytest
        from django.http import Http404

        from netbox_librenms_plugin.views.sync.interfaces import SyncInterfacesView

        view = SyncInterfacesView()
        with pytest.raises(Http404):
            view.get_required_permissions_for_object_type("invalid")

    def test_delete_interfaces_device_type(self):
        """DeleteNetBoxInterfacesView returns correct perms for device type."""
        from netbox_librenms_plugin.views.sync.interfaces import DeleteNetBoxInterfacesView

        view = DeleteNetBoxInterfacesView()
        perms = view.get_required_permissions_for_object_type("device")
        assert len(perms) == 1

    def test_delete_interfaces_vm_type(self):
        """DeleteNetBoxInterfacesView returns correct perms for virtualmachine type."""
        from netbox_librenms_plugin.views.sync.interfaces import DeleteNetBoxInterfacesView

        view = DeleteNetBoxInterfacesView()
        perms = view.get_required_permissions_for_object_type("virtualmachine")
        assert len(perms) == 1

    def test_delete_interfaces_invalid_type_raises_404(self):
        """DeleteNetBoxInterfacesView raises Http404 for invalid object type."""
        import pytest
        from django.http import Http404

        from netbox_librenms_plugin.views.sync.interfaces import DeleteNetBoxInterfacesView

        view = DeleteNetBoxInterfacesView()
        with pytest.raises(Http404):
            view.get_required_permissions_for_object_type("invalid")


# ---------------------------------------------------------------------------
# Tests for RemoveServerMappingView error handling (device_fields.py)
# ---------------------------------------------------------------------------


class TestRemoveServerMappingViewErrorHandling:
    """Test RemoveServerMappingView handles full_clean/save failures gracefully."""

    def _make_view(self, server_key, post_extra=None):
        """Return a (view, request) pair with permissions satisfied."""
        from unittest.mock import MagicMock

        from netbox_librenms_plugin.views.sync.device_fields import RemoveServerMappingView

        request = MagicMock()
        request.POST = {"server_key": server_key, **(post_extra or {})}
        request.user = MagicMock()
        request.user.has_perm.return_value = True

        view = RemoveServerMappingView()
        view.request = request  # required by mixin's has_write_permission
        return view, request

    def test_validation_error_returns_error_message(self):
        """ValidationError from full_clean leads to error message, not 500."""
        from unittest.mock import MagicMock, patch

        from django.core.exceptions import ValidationError

        view, request = self._make_view(server_key="orphan-server")

        mock_device = MagicMock()
        mock_device.custom_field_data = {"librenms_id": {"orphan-server": 99}}

        mock_locked = MagicMock()
        mock_locked.custom_field_data = {"librenms_id": {"orphan-server": 99}}
        mock_locked.full_clean.side_effect = ValidationError("CF validation failed")

        plugins_cfg = {"netbox_librenms_plugin": {"servers": {}}}  # orphan-server NOT configured

        with (
            patch("netbox_librenms_plugin.views.sync.device_fields.get_object_or_404", return_value=mock_device),
            patch("netbox_librenms_plugin.views.sync.device_fields.Device") as mock_Device_cls,
            patch("django.conf.settings") as mock_settings,
            patch("netbox_librenms_plugin.views.sync.device_fields.messages") as mock_messages,
            patch("netbox_librenms_plugin.views.sync.device_fields.redirect"),
            patch("netbox_librenms_plugin.views.sync.device_fields.transaction") as mock_tx,
        ):
            mock_settings.PLUGINS_CONFIG = plugins_cfg
            mock_Device_cls.objects.select_for_update.return_value.get.return_value = mock_locked

            # Make transaction.atomic() a no-op context manager
            mock_tx.atomic.return_value.__enter__ = lambda s: None
            mock_tx.atomic.return_value.__exit__ = lambda s, *a: None
            mock_tx.set_rollback = MagicMock()

            view.post(request, pk=1)

        mock_messages.error.assert_called_once()
        error_args = mock_messages.error.call_args[0]
        assert "Validation error" in str(error_args[1]) or "CF validation failed" in str(error_args[1])

    def test_configured_server_refused(self):
        """Configured server mapping cannot be removed — error message shown."""
        from unittest.mock import MagicMock, patch

        view, request = self._make_view(server_key="active-server")
        mock_device = MagicMock()
        mock_device.custom_field_data = {"librenms_id": {"active-server": 5}}

        plugins_cfg = {"netbox_librenms_plugin": {"servers": {"active-server": {"librenms_url": "http://x"}}}}

        with (
            patch("netbox_librenms_plugin.views.sync.device_fields.get_object_or_404", return_value=mock_device),
            patch("django.conf.settings") as mock_settings,
            patch("netbox_librenms_plugin.views.sync.device_fields.messages") as mock_messages,
            patch("netbox_librenms_plugin.views.sync.device_fields.redirect"),
        ):
            mock_settings.PLUGINS_CONFIG = plugins_cfg
            view.post(request, pk=1)

        mock_messages.error.assert_called_once()
        assert "Cannot remove" in mock_messages.error.call_args[0][1]

    def test_successful_removal_mutates_and_saves(self):
        """Successful removal deletes the key from custom_field_data and saves the device."""
        from unittest.mock import MagicMock, patch

        view, request = self._make_view(server_key="orphan-server")

        mock_device = MagicMock()
        mock_device.custom_field_data = {"librenms_id": {"orphan-server": 42, "other-server": 7}}

        mock_locked = MagicMock()
        mock_locked.custom_field_data = {"librenms_id": {"orphan-server": 42, "other-server": 7}}
        mock_locked.full_clean = MagicMock()
        mock_locked.save = MagicMock()

        plugins_cfg = {"netbox_librenms_plugin": {"servers": {}}}  # orphan-server NOT configured

        with (
            patch("netbox_librenms_plugin.views.sync.device_fields.get_object_or_404", return_value=mock_device),
            patch("netbox_librenms_plugin.views.sync.device_fields.Device") as mock_Device_cls,
            patch("django.conf.settings") as mock_settings,
            patch("netbox_librenms_plugin.views.sync.device_fields.messages") as mock_messages,
            patch("netbox_librenms_plugin.views.sync.device_fields.redirect"),
            patch("netbox_librenms_plugin.views.sync.device_fields.transaction") as mock_tx,
        ):
            mock_settings.PLUGINS_CONFIG = plugins_cfg
            mock_Device_cls.objects.select_for_update.return_value.get.return_value = mock_locked

            mock_tx.atomic.return_value.__enter__ = lambda s: None
            mock_tx.atomic.return_value.__exit__ = lambda s, *a: None
            mock_tx.set_rollback = MagicMock()

            view.post(request, pk=1)

        # The "orphan-server" key should have been removed and the device saved.
        # Assert the exact shape of custom_field_data so misspelled keys are caught.
        assert mock_locked.custom_field_data == {"librenms_id": {"other-server": 7}}
        remaining = mock_locked.custom_field_data["librenms_id"]
        assert "orphan-server" not in remaining
        assert remaining.get("other-server") == 7  # sibling key preserved
        mock_locked.save.assert_called_once()
        mock_messages.success.assert_called_once()
