"""
Coverage tests for base view classes:
  - views/base/cables_view.py   (~30% → target 95%+)
  - views/base/interfaces_view.py (~14% → target 95%+)
  - views/base/ip_addresses_view.py (~34% → target 95%+)

All tests follow the project conventions:
  - Plain pytest classes, NO @pytest.mark.django_db
  - Mock ALL database interactions with MagicMock
  - Inline imports inside test methods
  - assert x == y style
  - No RequestFactory — mock request objects directly
"""

from unittest.mock import MagicMock, patch


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


def _mock_obj(model_name="device", pk=1, name="test-device"):
    obj = MagicMock()
    obj._meta = MagicMock()
    obj._meta.model_name = model_name
    obj.pk = pk
    obj.id = pk
    obj.name = name
    return obj


def _mock_request(path="/plugins/librenms/device/1/cables/"):
    req = MagicMock()
    req.path = path
    req.GET = {}
    req.POST = {}
    req.headers = {}
    return req


# =============================================================================
# BaseCableTableView
# =============================================================================


class TestBaseCableTableViewGetLinksData:
    """Tests for BaseCableTableView.get_links_data."""

    def _make_view(self):
        from netbox_librenms_plugin.views.base.cables_view import BaseCableTableView

        view = object.__new__(BaseCableTableView)
        view.request = _mock_request()
        view.librenms_id = 42
        view._librenms_api = MagicMock()
        view._librenms_api.server_key = "default"
        return view

    def test_get_links_data_returns_none_on_api_error(self):
        """When API returns failure, get_links_data returns None."""
        view = self._make_view()
        view._librenms_api.get_device_links.return_value = (False, {"error": "timeout"})

        obj = _mock_obj()

        with patch.object(view, "get_ports_data", return_value={"ports": []}):
            result = view.get_links_data(obj)

        assert result is None

    def test_get_links_data_returns_none_when_error_key_present(self):
        """When response has 'error' key even with success=True, returns None."""
        view = self._make_view()
        view._librenms_api.get_device_links.return_value = (True, {"error": "some error"})

        obj = _mock_obj()

        with patch.object(view, "get_ports_data", return_value={"ports": []}):
            result = view.get_links_data(obj)

        assert result is None

    def test_get_links_data_success_returns_link_list(self):
        """Successful API call returns formatted link list."""
        view = self._make_view()

        links_data = {
            "links": [
                {
                    "local_port_id": 10,
                    "remote_port": "Gi0/1",
                    "remote_hostname": "switch-b",
                    "remote_port_id": 20,
                    "remote_device_id": 99,
                }
            ]
        }
        view._librenms_api.get_device_links.return_value = (True, links_data)

        ports = {
            "ports": [
                {"port_id": 10, "ifName": "Gi0/0"},
            ]
        }
        obj = _mock_obj()

        with (
            patch.object(view, "get_ports_data", return_value=ports),
            patch("netbox_librenms_plugin.views.base.cables_view.get_interface_name_field", return_value="ifName"),
        ):
            result = view.get_links_data(obj)

        assert result is not None
        assert len(result) == 1
        assert result[0]["local_port"] == "Gi0/0"
        assert result[0]["remote_device"] == "switch-b"

    def test_get_links_data_port_without_id_skipped(self):
        """Ports missing port_id are skipped when building local port map."""
        view = self._make_view()

        links_data = {"links": [{"local_port_id": 10, "remote_hostname": "sw", "remote_port": "Gi0/1"}]}
        view._librenms_api.get_device_links.return_value = (True, links_data)

        ports = {
            "ports": [
                {"port_id": None, "ifName": "Gi0/0"},  # No port_id — skipped
                {"port_id": 10, "ifName": "Gi0/0"},
            ]
        }
        obj = _mock_obj()

        with (
            patch.object(view, "get_ports_data", return_value=ports),
            patch("netbox_librenms_plugin.views.base.cables_view.get_interface_name_field", return_value="ifName"),
        ):
            result = view.get_links_data(obj)

        assert result is not None


class TestBaseCableTableViewGetDeviceByIdOrName:
    """Tests for BaseCableTableView.get_device_by_id_or_name."""

    def _make_view(self):
        from netbox_librenms_plugin.views.base.cables_view import BaseCableTableView

        view = object.__new__(BaseCableTableView)
        view._librenms_api = MagicMock()
        view._librenms_api.server_key = "default"
        return view

    def test_finds_device_by_librenms_id(self):
        """When remote_device_id matches librenms_id, device is returned."""
        from netbox_librenms_plugin.views.base.cables_view import BaseCableTableView

        view = object.__new__(BaseCableTableView)
        view._librenms_api = MagicMock()
        view._librenms_api.server_key = "default"

        mock_device = MagicMock()
        sentinel_q = object()

        with (
            patch("netbox_librenms_plugin.views.base.cables_view.Device") as MockDevice,
            patch("netbox_librenms_plugin.views.base.cables_view._librenms_id_q", return_value=sentinel_q) as mock_q,
        ):
            MockDevice.objects.get.return_value = mock_device

            device, found, error = view.get_device_by_id_or_name(42, "switch.example.com")

        assert found is True
        assert device is mock_device
        assert error is None
        mock_q.assert_called_once_with("default", 42)
        MockDevice.objects.get.assert_called_once_with(sentinel_q)

    def test_falls_back_to_name_when_no_id(self):
        """When remote_device_id is None, falls back to name lookup."""
        from netbox_librenms_plugin.views.base.cables_view import BaseCableTableView

        view = object.__new__(BaseCableTableView)
        view._librenms_api = MagicMock()
        view._librenms_api.server_key = "default"

        mock_device = MagicMock()

        with patch("netbox_librenms_plugin.views.base.cables_view.Device") as MockDevice:
            MockDevice.objects.get.return_value = mock_device
            device, found, error = view.get_device_by_id_or_name(None, "switch-a")

        assert found is True
        assert device is mock_device

    def test_falls_back_to_simple_hostname_when_fqdn_not_found(self):
        """When FQDN name not found, tries short hostname."""

        from netbox_librenms_plugin.views.base.cables_view import BaseCableTableView

        view = object.__new__(BaseCableTableView)
        view._librenms_api = MagicMock()
        view._librenms_api.server_key = "default"

        mock_device = MagicMock()
        call_args = []

        class _DoesNotExist(Exception):
            pass

        def get_side_effect(name=None, **kwargs):
            call_args.append(name)
            if name == "switch.example.com":
                raise _DoesNotExist("not found")
            if name == "switch":
                return mock_device
            raise Exception(f"Unexpected: {name}")

        with patch("netbox_librenms_plugin.views.base.cables_view.Device") as MockDevice:
            MockDevice.DoesNotExist = _DoesNotExist
            MockDevice.objects.get.side_effect = get_side_effect
            device, found, error = view.get_device_by_id_or_name(None, "switch.example.com")

        assert found is True
        assert device is mock_device
        assert "switch.example.com" in call_args
        assert "switch" in call_args

    def test_multiple_objects_returns_error_message(self):
        """MultipleObjectsReturned for name lookup returns error info."""
        from netbox_librenms_plugin.views.base.cables_view import BaseCableTableView

        view = object.__new__(BaseCableTableView)
        view._librenms_api = MagicMock()
        view._librenms_api.server_key = "default"

        with patch("netbox_librenms_plugin.views.base.cables_view.Device") as MockDevice:
            from django.core.exceptions import MultipleObjectsReturned

            class _DoesNotExist(Exception):
                pass

            MockDevice.DoesNotExist = _DoesNotExist
            MockDevice.objects.get.side_effect = MultipleObjectsReturned("Multiple")
            device, found, error = view.get_device_by_id_or_name(None, "duplicate-switch")

        assert found is False
        assert device is None
        assert error is not None
        assert "duplicate-switch" in error

    def test_device_not_found_returns_none_false_none(self):
        """When device not found by any method, returns (None, False, None)."""
        from netbox_librenms_plugin.views.base.cables_view import BaseCableTableView

        view = object.__new__(BaseCableTableView)
        view._librenms_api = MagicMock()
        view._librenms_api.server_key = "default"

        class _DoesNotExist(Exception):
            pass

        with patch("netbox_librenms_plugin.views.base.cables_view.Device") as MockDevice:
            MockDevice.DoesNotExist = _DoesNotExist
            MockDevice.objects.get.side_effect = _DoesNotExist("not found")
            device, found, error = view.get_device_by_id_or_name(None, "nonexistent")

        assert found is False
        assert device is None


