"""Coverage tests for import_utils/device_operations.py."""

from unittest.mock import MagicMock, patch


class TestTryChassisDeviceTypeMatch:
    """Tests for _try_chassis_device_type_match (lines 45-65)."""

    def test_api_failure_returns_none(self):
        from netbox_librenms_plugin.import_utils.device_operations import _try_chassis_device_type_match

        api = MagicMock()
        api.get_inventory_filtered.return_value = (False, [])
        result = _try_chassis_device_type_match(api, 1)
        assert result is None

    def test_empty_inventory_returns_none(self):
        from netbox_librenms_plugin.import_utils.device_operations import _try_chassis_device_type_match

        api = MagicMock()
        api.get_inventory_filtered.return_value = (True, [])
        result = _try_chassis_device_type_match(api, 1)
        assert result is None

    def test_matched_physical_name_returns_match(self):
        from netbox_librenms_plugin.import_utils.device_operations import _try_chassis_device_type_match

        mock_dt = MagicMock()
        api = MagicMock()
        api.get_inventory_filtered.return_value = (
            True,
            [{"entPhysicalName": "CHAS-BP-MX480-S", "entPhysicalModelName": "model1"}],
        )

        with patch(
            "netbox_librenms_plugin.import_utils.device_operations.match_librenms_hardware_to_device_type"
        ) as mock_match:
            mock_match.return_value = {"matched": True, "device_type": mock_dt, "match_type": "exact"}
            result = _try_chassis_device_type_match(api, 1)

        assert result is not None
        assert result["matched"] is True
        assert result["match_type"] == "chassis"
        assert result["chassis_model"] == "CHAS-BP-MX480-S"

    def test_skips_empty_values(self):
        from netbox_librenms_plugin.import_utils.device_operations import _try_chassis_device_type_match

        api = MagicMock()
        api.get_inventory_filtered.return_value = (True, [{"entPhysicalName": "", "entPhysicalModelName": "-"}])

        with patch(
            "netbox_librenms_plugin.import_utils.device_operations.match_librenms_hardware_to_device_type"
        ) as mock_match:
            mock_match.return_value = {"matched": False}
            result = _try_chassis_device_type_match(api, 1)

        mock_match.assert_not_called()
        assert result is None

    def test_exception_returns_none(self):
        from netbox_librenms_plugin.import_utils.device_operations import _try_chassis_device_type_match

        api = MagicMock()
        api.get_inventory_filtered.side_effect = RuntimeError("API Error")
        result = _try_chassis_device_type_match(api, 1)
        assert result is None

    def test_fallback_to_model_name_when_name_not_matched(self):
        from netbox_librenms_plugin.import_utils.device_operations import _try_chassis_device_type_match

        mock_dt = MagicMock()
        api = MagicMock()
        api.get_inventory_filtered.return_value = (
            True,
            [{"entPhysicalName": "Unrecognized", "entPhysicalModelName": "710-017414"}],
        )

        call_count = [0]

        def match_side_effect(value):
            call_count[0] += 1
            if value == "Unrecognized":
                return {"matched": False}
            return {"matched": True, "device_type": mock_dt, "match_type": "exact"}

        with patch(
            "netbox_librenms_plugin.import_utils.device_operations.match_librenms_hardware_to_device_type",
            side_effect=match_side_effect,
        ):
            result = _try_chassis_device_type_match(api, 1)

        assert result is not None
        assert result["matched"] is True
        assert result["chassis_model"] == "710-017414"

    def test_match_returning_none_is_skipped(self):
        """match_librenms_hardware_to_device_type returning None does not raise; continues."""
        from netbox_librenms_plugin.import_utils.device_operations import _try_chassis_device_type_match

        api = MagicMock()
        api.get_inventory_filtered.return_value = (
            True,
            [{"entPhysicalName": "SomeChassis", "entPhysicalModelName": ""}],
        )

        with patch(
            "netbox_librenms_plugin.import_utils.device_operations.match_librenms_hardware_to_device_type",
            return_value=None,
        ):
            result = _try_chassis_device_type_match(api, 1)

        # None return from matcher is safely skipped; function returns None overall
        assert result is None


class TestDetermineDeviceName:
    """Tests for _determine_device_name (lines 68-122)."""

    def test_use_sysname_true_prefers_sysname(self):
        from netbox_librenms_plugin.import_utils.device_operations import _determine_device_name

        result = _determine_device_name({"sysName": "router01", "hostname": "router01.example.com"}, use_sysname=True)
        assert result == "router01"

    def test_use_sysname_false_prefers_hostname(self):
        from netbox_librenms_plugin.import_utils.device_operations import _determine_device_name

        result = _determine_device_name({"sysName": "router01", "hostname": "router01.example.com"}, use_sysname=False)
        assert result == "router01.example.com"

    def test_fallback_to_device_id_when_no_name(self):
        from netbox_librenms_plugin.import_utils.device_operations import _determine_device_name

        result = _determine_device_name({}, device_id=42)
        assert result == "device-42"

    def test_fallback_to_device_id_field_when_no_name_no_id(self):
        from netbox_librenms_plugin.import_utils.device_operations import _determine_device_name

        result = _determine_device_name({"device_id": 99})
        assert result == "device-99"

    def test_strip_domain_true_strips_suffix(self):
        from netbox_librenms_plugin.import_utils.device_operations import _determine_device_name

        result = _determine_device_name({"sysName": "router01.example.com"}, strip_domain=True)
        assert result == "router01"

    def test_strip_domain_does_not_strip_ip(self):
        from netbox_librenms_plugin.import_utils.device_operations import _determine_device_name

        result = _determine_device_name({"sysName": "192.168.1.1"}, strip_domain=True)
        assert result == "192.168.1.1"

    def test_hostname_fallback_when_sysname_empty(self):
        from netbox_librenms_plugin.import_utils.device_operations import _determine_device_name

        result = _determine_device_name({"sysName": "", "hostname": "sw01.example.com"}, use_sysname=True)
        assert result == "sw01.example.com"

    def test_none_sysname_falls_back_to_hostname(self):
        from netbox_librenms_plugin.import_utils.device_operations import _determine_device_name

        result = _determine_device_name({"sysName": None, "hostname": "sw02"})
        assert result == "sw02"

    def test_none_hostname_falls_back_to_device_id(self):
        from netbox_librenms_plugin.import_utils.device_operations import _determine_device_name

        result = _determine_device_name({"sysName": None, "hostname": None, "device_id": 7})
        assert result == "device-7"

    def test_result_is_never_empty_string(self):
        from netbox_librenms_plugin.import_utils.device_operations import _determine_device_name

        result = _determine_device_name({})
        assert isinstance(result, str)
        assert result != ""

    def test_fqdn_multiple_dots_strips_to_first_label(self):
        from netbox_librenms_plugin.import_utils.device_operations import _determine_device_name

        result = _determine_device_name({"sysName": "a.b.c.d.example.com"}, strip_domain=True)
        assert result == "a"

    def test_strip_domain_false_keeps_fqdn(self):
        from netbox_librenms_plugin.import_utils.device_operations import _determine_device_name

        result = _determine_device_name({"sysName": "router.example.com"}, strip_domain=False)
        assert result == "router.example.com"


