"""Unit tests for SyncInterfacesView: update_interface_attributes and handle_mac_address."""

from unittest.mock import MagicMock, patch

import pytest


class TestUpdateInterfaceAttributes:
    """update_interface_attributes() must set fields respecting exclude_columns."""

    @pytest.fixture
    def view(self, mock_librenms_api):
        """Return a SyncInterfacesView wired to the shared mock API fixture."""
        from netbox_librenms_plugin.views.sync.interfaces import SyncInterfacesView

        v = object.__new__(SyncInterfacesView)
        v._librenms_api = mock_librenms_api
        v.request = MagicMock()
        v._lookup_maps = {}
        return v

    def _make_device_interface(self, **extra):
        """Return a MagicMock mimicking a dcim.Interface."""
        from dcim.models import Interface  # noqa: F401

        iface = MagicMock(
            spec=[
                "name",
                "type",
                "speed",
                "description",
                "mtu",
                "enabled",
                "save",
                "cf",
                "custom_field_data",
                "mac_addresses",
                "primary_mac_address",
            ]
        )
        iface.cf = {"librenms_id": {"default": 1}}
        iface.__class__ = Interface
        for k, v in extra.items():
            setattr(iface, k, v)
        return iface

    def test_sets_speed_via_convert(self, view):
        iface = self._make_device_interface()
        librenms_data = {"ifName": "eth0", "ifSpeed": 1_000_000_000}

        with patch(
            "netbox_librenms_plugin.views.sync.interfaces.convert_speed_to_kbps", return_value=1_000_000
        ) as mock_convert:
            with patch("netbox_librenms_plugin.views.sync.interfaces.set_librenms_device_id"):
                view.update_interface_attributes(iface, librenms_data, "1000base-t", set(), "ifName")

        mock_convert.assert_called_once_with(1_000_000_000)
        assert iface.speed == 1_000_000

    def test_skips_excluded_columns(self, view):
        speed_sentinel = object()
        iface = self._make_device_interface(speed=speed_sentinel)
        librenms_data = {"ifName": "eth0", "ifSpeed": 1_000_000_000, "ifAlias": "uplink"}

        with patch("netbox_librenms_plugin.views.sync.interfaces.convert_speed_to_kbps", return_value=1_000_000):
            with patch("netbox_librenms_plugin.views.sync.interfaces.set_librenms_device_id"):
                view.update_interface_attributes(iface, librenms_data, "1000base-t", {"speed"}, "ifName")

        # speed should NOT have been mutated (excluded)
        assert iface.speed is speed_sentinel

    def test_sets_type_for_device_interface(self, view):
        from dcim.models import Interface

        iface = MagicMock()
        iface.__class__ = Interface
        iface.cf = {}
        iface.mac_addresses = MagicMock()
        librenms_data = {"ifName": "eth0", "ifType": "ethernetCsmacd"}

        with patch("netbox_librenms_plugin.views.sync.interfaces.convert_speed_to_kbps", return_value=None):
            view.update_interface_attributes(iface, librenms_data, "1000base-t", set(), "ifName")

        assert iface.type == "1000base-t"

    def test_does_not_set_type_for_vm_interface(self, view):
        from virtualization.models import VMInterface

        iface = MagicMock()
        iface.__class__ = VMInterface
        iface.cf = {}
        iface.mac_addresses = MagicMock()
        original_type = "some_type"
        iface.type = original_type
        librenms_data = {"ifName": "eth0", "ifType": "ethernetCsmacd"}

        with patch("netbox_librenms_plugin.views.sync.interfaces.convert_speed_to_kbps", return_value=None):
            view.update_interface_attributes(iface, librenms_data, "1000base-t", set(), "ifName")

        # type is NOT in the mapping for non-device interfaces (type set only if is_device_interface)
        assert iface.type == original_type

    def test_sets_description_only_when_alias_differs_from_name(self, view):
        from dcim.models import Interface

        iface = MagicMock()
        iface.__class__ = Interface
        iface.cf = {}
        iface.mac_addresses = MagicMock()
        desc_sentinel = object()
        iface.description = desc_sentinel

        # ifAlias == interface name field value → description should NOT be set
        librenms_data = {"ifName": "eth0", "ifAlias": "eth0"}

        with patch("netbox_librenms_plugin.views.sync.interfaces.convert_speed_to_kbps", return_value=None):
            view.update_interface_attributes(iface, librenms_data, None, {"type", "speed", "mtu"}, "ifName")

        assert iface.description is desc_sentinel  # untouched: alias == name, no update

    def test_sets_description_when_alias_differs(self, view):
        from dcim.models import Interface

        iface = MagicMock()
        iface.__class__ = Interface
        iface.cf = {}
        iface.mac_addresses = MagicMock()

        librenms_data = {"ifName": "eth0", "ifAlias": "uplink-port"}

        with patch("netbox_librenms_plugin.views.sync.interfaces.convert_speed_to_kbps", return_value=None):
            view.update_interface_attributes(iface, librenms_data, None, {"type", "speed", "mtu"}, "ifName")

        assert iface.description == "uplink-port"

    def test_sets_librenms_id_when_port_id_present(self, view):
        """
        set_librenms_device_id() is called unconditionally when port_id is not None.

        Historically the call was guarded by ``"librenms_id" in interface.cf``, which
        prevented the mapping from being created for brand-new interfaces. This test
        ensures the mapping is created even when no existing custom-field mapping is present.
        """
        from dcim.models import Interface

        iface = MagicMock()
        iface.__class__ = Interface
        iface.cf = {}  # empty — first-time write, no existing mapping
        iface.mac_addresses = MagicMock()
        librenms_data = {"ifName": "eth0", "port_id": 77}

        with patch("netbox_librenms_plugin.views.sync.interfaces.convert_speed_to_kbps", return_value=None):
            with patch("netbox_librenms_plugin.views.sync.interfaces.set_librenms_device_id") as mock_set:
                view.update_interface_attributes(iface, librenms_data, None, {"type", "speed", "mtu"}, "ifName")

        mock_set.assert_called_once_with(iface, 77, view._librenms_api.server_key)

    def test_does_not_set_librenms_id_when_port_id_none(self, view):
        from dcim.models import Interface

        iface = MagicMock()
        iface.__class__ = Interface
        iface.cf = {"librenms_id": {"default": 1}}
        iface.mac_addresses = MagicMock()
        librenms_data = {"ifName": "eth0", "port_id": None}

        with patch("netbox_librenms_plugin.views.sync.interfaces.convert_speed_to_kbps", return_value=None):
            with patch("netbox_librenms_plugin.views.sync.interfaces.set_librenms_device_id") as mock_set:
                view.update_interface_attributes(iface, librenms_data, None, {"type", "speed", "mtu"}, "ifName")

        mock_set.assert_not_called()

    def test_sets_enabled_true_when_admin_status_none(self, view):
        from dcim.models import Interface

        iface = MagicMock()
        iface.__class__ = Interface
        iface.cf = {}
        iface.mac_addresses = MagicMock()
        librenms_data = {"ifName": "eth0", "ifAdminStatus": None}

        with patch("netbox_librenms_plugin.views.sync.interfaces.convert_speed_to_kbps", return_value=None):
            view.update_interface_attributes(iface, librenms_data, None, {"type", "speed", "mtu"}, "ifName")

        assert iface.enabled is True

    def test_sets_enabled_based_on_admin_status_string(self, view):
        from dcim.models import Interface

        iface = MagicMock()
        iface.__class__ = Interface
        iface.cf = {}
        iface.mac_addresses = MagicMock()
        librenms_data = {"ifName": "eth0", "ifAdminStatus": "down"}

        with patch("netbox_librenms_plugin.views.sync.interfaces.convert_speed_to_kbps", return_value=None):
            view.update_interface_attributes(iface, librenms_data, None, {"type", "speed", "mtu"}, "ifName")

        assert iface.enabled is False

    def test_calls_save_at_end(self, view):
        from dcim.models import Interface

        iface = MagicMock()
        iface.__class__ = Interface
        iface.cf = {}
        iface.mac_addresses = MagicMock()
        librenms_data = {"ifName": "eth0"}

        with patch("netbox_librenms_plugin.views.sync.interfaces.convert_speed_to_kbps", return_value=None):
            view.update_interface_attributes(iface, librenms_data, None, {"type", "speed", "mtu"}, "ifName")

        iface.save.assert_called_once()

    def test_excludes_mac_address_when_in_excluded(self, view):
        from dcim.models import Interface

        iface = MagicMock()
        iface.__class__ = Interface
        iface.cf = {}
        iface.mac_addresses = MagicMock()
        librenms_data = {"ifName": "eth0", "ifPhysAddress": "aa:bb:cc:dd:ee:ff"}

        with patch("netbox_librenms_plugin.views.sync.interfaces.convert_speed_to_kbps", return_value=None):
            with patch.object(view, "handle_mac_address") as mock_mac:
                view.update_interface_attributes(iface, librenms_data, None, {"mac_address"}, "ifName")

        mock_mac.assert_not_called()