class TestBaseCableTableViewEnrichLocalPort:
    """Tests for BaseCableTableView.enrich_local_port."""

    def _make_view(self):
        from netbox_librenms_plugin.views.base.cables_view import BaseCableTableView

        view = object.__new__(BaseCableTableView)
        view._librenms_api = MagicMock()
        view._librenms_api.server_key = "default"
        return view

    def test_no_local_port_skips_enrichment(self):
        """When local_port is absent, link dict is unchanged."""
        view = self._make_view()
        link = {"local_port": None}
        obj = _mock_obj()
        view.enrich_local_port(link, obj)
        assert "local_port_url" not in link

    def test_interface_found_by_librenms_id_adds_url(self):
        """When interface found by librenms_id, local_port_url and id are set."""
        view = self._make_view()

        iface = MagicMock()
        iface.pk = 5

        obj = _mock_obj()
        obj.virtual_chassis = None
        obj.interfaces = MagicMock()
        obj.interfaces.filter.return_value.first.return_value = iface

        link = {"local_port": "Gi0/0", "local_port_id": 10}

        with patch("netbox_librenms_plugin.views.base.cables_view.reverse", return_value="/dcim/interfaces/5/"):
            view.enrich_local_port(link, obj)

        assert link["local_port_url"] == "/dcim/interfaces/5/"
        assert link["netbox_local_interface_id"] == 5

    def test_interface_found_by_name_fallback(self):
        """When librenms_id match fails, falls back to name matching."""
        view = self._make_view()

        iface = MagicMock()
        iface.pk = 7

        obj = _mock_obj()
        obj.virtual_chassis = None
        obj.interfaces = MagicMock()
        # First filter (librenms_id) returns nothing, second (name) returns iface
        obj.interfaces.filter.return_value.first.side_effect = [None, iface]

        link = {"local_port": "Gi0/1", "local_port_id": 20}

        with patch("netbox_librenms_plugin.views.base.cables_view.reverse", return_value="/dcim/interfaces/7/"):
            view.enrich_local_port(link, obj)

        assert link.get("local_port_url") == "/dcim/interfaces/7/"

    def test_no_interface_found_leaves_link_unchanged(self):
        """When no interface found, link dict is not modified."""
        view = self._make_view()

        obj = _mock_obj()
        obj.virtual_chassis = None
        obj.interfaces = MagicMock()
        obj.interfaces.filter.return_value.first.return_value = None

        link = {"local_port": "Gi0/2", "local_port_id": 30}

        view.enrich_local_port(link, obj)

        assert "local_port_url" not in link

    def test_virtual_chassis_delegates_to_chassis_member(self):
        """With VC, chassis member interfaces are queried."""
        view = self._make_view()

        vc = MagicMock()
        member = MagicMock()
        member.interfaces = MagicMock()

        iface = MagicMock()
        iface.pk = 3
        member.interfaces.filter.return_value.first.return_value = iface

        obj = _mock_obj()
        obj.virtual_chassis = vc
        # Non-VC fallback returns nothing to confirm member path is used
        obj.interfaces = MagicMock()
        obj.interfaces.filter.return_value.first.return_value = None

        link = {"local_port": "Gi1/0/1", "local_port_id": 100}

        with (
            patch(
                "netbox_librenms_plugin.views.base.cables_view.get_virtual_chassis_member",
                return_value=member,
            ) as mock_get_vc_member,
            patch("netbox_librenms_plugin.views.base.cables_view.reverse", return_value="/dcim/interfaces/3/"),
        ):
            view.enrich_local_port(link, obj)

        mock_get_vc_member.assert_called_once_with(obj, "Gi1/0/1")
        assert link["local_port_url"] == "/dcim/interfaces/3/"


class TestBaseCableTableViewCheckCableStatus:
    """Tests for BaseCableTableView.check_cable_status."""

    def _make_view(self):
        from netbox_librenms_plugin.views.base.cables_view import BaseCableTableView

        view = object.__new__(BaseCableTableView)
        view._librenms_api = MagicMock()
        return view

    def test_cable_found_sets_cable_url(self):
        """Existing cable: cable_status='Cable Found' and cable_url set."""
        view = self._make_view()

        cable = MagicMock()
        cable.pk = 99

        local_iface = MagicMock()
        local_iface.cable = cable
        remote_iface = MagicMock()
        remote_iface.cable = None

        link = {"netbox_local_interface_id": 1, "netbox_remote_interface_id": 2}

        with (
            patch("netbox_librenms_plugin.views.base.cables_view.Interface") as MockIface,
            patch("netbox_librenms_plugin.views.base.cables_view.reverse", return_value="/dcim/cables/99/"),
        ):
            MockIface.objects.get.side_effect = [local_iface, remote_iface]
            result = view.check_cable_status(link)

        assert result["cable_status"] == "Cable Found"
        assert result["cable_url"] == "/dcim/cables/99/"
        assert result["can_create_cable"] is False

    def test_no_cable_sets_can_create_cable(self):
        """No cable: cable_status='No Cable' and can_create_cable=True."""
        view = self._make_view()

        local_iface = MagicMock()
        local_iface.cable = None
        remote_iface = MagicMock()
        remote_iface.cable = None

        link = {"netbox_local_interface_id": 1, "netbox_remote_interface_id": 2}

        with patch("netbox_librenms_plugin.views.base.cables_view.Interface") as MockIface:
            MockIface.objects.get.side_effect = [local_iface, remote_iface]
            result = view.check_cable_status(link)

        assert result["cable_status"] == "No Cable"
        assert result["can_create_cable"] is True

    def test_missing_local_interface_id(self):
        """Only remote interface found: status = 'Local Interface Not Found in Netbox'."""
        view = self._make_view()
        link = {"netbox_local_interface_id": None, "netbox_remote_interface_id": 2}

        result = view.check_cable_status(link)

        assert "Local Interface Not Found" in result["cable_status"]
        assert result["can_create_cable"] is False

    def test_missing_remote_interface_id(self):
        """Only local interface found: status = 'Remote Interface Not Found in Netbox'."""
        view = self._make_view()
        link = {"netbox_local_interface_id": 1, "netbox_remote_interface_id": None}

        result = view.check_cable_status(link)

        assert "Remote Interface Not Found" in result["cable_status"]

    def test_both_interfaces_missing(self):
        """Both interfaces missing: status = 'Both Interfaces Not Found in Netbox'."""
        view = self._make_view()
        link = {"netbox_local_interface_id": None, "netbox_remote_interface_id": None}

        result = view.check_cable_status(link)

        assert "Both Interfaces Not Found" in result["cable_status"]


class TestBaseCableTableViewEnrichLinksData:
    """Tests for BaseCableTableView.enrich_links_data."""

    def _make_view(self):
        from netbox_librenms_plugin.views.base.cables_view import BaseCableTableView

        view = object.__new__(BaseCableTableView)
        view._librenms_api = MagicMock()
        view._librenms_api.server_key = "default"
        return view

    def test_enrich_links_data_calls_enrich_per_link(self):
        """enrich_links_data calls enrich_local_port and process_remote_device per link."""
        view = self._make_view()

        link1 = {"local_port": "Gi0/0", "local_port_id": 1, "remote_device": "sw-b"}
        link2 = {"local_port": "Gi0/1", "local_port_id": 2, "remote_device": None}
        obj = _mock_obj()

        with (
            patch.object(view, "enrich_local_port") as mock_enrich_local,
            patch.object(view, "process_remote_device", return_value=link1) as mock_remote,
        ):
            view.enrich_links_data([link1, link2], obj)

        assert mock_enrich_local.call_count == 2
        assert mock_remote.call_count == 1  # Only link1 has remote_device

    def test_enrich_links_data_sets_device_id(self):
        """Each link gets device_id set to obj.id."""
        view = self._make_view()

        link = {"local_port": "Gi0/0", "local_port_id": 1, "remote_device": None}
        obj = _mock_obj()
        obj.id = 55

        with patch.object(view, "enrich_local_port"):
            view.enrich_links_data([link], obj)

        assert link["device_id"] == 55

    def test_check_cable_status_called_when_remote_device_resolved(self):
        """check_cable_status is called when remote device resolves successfully."""
        view = self._make_view()

        link = {
            "local_port": "Gi0/0",
            "local_port_id": 1,
            "remote_device": "sw-b",
            "remote_device_id": 5,
        }
        enriched = dict(link)
        enriched["netbox_remote_device_id"] = 10  # resolved

        obj = _mock_obj()

        with (
            patch.object(view, "enrich_local_port"),
            patch.object(view, "process_remote_device", return_value=enriched),
            patch.object(view, "check_cable_status", return_value=enriched) as mock_cable,
        ):
            view.enrich_links_data([link], obj)

        mock_cable.assert_called_once()


class TestBaseCableTableViewPrepareContext:
    """Tests for BaseCableTableView._prepare_context."""

    def _make_view(self):
        from netbox_librenms_plugin.views.base.cables_view import BaseCableTableView

        view = object.__new__(BaseCableTableView)
        view.request = _mock_request()
        view._librenms_api = MagicMock()
        view._librenms_api.server_key = "default"
        view._librenms_api.cache_timeout = 300
        return view

    def test_fetch_fresh_none_links_returns_none(self):
        """When get_links_data returns None, _prepare_context returns None."""
        view = self._make_view()
        obj = _mock_obj()

        with patch.object(view, "get_links_data", return_value=None):
            result = view._prepare_context(view.request, obj, fetch_fresh=True)

        assert result is None

    def test_cache_miss_returns_none(self):
        """When cache has no data and fetch_fresh=False, returns None."""
        view = self._make_view()
        obj = _mock_obj()

        with (
            patch("netbox_librenms_plugin.views.base.cables_view.cache") as mock_cache,
            patch.object(view, "get_cache_key", return_value="key"),
        ):
            mock_cache.get.return_value = None
            result = view._prepare_context(view.request, obj, fetch_fresh=False)

        assert result is None

    def test_fetch_fresh_caches_and_returns_context(self):
        """Successful fresh fetch caches data and returns context dict."""
        view = self._make_view()
        obj = _mock_obj()

        links = [{"local_port": "Gi0/0"}]
        mock_table = MagicMock()
        mock_table.configure = MagicMock()

        with (
            patch.object(view, "get_links_data", return_value=links),
            patch.object(view, "enrich_links_data", return_value=links),
            patch.object(view, "get_cache_key", return_value="cable-key"),
            patch.object(view, "get_table", return_value=mock_table),
            patch("netbox_librenms_plugin.views.base.cables_view.cache") as mock_cache,
            patch("netbox_librenms_plugin.views.base.cables_view.timezone") as mock_tz,
        ):
            mock_cache.ttl.return_value = 300
            mock_tz.now.return_value = MagicMock()
            mock_tz.timedelta.return_value = MagicMock()
            result = view._prepare_context(view.request, obj, fetch_fresh=True)

        assert result is not None
        assert result["object"] is obj
        assert result["table"] is mock_table
        mock_cache.set.assert_called()

    def test_cache_hit_re_enriches_and_returns_context(self):
        """Cached data is re-enriched and returned."""
        view = self._make_view()
        obj = _mock_obj()

        cached_links = [{"local_port": "Gi0/0", "remote_device": "sw-b"}]
        mock_table = MagicMock()
        mock_table.configure = MagicMock()

        with (
            patch.object(view, "get_cache_key", return_value="cable-key"),
            patch.object(view, "enrich_links_data", return_value=cached_links),
            patch.object(view, "get_table", return_value=mock_table),
            patch("netbox_librenms_plugin.views.base.cables_view.cache") as mock_cache,
            patch("netbox_librenms_plugin.views.base.cables_view.timezone") as mock_tz,
        ):
            mock_cache.get.return_value = {"links": cached_links}
            mock_cache.ttl.return_value = 200
            mock_tz.now.return_value = MagicMock()
            mock_tz.timedelta.return_value = MagicMock()
            result = view._prepare_context(view.request, obj, fetch_fresh=False)

        assert result is not None
        assert result["object"] is obj

    def test_non_default_server_key_forwarded_to_enrich(self):
        """When librenms_api.server_key is non-default, _prepare_context passes it to enrich_links_data."""
        view = self._make_view()
        view._librenms_api.server_key = "non-default"
        obj = _mock_obj()

        links = [
            {
                "local_port": "Gi0/0",
                "local_port_id": "1",
                "remote_device": None,
                "remote_port": None,
                "remote_port_id": None,
                "remote_device_id": None,
            }
        ]
        mock_table = MagicMock()
        mock_table.configure = MagicMock()

        with (
            patch.object(view, "get_links_data", return_value=links),
            patch.object(view, "enrich_links_data", return_value=links) as mock_enrich,
            patch.object(view, "get_cache_key", return_value="cable-key"),
            patch.object(view, "get_table", return_value=mock_table),
            patch("netbox_librenms_plugin.views.base.cables_view.get_librenms_sync_device", return_value=None),
            patch("netbox_librenms_plugin.views.base.cables_view.cache") as mock_cache,
            patch("netbox_librenms_plugin.views.base.cables_view.timezone") as mock_tz,
        ):
            mock_cache.ttl.return_value = 300
            mock_tz.now.return_value = MagicMock()
            mock_tz.timedelta.return_value = MagicMock()
            result = view._prepare_context(view.request, obj, fetch_fresh=True)

        assert result is not None
        # enrich_links_data must be called with the non-default server_key
        mock_enrich.assert_called_once()
        _, enrich_kwargs = mock_enrich.call_args
        assert enrich_kwargs.get("server_key") == "non-default"


