"""
Coverage tests for:
- netbox_librenms_plugin/api/views.py (sync_job_status, InterfaceTypeMappingViewSet)
- netbox_librenms_plugin/filtersets.py
- netbox_librenms_plugin/models.py (lines 45, 48, 68, 76)
"""

import json
from unittest.mock import MagicMock, patch


# ===========================================================================
# Helpers
# ===========================================================================


def _make_post_request():
    """Return a minimal Django HttpRequest suitable for DRF view tests."""
    from django.http import HttpRequest

    request = HttpRequest()
    request.method = "POST"
    request.META["SERVER_NAME"] = "localhost"
    request.META["SERVER_PORT"] = "80"
    return request


def _call_sync_job_status(job_pk, job_patch, rq_patch=None, queue_patch=None):
    """
    Call sync_job_status view, bypassing DRF auth/permission layer.

    Returns the raw Django response object.
    """
    from netbox_librenms_plugin.api.views import sync_job_status

    request = _make_post_request()

    # Bypass DRF initial() so we skip auth/permissions entirely
    with patch("rest_framework.views.APIView.initial"):
        with patch("netbox_librenms_plugin.api.views.Job", job_patch):
            if rq_patch is not None and queue_patch is not None:
                with patch("netbox_librenms_plugin.api.views.RQJob", rq_patch):
                    with patch("netbox_librenms_plugin.api.views.get_queue", queue_patch):
                        return sync_job_status(request, job_pk=job_pk)
            return sync_job_status(request, job_pk=job_pk)


# ===========================================================================
# api/views.py – sync_job_status
# ===========================================================================


class TestSyncJobStatusJobNotFound:
    """Test sync_job_status when the DB job does not exist."""

    def test_returns_404_when_job_missing(self):
        class _DoesNotExist(Exception):
            pass

        mock_job_cls = MagicMock()
        mock_job_cls.DoesNotExist = _DoesNotExist
        mock_job_cls.objects.get.side_effect = _DoesNotExist

        response = _call_sync_job_status(job_pk=999, job_patch=mock_job_cls)

        assert response.status_code == 404
        data = json.loads(response.content)
        assert data["error"] == "Job not found"


class TestSyncJobStatusRQJobActive:
    """Test sync_job_status when RQ job is still active (no update needed)."""

    def test_no_change_when_rq_job_running(self):
        from core.choices import JobStatusChoices

        mock_db_job = MagicMock()
        mock_db_job.pk = 1
        mock_db_job.status = JobStatusChoices.STATUS_RUNNING
        mock_db_job.completed = None

        class _DoesNotExist(Exception):
            pass

        mock_job_cls = MagicMock()
        mock_job_cls.DoesNotExist = _DoesNotExist
        mock_job_cls.objects.get.return_value = mock_db_job

        mock_rq_job = MagicMock()
        mock_rq_job.is_stopped = False
        mock_rq_job.is_failed = False
        mock_rq_job.get_status.return_value = "started"

        mock_rq_cls = MagicMock()
        mock_rq_cls.fetch.return_value = mock_rq_job

        mock_queue = MagicMock()
        mock_queue_fn = MagicMock(return_value=mock_queue)

        response = _call_sync_job_status(
            job_pk=1,
            job_patch=mock_job_cls,
            rq_patch=mock_rq_cls,
            queue_patch=mock_queue_fn,
        )

        assert response.status_code == 200
        data = json.loads(response.content)
        assert data["status"] == "no_change"
        assert data["rq_status"] == "started"
        mock_db_job.save.assert_not_called()