class TestValidateDeviceStateMachine:
    """Tests for validate_device_for_import is_ready / can_import state transitions."""

    def _run_validate(self, libre_device, patches_overrides=None, **kwargs):
        from unittest.mock import MagicMock, patch

        from netbox_librenms_plugin.import_utils.device_operations import validate_device_for_import

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

        mock_device = MagicMock()
        mock_device.objects.filter.return_value.first.return_value = None
        mock_device.objects.filter.return_value.exclude.return_value.first.return_value = None

        mock_vm = MagicMock()
        mock_vm.objects.filter.return_value.first.return_value = None

        mock_cluster = MagicMock()
        mock_cluster.objects.all.return_value = []

        mock_role = MagicMock()
        mock_role.objects.all.return_value = []

        mock_ip = MagicMock()
        mock_ip.objects.filter.return_value.first.return_value = None

        base_patches = [
            patch(
                "netbox_librenms_plugin.import_utils.device_operations.find_matching_site",
                return_value={"found": False, "site": None, "match_type": None, "suggestions": []},
            ),
            patch(
                "netbox_librenms_plugin.import_utils.device_operations.match_librenms_hardware_to_device_type",
                return_value={"matched": False, "device_type": None, "match_type": None},
            ),
            patch(
                "netbox_librenms_plugin.import_utils.device_operations.find_matching_platform",
                return_value={"found": False, "platform": None, "match_type": None},
            ),
            patch(
                "netbox_librenms_plugin.import_utils.device_operations.get_virtual_chassis_data",
                return_value={"is_stack": False, "member_count": 0, "members": []},
            ),
            patch("netbox_librenms_plugin.import_utils.device_operations.DeviceRole", mock_role),
            patch("netbox_librenms_plugin.import_utils.device_operations.Cluster", mock_cluster),
            patch("netbox_librenms_plugin.import_utils.device_operations.Device", mock_device),
            patch("netbox_librenms_plugin.import_utils.device_operations.DeviceType", MagicMock()),
            patch("netbox_librenms_plugin.import_utils.device_operations.Site", MagicMock()),
            patch("netbox_librenms_plugin.import_utils.device_operations.find_by_librenms_id", return_value=None),
            patch("netbox_librenms_plugin.import_utils.device_operations.cache"),
            patch("virtualization.models.VirtualMachine", mock_vm),
        ]
        if patches_overrides:
            base_patches.extend(patches_overrides)

        for p in base_patches:
            p.start()
        try:
            result = validate_device_for_import(libre_device, api=api, **kwargs)
        finally:
            for p in reversed(base_patches):
                p.stop()
        return result

    def _base_device(self, device_id=1, hostname="router01"):
        return {
            "device_id": device_id,
            "hostname": hostname,
            "sysName": hostname,
            "hardware": "-",
            "serial": "-",
            "os": "-",
            "location": "-",
        }

    def test_new_device_without_any_matches_is_not_ready(self):
        """New device with no site/type/role match must not be ready."""
        result = self._run_validate(self._base_device())
        assert result["existing_device"] is None
        assert result["is_ready"] is False
        assert result["can_import"] is False

    def test_new_vm_without_cluster_is_not_ready(self):
        """New VM import with no cluster available must not be ready."""
        result = self._run_validate(self._base_device(hostname="vm01"), import_as_vm=True)
        assert result["is_ready"] is False
        assert result.get("import_as_vm") is True
        assert result["cluster"]["found"] is False

    def test_new_device_site_and_type_found_but_role_manual(self):
        """
        New device with site+type matched still requires manual role selection.

        validate_device_for_import always sets device_role["found"]=False for new
        devices and adds an issue. is_ready becomes True only AFTER the user selects
        a role via apply_role_to_validation + recalculate_validation_status.
        """
        from unittest.mock import MagicMock, patch

        site_mock = MagicMock()
        dt_mock = MagicMock()
        role_mock = MagicMock()
        role_mock.pk = 1

        result = self._run_validate(
            self._base_device(),
            patches_overrides=[
                patch(
                    "netbox_librenms_plugin.import_utils.device_operations.find_matching_site",
                    return_value={"found": True, "site": site_mock, "match_type": "exact", "suggestions": []},
                ),
                patch(
                    "netbox_librenms_plugin.import_utils.device_operations.match_librenms_hardware_to_device_type",
                    return_value={"matched": True, "device_type": dt_mock, "match_type": "exact"},
                ),
            ],
        )
        # Site and type found, but role is always manual for new devices
        assert result["site"]["found"] is True
        assert result["device_type"]["found"] is True
        assert result["device_role"]["found"] is False
        assert result["can_import"] is False  # blocked by role issue

        # Simulate user selecting a role via apply_role_to_validation
        from netbox_librenms_plugin.import_validation_helpers import (
            apply_role_to_validation,
            recalculate_validation_status,
        )

        apply_role_to_validation(result, role=role_mock, is_vm=False)
        recalculate_validation_status(result, is_vm=False)

        # After role selection, device should be ready
        assert result["device_role"]["found"] is True
        assert result["can_import"] is True
        assert result["is_ready"] is True

    def test_is_ready_false_when_site_missing_even_with_type_and_role(self):
        """is_ready requires ALL of site+type+role; missing site -> False."""
        from unittest.mock import MagicMock, patch

        dt_mock = MagicMock()
        role_mock = MagicMock()

        result = self._run_validate(
            self._base_device(),
            patches_overrides=[
                patch(
                    "netbox_librenms_plugin.import_utils.device_operations.match_librenms_hardware_to_device_type",
                    return_value={"matched": True, "device_type": dt_mock, "match_type": "exact"},
                ),
                patch(
                    "netbox_librenms_plugin.import_utils.device_operations.DeviceRole",
                    MagicMock(objects=MagicMock(all=MagicMock(return_value=[role_mock]))),
                ),
            ],
        )
        assert result["is_ready"] is False
        assert result["site"]["found"] is False

    def test_import_as_vm_skips_device_only_fields(self):
        """VM import path must not fail on device-only type fields."""
        from unittest.mock import MagicMock, patch

        dt_mock = MagicMock()
        result = self._run_validate(
            self._base_device(hostname="vm01"),
            import_as_vm=True,
            patches_overrides=[
                patch(
                    "netbox_librenms_plugin.import_utils.device_operations.match_librenms_hardware_to_device_type",
                    return_value={"matched": True, "device_type": dt_mock, "match_type": "exact"},
                ),
            ],
        )
        assert result.get("import_as_vm") is True
        assert result["is_ready"] is False  # no cluster found


class TestGetLibreNMSDeviceById:
    """Tests for get_librenms_device_by_id (lines 912-933)."""

    def test_success_returns_device(self):
        from netbox_librenms_plugin.import_utils.device_operations import get_librenms_device_by_id

        api = MagicMock()
        device = {"device_id": 42, "hostname": "router01"}
        api.get_device_info.return_value = (True, device)

        result = get_librenms_device_by_id(api, 42)
        assert result is device

    def test_api_failure_returns_none(self):
        from netbox_librenms_plugin.import_utils.device_operations import get_librenms_device_by_id

        api = MagicMock()
        api.get_device_info.return_value = (False, None)

        result = get_librenms_device_by_id(api, 42)
        assert result is None

    def test_device_not_found_returns_none(self):
        from netbox_librenms_plugin.import_utils.device_operations import get_librenms_device_by_id

        api = MagicMock()
        api.get_device_info.return_value = (True, None)

        result = get_librenms_device_by_id(api, 42)
        assert result is None

    def test_exception_returns_none(self):
        from netbox_librenms_plugin.import_utils.device_operations import get_librenms_device_by_id

        api = MagicMock()
        api.get_device_info.side_effect = RuntimeError("Network error")

        result = get_librenms_device_by_id(api, 42)
        assert result is None