class TestBaseCableTableViewGetContextData:
    """Tests for BaseCableTableView.get_context_data."""

    def _make_view(self):
        from netbox_librenms_plugin.views.base.cables_view import BaseCableTableView

        view = object.__new__(BaseCableTableView)
        view._librenms_api = MagicMock()
        view._librenms_api.server_key = "default"
        return view

    def test_returns_empty_context_when_no_cache(self):
        """When _prepare_context returns None, get_context_data returns fallback context."""
        view = self._make_view()
        obj = _mock_obj()
        request = _mock_request()

        with patch.object(view, "_prepare_context", return_value=None):
            ctx = view.get_context_data(request, obj)

        assert ctx["table"] is None
        assert ctx["object"] is obj
        assert ctx["cache_expiry"] is None

    def test_returns_context_when_cache_populated(self):
        """When _prepare_context returns data, get_context_data returns it."""
        view = self._make_view()
        obj = _mock_obj()
        request = _mock_request()

        fake_context = {"table": MagicMock(), "object": obj, "cache_expiry": None, "server_key": "default"}

        with patch.object(view, "_prepare_context", return_value=fake_context):
            ctx = view.get_context_data(request, obj)

        assert ctx is fake_context


class TestBaseCableTableViewPost:
    """Tests for BaseCableTableView.post."""

    def _make_view(self):
        from netbox_librenms_plugin.views.base.cables_view import BaseCableTableView

        view = object.__new__(BaseCableTableView)
        view._librenms_api = MagicMock()
        view._librenms_api.server_key = "default"
        view.model = MagicMock()
        return view

    def test_post_no_links_shows_error_and_renders(self):
        """When no links found, renders template with error message."""
        view = self._make_view()
        obj = _mock_obj()
        request = _mock_request()

        with (
            patch.object(view, "get_object", return_value=obj),
            patch.object(view, "_prepare_context", return_value=None),
            patch("netbox_librenms_plugin.views.base.cables_view.messages") as mock_messages,
            patch("netbox_librenms_plugin.views.base.cables_view.render") as mock_render,
        ):
            mock_render.return_value = MagicMock()
            view.post(request, pk=1)

        mock_messages.error.assert_called_once()
        mock_render.assert_called_once()

    def test_post_success_shows_message_and_renders(self):
        """When links found, renders template with success message."""
        view = self._make_view()
        obj = _mock_obj()
        request = _mock_request()
        fake_context = {"table": MagicMock(), "object": obj, "cache_expiry": None}

        with (
            patch.object(view, "get_object", return_value=obj),
            patch.object(view, "_prepare_context", return_value=fake_context),
            patch("netbox_librenms_plugin.views.base.cables_view.messages") as mock_messages,
            patch("netbox_librenms_plugin.views.base.cables_view.render") as mock_render,
        ):
            mock_render.return_value = MagicMock()
            view.post(request, pk=1)

        mock_messages.success.assert_called_once()
        mock_render.assert_called_once()

    def test_get_ports_data_uses_cache_when_available(self):
        """get_ports_data returns cached data when present."""
        from netbox_librenms_plugin.views.base.cables_view import BaseCableTableView

        view = object.__new__(BaseCableTableView)
        view._librenms_api = MagicMock()
        view._librenms_api.server_key = "default"
        view.librenms_id = 42

        cached = {"ports": [{"port_id": 1, "ifName": "Gi0/0"}]}
        obj = _mock_obj()

        with (
            patch.object(view, "get_cache_key", return_value="ports-key"),
            patch("netbox_librenms_plugin.views.base.cables_view.cache") as mock_cache,
        ):
            mock_cache.get.return_value = cached
            result = view.get_ports_data(obj)

        assert result is cached

    def test_get_ports_data_fetches_from_api_on_cache_miss(self):
        """get_ports_data calls API when cache is empty."""
        from netbox_librenms_plugin.views.base.cables_view import BaseCableTableView

        view = object.__new__(BaseCableTableView)
        view._librenms_api = MagicMock()
        view._librenms_api.server_key = "default"
        view.librenms_id = 42

        api_data = {"ports": [{"port_id": 2}]}
        view._librenms_api.get_ports.return_value = (True, api_data)

        obj = _mock_obj()

        with (
            patch.object(view, "get_cache_key", return_value="ports-key"),
            patch("netbox_librenms_plugin.views.base.cables_view.cache") as mock_cache,
        ):
            mock_cache.get.return_value = None
            result = view.get_ports_data(obj)

        assert result is api_data


# =============================================================================
# BaseInterfaceTableView
# =============================================================================


class TestBaseInterfaceTableViewBasics:
    """Tests for BaseInterfaceTableView utility methods."""

    def _make_view(self, model_name="device"):
        from netbox_librenms_plugin.views.base.interfaces_view import BaseInterfaceTableView

        view = object.__new__(BaseInterfaceTableView)
        view._librenms_api = MagicMock()
        view._librenms_api.server_key = "default"
        view._librenms_api.cache_timeout = 300
        view.model = MagicMock()
        view.model.__name__ = model_name
        view.interface_name_field = None
        return view

    def test_get_ip_address_with_primary_ip(self):
        """Returns string IP when primary_ip is set."""
        view = self._make_view()
        obj = MagicMock()
        obj.primary_ip.address.ip = "192.168.1.1"
        result = view.get_ip_address(obj)
        assert result == "192.168.1.1"

    def test_get_ip_address_without_primary_ip(self):
        """Returns None when primary_ip is falsy."""
        view = self._make_view()
        obj = MagicMock()
        obj.primary_ip = None
        result = view.get_ip_address(obj)
        assert result is None

    def test_get_select_related_field_for_vm(self):
        """Returns 'virtual_machine' for VirtualMachine model."""
        from netbox_librenms_plugin.views.base.interfaces_view import BaseInterfaceTableView

        view = object.__new__(BaseInterfaceTableView)
        view.model = MagicMock()
        view.model.__name__ = "virtualmachine"
        obj = MagicMock()

        result = view.get_select_related_field(obj)

        assert result == "virtual_machine"

    def test_get_select_related_field_for_device(self):
        """Returns 'device' for Device model."""
        from netbox_librenms_plugin.views.base.interfaces_view import BaseInterfaceTableView

        view = object.__new__(BaseInterfaceTableView)
        view.model = MagicMock()
        view.model.__name__ = "device"
        obj = MagicMock()

        result = view.get_select_related_field(obj)

        assert result == "device"

    def test_enrich_ports_calls_parse_per_port(self):
        """_enrich_ports_with_vlan_data calls parse_port_vlan_data for each port."""
        view = self._make_view()
        view._librenms_api.parse_port_vlan_data.side_effect = lambda p, f: {"parsed": True}

        ports = [{"port_id": 1, "ifName": "Gi0/0"}, {"port_id": 2, "ifName": "Gi0/1"}]
        result = view._enrich_ports_with_vlan_data(ports, "ifName")

        assert len(result) == 2
        assert view._librenms_api.parse_port_vlan_data.call_count == 2