class TestHandleMacAddress:
    """
    handle_mac_address() must work for both Interface (has primary_mac_address)
    and VMInterface (does not have primary_mac_address)."""

    @pytest.fixture
    def view(self, mock_librenms_api):
        """Return a SyncInterfacesView wired to the shared mock API fixture."""
        from netbox_librenms_plugin.views.sync.interfaces import SyncInterfacesView

        v = object.__new__(SyncInterfacesView)
        v._librenms_api = mock_librenms_api
        v.request = MagicMock()
        v._lookup_maps = {}
        return v

    def test_creates_new_mac_and_adds_to_interface(self, view):
        iface = MagicMock()
        iface.mac_addresses = MagicMock()
        iface.mac_addresses.filter.return_value.first.return_value = None
        new_mac = MagicMock()

        with patch("netbox_librenms_plugin.views.sync.interfaces.MACAddress") as mock_cls:
            mock_cls.objects.create.return_value = new_mac
            view.handle_mac_address(iface, "aa:bb:cc:dd:ee:ff")

        mock_cls.objects.create.assert_called_once_with(mac_address="aa:bb:cc:dd:ee:ff")
        iface.mac_addresses.add.assert_called_once_with(new_mac)

    def test_reuses_existing_mac(self, view):
        existing_mac = MagicMock()
        iface = MagicMock()
        iface.mac_addresses = MagicMock()
        iface.mac_addresses.filter.return_value.first.return_value = existing_mac

        with patch("netbox_librenms_plugin.views.sync.interfaces.MACAddress") as mock_cls:
            view.handle_mac_address(iface, "aa:bb:cc:dd:ee:ff")

        mock_cls.objects.create.assert_not_called()
        iface.mac_addresses.add.assert_called_once_with(existing_mac)

    def test_sets_primary_mac_when_attribute_present(self, view):
        mac_obj = MagicMock()
        iface = MagicMock(spec=["mac_addresses", "primary_mac_address"])
        iface.mac_addresses = MagicMock()
        iface.mac_addresses.filter.return_value.first.return_value = None

        with patch("netbox_librenms_plugin.views.sync.interfaces.MACAddress") as mock_cls:
            mock_cls.objects.create.return_value = mac_obj
            view.handle_mac_address(iface, "aa:bb:cc:dd:ee:ff")

        assert iface.primary_mac_address is mac_obj

    def test_no_error_when_primary_mac_attribute_absent(self, view):
        """VMInterface does not have primary_mac_address — handle_mac_address must not raise."""
        mac_obj = MagicMock()
        iface = MagicMock(spec=["mac_addresses"])  # no primary_mac_address attr
        iface.mac_addresses = MagicMock()
        iface.mac_addresses.filter.return_value.first.return_value = None

        with patch("netbox_librenms_plugin.views.sync.interfaces.MACAddress") as mock_cls:
            mock_cls.objects.create.return_value = mac_obj
            # Must not raise AttributeError
            view.handle_mac_address(iface, "aa:bb:cc:dd:ee:ff")

    def test_noop_when_mac_address_is_falsy(self, view):
        iface = MagicMock()
        with patch("netbox_librenms_plugin.views.sync.interfaces.MACAddress") as mock_cls:
            view.handle_mac_address(iface, "")
            view.handle_mac_address(iface, None)

        mock_cls.objects.create.assert_not_called()