class TestFetchDeviceWithCache:
    """Tests for fetch_device_with_cache (lines 936-987)."""

    @patch("netbox_librenms_plugin.import_utils.device_operations.cache")
    def test_from_pre_fetched_cache_dict(self, mock_cache):
        from netbox_librenms_plugin.import_utils.device_operations import fetch_device_with_cache

        api = MagicMock()
        api.server_key = "default"
        device = {"device_id": 1}
        cache_dict = {1: device}

        result = fetch_device_with_cache(1, api, libre_devices_cache=cache_dict)
        assert result is device
        mock_cache.get.assert_not_called()

    @patch("netbox_librenms_plugin.import_utils.device_operations.cache")
    def test_from_django_cache(self, mock_cache):
        from netbox_librenms_plugin.import_utils.device_operations import fetch_device_with_cache

        api = MagicMock()
        api.server_key = "default"
        device = {"device_id": 1}
        mock_cache.get.return_value = device

        result = fetch_device_with_cache(1, api)
        assert result is device
        api.get_device_info.assert_not_called()

    @patch("netbox_librenms_plugin.import_utils.device_operations.cache")
    def test_cache_miss_falls_back_to_api(self, mock_cache):
        from netbox_librenms_plugin.import_utils.device_operations import fetch_device_with_cache

        api = MagicMock()
        api.server_key = "default"
        api.cache_timeout = 300
        device = {"device_id": 1}
        mock_cache.get.return_value = None
        api.get_device_info.return_value = (True, device)

        result = fetch_device_with_cache(1, api)
        assert result is device
        mock_cache.set.assert_called_once()

    @patch("netbox_librenms_plugin.import_utils.device_operations.cache")
    def test_api_returns_none_returns_none(self, mock_cache):
        from netbox_librenms_plugin.import_utils.device_operations import fetch_device_with_cache

        api = MagicMock()
        api.server_key = "default"
        mock_cache.get.return_value = None
        api.get_device_info.return_value = (False, None)

        result = fetch_device_with_cache(1, api)
        assert result is None

    @patch("netbox_librenms_plugin.import_utils.device_operations.cache")
    def test_uses_provided_server_key(self, mock_cache):
        from netbox_librenms_plugin.import_utils.device_operations import fetch_device_with_cache

        api = MagicMock()
        api.server_key = "default"
        api.cache_timeout = 300
        mock_cache.get.return_value = None
        api.get_device_info.return_value = (True, {"device_id": 1})

        fetch_device_with_cache(1, api, server_key="secondary")
        # The cache key should use "secondary"
        cache_key = mock_cache.get.call_args[0][0]
        assert "secondary" in cache_key


class TestValidateDeviceForImport:
    """Tests for validate_device_for_import main validation logic."""

    def _make_api(self):
        api = MagicMock()
        api.server_key = "default"
        api.cache_timeout = 300
        api.get_device_info.return_value = (True, {"device_id": 1})
        return api

    def _patch_all_db(self):
        """Context manager patches for all DB interactions."""
        mock_device = MagicMock()
        mock_device.objects.filter.return_value.first.return_value = None
        mock_device.objects.filter.return_value.exclude.return_value.first.return_value = None
        mock_device.objects.filter.return_value.select_for_update.return_value.filter.return_value.exclude.return_value.first.return_value = None
        mock_device.objects.all.return_value = []

        mock_vm = MagicMock()
        mock_vm.objects.filter.return_value.first.return_value = None

        mock_cluster = MagicMock()
        mock_cluster.objects.all.return_value = []

        mock_device_role = MagicMock()
        mock_device_role.objects.all.return_value = []

        mock_site = MagicMock()
        mock_site.objects.all.return_value = []

        mock_ip = MagicMock()
        mock_ip.objects.filter.return_value.first.return_value = None

        patches = [
            patch(
                "netbox_librenms_plugin.import_utils.device_operations.find_matching_site",
                return_value={"found": False, "site": None, "match_type": None, "suggestions": []},
            ),
            patch(
                "netbox_librenms_plugin.import_utils.device_operations.match_librenms_hardware_to_device_type",
                return_value={"matched": False, "device_type": None, "match_type": None},
            ),
            patch(
                "netbox_librenms_plugin.import_utils.device_operations.find_matching_platform",
                return_value={"found": False, "platform": None, "match_type": None},
            ),
            patch(
                "netbox_librenms_plugin.import_utils.device_operations.get_virtual_chassis_data",
                return_value={"is_stack": False, "member_count": 0, "members": []},
            ),
            patch("netbox_librenms_plugin.import_utils.device_operations.DeviceRole", mock_device_role),
            patch("netbox_librenms_plugin.import_utils.device_operations.Cluster", mock_cluster),
            patch("netbox_librenms_plugin.import_utils.device_operations.Device", mock_device),
            patch("netbox_librenms_plugin.import_utils.device_operations.DeviceType", MagicMock()),
            patch("netbox_librenms_plugin.import_utils.device_operations.Site", mock_site),
            patch("netbox_librenms_plugin.import_utils.device_operations.find_by_librenms_id", return_value=None),
            patch("netbox_librenms_plugin.import_utils.device_operations.cache"),
            patch("virtualization.models.VirtualMachine", mock_vm),
            patch("ipam.models.IPAddress", mock_ip),
        ]
        return patches

    def test_minimal_device_validation(self):
        from netbox_librenms_plugin.import_utils.device_operations import validate_device_for_import

        libre_device = {
            "device_id": 1,
            "hostname": "router01",
            "sysName": "router01",
            "hardware": "-",
            "serial": "-",
            "os": "-",
            "location": "-",
            "type": "network",
        }
        api = self._make_api()

        patches = self._patch_all_db()
        try:
            for p in patches:
                p.start()
            result = validate_device_for_import(libre_device, api=api)
        finally:
            for p in patches:
                p.stop()

        assert result is not None
        assert "status" in result or "is_ready" in result

    def test_vm_import_uses_correct_model(self):
        from netbox_librenms_plugin.import_utils.device_operations import validate_device_for_import

        libre_device = {
            "device_id": 1,
            "hostname": "vm01",
            "sysName": "vm01",
            "hardware": "-",
            "serial": "-",
            "os": "-",
            "location": "-",
            "type": "network",
        }
        api = self._make_api()

        patches = self._patch_all_db()
        try:
            for p in patches:
                p.start()
            result = validate_device_for_import(libre_device, import_as_vm=True, api=api)
        finally:
            for p in patches:
                p.stop()

        assert result is not None
        assert result.get("import_as_vm") is True

    def test_existing_device_detected(self):
        """When device with same librenms_id exists, sets existing_device in result."""
        from netbox_librenms_plugin.import_utils.device_operations import validate_device_for_import

        libre_device = {
            "device_id": 1,
            "hostname": "router01",
            "sysName": "router01",
            "hardware": "-",
            "serial": "-",
            "os": "-",
            "location": "-",
        }
        api = self._make_api()

        existing = MagicMock()
        existing.name = "router01"
        existing.serial = ""

        patches = self._patch_all_db()
        # Override find_by_librenms_id: return None for VM, existing for Device
        try:
            for p in patches:
                p.start()

            def _find_side_effect(model, device_id, server_key):
                from virtualization.models import VirtualMachine as VM

                return None if model is VM else existing

            with patch(
                "netbox_librenms_plugin.import_utils.device_operations.find_by_librenms_id",
                side_effect=_find_side_effect,
            ):
                result = validate_device_for_import(libre_device, api=api)
        finally:
            for p in patches:
                p.stop()

        assert result.get("existing_device") is existing