class TestBaseInterfaceTableViewPost:
    """Tests for BaseInterfaceTableView.post."""

    def _make_view(self):
        from netbox_librenms_plugin.views.base.interfaces_view import BaseInterfaceTableView

        view = object.__new__(BaseInterfaceTableView)
        view._librenms_api = MagicMock()
        view._librenms_api.server_key = "default"
        view._librenms_api.cache_timeout = 300
        view.model = MagicMock()
        view.partial_template_name = "test_template.html"
        return view

    def test_post_no_librenms_id_redirects_with_error(self):
        """When librenms_id not found, error message and redirect."""
        view = self._make_view()
        obj = _mock_obj()
        request = _mock_request()

        view._librenms_api.get_librenms_id.return_value = None

        with (
            patch.object(view, "get_object", return_value=obj),
            patch.object(view, "get_redirect_url", return_value="/device/1/"),
            patch("netbox_librenms_plugin.views.base.interfaces_view.get_interface_name_field", return_value="ifName"),
            patch("netbox_librenms_plugin.views.base.interfaces_view.messages") as mock_messages,
            patch("netbox_librenms_plugin.views.base.interfaces_view.redirect") as mock_redirect,
        ):
            mock_redirect.return_value = MagicMock()
            view.post(request, pk=1)

        mock_messages.error.assert_called_once()
        mock_redirect.assert_called_once_with("/device/1/")

    def test_post_api_error_redirects_with_error(self):
        """When API returns failure, error message and redirect."""
        view = self._make_view()
        obj = _mock_obj()
        request = _mock_request()

        view._librenms_api.get_librenms_id.return_value = 42
        view._librenms_api.get_ports.return_value = (False, "Connection refused")

        with (
            patch.object(view, "get_object", return_value=obj),
            patch.object(view, "get_redirect_url", return_value="/device/1/"),
            patch("netbox_librenms_plugin.views.base.interfaces_view.get_interface_name_field", return_value="ifName"),
            patch("netbox_librenms_plugin.views.base.interfaces_view.messages") as mock_messages,
            patch("netbox_librenms_plugin.views.base.interfaces_view.redirect") as mock_redirect,
        ):
            mock_redirect.return_value = MagicMock()
            view.post(request, pk=1)

        mock_messages.error.assert_called_once_with(request, "Connection refused")
        mock_redirect.assert_called_once_with("/device/1/")

    def test_post_success_caches_and_renders(self):
        """Successful fetch caches data and renders template."""
        view = self._make_view()
        obj = _mock_obj()
        request = _mock_request()

        view._librenms_api.get_librenms_id.return_value = 42
        view._librenms_api.get_ports.return_value = (True, {"ports": [{"port_id": 1, "ifName": "Gi0/0"}]})

        with (
            patch.object(view, "get_object", return_value=obj),
            patch.object(view, "get_redirect_url", return_value="/device/1/"),
            patch.object(view, "_enrich_ports_with_vlan_data", return_value=[]),
            patch.object(view, "get_context_data", return_value={}),
            patch.object(view, "get_cache_key", return_value="cache-key") as mock_get_cache_key,
            patch.object(view, "get_last_fetched_key", return_value="last-key") as mock_get_last_fetched_key,
            patch("netbox_librenms_plugin.views.base.interfaces_view.get_interface_name_field", return_value="ifName"),
            patch("netbox_librenms_plugin.views.base.interfaces_view.messages") as mock_messages,
            patch("netbox_librenms_plugin.views.base.interfaces_view.render") as mock_render,
            patch("netbox_librenms_plugin.views.base.interfaces_view.cache") as mock_cache,
            patch("netbox_librenms_plugin.views.base.interfaces_view.timezone"),
        ):
            mock_render.return_value = MagicMock()
            view.post(request, pk=1)

        mock_messages.success.assert_called_once()
        mock_render.assert_called_once()
        mock_cache.set.assert_called()
        # Verify server_key is forwarded to cache key helpers (server-specific namespacing)
        mock_get_cache_key.assert_called_with(obj, "ports", "default")
        mock_get_last_fetched_key.assert_called_with(obj, "ports", "default")


class TestBaseInterfaceTableViewGetContextData:
    """Tests for BaseInterfaceTableView.get_context_data."""

    def _make_view(self):
        from netbox_librenms_plugin.views.base.interfaces_view import BaseInterfaceTableView

        view = object.__new__(BaseInterfaceTableView)
        view._librenms_api = MagicMock()
        view._librenms_api.server_key = "default"
        view.model = MagicMock()
        view.model.__name__ = "device"
        view.interface_name_field = None
        return view

    def test_cache_miss_returns_empty_table(self):
        """When no cached data, table is None."""
        view = self._make_view()
        obj = _mock_obj()
        request = _mock_request()

        with (
            patch.object(view, "get_cache_key", return_value="key"),
            patch.object(view, "get_last_fetched_key", return_value="last-key"),
            patch.object(view, "get_vlan_overrides_key", return_value="overrides-key"),
            patch.object(view, "get_vlan_groups_for_device", return_value=[]),
            patch.object(view, "_build_vlan_lookup_maps", return_value={}),
            patch("netbox_librenms_plugin.views.base.interfaces_view.get_interface_name_field", return_value="ifName"),
            patch("netbox_librenms_plugin.views.base.interfaces_view.cache") as mock_cache,
            patch("netbox_librenms_plugin.views.base.interfaces_view.timezone"),
        ):
            mock_cache.get.return_value = None
            mock_cache.ttl.return_value = None
            ctx = view.get_context_data(request, obj, "ifName")

        assert ctx["table"] is None

    def test_cache_hit_non_vc_builds_table(self):
        """Cached data without VC produces table."""
        view = self._make_view()
        obj = _mock_obj()
        obj.virtual_chassis = None
        request = _mock_request()

        cached_data = {
            "ports": [{"port_id": 1, "ifName": "Gi0/0", "ifAdminStatus": "up", "ifAlias": None, "ifDescr": "Gi0/0"}]
        }

        mock_iface = MagicMock()
        mock_iface.name = "Gi0/0"
        mock_ifaces_qs = MagicMock()
        mock_ifaces_qs.select_related.return_value = [mock_iface]

        mock_table = MagicMock()
        mock_table.configure = MagicMock()

        with (
            patch.object(view, "get_cache_key", return_value="key"),
            patch.object(view, "get_last_fetched_key", return_value="last-key"),
            patch.object(view, "get_vlan_overrides_key", return_value="overrides-key"),
            patch.object(view, "get_vlan_groups_for_device", return_value=[]),
            patch.object(view, "_build_vlan_lookup_maps", return_value={"vid_to_groups": {}, "vid_to_vlans": {}}),
            patch.object(view, "get_interfaces", return_value=mock_ifaces_qs),
            patch.object(view, "_add_vlan_group_selection"),
            patch.object(view, "_add_missing_vlans_info"),
            patch.object(view, "get_table", return_value=mock_table),
            patch("netbox_librenms_plugin.views.base.interfaces_view.get_interface_name_field", return_value="ifName"),
            patch("netbox_librenms_plugin.views.base.interfaces_view.cache") as mock_cache,
            patch("netbox_librenms_plugin.views.base.interfaces_view.timezone") as mock_tz,
        ):
            mock_cache.get.side_effect = lambda key: cached_data if key == "key" else None
            mock_cache.ttl.return_value = 300
            mock_tz.now.return_value = MagicMock()
            mock_tz.timedelta.return_value = MagicMock()
            ctx = view.get_context_data(request, obj, "ifName")

        assert ctx["table"] is mock_table

    def test_cache_hit_with_vc_uses_vc_members(self):
        """Cached data with VC queries each chassis member's interfaces."""
        view = self._make_view()

        vc = MagicMock()
        member1 = MagicMock()
        member1.id = 10
        member2 = MagicMock()
        member2.id = 11
        vc.members.all.return_value = [member1, member2]

        obj = _mock_obj()
        obj.virtual_chassis = vc
        obj.id = 9999  # distinct from all member IDs so VC path is unambiguous
        request = _mock_request()

        cached_data = {
            "ports": [{"port_id": 1, "ifName": "Gi0/0", "ifAdminStatus": "up", "ifAlias": "test", "ifDescr": "Gi0/0"}]
        }

        mock_iface_qs = MagicMock()
        mock_iface_qs.select_related.return_value = []

        mock_table = MagicMock()
        mock_table.configure = MagicMock()

        with (
            patch.object(view, "get_cache_key", return_value="key"),
            patch.object(view, "get_last_fetched_key", return_value="last-key"),
            patch.object(view, "get_vlan_overrides_key", return_value="overrides-key"),
            patch.object(view, "get_vlan_groups_for_device", return_value=[]),
            patch.object(view, "_build_vlan_lookup_maps", return_value={"vid_to_groups": {}, "vid_to_vlans": {}}),
            patch.object(view, "get_interfaces", return_value=mock_iface_qs),
            patch.object(view, "_add_vlan_group_selection"),
            patch.object(view, "_add_missing_vlans_info"),
            patch.object(view, "get_table", return_value=mock_table),
            patch(
                "netbox_librenms_plugin.views.base.interfaces_view.get_virtual_chassis_member",
                return_value=member1,
            ) as mock_get_vc_member,
            patch("netbox_librenms_plugin.views.base.interfaces_view.get_interface_name_field", return_value="ifName"),
            patch("netbox_librenms_plugin.views.base.interfaces_view.cache") as mock_cache,
            patch("netbox_librenms_plugin.views.base.interfaces_view.timezone") as mock_tz,
        ):
            mock_cache.get.side_effect = lambda key: cached_data if key == "key" else None
            mock_cache.ttl.return_value = 300
            mock_tz.now.return_value = MagicMock()
            mock_tz.timedelta.return_value = MagicMock()
            ctx = view.get_context_data(request, obj, "ifName")

        # VC members should be included
        assert ctx["virtual_chassis_members"] is not None
        # get_virtual_chassis_member should have been called with obj and the port name
        mock_get_vc_member.assert_called_once_with(obj, "Gi0/0")