class TestSyncJobStatusRQJobStopped:
    """Test sync_job_status when RQ job is stopped/failed."""

    def test_updates_db_when_rq_stopped_and_not_yet_completed(self):
        from core.choices import JobStatusChoices

        mock_db_job = MagicMock()
        mock_db_job.pk = 2
        mock_db_job.status = JobStatusChoices.STATUS_RUNNING
        mock_db_job.completed = None

        class _DoesNotExist(Exception):
            pass

        mock_job_cls = MagicMock()
        mock_job_cls.DoesNotExist = _DoesNotExist
        mock_job_cls.objects.get.return_value = mock_db_job

        mock_rq_job = MagicMock()
        mock_rq_job.is_stopped = True
        mock_rq_job.is_failed = False
        mock_rq_job.get_status.return_value = "stopped"

        mock_rq_cls = MagicMock()
        mock_rq_cls.fetch.return_value = mock_rq_job

        mock_queue = MagicMock()
        mock_queue_fn = MagicMock(return_value=mock_queue)

        with patch("netbox_librenms_plugin.api.views.timezone") as mock_tz:
            mock_tz.now.return_value = "2024-01-01"
            response = _call_sync_job_status(
                job_pk=2,
                job_patch=mock_job_cls,
                rq_patch=mock_rq_cls,
                queue_patch=mock_queue_fn,
            )

        assert response.status_code == 200
        data = json.loads(response.content)
        assert data["status"] == "updated"
        assert data["rq_status"] == "stopped"
        mock_db_job.save.assert_called_once_with(update_fields=["status", "completed"])
        assert mock_db_job.completed == "2024-01-01"

    def test_updates_db_when_rq_failed(self):
        from core.choices import JobStatusChoices

        mock_db_job = MagicMock()
        mock_db_job.pk = 3
        mock_db_job.status = JobStatusChoices.STATUS_RUNNING
        mock_db_job.completed = None

        class _DoesNotExist(Exception):
            pass

        mock_job_cls = MagicMock()
        mock_job_cls.DoesNotExist = _DoesNotExist
        mock_job_cls.objects.get.return_value = mock_db_job

        mock_rq_job = MagicMock()
        mock_rq_job.is_stopped = False
        mock_rq_job.is_failed = True
        mock_rq_job.get_status.return_value = "failed"

        mock_rq_cls = MagicMock()
        mock_rq_cls.fetch.return_value = mock_rq_job

        mock_queue = MagicMock()
        mock_queue_fn = MagicMock(return_value=mock_queue)

        with patch("netbox_librenms_plugin.api.views.timezone") as mock_tz:
            mock_tz.now.return_value = "2024-01-02"
            response = _call_sync_job_status(
                job_pk=3,
                job_patch=mock_job_cls,
                rq_patch=mock_rq_cls,
                queue_patch=mock_queue_fn,
            )

        assert response.status_code == 200
        data = json.loads(response.content)
        assert data["status"] == "updated"
        assert data["rq_status"] == "failed"
        assert mock_db_job.completed == "2024-01-02"
        mock_db_job.save.assert_called_once_with(update_fields=["status", "completed"])

    def test_does_not_overwrite_existing_completed_timestamp(self):
        from core.choices import JobStatusChoices

        mock_db_job = MagicMock()
        mock_db_job.pk = 4
        mock_db_job.status = JobStatusChoices.STATUS_RUNNING
        mock_db_job.completed = "2024-01-01T10:00:00"  # already set

        class _DoesNotExist(Exception):
            pass

        mock_job_cls = MagicMock()
        mock_job_cls.DoesNotExist = _DoesNotExist
        mock_job_cls.objects.get.return_value = mock_db_job

        mock_rq_job = MagicMock()
        mock_rq_job.is_stopped = True
        mock_rq_job.is_failed = False
        mock_rq_job.get_status.return_value = "stopped"

        mock_rq_cls = MagicMock()
        mock_rq_cls.fetch.return_value = mock_rq_job

        mock_queue = MagicMock()
        mock_queue_fn = MagicMock(return_value=mock_queue)

        with patch("netbox_librenms_plugin.api.views.timezone") as mock_tz:
            response = _call_sync_job_status(
                job_pk=4,
                job_patch=mock_job_cls,
                rq_patch=mock_rq_cls,
                queue_patch=mock_queue_fn,
            )

        # timezone.now() should NOT have been called since completed is already set
        mock_tz.now.assert_not_called()
        assert response.status_code == 200
        from core.choices import JobStatusChoices

        assert mock_db_job.status == JobStatusChoices.STATUS_FAILED
        mock_db_job.save.assert_called_once_with(update_fields=["status", "completed"])