class TestImportSingleDevice:
    """Tests for import_single_device (lines 689-910)."""

    def _make_libre_device(self):
        return {
            "device_id": 1,
            "hostname": "router01",
            "sysName": "router01",
            "hardware": "Cisco",
            "serial": "SN001",
            "os": "ios",
            "status": 1,
            "location": "-",
        }

    @patch("netbox_librenms_plugin.import_utils.device_operations.LibreNMSAPI")
    def test_missing_site_returns_error(self, MockAPI):
        from netbox_librenms_plugin.import_utils.device_operations import import_single_device

        libre_device = self._make_libre_device()

        validation = {
            "existing_device": None,
            "site": {"found": False, "site": None},
            "device_type": {"matched": True, "device_type": MagicMock()},
            "device_role": {"found": True, "role": MagicMock()},
            "platform": {"found": False, "platform": None},
            "rack": {"rack": None},
        }

        with (
            patch("netbox_librenms_plugin.import_utils.device_operations.Site"),
            patch("netbox_librenms_plugin.import_utils.device_operations.DeviceType"),
            patch("netbox_librenms_plugin.import_utils.device_operations.DeviceRole"),
            patch("netbox_librenms_plugin.import_utils.device_operations.Rack"),
        ):
            result = import_single_device(1, server_key="default", validation=validation, libre_device=libre_device)
        assert result["success"] is False
        assert "Site" in result["error"]

    @patch("netbox_librenms_plugin.import_utils.device_operations.LibreNMSAPI")
    def test_missing_device_type_returns_error(self, MockAPI):
        from netbox_librenms_plugin.import_utils.device_operations import import_single_device

        libre_device = self._make_libre_device()

        validation = {
            "existing_device": None,
            "site": {"found": True, "site": MagicMock()},
            "device_type": {"matched": False, "device_type": None},
            "device_role": {"found": True, "role": MagicMock()},
            "platform": {"found": False, "platform": None},
            "rack": {"rack": None},
        }

        with (
            patch("netbox_librenms_plugin.import_utils.device_operations.Site"),
            patch("netbox_librenms_plugin.import_utils.device_operations.DeviceType"),
            patch("netbox_librenms_plugin.import_utils.device_operations.DeviceRole"),
            patch("netbox_librenms_plugin.import_utils.device_operations.Rack"),
        ):
            result = import_single_device(1, server_key="default", validation=validation, libre_device=libre_device)
        assert result["success"] is False
        assert "device type" in result["error"].lower()

    @patch("netbox_librenms_plugin.import_utils.device_operations.LibreNMSAPI")
    def test_missing_device_role_returns_error(self, MockAPI):
        from netbox_librenms_plugin.import_utils.device_operations import import_single_device

        libre_device = self._make_libre_device()

        validation = {
            "existing_device": None,
            "site": {"found": True, "site": MagicMock()},
            "device_type": {"matched": True, "device_type": MagicMock()},
            "device_role": {"found": False, "role": None},
            "platform": {"found": False, "platform": None},
            "rack": {"rack": None},
        }

        with (
            patch("netbox_librenms_plugin.import_utils.device_operations.Site"),
            patch("netbox_librenms_plugin.import_utils.device_operations.DeviceType"),
            patch("netbox_librenms_plugin.import_utils.device_operations.DeviceRole"),
            patch("netbox_librenms_plugin.import_utils.device_operations.Rack"),
        ):
            result = import_single_device(1, server_key="default", validation=validation, libre_device=libre_device)
        assert result["success"] is False
        assert "role" in result["error"].lower()