class TestBaseInterfaceTableViewAddVlanGroupSelection:
    """Tests for BaseInterfaceTableView._add_vlan_group_selection."""

    def _make_view(self):
        from netbox_librenms_plugin.views.base.interfaces_view import BaseInterfaceTableView

        view = object.__new__(BaseInterfaceTableView)
        view._librenms_api = MagicMock()
        return view

    def test_no_vlans_produces_empty_map(self):
        """Port with no tagged or untagged VLANs gets empty vlan_group_map."""
        view = self._make_view()
        port = {"untagged_vlan": None, "tagged_vlans": []}
        lookup_maps = {"vid_to_groups": {}}
        device = MagicMock()

        view._add_vlan_group_selection(port, lookup_maps, device)

        assert port["vlan_group_map"] == {}

    def test_single_group_for_vid_maps_directly(self):
        """When exactly one group contains the VID, it maps without ambiguity."""
        view = self._make_view()

        group = MagicMock()
        group.pk = 1
        group.name = "Corp"

        port = {"untagged_vlan": 100, "tagged_vlans": []}
        lookup_maps = {"vid_to_groups": {100: [group]}}
        device = MagicMock()

        view._add_vlan_group_selection(port, lookup_maps, device)

        assert port["vlan_group_map"][100]["group_id"] == "1"
        assert port["vlan_group_map"][100]["is_ambiguous"] is False

    def test_multiple_groups_with_most_specific_winner(self):
        """Multiple groups: _select_most_specific_group determines winner."""
        view = self._make_view()

        group_a = MagicMock()
        group_a.pk = 1
        group_a.name = "Rack-Group"

        group_b = MagicMock()
        group_b.pk = 2
        group_b.name = "Site-Group"

        port = {"untagged_vlan": 50, "tagged_vlans": []}
        lookup_maps = {"vid_to_groups": {50: [group_a, group_b]}}
        device = MagicMock()

        with patch.object(view, "_select_most_specific_group", return_value=group_a):
            view._add_vlan_group_selection(port, lookup_maps, device)

        assert port["vlan_group_map"][50]["group_id"] == "1"
        assert port["vlan_group_map"][50]["is_ambiguous"] is False

    def test_multiple_groups_no_winner_marks_ambiguous(self):
        """Multiple groups with no clear winner produces is_ambiguous=True."""
        view = self._make_view()

        group_a = MagicMock()
        group_b = MagicMock()

        port = {"untagged_vlan": 50, "tagged_vlans": []}
        lookup_maps = {"vid_to_groups": {50: [group_a, group_b]}}
        device = MagicMock()

        with patch.object(view, "_select_most_specific_group", return_value=None):
            view._add_vlan_group_selection(port, lookup_maps, device)

        assert port["vlan_group_map"][50]["is_ambiguous"] is True

    def test_vid_not_in_any_group_gets_global_entry(self):
        """VID not found in vid_to_groups produces group_id='' and group_name='Global'."""
        view = self._make_view()
        port = {"untagged_vlan": 999, "tagged_vlans": []}
        lookup_maps = {"vid_to_groups": {}}
        device = MagicMock()

        view._add_vlan_group_selection(port, lookup_maps, device)

        assert port["vlan_group_map"][999]["group_id"] == ""
        assert port["vlan_group_map"][999]["group_name"] == "Global"

    def test_vlan_group_overrides_applied(self):
        """vlan_group_overrides replace auto-selection for matching VIDs."""
        view = self._make_view()

        override_group = MagicMock()
        override_group.pk = 99
        override_group.name = "Override-Group"

        port = {"untagged_vlan": 100, "tagged_vlans": []}
        lookup_maps = {"vid_to_groups": {}}
        device = MagicMock()

        with patch("ipam.models.VLANGroup") as MockVLANGroup:
            MockVLANGroup.objects.in_bulk.return_value = {99: override_group}
            view._add_vlan_group_selection(port, lookup_maps, device, vlan_group_overrides={"100": "99"})

        assert port["vlan_group_map"][100]["group_id"] == "99"
        assert port["vlan_group_map"][100]["group_name"] == "Override-Group"

    def test_override_with_empty_string_forces_global(self):
        """Override with empty string means 'No Group (Global)'."""
        view = self._make_view()

        port = {"untagged_vlan": 100, "tagged_vlans": []}
        lookup_maps = {"vid_to_groups": {}}
        device = MagicMock()

        with patch("ipam.models.VLANGroup") as MockVLANGroup:
            MockVLANGroup.objects.in_bulk.return_value = {}
            view._add_vlan_group_selection(port, lookup_maps, device, vlan_group_overrides={"100": ""})

        assert port["vlan_group_map"][100]["group_id"] == ""
        assert port["vlan_group_map"][100]["group_name"] == "Global"


class TestBaseInterfaceTableViewAddMissingVlansInfo:
    """Tests for BaseInterfaceTableView._add_missing_vlans_info."""

    def _make_view(self):
        from netbox_librenms_plugin.views.base.interfaces_view import BaseInterfaceTableView

        view = object.__new__(BaseInterfaceTableView)
        return view

    def test_all_vlans_found_empty_missing(self):
        """When all VIDs are in vid_to_vlans, missing_vlans is empty."""
        view = self._make_view()
        port = {"untagged_vlan": 10, "tagged_vlans": [20, 30]}
        lookup_maps = {"vid_to_vlans": {10: [MagicMock()], 20: [MagicMock()], 30: [MagicMock()]}}

        view._add_missing_vlans_info(port, lookup_maps)

        assert port["missing_vlans"] == []

    def test_missing_untagged_vlan_added(self):
        """Untagged VID not in vid_to_vlans appears in missing_vlans."""
        view = self._make_view()
        port = {"untagged_vlan": 99, "tagged_vlans": []}
        lookup_maps = {"vid_to_vlans": {}}

        view._add_missing_vlans_info(port, lookup_maps)

        assert 99 in port["missing_vlans"]

    def test_missing_tagged_vlans_added(self):
        """Tagged VIDs not in vid_to_vlans appear in missing_vlans."""
        view = self._make_view()
        port = {"untagged_vlan": None, "tagged_vlans": [100, 200]}
        lookup_maps = {"vid_to_vlans": {100: [MagicMock()]}}  # 200 is missing

        view._add_missing_vlans_info(port, lookup_maps)

        assert 200 in port["missing_vlans"]
        assert 100 not in port["missing_vlans"]

    def test_no_vlans_produces_empty_missing(self):
        """Port with no VLANs results in empty missing_vlans."""
        view = self._make_view()
        port = {"untagged_vlan": None, "tagged_vlans": []}
        lookup_maps = {"vid_to_vlans": {}}

        view._add_missing_vlans_info(port, lookup_maps)

        assert port["missing_vlans"] == []


# =============================================================================
# BaseIPAddressTableView
# =============================================================================


class TestBaseIPAddressTableViewCreateBaseIpEntry:
    """Tests for BaseIPAddressTableView._create_base_ip_entry."""

    def _make_view(self):
        from netbox_librenms_plugin.views.base.ip_addresses_view import BaseIPAddressTableView

        view = object.__new__(BaseIPAddressTableView)
        view._librenms_api = MagicMock()
        return view

    def test_unified_format_preferred(self):
        """When ip_address/prefix_length present, those fields are used."""
        view = self._make_view()
        ip_entry = {"ip_address": "10.0.0.1", "prefix_length": 24, "port_id": 5}
        obj = MagicMock()
        obj.name = "router"
        obj.get_absolute_url.return_value = "/dcim/devices/1/"

        result = view._create_base_ip_entry(ip_entry, obj, vrfs=[])

        assert result["ip_address"] == "10.0.0.1"
        assert result["prefix_length"] == 24
        assert result["ip_with_mask"] == "10.0.0.1/24"

    def test_legacy_ipv4_format(self):
        """Legacy ipv4_address/ipv4_prefixlen format is handled."""
        view = self._make_view()
        ip_entry = {"ipv4_address": "192.168.1.100", "ipv4_prefixlen": 24, "port_id": 10}
        obj = MagicMock()
        obj.name = "router"
        obj.get_absolute_url.return_value = "/dcim/devices/1/"

        result = view._create_base_ip_entry(ip_entry, obj, vrfs=[])

        assert result["ip_address"] == "192.168.1.100"
        assert result["ip_with_mask"] == "192.168.1.100/24"

    def test_legacy_ipv6_format(self):
        """Legacy ipv6_compressed/ipv6_prefixlen format is handled."""
        view = self._make_view()
        ip_entry = {"ipv6_compressed": "2001:db8::1", "ipv6_prefixlen": 64, "port_id": 15}
        obj = MagicMock()
        obj.name = "router"
        obj.get_absolute_url.return_value = "/dcim/devices/1/"

        result = view._create_base_ip_entry(ip_entry, obj, vrfs=[])

        assert result["ip_address"] == "2001:db8::1"
        assert result["ip_with_mask"] == "2001:db8::1/64"

    def test_no_valid_format_raises_key_error(self):
        """When no valid IP format is found, KeyError is raised."""
        import pytest

        view = self._make_view()
        ip_entry = {"port_id": 1}  # No IP address fields
        obj = MagicMock()

        with pytest.raises(KeyError):
            view._create_base_ip_entry(ip_entry, obj, vrfs=[])


class TestBaseIPAddressTableViewEnrichIpData:
    """Tests for BaseIPAddressTableView.enrich_ip_data."""

    def _make_view(self):
        from netbox_librenms_plugin.views.base.ip_addresses_view import BaseIPAddressTableView

        view = object.__new__(BaseIPAddressTableView)
        view._librenms_api = MagicMock()
        view._librenms_api.server_key = "default"
        return view

    def test_non_dict_entries_skipped(self):
        """Non-dict items in ip_data are silently skipped."""
        view = self._make_view()
        obj = _mock_obj()

        with patch.object(
            view,
            "_prefetch_netbox_data",
            return_value={
                "vrfs": [],
                "ip_addresses_map": {},
                "interfaces_by_librenms_id": {},
                "interfaces_by_name": {},
                "all_interfaces": [],
                "device": obj,
            },
        ):
            result = view.enrich_ip_data(["not-a-dict", 42, None], obj, "ifName")

        assert result == []

    def test_entries_without_port_id_skipped(self):
        """Entries missing port_id field are skipped."""
        view = self._make_view()
        obj = _mock_obj()
        ip_data = [{"ip_address": "10.0.0.1", "prefix_length": 24}]  # no port_id

        with patch.object(
            view,
            "_prefetch_netbox_data",
            return_value={
                "vrfs": [],
                "ip_addresses_map": {},
                "interfaces_by_librenms_id": {},
                "interfaces_by_name": {},
                "all_interfaces": [],
                "device": obj,
            },
        ):
            result = view.enrich_ip_data(ip_data, obj, "ifName")

        assert result == []

    def test_new_ip_gets_sync_status(self):
        """IP not in NetBox gets exists=False, status='sync'."""
        view = self._make_view()
        obj = _mock_obj()

        ip_data = [{"ip_address": "10.1.1.1", "prefix_length": 24, "port_id": 10}]
        prefetched = {
            "vrfs": [],
            "ip_addresses_map": {},
            "interfaces_by_librenms_id": {},
            "interfaces_by_name": {},
            "all_interfaces": [],
            "device": obj,
        }

        with (
            patch.object(view, "_prefetch_netbox_data", return_value=prefetched),
            patch.object(view, "_get_port_info", return_value=None),
            patch.object(view, "_add_interface_info_to_ip"),
        ):
            result = view.enrich_ip_data(ip_data, obj, "ifName")

        assert len(result) == 1
        assert result[0]["exists"] is False
        assert result[0]["status"] == "sync"

    def test_existing_ip_gets_enriched(self):
        """IP that exists in NetBox goes through enrich_existing path."""
        view = self._make_view()
        obj = _mock_obj()

        existing_ip = MagicMock()
        existing_ip.get_absolute_url.return_value = "/ipam/ip-addresses/1/"
        existing_ip.vrf = None
        existing_ip.assigned_object = None

        ip_data = [{"ip_address": "192.168.1.1", "prefix_length": 24, "port_id": 20}]
        prefetched = {
            "vrfs": [],
            "ip_addresses_map": {"192.168.1.1/24": existing_ip},
            "interfaces_by_librenms_id": {},
            "interfaces_by_name": {},
            "all_interfaces": [],
            "device": obj,
        }

        with (
            patch.object(view, "_prefetch_netbox_data", return_value=prefetched),
            patch.object(view, "_get_port_info", return_value=None),
            patch.object(view, "_enrich_existing_ip") as mock_enrich,
            patch.object(view, "_add_interface_info_to_ip"),
        ):
            view.enrich_ip_data(ip_data, obj, "ifName")

        mock_enrich.assert_called_once()