class TestSyncJobStatusRQJobNotInQueue:
    """Test sync_job_status when RQ.fetch raises NoSuchJobError."""

    def test_updates_db_to_failed_when_running_and_not_in_rq(self):
        from core.choices import JobStatusChoices
        from rq.exceptions import NoSuchJobError

        mock_db_job = MagicMock()
        mock_db_job.pk = 5
        mock_db_job.status = JobStatusChoices.STATUS_RUNNING
        mock_db_job.completed = None

        class _DoesNotExist(Exception):
            pass

        mock_job_cls = MagicMock()
        mock_job_cls.DoesNotExist = _DoesNotExist
        mock_job_cls.objects.get.return_value = mock_db_job

        mock_rq_cls = MagicMock()
        mock_rq_cls.fetch.side_effect = NoSuchJobError("not found in redis")

        mock_queue = MagicMock()
        mock_queue_fn = MagicMock(return_value=mock_queue)

        with patch("netbox_librenms_plugin.api.views.timezone") as mock_tz:
            mock_tz.now.return_value = "2024-01-03"
            response = _call_sync_job_status(
                job_pk=5,
                job_patch=mock_job_cls,
                rq_patch=mock_rq_cls,
                queue_patch=mock_queue_fn,
            )

        assert response.status_code == 200
        data = json.loads(response.content)
        assert data["status"] == "updated"
        assert data["rq_status"] == "not_found"
        mock_db_job.save.assert_called_once_with(update_fields=["status", "completed"])

    def test_no_change_when_not_running_and_not_in_rq(self):
        from core.choices import JobStatusChoices
        from rq.exceptions import NoSuchJobError

        mock_db_job = MagicMock()
        mock_db_job.pk = 6
        mock_db_job.status = JobStatusChoices.STATUS_COMPLETED
        mock_db_job.completed = "2024-01-01"

        class _DoesNotExist(Exception):
            pass

        mock_job_cls = MagicMock()
        mock_job_cls.DoesNotExist = _DoesNotExist
        mock_job_cls.objects.get.return_value = mock_db_job

        mock_rq_cls = MagicMock()
        mock_rq_cls.fetch.side_effect = NoSuchJobError("not found in redis")

        mock_queue = MagicMock()
        mock_queue_fn = MagicMock(return_value=mock_queue)

        response = _call_sync_job_status(
            job_pk=6,
            job_patch=mock_job_cls,
            rq_patch=mock_rq_cls,
            queue_patch=mock_queue_fn,
        )

        assert response.status_code == 200
        data = json.loads(response.content)
        assert data["status"] == "no_change"
        assert data["rq_status"] == "not_found"
        mock_db_job.save.assert_not_called()

    def test_does_not_overwrite_completed_when_not_in_rq(self):
        from core.choices import JobStatusChoices
        from rq.exceptions import NoSuchJobError

        mock_db_job = MagicMock()
        mock_db_job.pk = 7
        mock_db_job.status = JobStatusChoices.STATUS_RUNNING
        mock_db_job.completed = "2024-01-01T08:00:00"  # already set

        class _DoesNotExist(Exception):
            pass

        mock_job_cls = MagicMock()
        mock_job_cls.DoesNotExist = _DoesNotExist
        mock_job_cls.objects.get.return_value = mock_db_job

        mock_rq_cls = MagicMock()
        mock_rq_cls.fetch.side_effect = NoSuchJobError("gone")

        mock_queue = MagicMock()
        mock_queue_fn = MagicMock(return_value=mock_queue)

        with patch("netbox_librenms_plugin.api.views.timezone") as mock_tz:
            response = _call_sync_job_status(
                job_pk=7,
                job_patch=mock_job_cls,
                rq_patch=mock_rq_cls,
                queue_patch=mock_queue_fn,
            )

        mock_tz.now.assert_not_called()
        assert response.status_code == 200


# ===========================================================================
# api/views.py – InterfaceTypeMappingViewSet (class attributes)
# ===========================================================================


class TestInterfaceTypeMappingViewSet:
    """Test that InterfaceTypeMappingViewSet has expected class-level attributes."""

    def test_viewset_has_correct_permission_classes(self):
        from netbox_librenms_plugin.api.views import InterfaceTypeMappingViewSet, LibreNMSPluginPermission

        assert LibreNMSPluginPermission in InterfaceTypeMappingViewSet.permission_classes

    def test_viewset_has_serializer_class(self):
        from netbox_librenms_plugin.api.views import InterfaceTypeMappingViewSet
        from netbox_librenms_plugin.api.serializers import InterfaceTypeMappingSerializer

        assert InterfaceTypeMappingViewSet.serializer_class is InterfaceTypeMappingSerializer