class TestValidateDeviceForImportEdgeCases:
    """Additional edge case tests to cover missing lines."""

    def _make_api(self):
        api = MagicMock()
        api.server_key = "default"
        api.cache_timeout = 300
        return api

    def _start_patches(self, extra_patches=None):
        mock_device = MagicMock()
        mock_device.objects.filter.return_value.first.return_value = None
        mock_device.objects.filter.return_value.exclude.return_value.first.return_value = None
        mock_device.objects.all.return_value = []

        mock_vm = MagicMock()
        mock_vm.objects.filter.return_value.first.return_value = None

        mock_cluster = MagicMock()
        mock_cluster.objects.all.return_value = []

        mock_role = MagicMock()
        mock_role.objects.all.return_value = []

        mock_ip = MagicMock()
        mock_ip.objects.filter.return_value.first.return_value = None

        mock_site = MagicMock()
        mock_site.objects.all.return_value = []

        patches = [
            patch(
                "netbox_librenms_plugin.import_utils.device_operations.find_matching_site",
                return_value={"found": False, "site": None, "match_type": None, "suggestions": []},
            ),
            patch(
                "netbox_librenms_plugin.import_utils.device_operations.match_librenms_hardware_to_device_type",
                return_value={"matched": False, "device_type": None, "match_type": None},
            ),
            patch(
                "netbox_librenms_plugin.import_utils.device_operations.find_matching_platform",
                return_value={"found": False, "platform": None, "match_type": None},
            ),
            patch(
                "netbox_librenms_plugin.import_utils.device_operations.get_virtual_chassis_data",
                return_value={"is_stack": False, "member_count": 0, "members": []},
            ),
            patch("netbox_librenms_plugin.import_utils.device_operations.DeviceRole", mock_role),
            patch("netbox_librenms_plugin.import_utils.device_operations.Cluster", mock_cluster),
            patch("netbox_librenms_plugin.import_utils.device_operations.Device", mock_device),
            patch("netbox_librenms_plugin.import_utils.device_operations.DeviceType", MagicMock()),
            patch("netbox_librenms_plugin.import_utils.device_operations.Site", mock_site),
            patch("netbox_librenms_plugin.import_utils.device_operations.find_by_librenms_id", return_value=None),
            patch("netbox_librenms_plugin.import_utils.device_operations.cache"),
            patch("virtualization.models.VirtualMachine", mock_vm),
            patch("ipam.models.IPAddress", mock_ip),
        ]
        if extra_patches:
            patches.extend(extra_patches)
        started = []
        for p in patches:
            started.append(p.start())
        return patches, started

    def _stop_patches(self, patches):
        for p in reversed(patches):
            p.stop()

    def test_vm_librenms_id_not_int_falls_back(self):
        """Lines 288-290: librenms_id ValueError/TypeError in VM check."""
        from netbox_librenms_plugin.import_utils.device_operations import validate_device_for_import

        libre_device = {
            "device_id": None,
            "hostname": "vm01",
            "sysName": "vm01",
            "hardware": "-",
            "serial": "-",
            "os": "-",
            "location": "-",
        }
        api = self._make_api()

        patches, _ = self._start_patches()
        try:
            result = validate_device_for_import(libre_device, api=api)
        finally:
            self._stop_patches(patches)

        assert result is not None

    def test_vm_with_legacy_librenms_id_flags_migration(self):
        """Line 307: existing VM has legacy bare-int librenms_id → flags migration."""
        from netbox_librenms_plugin.import_utils.device_operations import validate_device_for_import

        libre_device = {
            "device_id": 42,
            "hostname": "vm01",
            "sysName": "vm01",
            "hardware": "-",
            "serial": "-",
            "os": "-",
            "location": "-",
        }
        api = self._make_api()

        existing_vm = MagicMock()
        existing_vm.name = "vm01"
        existing_vm.serial = ""
        existing_vm.custom_field_data = {"librenms_id": 42}  # Legacy bare-int

        patches, _ = self._start_patches()
        try:
            with patch("netbox_librenms_plugin.import_utils.device_operations.find_by_librenms_id") as mock_find:
                mock_find.side_effect = [existing_vm, None]  # VM found, then no Device
                result = validate_device_for_import(libre_device, import_as_vm=True, api=api)
        finally:
            self._stop_patches(patches)

        assert result.get("existing_device") is existing_vm
        assert result.get("librenms_id_needs_migration") is True

    def test_vc_detection_called_for_device_with_api(self):
        """Lines 616-638: VC detection executed when include_vc_detection=True and api provided."""
        from netbox_librenms_plugin.import_utils.device_operations import validate_device_for_import

        libre_device = {
            "device_id": 1,
            "hostname": "sw01",
            "sysName": "sw01",
            "hardware": "Cisco",
            "serial": "SN001",
            "os": "ios",
            "location": "-",
        }
        api = self._make_api()

        vc_data = {"is_stack": True, "member_count": 2, "members": [{"serial": "SN001"}, {"serial": "SN002"}]}

        patches, _ = self._start_patches(
            [
                patch(
                    "netbox_librenms_plugin.import_utils.device_operations.update_vc_member_suggested_names",
                    return_value=vc_data,
                ),
            ]
        )
        # Override get_virtual_chassis_data to return VC stack

        try:
            with patch(
                "netbox_librenms_plugin.import_utils.device_operations.get_virtual_chassis_data", return_value=vc_data
            ) as mock_get_vc:
                with patch(
                    "netbox_librenms_plugin.import_utils.device_operations.update_vc_member_suggested_names",
                    return_value=vc_data,
                ) as mock_update_vc:
                    result = validate_device_for_import(libre_device, api=api)
        finally:
            self._stop_patches(patches)

        assert result["virtual_chassis"] is not None
        mock_get_vc.assert_called_once()
        mock_update_vc.assert_called_once()

    def test_no_vc_detection_when_disabled(self):
        """VC detection skipped when include_vc_detection=False."""
        from netbox_librenms_plugin.import_utils.device_operations import validate_device_for_import

        libre_device = {
            "device_id": 1,
            "hostname": "sw01",
            "sysName": "sw01",
            "hardware": "-",
            "serial": "-",
            "os": "-",
            "location": "-",
        }
        api = self._make_api()

        patches, _ = self._start_patches()
        try:
            with patch("netbox_librenms_plugin.import_utils.device_operations.get_virtual_chassis_data") as mock_vc:
                validate_device_for_import(libre_device, api=api, include_vc_detection=False)
                mock_vc.assert_not_called()
        finally:
            self._stop_patches(patches)

    def test_chassis_inventory_fallback_used(self):
        """Lines 534-539: Chassis inventory fallback when hardware doesn't match."""
        from netbox_librenms_plugin.import_utils.device_operations import _try_chassis_device_type_match

        api = MagicMock()
        mock_dt = MagicMock()

        api.get_inventory_filtered.return_value = (
            True,
            [{"entPhysicalName": "MX480", "entPhysicalModelName": "Juniper MX480"}],
        )

        with patch(
            "netbox_librenms_plugin.import_utils.device_operations.match_librenms_hardware_to_device_type"
        ) as mock_match:
            mock_match.side_effect = [
                {"matched": False},
                {"matched": True, "device_type": mock_dt, "match_type": "exact"},
            ]
            result = _try_chassis_device_type_match(api, 1)

        # Should have found a match via model name fallback
        assert result is not None
        assert mock_match.call_count == 2
        assert result["matched"] is True
        assert result.get("device_type") is mock_dt

    def test_primary_ip_match_check(self):
        """Lines 474-489: IP address match detection."""
        from netbox_librenms_plugin.import_utils.device_operations import validate_device_for_import

        libre_device = {
            "device_id": 1,
            "hostname": "router01",
            "sysName": "router01",
            "hardware": "-",
            "serial": "-",
            "os": "-",
            "location": "-",
            "ip": "192.168.1.1",
        }
        api = self._make_api()

        mock_device = MagicMock()
        mock_device.name = "existing_router"

        mock_ip = MagicMock()
        mock_ip.assigned_object.device = mock_device
        mock_ip_model = MagicMock()
        mock_ip_model.objects.filter.return_value.first.return_value = mock_ip

        patches, _ = self._start_patches()
        try:
            with patch("ipam.models.IPAddress", mock_ip_model):
                result = validate_device_for_import(libre_device, api=api)
        finally:
            self._stop_patches(patches)

        # The IP match should set existing_device to the mock device and match_type to "primary_ip"
        assert result.get("existing_device") is mock_device
        assert result.get("existing_match_type") == "primary_ip"

    def test_no_hostname_adds_issue(self):
        """When hostname and sysName are both empty, _determine_device_name falls back to device-{id}."""
        from netbox_librenms_plugin.import_utils.device_operations import validate_device_for_import

        libre_device = {
            "device_id": 1,
            "hostname": "",
            "sysName": "",
            "hardware": "-",
            "serial": "-",
            "os": "-",
            "location": "-",
        }
        api = self._make_api()

        patches, _ = self._start_patches()
        try:
            result = validate_device_for_import(libre_device, api=api)
        finally:
            self._stop_patches(patches)

        # _determine_device_name always falls back to "device-{id}" so
        # "Device has no hostname" issue is not expected here.
        assert isinstance(result, dict)
        assert "Device has no hostname" not in result.get("issues", [])
        assert result.get("resolved_name", "").startswith("device-")