class TestBaseIPAddressTableViewGetPortInfo:
    """Tests for BaseIPAddressTableView._get_port_info."""

    def _make_view(self):
        from netbox_librenms_plugin.views.base.ip_addresses_view import BaseIPAddressTableView

        view = object.__new__(BaseIPAddressTableView)
        view._librenms_api = MagicMock()
        return view

    def test_cache_miss_calls_api(self):
        """First call for a port_id queries the API."""
        view = self._make_view()
        port_data = {"port": [{"port_id": 10, "ifName": "Gi0/0"}]}
        view._librenms_api.get_port_by_id.return_value = (True, port_data)

        cache = {}
        result = view._get_port_info(10, cache, "ifName")

        assert result == port_data["port"][0]
        view._librenms_api.get_port_by_id.assert_called_once_with(10)

    def test_cache_hit_skips_api(self):
        """Subsequent call for same port_id uses cache."""
        view = self._make_view()
        cached_port = {"port_id": 10, "ifName": "Gi0/0"}
        cache = {10: cached_port}

        result = view._get_port_info(10, cache, "ifName")

        assert result is cached_port
        view._librenms_api.get_port_by_id.assert_not_called()

    def test_api_failure_caches_none(self):
        """When API fails, caches None and returns None."""
        view = self._make_view()
        view._librenms_api.get_port_by_id.return_value = (False, {})

        cache = {}
        result = view._get_port_info(99, cache, "ifName")

        assert result is None
        assert cache[99] is None

    def test_api_empty_port_list_caches_none(self):
        """When API returns empty port list, caches None."""
        view = self._make_view()
        view._librenms_api.get_port_by_id.return_value = (True, {"port": []})

        cache = {}
        result = view._get_port_info(99, cache, "ifName")

        assert result is None


class TestBaseIPAddressTableViewEnrichExistingIp:
    """Tests for BaseIPAddressTableView._enrich_existing_ip."""

    def _make_view(self):
        from netbox_librenms_plugin.views.base.ip_addresses_view import BaseIPAddressTableView

        view = object.__new__(BaseIPAddressTableView)
        view._librenms_api = MagicMock()
        view._librenms_api.server_key = "default"
        return view

    def test_matched_by_librenms_id(self):
        """IP matched by LibreNMS port_id → status='matched'."""
        view = self._make_view()

        assigned_iface = MagicMock()
        existing_ip = MagicMock()
        existing_ip.vrf = None
        existing_ip.assigned_object = assigned_iface
        existing_ip.get_absolute_url.return_value = "/ipam/ip-addresses/1/"

        prefetched = {
            "interfaces_by_librenms_id": {"10": assigned_iface},
        }

        enriched = {}
        view._enrich_existing_ip(enriched, existing_ip, 10, "Gi0/0", prefetched)

        assert enriched["status"] == "matched"

    def test_matched_by_interface_name(self):
        """IP matched by interface name → status='matched'."""
        view = self._make_view()

        assigned_iface = MagicMock()
        assigned_iface.name = "Gi0/0"
        assigned_iface.get_absolute_url.return_value = "/dcim/interfaces/5/"
        existing_ip = MagicMock()
        existing_ip.vrf = None
        existing_ip.assigned_object = assigned_iface
        existing_ip.get_absolute_url.return_value = "/ipam/ip-addresses/1/"

        prefetched = {
            "interfaces_by_librenms_id": {},
        }

        enriched = {}
        view._enrich_existing_ip(enriched, existing_ip, 10, "Gi0/0", prefetched)

        assert enriched["status"] == "matched"

    def test_update_status_when_not_matched(self):
        """IP exists but interface doesn't match → status='update'."""
        view = self._make_view()

        other_iface = MagicMock()
        other_iface.name = "Gi0/1"  # Different from librenms interface
        existing_ip = MagicMock()
        existing_ip.vrf = None
        existing_ip.assigned_object = other_iface
        existing_ip.get_absolute_url.return_value = "/ipam/ip-addresses/1/"

        prefetched = {
            "interfaces_by_librenms_id": {},  # No librenms_id match
        }

        enriched = {}
        view._enrich_existing_ip(enriched, existing_ip, 10, "Gi0/0", prefetched)

        assert enriched["status"] == "update"

    def test_vrf_info_added_when_present(self):
        """VRF info is added to enriched_ip when IP has a VRF."""
        view = self._make_view()

        vrf = MagicMock()
        vrf.pk = 1
        vrf.name = "MGMT-VRF"

        existing_ip = MagicMock()
        existing_ip.vrf = vrf
        existing_ip.assigned_object = None
        existing_ip.get_absolute_url.return_value = "/ipam/ip-addresses/1/"

        prefetched = {"interfaces_by_librenms_id": {}}

        enriched = {}
        view._enrich_existing_ip(enriched, existing_ip, 10, None, prefetched)

        assert enriched["vrf_id"] == 1
        assert enriched["vrf"] == "MGMT-VRF"

    def test_not_assigned_returns_update_early(self):
        """When IP is not assigned to any object, returns early with status='update'."""
        view = self._make_view()

        existing_ip = MagicMock()
        existing_ip.vrf = None
        existing_ip.assigned_object = None
        existing_ip.get_absolute_url.return_value = "/ipam/ip-addresses/1/"

        prefetched = {"interfaces_by_librenms_id": {}}

        enriched = {}
        view._enrich_existing_ip(enriched, existing_ip, 10, "Gi0/0", prefetched)

        assert enriched["status"] == "update"


class TestBaseIPAddressTableViewAddInterfaceInfo:
    """Tests for BaseIPAddressTableView._add_interface_info_to_ip."""

    def _make_view(self):
        from netbox_librenms_plugin.views.base.ip_addresses_view import BaseIPAddressTableView

        view = object.__new__(BaseIPAddressTableView)
        view._librenms_api = MagicMock()
        view._librenms_api.server_key = "default"
        return view

    def test_matches_by_librenms_id_first(self):
        """Interface matched by librenms_id takes priority over name match."""
        view = self._make_view()

        iface_by_id = MagicMock()
        iface_by_id.name = "Gi0/0"
        iface_by_id.get_absolute_url.return_value = "/dcim/interfaces/1/"

        prefetched = {
            "interfaces_by_librenms_id": {"10": iface_by_id},
            "interfaces_by_name": {},
        }

        enriched = {}
        view._add_interface_info_to_ip(enriched, 10, "Gi0/0", prefetched)

        assert enriched["interface_name"] == "Gi0/0"
        assert enriched["interface_url"] == "/dcim/interfaces/1/"

    def test_falls_back_to_name_match(self):
        """When no librenms_id match, falls back to interface name match."""
        view = self._make_view()

        iface_by_name = MagicMock()
        iface_by_name.get_absolute_url.return_value = "/dcim/interfaces/2/"

        prefetched = {
            "interfaces_by_librenms_id": {},
            "interfaces_by_name": {"Gi0/1": iface_by_name},
        }

        enriched = {}
        view._add_interface_info_to_ip(enriched, 20, "Gi0/1", prefetched)

        assert enriched["interface_url"] == "/dcim/interfaces/2/"

    def test_no_match_leaves_enriched_unchanged(self):
        """When no interface found, enriched dict is not modified."""
        view = self._make_view()

        prefetched = {
            "interfaces_by_librenms_id": {},
            "interfaces_by_name": {},
        }

        enriched = {"ip_address": "10.0.0.1"}
        view._add_interface_info_to_ip(enriched, 30, "Gi0/2", prefetched)

        assert "interface_url" not in enriched