# ===========================================================================
# filtersets.py – SiteLocationFilterSet
# ===========================================================================


class TestSiteLocationFilterSet:
    """Tests for SiteLocationFilterSet."""

    def test_qs_returns_full_queryset_when_no_q(self):
        from netbox_librenms_plugin.filtersets import SiteLocationFilterSet

        mock_item = MagicMock()
        queryset = [mock_item]
        fs = SiteLocationFilterSet(data={}, queryset=queryset)
        assert fs.qs == queryset

    def test_qs_filters_when_q_provided(self):
        from netbox_librenms_plugin.filtersets import SiteLocationFilterSet

        matching_item = MagicMock()
        matching_item.netbox_site.name = "Amsterdam"
        matching_item.netbox_site.latitude = "52.37"
        matching_item.netbox_site.longitude = "4.89"
        matching_item.librenms_location = "AMS-DC1"

        non_matching_item = MagicMock()
        non_matching_item.netbox_site.name = "London"
        non_matching_item.netbox_site.latitude = "51.5"
        non_matching_item.netbox_site.longitude = "-0.12"
        non_matching_item.librenms_location = "LON-DC1"

        fs = SiteLocationFilterSet(data={"q": "amsterdam"}, queryset=[matching_item, non_matching_item])
        result = fs.qs
        assert len(result) == 1
        assert result[0] is matching_item

    def test_qs_empty_q_returns_all(self):
        """Empty string for q is falsy – should return the full queryset."""
        from netbox_librenms_plugin.filtersets import SiteLocationFilterSet

        items = [MagicMock(), MagicMock()]
        fs = SiteLocationFilterSet(data={"q": ""}, queryset=items)
        assert fs.qs == items

    def test_matches_by_site_name(self):
        from netbox_librenms_plugin.filtersets import SiteLocationFilterSet

        item = MagicMock()
        item.netbox_site.name = "TestSite"
        item.netbox_site.latitude = "0"
        item.netbox_site.longitude = "0"
        item.librenms_location = None

        fs = SiteLocationFilterSet(data={"q": "testsite"}, queryset=[item])
        assert fs.qs == [item]

    def test_matches_by_latitude(self):
        from netbox_librenms_plugin.filtersets import SiteLocationFilterSet

        item = MagicMock()
        item.netbox_site.name = "Nowhere"
        item.netbox_site.latitude = "48.8566"
        item.netbox_site.longitude = "0.0"
        item.librenms_location = None

        fs = SiteLocationFilterSet(data={"q": "48.8566"}, queryset=[item])
        assert fs.qs == [item]

    def test_matches_by_librenms_location(self):
        from netbox_librenms_plugin.filtersets import SiteLocationFilterSet

        item = MagicMock()
        item.netbox_site.name = "X"
        item.netbox_site.latitude = "0"
        item.netbox_site.longitude = "0"
        item.librenms_location = "Paris-DC"

        fs = SiteLocationFilterSet(data={"q": "paris"}, queryset=[item])
        assert fs.qs == [item]

    def test_no_match_returns_empty(self):
        from netbox_librenms_plugin.filtersets import SiteLocationFilterSet

        item = MagicMock()
        item.netbox_site.name = "Tokyo"
        item.netbox_site.latitude = "35.0"
        item.netbox_site.longitude = "139.0"
        item.librenms_location = "TKY-1"

        fs = SiteLocationFilterSet(data={"q": "berlin"}, queryset=[item])
        assert fs.qs == []

    def test_librenms_location_none_does_not_raise(self):
        from netbox_librenms_plugin.filtersets import SiteLocationFilterSet

        item = MagicMock()
        item.netbox_site.name = "NoLocation"
        item.netbox_site.latitude = "10"
        item.netbox_site.longitude = "20"
        item.librenms_location = None

        fs = SiteLocationFilterSet(data={"q": "nolocation"}, queryset=[item])
        # Should not raise, librenms_location treated as empty string
        result = fs.qs
        assert len(result) == 1

    def test_form_property_returns_bound_form(self):
        from netbox_librenms_plugin.filtersets import SiteLocationFilterSet
        from django import forms

        fs = SiteLocationFilterSet(data={"q": "test"}, queryset=[])
        form = fs.form
        assert isinstance(form, forms.Form)
        assert form.is_bound
        assert "q" in form.fields

    def test_form_property_returns_unbound_form_when_no_data(self):
        from netbox_librenms_plugin.filtersets import SiteLocationFilterSet
        from django import forms

        fs = SiteLocationFilterSet(data=None, queryset=[])
        form = fs.form
        assert isinstance(form, forms.Form)
        assert not form.is_bound