class TestValidateDeviceMoreEdgeCases:
    """More edge case tests for validate_device_for_import."""

    def _make_api(self):
        api = MagicMock()
        api.server_key = "default"
        api.cache_timeout = 300
        return api

    def _get_patches(self):
        mock_device = MagicMock()
        mock_device.objects.filter.return_value.first.return_value = None
        mock_device.objects.filter.return_value.exclude.return_value.first.return_value = None
        mock_device.objects.all.return_value = []

        mock_vm = MagicMock()
        mock_vm.objects.filter.return_value.first.return_value = None

        mock_site = MagicMock()
        mock_site.objects.all.return_value = []

        mock_cluster = MagicMock()
        mock_cluster.objects.all.return_value = []
        mock_role = MagicMock()
        mock_role.objects.all.return_value = []
        mock_ip = MagicMock()
        mock_ip.objects.filter.return_value.first.return_value = None

        return [
            patch(
                "netbox_librenms_plugin.import_utils.device_operations.find_matching_site",
                return_value={"found": False, "site": None, "match_type": None, "suggestions": []},
            ),
            patch(
                "netbox_librenms_plugin.import_utils.device_operations.match_librenms_hardware_to_device_type",
                return_value={"matched": False, "device_type": None, "match_type": None},
            ),
            patch(
                "netbox_librenms_plugin.import_utils.device_operations.find_matching_platform",
                return_value={"found": False, "platform": None, "match_type": None},
            ),
            patch(
                "netbox_librenms_plugin.import_utils.device_operations.get_virtual_chassis_data",
                return_value={"is_stack": False, "member_count": 0, "members": []},
            ),
            patch("netbox_librenms_plugin.import_utils.device_operations.DeviceRole", mock_role),
            patch("netbox_librenms_plugin.import_utils.device_operations.Cluster", mock_cluster),
            patch("netbox_librenms_plugin.import_utils.device_operations.Device", mock_device),
            patch("netbox_librenms_plugin.import_utils.device_operations.DeviceType", MagicMock()),
            patch("netbox_librenms_plugin.import_utils.device_operations.Site", mock_site),
            patch("netbox_librenms_plugin.import_utils.device_operations.find_by_librenms_id", return_value=None),
            patch("netbox_librenms_plugin.import_utils.device_operations.cache"),
            patch("virtualization.models.VirtualMachine", mock_vm),
            patch("ipam.models.IPAddress", mock_ip),
        ]

    def test_serial_dash_normalized(self):
        """Line 346: serial '-' is normalized to empty string."""
        from netbox_librenms_plugin.import_utils.device_operations import validate_device_for_import

        existing = MagicMock()
        existing.name = "router01"
        existing.serial = "SN001"
        existing.custom_field_data = {"librenms_id": {"default": 1}}
        existing.virtual_chassis = MagicMock()  # Has VC
        existing.vc_position = 1

        libre_device = {
            "device_id": 1,
            "hostname": "router01",
            "sysName": "router01",
            "hardware": "-",
            "serial": "-",  # Dash serial
            "os": "-",
            "location": "",
        }
        api = self._make_api()

        patches = self._get_patches()
        try:
            for p in patches:
                p.start()

            # Return None for VM, existing for Device
            def _device_side_effect(model, device_id, server_key):
                from virtualization.models import VirtualMachine as VM

                return None if model is VM else existing

            with patch(
                "netbox_librenms_plugin.import_utils.device_operations.find_by_librenms_id",
                side_effect=_device_side_effect,
            ):
                result = validate_device_for_import(libre_device, api=api)
        finally:
            for p in patches:
                p.stop()

        assert result is not None
        # serial '-' must be treated as empty — no serial mismatch should be flagged
        assert result.get("serial_action") is None

    def test_serial_conflict_with_existing_device(self):
        """Lines 373-375: incoming serial already used by another device."""
        from netbox_librenms_plugin.import_utils.device_operations import validate_device_for_import

        existing = MagicMock()
        existing.name = "router01"
        existing.serial = "OLD_SN"  # Different from incoming
        existing.custom_field_data = {"librenms_id": {"default": 1}}
        existing.virtual_chassis = None

        conflict_device = MagicMock()
        conflict_device.name = "router02"
        conflict_device.pk = 99

        libre_device = {
            "device_id": 1,
            "hostname": "router01",
            "sysName": "router01",
            "hardware": "-",
            "serial": "NEW_SN",  # Different serial
            "os": "-",
            "location": "",
        }
        api = self._make_api()

        mock_device = MagicMock()
        mock_device.objects.filter.return_value.first.return_value = None
        mock_device.objects.filter.return_value.exclude.return_value.first.return_value = conflict_device

        patches = self._get_patches()
        try:
            for p in patches:
                p.start()

            # Return None for VM check, existing for Device check
            def _find_side_effect(model, device_id, server_key):
                from virtualization.models import VirtualMachine as VM

                if model is VM:
                    return None
                return existing

            with (
                patch(
                    "netbox_librenms_plugin.import_utils.device_operations.find_by_librenms_id",
                    side_effect=_find_side_effect,
                ),
                patch("netbox_librenms_plugin.import_utils.device_operations.Device", mock_device),
            ):
                result = validate_device_for_import(libre_device, api=api)
        finally:
            for p in patches:
                p.stop()

        assert result.get("serial_action") == "conflict"

    def test_both_vm_and_device_with_same_hostname(self):
        """Lines 395-399: both VM and Device have same hostname - ambiguous match."""
        from netbox_librenms_plugin.import_utils.device_operations import validate_device_for_import

        libre_device = {
            "device_id": 1,
            "hostname": "server01",
            "sysName": "server01",
            "hardware": "-",
            "serial": "-",
            "os": "-",
            "location": "",
        }
        api = self._make_api()

        existing_vm = MagicMock()
        existing_vm.name = "server01"
        existing_device = MagicMock()
        existing_device.name = "server01"

        mock_vm = MagicMock()
        mock_vm.objects.filter.return_value.first.return_value = existing_vm

        mock_device = MagicMock()
        mock_device.objects.filter.return_value.first.return_value = existing_device
        mock_device.objects.filter.return_value.exclude.return_value.first.return_value = None
        mock_device.objects.all.return_value = []

        mock_site = MagicMock()
        mock_site.objects.all.return_value = []

        patches = self._get_patches()
        try:
            for p in patches:
                p.start()
            with (
                patch("virtualization.models.VirtualMachine", mock_vm),
                patch("netbox_librenms_plugin.import_utils.device_operations.Device", mock_device),
                patch("netbox_librenms_plugin.import_utils.device_operations.Site", mock_site),
            ):
                result = validate_device_for_import(libre_device, api=api)
        finally:
            for p in patches:
                p.stop()

        # Ambiguous - should have a warning about both existing
        assert result is not None
        assert any("VM" in w and "Device" in w for w in result.get("warnings", []))

    def test_existing_vm_by_hostname(self):
        """Lines 406-413: VM found by hostname (no Device match)."""
        from netbox_librenms_plugin.import_utils.device_operations import validate_device_for_import

        libre_device = {
            "device_id": 1,
            "hostname": "vm01",
            "sysName": "vm01",
            "hardware": "-",
            "serial": "-",
            "os": "-",
            "location": "",
        }
        api = self._make_api()

        existing_vm = MagicMock()
        existing_vm.name = "vm01"
        existing_vm.custom_field_data = {}

        mock_vm = MagicMock()
        mock_vm.objects.filter.return_value.first.return_value = existing_vm  # VM found

        mock_device = MagicMock()
        mock_device.objects.filter.return_value.first.return_value = None  # No device match
        mock_device.objects.filter.return_value.exclude.return_value.first.return_value = None
        mock_device.objects.all.return_value = []

        patches = self._get_patches()
        try:
            for p in patches:
                p.start()
            with (
                patch("virtualization.models.VirtualMachine", mock_vm),
                patch("netbox_librenms_plugin.import_utils.device_operations.Device", mock_device),
            ):
                result = validate_device_for_import(libre_device, api=api)
        finally:
            for p in patches:
                p.stop()

        assert result.get("existing_device") is existing_vm
        assert result.get("existing_match_type") == "hostname"

    def test_vc_detection_exception_handled(self):
        """Lines 634-636: VC detection exception is caught and stored."""
        from netbox_librenms_plugin.import_utils.device_operations import validate_device_for_import

        libre_device = {
            "device_id": 1,
            "hostname": "sw01",
            "sysName": "sw01",
            "hardware": "-",
            "serial": "-",
            "os": "-",
            "location": "",
        }
        api = self._make_api()

        patches = self._get_patches()
        try:
            for p in patches:
                p.start()
            with patch(
                "netbox_librenms_plugin.import_utils.device_operations.get_virtual_chassis_data",
                side_effect=Exception("VC error"),
            ):
                result = validate_device_for_import(libre_device, api=api)
        finally:
            for p in patches:
                p.stop()

        assert result is not None
        assert "detection_error" in result.get("virtual_chassis", {})