class TestBaseIPAddressTableViewPrepareContext:
    """Tests for BaseIPAddressTableView._prepare_context."""

    def _make_view(self):
        from netbox_librenms_plugin.views.base.ip_addresses_view import BaseIPAddressTableView

        view = object.__new__(BaseIPAddressTableView)
        view._librenms_api = MagicMock()
        view._librenms_api.server_key = "default"
        view._librenms_api.cache_timeout = 300
        return view

    def test_cache_miss_fetch_fresh_false_returns_none(self):
        """When no cached data and fetch_fresh=False, returns None."""
        view = self._make_view()
        obj = _mock_obj()
        request = _mock_request()

        with (
            patch.object(view, "get_cache_key", return_value="ip-key"),
            patch("netbox_librenms_plugin.views.base.ip_addresses_view.cache") as mock_cache,
            patch(
                "netbox_librenms_plugin.views.base.ip_addresses_view.get_interface_name_field",
                return_value="ifName",
            ),
        ):
            mock_cache.get.return_value = None
            result = view._prepare_context(request, obj, "ifName", fetch_fresh=False)

        assert result is None

    def test_fetch_fresh_caches_enriched_data(self):
        """When fetch_fresh=True, IP data is fetched, enriched, cached and returned."""
        view = self._make_view()
        obj = _mock_obj()
        request = _mock_request()

        raw_ips = [{"ip_address": "10.0.0.1", "prefix_length": 24, "port_id": 1}]
        enriched_ips = [{"ip_with_mask": "10.0.0.1/24", "status": "sync"}]
        mock_table = MagicMock()
        mock_table.configure = MagicMock()

        with (
            patch.object(view, "get_ip_addresses", return_value=(True, raw_ips)),
            patch.object(view, "enrich_ip_data", return_value=enriched_ips),
            patch.object(view, "get_table", return_value=mock_table),
            patch.object(view, "get_cache_key", return_value="ip-key"),
            patch("netbox_librenms_plugin.views.base.ip_addresses_view.cache") as mock_cache,
            patch("netbox_librenms_plugin.views.base.ip_addresses_view.timezone") as mock_tz,
            patch(
                "netbox_librenms_plugin.views.base.ip_addresses_view.get_interface_name_field",
                return_value="ifName",
            ),
        ):
            mock_cache.ttl.return_value = 300
            mock_tz.now.return_value = MagicMock()
            mock_tz.timedelta.return_value = MagicMock()
            result = view._prepare_context(request, obj, "ifName", fetch_fresh=True)

        assert result is not None
        assert result["table"] is mock_table
        mock_cache.set.assert_called()

    def test_cache_hit_fetch_fresh_false_uses_cached_data(self):
        """Cached data available with fetch_fresh=False returns context."""
        view = self._make_view()
        obj = _mock_obj()
        request = _mock_request()

        cached_ips = [{"ip_with_mask": "192.168.1.1/24", "status": "matched"}]
        mock_table = MagicMock()
        mock_table.configure = MagicMock()

        with (
            patch.object(view, "get_cache_key", return_value="ip-key"),
            patch.object(view, "enrich_ip_data", return_value=cached_ips),
            patch.object(view, "get_table", return_value=mock_table),
            patch("netbox_librenms_plugin.views.base.ip_addresses_view.cache") as mock_cache,
            patch("netbox_librenms_plugin.views.base.ip_addresses_view.timezone") as mock_tz,
            patch(
                "netbox_librenms_plugin.views.base.ip_addresses_view.get_interface_name_field",
                return_value="ifName",
            ),
        ):
            mock_cache.get.return_value = {"ip_addresses": cached_ips}
            mock_cache.ttl.return_value = 200
            mock_tz.now.return_value = MagicMock()
            mock_tz.timedelta.return_value = MagicMock()
            result = view._prepare_context(request, obj, "ifName", fetch_fresh=False)

        assert result is not None
        assert result["table"] is mock_table


class TestBaseIPAddressTableViewGetContextData:
    """Tests for BaseIPAddressTableView.get_context_data."""

    def _make_view(self):
        from netbox_librenms_plugin.views.base.ip_addresses_view import BaseIPAddressTableView

        view = object.__new__(BaseIPAddressTableView)
        view._librenms_api = MagicMock()
        view._librenms_api.server_key = "default"
        return view

    def test_returns_empty_context_when_no_cache(self):
        """When _prepare_context returns None, returns fallback context with table=None."""
        view = self._make_view()
        obj = _mock_obj()
        request = _mock_request()

        with (
            patch.object(view, "_prepare_context", return_value=None),
            patch(
                "netbox_librenms_plugin.views.base.ip_addresses_view.get_interface_name_field",
                return_value="ifName",
            ),
        ):
            ctx = view.get_context_data(request, obj)

        assert ctx["table"] is None
        assert ctx["object"] is obj
        assert ctx["cache_expiry"] is None

    def test_returns_context_when_cache_populated(self):
        """When _prepare_context returns context, that context is returned."""
        view = self._make_view()
        obj = _mock_obj()
        request = _mock_request()

        prepared = {"table": MagicMock(), "object": obj, "cache_expiry": None, "server_key": "default"}

        with (
            patch.object(view, "_prepare_context", return_value=prepared),
            patch(
                "netbox_librenms_plugin.views.base.ip_addresses_view.get_interface_name_field",
                return_value="ifName",
            ),
        ):
            ctx = view.get_context_data(request, obj)

        assert ctx is prepared


class TestBaseIPAddressTableViewPost:
    """Tests for BaseIPAddressTableView.post."""

    def _make_view(self):
        from netbox_librenms_plugin.views.base.ip_addresses_view import BaseIPAddressTableView

        view = object.__new__(BaseIPAddressTableView)
        view._librenms_api = MagicMock()
        view._librenms_api.server_key = "default"
        view.model = MagicMock()
        view.partial_template_name = "test_template.html"
        return view

    def test_post_no_ips_renders_error(self):
        """When _prepare_context returns None, renders with error."""
        view = self._make_view()
        obj = _mock_obj()
        request = _mock_request()

        with (
            patch.object(view, "get_object", return_value=obj),
            patch.object(view, "_prepare_context", return_value=None),
            patch(
                "netbox_librenms_plugin.views.base.ip_addresses_view.get_interface_name_field",
                return_value="ifName",
            ),
            patch("netbox_librenms_plugin.views.base.ip_addresses_view.messages") as mock_messages,
            patch("netbox_librenms_plugin.views.base.ip_addresses_view.render") as mock_render,
        ):
            mock_render.return_value = MagicMock()
            view.post(request, pk=1)

        mock_messages.error.assert_called_once()
        mock_render.assert_called_once()

    def test_post_success_renders_with_context(self):
        """Successful fetch renders template with context."""
        view = self._make_view()
        obj = _mock_obj()
        request = _mock_request()
        fake_ctx = {"table": MagicMock(), "object": obj}

        with (
            patch.object(view, "get_object", return_value=obj),
            patch.object(view, "_prepare_context", return_value=fake_ctx),
            patch(
                "netbox_librenms_plugin.views.base.ip_addresses_view.get_interface_name_field",
                return_value="ifName",
            ),
            patch("netbox_librenms_plugin.views.base.ip_addresses_view.messages") as mock_messages,
            patch("netbox_librenms_plugin.views.base.ip_addresses_view.render") as mock_render,
        ):
            mock_render.return_value = MagicMock()
            view.post(request, pk=1)

        mock_messages.success.assert_called_once()
        mock_render.assert_called_once()
        render_call_kwargs = mock_render.call_args[0]
        assert "ip_sync" in render_call_kwargs[2]


class TestBaseIPAddressTableViewPrefetchNetboxData:
    """Tests for BaseIPAddressTableView._prefetch_netbox_data."""

    def _make_view(self):
        from netbox_librenms_plugin.views.base.ip_addresses_view import BaseIPAddressTableView

        view = object.__new__(BaseIPAddressTableView)
        view._librenms_api = MagicMock()
        view._librenms_api.server_key = "default"
        return view

    def test_builds_lookup_maps(self):
        """_prefetch_netbox_data builds interface and IP lookup maps."""
        view = self._make_view()

        iface = MagicMock()
        iface.name = "Gi0/0"

        obj = MagicMock()
        obj.interfaces = MagicMock()
        obj.interfaces.all.return_value = [iface]

        with (
            patch(
                "netbox_librenms_plugin.views.base.ip_addresses_view.get_librenms_device_id",
                return_value=10,
            ) as mock_get_librenms_id,
            patch("netbox_librenms_plugin.views.base.ip_addresses_view.IPAddress") as MockIP,
            patch("netbox_librenms_plugin.views.base.ip_addresses_view.VRF") as MockVRF,
        ):
            MockIP.objects.select_related.return_value = []
            MockVRF.objects.all.return_value = []
            result = view._prefetch_netbox_data(obj)

        assert "interfaces_by_librenms_id" in result
        assert "interfaces_by_name" in result
        assert "ip_addresses_map" in result
        assert result["interfaces_by_name"]["Gi0/0"] is iface
        assert result["interfaces_by_librenms_id"]["10"] is iface
        # Verify server_key is forwarded so IDs are scoped per-server
        mock_get_librenms_id.assert_called_once_with(iface, "default", auto_save=False)


class TestBaseIPAddressTableViewGetTable:
    """Tests for BaseIPAddressTableView.get_table."""

    def _make_view(self):
        from netbox_librenms_plugin.views.base.ip_addresses_view import BaseIPAddressTableView

        view = object.__new__(BaseIPAddressTableView)
        view._librenms_api = MagicMock()
        view._librenms_api.server_key = "default"
        view.request = _mock_request()
        return view

    def test_get_table_sets_htmx_url(self):
        """get_table creates table and sets htmx_url with tab parameter."""
        view = self._make_view()
        obj = _mock_obj()

        with patch("netbox_librenms_plugin.views.base.ip_addresses_view.IPAddressTable") as MockTable:
            mock_table = MagicMock()
            MockTable.return_value = mock_table
            view.get_table([], obj, view.request)

        assert isinstance(mock_table.htmx_url, str)
        assert "ipaddresses" in mock_table.htmx_url
        assert "server_key=default" in mock_table.htmx_url


# ===========================================================================
# BaseInterfaceTableView — missing line coverage (lines 31, 44, 51, 70, 149, 203, 224-230)
# ===========================================================================