# ===========================================================================
# filtersets.py – DeviceStatusFilterSet.search()
# ===========================================================================


class TestDeviceStatusFilterSetSearch:
    """Tests for DeviceStatusFilterSet.search()."""

    def test_search_empty_value_returns_queryset_unchanged(self):
        from netbox_librenms_plugin.filtersets import DeviceStatusFilterSet

        fs = object.__new__(DeviceStatusFilterSet)
        mock_qs = MagicMock()
        result = fs.search(mock_qs, "name", "   ")
        assert result is mock_qs
        mock_qs.filter.assert_not_called()

    def test_search_with_value_calls_filter(self):
        from netbox_librenms_plugin.filtersets import DeviceStatusFilterSet

        fs = object.__new__(DeviceStatusFilterSet)
        mock_qs = MagicMock()
        mock_qs.filter.return_value = mock_qs

        result = fs.search(mock_qs, "name", "router01")
        mock_qs.filter.assert_called_once()
        assert result is mock_qs

    def test_search_builds_q_filter_for_name(self):
        from netbox_librenms_plugin.filtersets import DeviceStatusFilterSet

        fs = object.__new__(DeviceStatusFilterSet)
        mock_qs = MagicMock()
        mock_qs.filter.return_value = mock_qs

        fs.search(mock_qs, "name", "router")

        call_args = mock_qs.filter.call_args
        assert call_args is not None
        q_obj = call_args[0][0]
        q_str = str(q_obj)
        assert "name__icontains" in q_str
        assert "site__name__icontains" in q_str
        assert "device_type__model__icontains" in q_str
        assert "role__name__icontains" in q_str
        assert "rack__name__icontains" in q_str

    def test_search_whitespace_only_returns_qs(self):
        from netbox_librenms_plugin.filtersets import DeviceStatusFilterSet

        fs = object.__new__(DeviceStatusFilterSet)
        mock_qs = MagicMock()
        result = fs.search(mock_qs, "q", "\t\n")
        assert result is mock_qs


# ===========================================================================
# filtersets.py – VMStatusFilterSet.search()
# ===========================================================================


class TestVMStatusFilterSetSearch:
    """Tests for VMStatusFilterSet.search()."""

    def test_search_empty_value_returns_queryset_unchanged(self):
        from netbox_librenms_plugin.filtersets import VMStatusFilterSet

        fs = object.__new__(VMStatusFilterSet)
        mock_qs = MagicMock()
        result = fs.search(mock_qs, "name", "")
        assert result is mock_qs
        mock_qs.filter.assert_not_called()

    def test_search_with_value_calls_filter(self):
        from netbox_librenms_plugin.filtersets import VMStatusFilterSet

        fs = object.__new__(VMStatusFilterSet)
        mock_qs = MagicMock()
        mock_qs.filter.return_value = mock_qs

        result = fs.search(mock_qs, "name", "vm-prod-01")
        mock_qs.filter.assert_called_once()
        assert result is mock_qs

    def test_search_builds_filter_with_name_site_cluster_platform(self):
        from netbox_librenms_plugin.filtersets import VMStatusFilterSet

        fs = object.__new__(VMStatusFilterSet)
        mock_qs = MagicMock()
        mock_qs.filter.return_value = mock_qs

        fs.search(mock_qs, "q", "production")

        call_args = mock_qs.filter.call_args
        assert call_args is not None
        q_obj = call_args[0][0]
        q_str = str(q_obj)
        assert "name__icontains" in q_str
        assert "site__name__icontains" in q_str
        assert "cluster__name__icontains" in q_str
        assert "platform__name__icontains" in q_str

    def test_search_whitespace_only_returns_qs(self):
        from netbox_librenms_plugin.filtersets import VMStatusFilterSet

        fs = object.__new__(VMStatusFilterSet)
        mock_qs = MagicMock()
        result = fs.search(mock_qs, "q", "   ")
        assert result is mock_qs