class TestImportSingleDeviceEdgeCases:
    """Tests for import_single_device edge cases (lines 737-739, 777-789)."""

    @patch("netbox_librenms_plugin.import_utils.device_operations.LibreNMSAPI")
    def test_no_libre_device_api_failure(self, MockAPI):
        """Lines 737-739: libre_device=None and API fails → returns error dict."""
        from netbox_librenms_plugin.import_utils.device_operations import import_single_device

        mock_api = MagicMock()
        mock_api.server_key = "default"
        mock_api.get_device_info.return_value = (False, None)
        MockAPI.return_value = mock_api

        result = import_single_device(device_id=1, libre_device=None, server_key="default")
        assert result["success"] is False
        assert "Failed to retrieve device" in result.get("error", "")

    @patch("netbox_librenms_plugin.import_utils.device_operations.LibreNMSAPI")
    def test_manual_mappings_are_applied(self, MockAPI):
        """Lines 777-789: manual_mappings override site/device_type/device_role."""
        from netbox_librenms_plugin.import_utils.device_operations import import_single_device

        mock_api = MagicMock()
        mock_api.server_key = "default"
        MockAPI.return_value = mock_api

        libre_device = {
            "device_id": 1,
            "hostname": "router01",
            "hardware": "Cisco",
            "serial": "SN001",
            "os": "ios",
            "location": "",
        }
        validation = {
            "is_ready": True,
            "can_import": True,
            "existing_device": None,
            "import_as_vm": False,
            "site": {"found": True, "site": None},
            "device_type": {"found": True, "device_type": None},
            "device_role": {"found": False, "role": None},
            "platform": {"found": False, "platform": None},
            "rack": {"rack": None},
            "issues": [],
        }

        mock_site = MagicMock()
        mock_site.pk = 1
        mock_dt = MagicMock()
        mock_dt.pk = 1
        mock_role = MagicMock()
        mock_role.pk = 1

        manual_mappings = {"site_id": 1, "device_type_id": 1, "device_role_id": 1}

        mock_tx = MagicMock()
        mock_tx.atomic.return_value.__enter__ = MagicMock(return_value=None)
        mock_tx.atomic.return_value.__exit__ = MagicMock(return_value=False)

        with patch("netbox_librenms_plugin.import_utils.device_operations.transaction", mock_tx):
            with patch("netbox_librenms_plugin.import_utils.device_operations.Site") as mock_site_cls:
                mock_site_cls.objects.filter.return_value.first.return_value = mock_site
                with patch("netbox_librenms_plugin.import_utils.device_operations.DeviceType") as mock_dt_cls:
                    mock_dt_cls.objects.filter.return_value.first.return_value = mock_dt
                    with patch("netbox_librenms_plugin.import_utils.device_operations.DeviceRole") as mock_role_cls:
                        mock_role_cls.objects.filter.return_value.first.return_value = mock_role
                        with patch("netbox_librenms_plugin.import_utils.device_operations.Rack") as mock_rack_cls:
                            mock_rack_cls.objects.select_related.return_value.filter.return_value.first.return_value = (
                                None
                            )
                            with patch(
                                "netbox_librenms_plugin.import_utils.device_operations.Device"
                            ) as mock_device_cls:
                                mock_device_cls.objects.filter.return_value.first.return_value = None
                                mock_new_device = MagicMock()
                                mock_device_cls.return_value = mock_new_device
                                mock_new_device.full_clean.return_value = None
                                mock_new_device.save.return_value = None
                                mock_new_device.pk = 99
                                with patch(
                                    "netbox_librenms_plugin.import_utils.device_operations.set_librenms_device_id"
                                ) as mock_set_id:
                                    with patch(
                                        "netbox_librenms_plugin.import_utils.device_operations.validate_device_for_import",
                                        return_value=validation,
                                    ):
                                        with patch(
                                            "netbox_librenms_plugin.import_utils.device_operations.timezone"
                                        ) as mock_tz:
                                            mock_tz.now.return_value.strftime.return_value = "2024-01-01 00:00:00 UTC"
                                            result = import_single_device(
                                                device_id=1,
                                                libre_device=libre_device,
                                                validation=validation,
                                                manual_mappings=manual_mappings,
                                                server_key="default",
                                            )
        # Should have succeeded
        assert result.get("success") is True
        mock_new_device.full_clean.assert_called_once()
        mock_new_device.save.assert_called_once()
        mock_set_id.assert_called_once()


class TestImportSingleDeviceMoreEdgeCases:
    """Tests for device_operations additional coverage (lines 539, 783-785, 789)."""

    def _make_api(self):
        api = MagicMock()
        api.server_key = "default"
        api.cache_timeout = 300
        return api

    def _base_validation(self):
        return {
            "is_ready": True,
            "can_import": True,
            "existing_device": None,
            "import_as_vm": False,
            "site": {"found": True, "site": MagicMock()},
            "device_type": {"found": True, "device_type": MagicMock()},
            "device_role": {"found": True, "role": MagicMock()},
            "platform": {"found": False, "platform": None},
            "rack": {"rack": None},
            "issues": [],
        }

    def _mock_tx(self):
        mock_tx = MagicMock()
        mock_tx.atomic.return_value.__enter__ = MagicMock(return_value=None)
        mock_tx.atomic.return_value.__exit__ = MagicMock(return_value=False)
        return mock_tx

    def test_platform_manual_mapping(self):
        """Lines 783-785: manual_mappings with platform_id applied."""
        from netbox_librenms_plugin.import_utils.device_operations import import_single_device

        libre_device = {"device_id": 1, "hostname": "r01", "serial": "-", "hardware": "-", "os": "-", "location": ""}
        validation = self._base_validation()
        manual_mappings = {"platform_id": 3}

        mock_platform = MagicMock()
        mock_new_device = MagicMock()
        mock_new_device.full_clean.return_value = None
        mock_new_device.save.return_value = None
        mock_new_device.pk = 10

        with patch("netbox_librenms_plugin.import_utils.device_operations.transaction", self._mock_tx()):
            with patch("netbox_librenms_plugin.import_utils.device_operations.Site") as MockSite:
                MockSite.objects.filter.return_value.first.return_value = MagicMock()
                with patch("netbox_librenms_plugin.import_utils.device_operations.DeviceType") as MockDT:
                    MockDT.objects.filter.return_value.first.return_value = MagicMock()
                    with patch("netbox_librenms_plugin.import_utils.device_operations.DeviceRole") as MockRole:
                        MockRole.objects.filter.return_value.first.return_value = MagicMock()
                        with patch("netbox_librenms_plugin.import_utils.device_operations.Device") as MockDevice:
                            MockDevice.objects.filter.return_value.first.return_value = None
                            MockDevice.return_value = mock_new_device
                            with patch("dcim.models.Platform") as MockPlatform:
                                MockPlatform.objects.filter.return_value.first.return_value = mock_platform
                                with patch(
                                    "netbox_librenms_plugin.import_utils.device_operations.set_librenms_device_id"
                                ):
                                    with patch(
                                        "netbox_librenms_plugin.import_utils.device_operations.LibreNMSAPI"
                                    ) as MockAPI:
                                        MockAPI.return_value = self._make_api()
                                        with patch(
                                            "netbox_librenms_plugin.import_utils.device_operations.timezone"
                                        ) as mock_tz:
                                            mock_tz.now.return_value.strftime.return_value = "2024-01-01"
                                            result = import_single_device(
                                                device_id=1,
                                                libre_device=libre_device,
                                                validation=validation,
                                                manual_mappings=manual_mappings,
                                                server_key="default",
                                            )

        assert result.get("success") is True
        # Verify that the platform was looked up with the correct ID and passed to Device()
        MockPlatform.objects.filter.assert_called_with(id=manual_mappings["platform_id"])
        assert MockDevice.call_args.kwargs.get("platform") is mock_platform

    def test_rack_manual_mapping(self):
        """Line 789: manual_mappings with rack_id applied."""
        from netbox_librenms_plugin.import_utils.device_operations import import_single_device

        libre_device = {"device_id": 1, "hostname": "r01", "serial": "-", "hardware": "-", "os": "-", "location": ""}
        validation = self._base_validation()
        manual_mappings = {"rack_id": 5}

        mock_rack = MagicMock()
        mock_new_device = MagicMock()
        mock_new_device.full_clean.return_value = None
        mock_new_device.save.return_value = None
        mock_new_device.pk = 10

        with patch("netbox_librenms_plugin.import_utils.device_operations.transaction", self._mock_tx()):
            with patch("netbox_librenms_plugin.import_utils.device_operations.Site") as MockSite:
                MockSite.objects.filter.return_value.first.return_value = MagicMock()
                with patch("netbox_librenms_plugin.import_utils.device_operations.DeviceType") as MockDT:
                    MockDT.objects.filter.return_value.first.return_value = MagicMock()
                    with patch("netbox_librenms_plugin.import_utils.device_operations.DeviceRole") as MockRole:
                        MockRole.objects.filter.return_value.first.return_value = MagicMock()
                        with patch("netbox_librenms_plugin.import_utils.device_operations.Device") as MockDevice:
                            MockDevice.objects.filter.return_value.first.return_value = None
                            MockDevice.return_value = mock_new_device
                            with patch("netbox_librenms_plugin.import_utils.device_operations.Rack") as MockRack:
                                MockRack.objects.select_related.return_value.filter.return_value.first.return_value = (
                                    mock_rack
                                )
                                with patch(
                                    "netbox_librenms_plugin.import_utils.device_operations.set_librenms_device_id"
                                ):
                                    with patch(
                                        "netbox_librenms_plugin.import_utils.device_operations.LibreNMSAPI"
                                    ) as MockAPI:
                                        MockAPI.return_value = self._make_api()
                                        with patch(
                                            "netbox_librenms_plugin.import_utils.device_operations.timezone"
                                        ) as mock_tz:
                                            mock_tz.now.return_value.strftime.return_value = "2024-01-01"
                                            result = import_single_device(
                                                device_id=1,
                                                libre_device=libre_device,
                                                validation=validation,
                                                manual_mappings=manual_mappings,
                                                server_key="default",
                                            )

        assert result.get("success") is True
        # Verify that the rack was looked up with the correct ID and passed to Device()
        MockRack.objects.select_related.return_value.filter.assert_called_with(id=manual_mappings["rack_id"])
        assert MockDevice.call_args.kwargs.get("rack") is mock_rack