class TestBaseInterfaceTableViewMissingLines:
    """Targeted tests for remaining uncovered lines in BaseInterfaceTableView."""

    def _make_view(self):
        from unittest.mock import MagicMock

        from netbox_librenms_plugin.views.base.interfaces_view import BaseInterfaceTableView

        view = object.__new__(BaseInterfaceTableView)
        view._librenms_api = MagicMock()
        view._librenms_api.server_key = "default"
        view._librenms_api.cache_timeout = 300
        view.model = MagicMock()
        view.model.__name__ = "device"
        view.interface_name_field = None
        return view

    def test_get_object_calls_get_object_or_404(self):
        """get_object delegates to get_object_or_404(self.model, pk=pk)."""
        from unittest.mock import MagicMock, patch

        view = self._make_view()
        mock_obj = MagicMock()

        with patch(
            "netbox_librenms_plugin.views.base.interfaces_view.get_object_or_404",
            return_value=mock_obj,
        ) as mock_404:
            result = view.get_object(42)

        mock_404.assert_called_once_with(view.model, pk=42)
        assert result is mock_obj

    def test_get_interfaces_raises_not_implemented(self):
        """get_interfaces raises NotImplementedError — must be overridden."""
        from unittest.mock import MagicMock

        view = self._make_view()
        try:
            view.get_interfaces(MagicMock())
            assert False, "Expected NotImplementedError"
        except NotImplementedError:
            pass

    def test_get_redirect_url_raises_not_implemented(self):
        """get_redirect_url raises NotImplementedError — must be overridden."""
        from unittest.mock import MagicMock

        view = self._make_view()
        try:
            view.get_redirect_url(MagicMock())
            assert False, "Expected NotImplementedError"
        except NotImplementedError:
            pass

    def test_get_table_raises_not_implemented(self):
        """get_table raises NotImplementedError — must be overridden."""
        from unittest.mock import MagicMock

        view = self._make_view()
        try:
            view.get_table([], MagicMock(), "ifName")
            assert False, "Expected NotImplementedError"
        except NotImplementedError:
            pass

    def test_get_context_data_with_none_interface_name_field_calls_helper(self):
        """When interface_name_field=None, get_interface_name_field(request) is called."""
        from unittest.mock import MagicMock, patch

        view = self._make_view()
        obj = MagicMock()
        obj.virtual_chassis = None
        obj.id = 1
        request = MagicMock()

        with (
            patch.object(view, "get_cache_key", return_value="k"),
            patch.object(view, "get_last_fetched_key", return_value="lk"),
            patch.object(view, "get_vlan_overrides_key", return_value="vk"),
            patch.object(view, "get_vlan_groups_for_device", return_value=[]),
            patch.object(view, "_build_vlan_lookup_maps", return_value={}),
            patch(
                "netbox_librenms_plugin.views.base.interfaces_view.get_interface_name_field",
                return_value="ifDescr",
            ) as mock_gnf,
            patch("netbox_librenms_plugin.views.base.interfaces_view.cache") as mock_cache,
            patch("netbox_librenms_plugin.views.base.interfaces_view.timezone"),
        ):
            mock_cache.get.return_value = None
            mock_cache.ttl.return_value = None
            view.get_context_data(request, obj, interface_name_field=None)

        mock_gnf.assert_called_once_with(request)

    def test_ifalias_cleared_when_matches_ifdescr(self):
        """port['ifAlias'] is set to '' when it equals ifDescr (line 203)."""
        from unittest.mock import MagicMock, patch

        view = self._make_view()
        obj = MagicMock()
        obj.virtual_chassis = None
        obj.id = 1
        request = MagicMock()

        ports_data = [
            {
                "port_id": 1,
                "ifName": "Gi0/0",
                "ifDescr": "GigabitEthernet0/0",
                "ifAlias": "GigabitEthernet0/0",  # matches ifDescr -> cleared
                "ifAdminStatus": "up",
            }
        ]
        cached_data = {"ports": ports_data}

        mock_iface = MagicMock()
        mock_iface.name = "Gi0/0"
        mock_ifaces_qs = MagicMock()
        mock_ifaces_qs.select_related.return_value = [mock_iface]

        mock_table = MagicMock()
        mock_table.configure = MagicMock()

        with (
            patch.object(view, "get_cache_key", return_value="k"),
            patch.object(view, "get_last_fetched_key", return_value="lk"),
            patch.object(view, "get_vlan_overrides_key", return_value="vk"),
            patch.object(view, "get_vlan_groups_for_device", return_value=[]),
            patch.object(view, "_build_vlan_lookup_maps", return_value={"vid_to_groups": {}, "vid_to_vlans": {}}),
            patch.object(view, "get_interfaces", return_value=mock_ifaces_qs),
            patch.object(view, "_add_vlan_group_selection"),
            patch.object(view, "_add_missing_vlans_info"),
            patch.object(view, "get_table", return_value=mock_table),
            patch("netbox_librenms_plugin.views.base.interfaces_view.get_interface_name_field", return_value="ifName"),
            patch("netbox_librenms_plugin.views.base.interfaces_view.cache") as mock_cache,
            patch("netbox_librenms_plugin.views.base.interfaces_view.timezone") as mock_tz,
        ):
            mock_cache.get.side_effect = lambda key: cached_data if key == "k" else None
            mock_cache.ttl.return_value = 300
            mock_tz.now.return_value = MagicMock()
            mock_tz.timedelta.return_value = MagicMock()
            view.get_context_data(request, obj, "ifName")

        assert ports_data[0]["ifAlias"] == ""

    def test_netbox_only_interfaces_vc_device_name_lookup(self):
        """VC branch fetches device_name from VC members for netbox-only interfaces (lines 224-226)."""
        from unittest.mock import MagicMock, patch

        view = self._make_view()

        member1 = MagicMock()
        member1.id = 10
        member1.name = "switch-1"

        vc = MagicMock()
        vc.members.all.return_value = [member1]
        vc.members.get.return_value = member1

        obj = MagicMock()
        obj.virtual_chassis = vc
        obj.id = 10
        request = MagicMock()

        # Gi0/0 is in LibreNMS; Gi0/1 is only in NetBox
        cached_data = {"ports": [{"port_id": 1, "ifName": "Gi0/0", "ifAdminStatus": "up", "ifAlias": "x"}]}

        netbox_only_iface = MagicMock()
        netbox_only_iface.name = "Gi0/1"
        netbox_only_iface.id = 99
        netbox_only_iface.type = "1000base-t"

        mock_ifaces_qs = MagicMock()
        mock_ifaces_qs.select_related.return_value = [netbox_only_iface]

        mock_table = MagicMock()
        mock_table.configure = MagicMock()

        with (
            patch.object(view, "get_cache_key", return_value="k"),
            patch.object(view, "get_last_fetched_key", return_value="lk"),
            patch.object(view, "get_vlan_overrides_key", return_value="vk"),
            patch.object(view, "get_vlan_groups_for_device", return_value=[]),
            patch.object(view, "_build_vlan_lookup_maps", return_value={"vid_to_groups": {}, "vid_to_vlans": {}}),
            patch.object(view, "get_interfaces", return_value=mock_ifaces_qs),
            patch.object(view, "_add_vlan_group_selection"),
            patch.object(view, "_add_missing_vlans_info"),
            patch.object(view, "get_table", return_value=mock_table),
            patch("netbox_librenms_plugin.views.base.interfaces_view.get_virtual_chassis_member", return_value=member1),
            patch("netbox_librenms_plugin.views.base.interfaces_view.get_interface_name_field", return_value="ifName"),
            patch("netbox_librenms_plugin.views.base.interfaces_view.cache") as mock_cache,
            patch("netbox_librenms_plugin.views.base.interfaces_view.timezone") as mock_tz,
        ):
            mock_cache.get.side_effect = lambda key: cached_data if key == "k" else None
            mock_cache.ttl.return_value = 300
            mock_tz.now.return_value = MagicMock()
            mock_tz.timedelta.return_value = MagicMock()
            ctx = view.get_context_data(request, obj, "ifName")

        netbox_only = ctx.get("netbox_only_interfaces", [])
        assert any(item["name"] == "Gi0/1" for item in netbox_only)
        gi01 = next(i for i in netbox_only if i["name"] == "Gi0/1")
        assert gi01["device_name"] == "switch-1"

    def test_netbox_only_interfaces_non_vc_device_name_from_obj(self):
        """Non-VC branch uses obj.name directly for device_name (line 228)."""
        from unittest.mock import MagicMock, patch

        view = self._make_view()
        obj = MagicMock()
        obj.virtual_chassis = None
        obj.id = 1
        obj.name = "router-1"
        request = MagicMock()

        # Gi0/0 in LibreNMS; Gi0/1 only in NetBox
        cached_data = {"ports": [{"port_id": 1, "ifName": "Gi0/0", "ifAdminStatus": "up", "ifAlias": None}]}

        netbox_only_iface = MagicMock()
        netbox_only_iface.name = "Gi0/1"
        netbox_only_iface.id = 55
        netbox_only_iface.type = "1000base-t"

        mock_ifaces_qs = MagicMock()
        mock_ifaces_qs.select_related.return_value = [netbox_only_iface]

        mock_table = MagicMock()
        mock_table.configure = MagicMock()

        with (
            patch.object(view, "get_cache_key", return_value="k"),
            patch.object(view, "get_last_fetched_key", return_value="lk"),
            patch.object(view, "get_vlan_overrides_key", return_value="vk"),
            patch.object(view, "get_vlan_groups_for_device", return_value=[]),
            patch.object(view, "_build_vlan_lookup_maps", return_value={"vid_to_groups": {}, "vid_to_vlans": {}}),
            patch.object(view, "get_interfaces", return_value=mock_ifaces_qs),
            patch.object(view, "_add_vlan_group_selection"),
            patch.object(view, "_add_missing_vlans_info"),
            patch.object(view, "get_table", return_value=mock_table),
            patch("netbox_librenms_plugin.views.base.interfaces_view.get_interface_name_field", return_value="ifName"),
            patch("netbox_librenms_plugin.views.base.interfaces_view.cache") as mock_cache,
            patch("netbox_librenms_plugin.views.base.interfaces_view.timezone") as mock_tz,
        ):
            mock_cache.get.side_effect = lambda key: cached_data if key == "k" else None
            mock_cache.ttl.return_value = 300
            mock_tz.now.return_value = MagicMock()
            mock_tz.timedelta.return_value = MagicMock()
            ctx = view.get_context_data(request, obj, "ifName")

        netbox_only = ctx.get("netbox_only_interfaces", [])
        gi01 = next((i for i in netbox_only if i["name"] == "Gi0/1"), None)
        assert gi01 is not None
        assert gi01["device_name"] == "router-1"