# ===========================================================================
# models.py – missing lines 45, 48, 68, 76
# ===========================================================================


class TestLibreNMSSettingsModel:
    """Tests for LibreNMSSettings model methods (lines 45, 48)."""

    def test_get_absolute_url_calls_reverse(self):
        """Line 45: get_absolute_url() returns the settings page URL."""
        from netbox_librenms_plugin.models import LibreNMSSettings

        instance = object.__new__(LibreNMSSettings)
        instance.selected_server = "default"

        with patch("netbox_librenms_plugin.models.reverse") as mock_reverse:
            mock_reverse.return_value = "/plugins/librenms/settings/"
            url = instance.get_absolute_url()

        mock_reverse.assert_called_once_with("plugins:netbox_librenms_plugin:settings")
        assert url == "/plugins/librenms/settings/"

    def test_str_returns_formatted_string(self):
        """Line 48: __str__() includes selected_server name."""
        from netbox_librenms_plugin.models import LibreNMSSettings

        instance = object.__new__(LibreNMSSettings)
        instance.selected_server = "my_server"

        result = str(instance)
        assert result == "LibreNMS Settings - Server: my_server"

    def test_str_with_default_server(self):
        """__str__() works with 'default' server."""
        from netbox_librenms_plugin.models import LibreNMSSettings

        instance = object.__new__(LibreNMSSettings)
        instance.selected_server = "default"

        assert str(instance) == "LibreNMS Settings - Server: default"


class TestInterfaceTypeMappingModel:
    """Tests for InterfaceTypeMapping model methods (lines 68, 76)."""

    def test_get_absolute_url_calls_reverse_with_pk(self):
        """Line 68: get_absolute_url() passes self.pk to reverse."""
        from netbox_librenms_plugin.models import InterfaceTypeMapping

        instance = object.__new__(InterfaceTypeMapping)
        instance.pk = 42

        with patch("netbox_librenms_plugin.models.reverse") as mock_reverse:
            mock_reverse.return_value = "/plugins/librenms/mappings/42/"
            url = instance.get_absolute_url()

        mock_reverse.assert_called_once_with(
            "plugins:netbox_librenms_plugin:interfacetypemapping_detail",
            args=[42],
        )
        assert url == "/plugins/librenms/mappings/42/"

    def test_str_returns_type_speed_netbox_type(self):
        """Line 76: __str__() formats librenms_type + librenms_speed -> netbox_type."""
        from netbox_librenms_plugin.models import InterfaceTypeMapping

        instance = object.__new__(InterfaceTypeMapping)
        instance.librenms_type = "ethernet"
        instance.librenms_speed = 1000000
        instance.netbox_type = "1000base-t"

        result = str(instance)
        assert result == "ethernet + 1000000 -> 1000base-t"

    def test_str_with_none_speed(self):
        """__str__() works when librenms_speed is None."""
        from netbox_librenms_plugin.models import InterfaceTypeMapping

        instance = object.__new__(InterfaceTypeMapping)
        instance.librenms_type = "fiber"
        instance.librenms_speed = None
        instance.netbox_type = "other"

        result = str(instance)
        assert result == "fiber + None -> other"

    def test_get_absolute_url_with_different_pk(self):
        """get_absolute_url() works for any pk value."""
        from netbox_librenms_plugin.models import InterfaceTypeMapping

        instance = object.__new__(InterfaceTypeMapping)
        instance.pk = 1

        with patch("netbox_librenms_plugin.models.reverse") as mock_reverse:
            mock_reverse.return_value = "/plugins/librenms/mappings/1/"
            url = instance.get_absolute_url()

        assert url == "/plugins/librenms/mappings/1/"