class TestValidateDeviceExistingVMGuard:
    """Test that existing VMs skip device-specific validations (g06 fix)."""

    def _make_api(self):
        api = MagicMock()
        api.server_key = "default"
        api.cache_timeout = 300
        return api

    def test_existing_vm_skips_device_validations(self):
        """
        When import_as_vm=True and existing_device is set, site/device_type/device_role
        are marked found=True without running device-specific validation logic."""
        from netbox_librenms_plugin.import_utils.device_operations import validate_device_for_import

        existing_vm = MagicMock()
        existing_vm.name = "vm01"
        existing_vm.custom_field_data = {}

        libre_device = {
            "device_id": 1,
            "hostname": "vm01",
            "sysName": "vm01",
            "hardware": "-",
            "serial": "-",
            "os": "-",
            "location": "unknown-location",
        }
        api = self._make_api()

        mock_vm_model = MagicMock()
        mock_vm_model.objects.filter.return_value.first.return_value = existing_vm

        mock_device_model = MagicMock()
        mock_device_model.objects.filter.return_value.first.return_value = None
        mock_device_model.objects.filter.return_value.exclude.return_value.first.return_value = None
        mock_device_model.objects.all.return_value = []

        with (
            patch("netbox_librenms_plugin.import_utils.device_operations.Site"),
            patch("netbox_librenms_plugin.import_utils.device_operations.DeviceType"),
            patch("netbox_librenms_plugin.import_utils.device_operations.DeviceRole"),
            patch("netbox_librenms_plugin.import_utils.device_operations.Device", new=mock_device_model),
            patch("netbox_librenms_plugin.import_utils.device_operations.Cluster"),
            patch("netbox_librenms_plugin.import_utils.device_operations.cache"),
            patch("virtualization.models.VirtualMachine", new=mock_vm_model),
            patch("ipam.models.IPAddress"),
            patch("netbox_librenms_plugin.import_utils.device_operations.find_by_librenms_id", return_value=None),
            patch(
                "netbox_librenms_plugin.import_utils.device_operations.match_librenms_hardware_to_device_type"
            ) as mock_match,
            patch("netbox_librenms_plugin.import_utils.device_operations.find_matching_site") as mock_site,
        ):
            result = validate_device_for_import(libre_device, import_as_vm=True, api=api)

        # find_matching_site and match_librenms_hardware_to_device_type should NOT be called for VMs
        mock_site.assert_not_called()
        mock_match.assert_not_called()
        # Device-specific fields are marked found=True for all VMs
        assert result["site"]["found"] is True
        assert result["device_type"]["found"] is True
        assert result["device_role"]["found"] is True
        # No cluster-required error for existing VMs
        assert not any("Cluster must be" in i for i in result.get("issues", []))


class TestValidateDeviceChassisMatch:
    """Test chassis match path (line 539) in validate_device_for_import."""

    def _make_api(self):
        api = MagicMock()
        api.server_key = "default"
        return api

    def test_chassis_match_overrides_hardware_match(self):
        """Line 539: chassis_match succeeds → dt_match = chassis_match."""
        from netbox_librenms_plugin.import_utils.device_operations import validate_device_for_import

        libre_device = {
            "device_id": 1,
            "hostname": "sw01",
            "sysName": "sw01",
            "hardware": "Cisco Catalyst 9300",
            "serial": "SN001",
            "os": "ios",
            "location": "",
        }
        api = self._make_api()

        chassis_dt = MagicMock()
        chassis_dt.model = "Catalyst 9300"
        chassis_match = {"matched": True, "device_type": chassis_dt, "match_type": "chassis_inventory"}

        vm_no_match = MagicMock()
        vm_no_match.objects.filter.return_value.first.return_value = None  # no hostname collision

        device_patch = patch("netbox_librenms_plugin.import_utils.device_operations.Device")
        mock_device_cls = device_patch.start()
        mock_device_cls.objects.filter.return_value.first.return_value = None
        mock_device_cls.objects.filter.return_value.exclude.return_value.first.return_value = None

        patches = [
            patch("netbox_librenms_plugin.import_utils.device_operations.Site"),
            patch("netbox_librenms_plugin.import_utils.device_operations.DeviceType"),
            patch("netbox_librenms_plugin.import_utils.device_operations.DeviceRole"),
            patch("netbox_librenms_plugin.import_utils.device_operations.cache"),
            patch("virtualization.models.VirtualMachine", new=vm_no_match),
            patch("ipam.models.IPAddress"),
            patch("netbox_librenms_plugin.import_utils.device_operations.find_by_librenms_id", return_value=None),
            patch(
                "netbox_librenms_plugin.import_utils.device_operations.match_librenms_hardware_to_device_type",
                return_value={"matched": False},
            ),
            patch(
                "netbox_librenms_plugin.import_utils.device_operations._try_chassis_device_type_match",
                return_value=chassis_match,
            ),
            patch(
                "netbox_librenms_plugin.import_utils.device_operations.find_matching_platform",
                return_value={"found": False, "platform": None, "match_type": None},
            ),
        ]

        for p in patches:
            p.start()

        try:
            result = validate_device_for_import(libre_device, api=api)
        finally:
            for p in patches:
                p.stop()
            device_patch.stop()

        assert result["device_type"].get("device_type") is chassis_dt
