"""Coverage tests for librenms_api.py missing lines."""

from unittest.mock import MagicMock, patch

import requests


def _make_api(url="https://librenms.example.com", token="test-token"):
    """Create a LibreNMSAPI instance without database calls."""
    with patch("netbox_librenms_plugin.librenms_api.get_plugin_config") as mock_cfg:
        mock_cfg.side_effect = lambda plugin, key, default=None: {
            "servers": None,
            "librenms_url": url,
            "api_token": token,
            "cache_timeout": 300,
            "verify_ssl": True,
        }.get(key, default)

        from netbox_librenms_plugin.librenms_api import LibreNMSAPI

        return LibreNMSAPI(server_key="default")


class TestLibreNMSAPIInitFallback:
    """Tests for __init__ fallback when no server_key (lines 35-36)."""

    def test_init_reads_selected_server_from_settings(self):
        """When no server_key, tries to get selected_server from LibreNMSSettings."""
        servers_config = {
            "primary": {
                "librenms_url": "https://primary.example.com",
                "api_token": "tok",
                "cache_timeout": 300,
                "verify_ssl": True,
            }
        }

        with patch("netbox_librenms_plugin.librenms_api.get_plugin_config") as mock_cfg:
            mock_cfg.side_effect = lambda plugin, key, default=None: servers_config if key == "servers" else default

            mock_settings_obj = MagicMock()
            mock_settings_obj.selected_server = "primary"
            mock_settings_class = MagicMock()
            mock_settings_class.objects.first.return_value = mock_settings_obj

            # LibreNMSSettings is imported inline in __init__, patch via models
            with patch.dict(
                "sys.modules", {"netbox_librenms_plugin.models": MagicMock(LibreNMSSettings=mock_settings_class)}
            ):
                from netbox_librenms_plugin.librenms_api import LibreNMSAPI

                api = LibreNMSAPI()
                assert api.server_key == "primary"
                assert api.librenms_url == "https://primary.example.com"
                assert api.api_token == "tok"

    def test_init_settings_import_error_defaults_to_default(self):
        """When LibreNMSSettings can't be imported, defaults to 'default'."""
        with patch("netbox_librenms_plugin.librenms_api.get_plugin_config") as mock_cfg:
            mock_cfg.side_effect = lambda plugin, key, default=None: {
                "servers": None,
                "librenms_url": "https://x.example.com",
                "api_token": "tok",
                "cache_timeout": 300,
                "verify_ssl": True,
            }.get(key, default)

            # Simulate AttributeError when accessing LibreNMSSettings (covers except branch)
            mock_models = MagicMock()
            mock_models.LibreNMSSettings.objects.first.side_effect = AttributeError("no attr")

            with patch.dict("sys.modules", {"netbox_librenms_plugin.models": mock_models}):
                from netbox_librenms_plugin.librenms_api import LibreNMSAPI

                api = LibreNMSAPI()
                assert api.server_key == "default"
                assert api.librenms_url == "https://x.example.com"
                assert api.api_token == "tok"


class TestTestConnectionErrors:
    """Tests for test_connection error paths (lines 116, 121, 137, 146-147, 157-171)."""

    def test_http_403_returns_error(self):
        api = _make_api()
        mock_resp = MagicMock()
        mock_resp.status_code = 403
        with patch("requests.get", return_value=mock_resp):
            result = api.test_connection()
        assert result["error"] is True
        assert "forbidden" in result["message"].lower()

    def test_http_404_returns_error(self):
        api = _make_api()
        mock_resp = MagicMock()
        mock_resp.status_code = 404
        with patch("requests.get", return_value=mock_resp):
            result = api.test_connection()
        assert result["error"] is True
        assert "not found" in result["message"].lower()

    def test_http_500_returns_error(self):
        api = _make_api()
        mock_resp = MagicMock()
        mock_resp.status_code = 500
        with patch("requests.get", return_value=mock_resp):
            result = api.test_connection()
        assert result["error"] is True
        assert "server error" in result["message"].lower()

    def test_http_unexpected_code_returns_error(self):
        api = _make_api()
        mock_resp = MagicMock()
        mock_resp.status_code = 302
        with patch("requests.get", return_value=mock_resp):
            result = api.test_connection()
        assert result["error"] is True
        assert "302" in result["message"]

    def test_ssl_error_returns_error(self):
        api = _make_api()
        with patch("requests.get", side_effect=requests.exceptions.SSLError("cert failed")):
            result = api.test_connection()
        assert result["error"] is True
        assert "SSL" in result["message"] or "ssl" in result["message"].lower()

    def test_connection_error_returns_error(self):
        api = _make_api()
        with patch("requests.get", side_effect=requests.exceptions.ConnectionError("unreachable")):
            result = api.test_connection()
        assert result["error"] is True
        assert "Connection failed" in result["message"]

    def test_timeout_returns_error(self):
        api = _make_api()
        with patch("requests.get", side_effect=requests.exceptions.Timeout("timed out")):
            result = api.test_connection()
        assert result["error"] is True
        assert "timeout" in result["message"].lower()

    def test_generic_exception_returns_error(self):
        api = _make_api()
        with patch("requests.get", side_effect=ValueError("something weird")):
            result = api.test_connection()
        assert result["error"] is True
        assert "Unexpected error" in result["message"]


class TestGetAvailableServersLegacy:
    """Tests for get_available_servers legacy path (line 231)."""

    def test_legacy_config_no_servers(self):
        """When no servers_config, returns default server with legacy URL."""
        with patch("netbox_librenms_plugin.librenms_api.get_plugin_config") as mock_cfg:

            def side_effect(plugin, key, default=None):
                if key == "servers":
                    return None
                if key == "librenms_url":
                    return "https://legacy.example.com"
                return default

            mock_cfg.side_effect = side_effect
            from netbox_librenms_plugin.librenms_api import LibreNMSAPI

            result = LibreNMSAPI.get_available_servers()
            assert "default" in result
            assert "legacy.example.com" in result["default"]

    def test_no_legacy_url_returns_default_label(self):
        """When no servers_config and no legacy URL, returns default label."""
        with patch("netbox_librenms_plugin.librenms_api.get_plugin_config") as mock_cfg:

            def side_effect(plugin, key, default=None):
                return None

            mock_cfg.side_effect = side_effect
            from netbox_librenms_plugin.librenms_api import LibreNMSAPI

            result = LibreNMSAPI.get_available_servers()
            assert "default" in result
            assert result["default"] == "Default Server"


class TestGetLibreNMSIdDictServerKey:
    """Tests for get_librenms_id → _store_librenms_id with dict CF (lines 259-262)."""

    def test_dict_cf_routes_to_get_librenms_device_id(self):
        """When CF has a dict 'librenms_id', get_librenms_id uses get_librenms_device_id(obj, server_key, auto_save=False)."""
        api = _make_api()

        obj = MagicMock()
        obj.cf = {"librenms_id": {"default": None}}
        obj.custom_field_data = {"librenms_id": {"default": None}}
        obj._meta.model_name = "device"
        obj.pk = 42
        obj.primary_ip = None
        obj.name = None

        with patch("netbox_librenms_plugin.utils.get_librenms_device_id", return_value=None) as mock_get_id:
            with patch("netbox_librenms_plugin.librenms_api.cache") as mock_cache:
                mock_cache.get.return_value = None
                result = api.get_librenms_id(obj)
                assert result is None
                mock_get_id.assert_called_once_with(obj, "default", auto_save=False)

    def test_store_librenms_id_via_hostname_lookup(self):
        """get_librenms_id reaches _store_librenms_id when CF/cache miss but hostname API hit."""
        api = _make_api()

        obj = MagicMock()
        obj.cf = {"librenms_id": None}  # CF key present so _store_librenms_id uses CF path
        obj.custom_field_data = {}
        obj._meta.model_name = "device"
        obj.pk = 99
        obj.primary_ip = None
        obj.name = "hostname"

        with patch("netbox_librenms_plugin.utils.get_librenms_device_id", return_value=None):
            with patch("netbox_librenms_plugin.librenms_api.cache") as mock_cache:
                mock_cache.get.return_value = None
                with patch.object(api, "get_device_id_by_hostname", return_value=42) as mock_by_hostname:
                    with patch("netbox_librenms_plugin.utils.set_librenms_device_id") as mock_set_id:
                        result = api.get_librenms_id(obj)

        assert result == 42
        mock_by_hostname.assert_called_once_with("hostname")
        mock_set_id.assert_called_once_with(obj, 42, "default")


class TestGetPortsErrors:
    """Tests for get_ports error paths (lines 373, 375-376)."""

    def test_http_error_404_returns_false(self):
        api = _make_api()
        http_err = requests.exceptions.HTTPError(response=MagicMock(status_code=404))
        with patch("requests.get", side_effect=http_err):
            ok, msg = api.get_ports(1)
        assert ok is False
        assert "not found" in msg.lower()

    def test_http_error_other_returns_false(self):
        api = _make_api()
        http_err = requests.exceptions.HTTPError(response=MagicMock(status_code=500))
        http_err.response = MagicMock(status_code=500)
        with patch("requests.get", side_effect=http_err):
            ok, msg = api.get_ports(1)
        assert ok is False

    def test_request_exception_returns_false(self):
        api = _make_api()
        with patch("requests.get", side_effect=requests.exceptions.ConnectionError("conn error")):
            ok, msg = api.get_ports(1)
        assert ok is False


class TestGetInventoryFilteredErrors:
    """Tests for get_inventory_filtered error paths (lines 405, 407, 409, 411)."""

    def test_request_exception_returns_false(self):
        api = _make_api()
        with patch("requests.get", side_effect=requests.exceptions.RequestException("error")):
            ok, result = api.get_inventory_filtered(1)
        assert ok is False
        assert isinstance(result, str)  # error message string

    def test_empty_results_with_no_filters_returns_true_empty_list(self):
        """Empty inventory with status:ok is a valid successful empty response."""
        api = _make_api()
        mock_resp = MagicMock()
        mock_resp.status_code = 200
        mock_resp.raise_for_status.return_value = None
        mock_resp.json.return_value = {"status": "ok", "inventory": []}

        with patch("requests.get", return_value=mock_resp):
            ok, result = api.get_inventory_filtered(1)
        assert ok is True
        assert result == []

    def test_fallback_to_all_endpoint_when_filtered_empty(self):
        """If filtered endpoint returns empty and params present, falls back to /all."""
        api = _make_api()
        mock_resp = MagicMock()
        mock_resp.status_code = 200
        mock_resp.raise_for_status.return_value = None
        mock_resp.json.return_value = {"status": "ok", "inventory": []}

        all_inventory = [{"entPhysicalClass": "chassis", "entPhysicalContainedIn": 0}]

        with patch("requests.get", return_value=mock_resp):
            with patch.object(api, "get_device_inventory", return_value=(True, all_inventory)):
                ok, result = api.get_inventory_filtered(1, ent_physical_class="chassis")

        assert ok is True
        assert len(result) == 1

    def test_fallback_fails_when_all_endpoint_fails(self):
        """If /all fallback also fails, returns False, []."""
        api = _make_api()
        mock_resp = MagicMock()
        mock_resp.status_code = 200
        mock_resp.raise_for_status.return_value = None
        mock_resp.json.return_value = {"status": "ok", "inventory": []}

        with patch("requests.get", return_value=mock_resp):
            with patch.object(api, "get_device_inventory", return_value=(False, [])):
                ok, result = api.get_inventory_filtered(1, ent_physical_class="chassis")

        assert ok is False
        assert result == []


class TestGetDeviceVlansErrors:
    """Tests for get_device_vlans error paths (lines 474-480)."""

    def test_http_error_404_returns_false(self):
        api = _make_api()
        http_err = requests.exceptions.HTTPError(response=MagicMock(status_code=404))
        http_err.response = MagicMock(status_code=404)
        with patch("requests.get", side_effect=http_err):
            ok, msg = api.get_device_vlans(1)
        assert ok is False

    def test_request_exception_returns_false(self):
        api = _make_api()
        with patch("requests.get", side_effect=requests.exceptions.ConnectionError("error")):
            ok, msg = api.get_device_vlans(1)
        assert ok is False

    def test_non_200_returns_http_status(self):
        api = _make_api()
        mock_resp = MagicMock()
        mock_resp.status_code = 503
        http_err = requests.exceptions.HTTPError("503 Service Unavailable")
        http_err.response = mock_resp
        mock_resp.raise_for_status.side_effect = http_err
        with patch("requests.get", return_value=mock_resp):
            ok, msg = api.get_device_vlans(1)
        assert ok is False
        assert "503" in msg


class TestGetDeviceLinksErrors:
    """Tests for get_device_links error paths (lines 505-508)."""

    def test_request_exception_returns_false(self):
        api = _make_api()
        with patch("requests.get", side_effect=requests.exceptions.ConnectionError("error")):
            ok, msg = api.get_device_links(1)
        assert ok is False

    def test_request_exception_base_returns_false(self):
        api = _make_api()
        with patch("requests.get", side_effect=requests.exceptions.RequestException("error")):
            ok, msg = api.get_device_links(1)
        assert ok is False


class TestListDevicesErrors:
    """Tests for list_devices error paths (lines 542-547)."""

    def test_request_exception_returns_false(self):
        api = _make_api()
        with patch("requests.get", side_effect=requests.exceptions.ConnectionError("error")):
            ok, msg = api.list_devices()
        assert ok is False

    def test_non_200_returns_empty(self):
        api = _make_api()
        mock_resp = MagicMock()
        mock_resp.status_code = 500
        mock_resp.raise_for_status.return_value = None
        mock_resp.json.return_value = {"status": "error"}
        with patch("requests.get", return_value=mock_resp):
            ok, result = api.list_devices()
        assert ok is False


class TestGetDeviceIpsErrors:
    """Tests for get_device_ips error paths (lines 580-585)."""

    def test_request_exception_returns_false(self):
        api = _make_api()
        with patch("requests.get", side_effect=requests.exceptions.ConnectionError("error")):
            ok, msg = api.get_device_ips(1)
        assert ok is False


class TestGetDeviceInfoErrors:
    """Tests for get_device_info error paths (lines 606-607)."""

    def test_non_200_returns_false(self):
        api = _make_api()
        mock_resp = MagicMock()
        mock_resp.status_code = 404
        mock_resp.raise_for_status.return_value = None
        with patch("requests.get", return_value=mock_resp):
            ok, data = api.get_device_info(1)
        assert ok is False
        assert data is None

    def test_request_exception_returns_false(self):
        api = _make_api()
        with patch("requests.get", side_effect=requests.exceptions.RequestException("error")):
            ok, data = api.get_device_info(1)
        assert ok is False
        assert data is None


class TestGetPortVlanDetailsErrors:
    """Tests for get_port_vlan_details error paths."""

    def test_http_error_404_returns_false(self):
        api = _make_api()
        http_err = requests.exceptions.HTTPError(response=MagicMock(status_code=404))
        http_err.response = MagicMock(status_code=404)
        with patch("requests.get", side_effect=http_err):
            ok, msg = api.get_port_vlan_details(1)
        assert ok is False
        assert "not found" in msg.lower()

    def test_non_404_http_error_returns_false(self):
        api = _make_api()
        http_err = requests.exceptions.HTTPError(response=MagicMock(status_code=500))
        http_err.response = MagicMock(status_code=500)
        with patch("requests.get", side_effect=http_err):
            ok, msg = api.get_port_vlan_details(1)
        assert ok is False

    def test_request_exception_returns_false(self):
        api = _make_api()
        with patch("requests.get", side_effect=requests.exceptions.ConnectionError("error")):
            ok, msg = api.get_port_vlan_details(1)
        assert ok is False

    def test_non_200_returns_false(self):
        api = _make_api()
        mock_resp = MagicMock()
        mock_resp.status_code = 503
        mock_resp.raise_for_status.return_value = None
        with patch("requests.get", return_value=mock_resp):
            ok, msg = api.get_port_vlan_details(1)
        assert ok is False

    def test_request_exception_base_returns_false(self):
        api = _make_api()
        with patch("requests.get", side_effect=requests.exceptions.RequestException("error")):
            ok, msg = api.get_port_vlan_details(1)
        assert ok is False

    def test_http_404_via_raise_for_status_returns_false(self):
        api = _make_api()
        mock_resp = MagicMock()
        mock_resp.status_code = 404
        http_error = requests.exceptions.HTTPError(response=mock_resp)
        mock_resp.raise_for_status.side_effect = http_error
        with patch("requests.get", return_value=mock_resp):
            ok, msg = api.get_port_vlan_details(1)
        assert ok is False


class TestListDevicesSuccess:
    """Tests for list_devices success (lines 805+)."""

    def test_list_devices_with_filters(self):
        api = _make_api()
        mock_resp = MagicMock()
        mock_resp.status_code = 200
        mock_resp.raise_for_status.return_value = None
        mock_resp.json.return_value = {"status": "ok", "devices": [{"device_id": 1}]}

        with patch("requests.get", return_value=mock_resp) as mock_get:
            ok, result = api.list_devices({"type": "network"})

        assert ok is True
        assert len(result) == 1
        # Verify the filter was forwarded to the outgoing request
        assert mock_get.call_args[1]["params"] == {"type": "network"}

    def test_list_devices_no_filters(self):
        api = _make_api()
        mock_resp = MagicMock()
        mock_resp.status_code = 200
        mock_resp.raise_for_status.return_value = None
        mock_resp.json.return_value = {"status": "ok", "devices": []}

        with patch("requests.get", return_value=mock_resp):
            ok, result = api.list_devices()

        assert ok is True
        assert result == []


class TestGetPoller:
    """Tests for get_poller_groups error path."""

    def test_request_exception_returns_false(self):
        api = _make_api()
        with patch("requests.get", side_effect=requests.exceptions.RequestException("error")):
            ok, result = api.get_poller_groups()
        assert ok is False

    def test_non_ok_status_returns_false(self):
        api = _make_api()
        mock_resp = MagicMock()
        mock_resp.status_code = 500
        mock_resp.raise_for_status.return_value = None
        mock_resp.json.return_value = {"status": "error"}
        with patch("requests.get", return_value=mock_resp):
            ok, result = api.get_poller_groups()
        assert ok is False


class TestAddDeviceErrors:
    """Tests for add_device errors."""

    def _make_device_data(self):
        return {"hostname": "router01", "snmp_version": "v2c", "community": "public", "force_add": False}

    def test_request_exception_returns_false(self):
        api = _make_api()
        with patch("requests.post", side_effect=requests.exceptions.RequestException("error")):
            ok, msg = api.add_device(self._make_device_data())
        assert ok is False

    def test_non_ok_response_returns_false(self):
        api = _make_api()
        mock_resp = MagicMock()
        mock_resp.status_code = 200
        mock_resp.raise_for_status.return_value = None
        mock_resp.json.return_value = {"status": "error", "message": "Already exists"}
        with patch("requests.post", return_value=mock_resp):
            ok, msg = api.add_device(self._make_device_data())
        assert ok is False


class TestGetLocationsErrors:
    """Tests for get_locations errors."""

    def test_request_exception_returns_false(self):
        api = _make_api()
        with patch("requests.get", side_effect=requests.exceptions.RequestException("error")):
            ok, msg = api.get_locations()
        assert ok is False


class TestUpdateDeviceFieldErrors:
    """Tests for update_device_field errors."""

    def test_request_exception_returns_false(self):
        api = _make_api()
        with patch("requests.patch", side_effect=requests.exceptions.RequestException("error")):
            ok, msg = api.update_device_field(1, {"field": "value"})
        assert ok is False


class TestGetDeviceIdByIPErrors:
    """Tests for get_device_id_by_ip errors."""

    def test_request_exception_returns_false(self):
        api = _make_api()
        with patch("requests.get", side_effect=requests.exceptions.RequestException("error")):
            result = api.get_device_id_by_ip("192.168.1.1")
        assert result is None

    def test_non_200_returns_none(self):
        api = _make_api()
        mock_resp = MagicMock()
        mock_resp.status_code = 404
        http_error = requests.exceptions.HTTPError(response=mock_resp)
        mock_resp.raise_for_status.side_effect = http_error
        with patch("requests.get", return_value=mock_resp):
            result = api.get_device_id_by_ip("192.168.1.1")
        assert result is None

    def test_null_devices_field_returns_none(self):
        """API returns {"devices": null} — TypeError must be caught, not propagate."""
        api = _make_api()
        mock_resp = MagicMock()
        mock_resp.status_code = 200
        mock_resp.raise_for_status.return_value = None
        mock_resp.json.return_value = {"devices": None}
        with patch("requests.get", return_value=mock_resp):
            result = api.get_device_id_by_ip("192.168.1.1")
        assert result is None

    def test_empty_devices_list_returns_none(self):
        """API returns {"devices": []} — no match, returns None."""
        api = _make_api()
        mock_resp = MagicMock()
        mock_resp.status_code = 200
        mock_resp.raise_for_status.return_value = None
        mock_resp.json.return_value = {"devices": []}
        with patch("requests.get", return_value=mock_resp):
            result = api.get_device_id_by_ip("192.168.1.1")
        assert result is None


class TestGetDeviceIdByHostnameErrors:
    """Tests for get_device_id_by_hostname errors."""

    def test_request_exception_returns_none(self):
        api = _make_api()
        with patch("requests.get", side_effect=requests.exceptions.RequestException("error")):
            result = api.get_device_id_by_hostname("router01")
        assert result is None

    def test_null_devices_field_returns_none(self):
        """API returns {"devices": null} — TypeError must be caught, not propagate."""
        api = _make_api()
        mock_resp = MagicMock()
        mock_resp.raise_for_status.return_value = None
        mock_resp.json.return_value = {"devices": None}
        with patch("requests.get", return_value=mock_resp):
            result = api.get_device_id_by_hostname("router01")
        assert result is None

    def test_empty_devices_list_returns_none(self):
        """API returns {"devices": []} — no match, returns None."""
        api = _make_api()
        mock_resp = MagicMock()
        mock_resp.raise_for_status.return_value = None
        mock_resp.json.return_value = {"devices": []}
        with patch("requests.get", return_value=mock_resp):
            result = api.get_device_id_by_hostname("router01")
        assert result is None


class TestStorelibrenmsId:
    """Tests for _store_librenms_id (lines 259-262)."""

    def test_stores_via_set_librenms_device_id_when_cf_has_key(self):
        api = _make_api()
        obj = MagicMock()
        obj.cf = {"librenms_id": {"default": None}}
        obj.custom_field_data = {"librenms_id": {"default": None}}

        with patch("netbox_librenms_plugin.utils.set_librenms_device_id") as mock_set:
            api._store_librenms_id(obj, 42)
        mock_set.assert_called_once_with(obj, 42, "default")
        obj.save.assert_called_once()

    def test_stores_in_cache_when_no_cf_key(self):
        api = _make_api()
        obj = MagicMock()
        obj.cf = {}  # No 'librenms_id' key

        with patch("netbox_librenms_plugin.librenms_api.cache") as mock_cache:
            api._store_librenms_id(obj, 42)
        mock_cache.set.assert_called_once()
        cache_key_used = mock_cache.set.call_args[0][0]
        assert api.server_key in cache_key_used


class TestParsePortVlanData:
    """Tests for parse_port_vlan_data (lines 978+)."""

    def test_no_if_vlan_returns_mode_none(self):
        api = _make_api()
        port_data = {"port_id": 1, "ifName": "Gi0/1", "ifDescr": "GigabitEthernet", "ifVlan": ""}
        result = api.parse_port_vlan_data(port_data)
        assert result["mode"] is None

    def test_trunk_mode_set_correctly(self):
        api = _make_api()
        port_data = {
            "port_id": 1,
            "ifName": "Gi0/1",
            "ifDescr": "GE",
            "ifVlan": "100",
            "ifTrunk": "dot1Q",
            "vlans": [{"vlan": 100, "untagged": 0}, {"vlan": 200, "untagged": 0}],
        }
        result = api.parse_port_vlan_data(port_data)
        assert result["mode"] == "tagged"
        assert 100 in result["tagged_vlans"]
        assert 200 in result["tagged_vlans"]

    def test_access_mode_from_vlan_array(self):
        api = _make_api()
        port_data = {
            "port_id": 1,
            "ifName": "Gi0/2",
            "ifDescr": "GE",
            "ifVlan": "100",
            "vlans": [{"vlan": 100, "untagged": 1}],
        }
        result = api.parse_port_vlan_data(port_data)
        assert result["mode"] == "access"
        assert result["untagged_vlan"] == 100

    def test_fallback_to_if_vlan_when_no_vlans_array(self):
        api = _make_api()
        port_data = {"port_id": 1, "ifName": "Gi0/3", "ifDescr": "GE", "ifVlan": "50", "ifTrunk": None}
        result = api.parse_port_vlan_data(port_data)
        assert result["mode"] == "access"
        assert result["untagged_vlan"] == 50

    def test_invalid_if_vlan_fallback_returns_none(self):
        """Lines 1028-1029: ValueError when ifVlan is not an integer."""
        api = _make_api()
        port_data = {"port_id": 1, "ifName": "Gi0/4", "ifDescr": "GE", "ifVlan": "not-a-number"}
        result = api.parse_port_vlan_data(port_data)
        assert result["untagged_vlan"] is None

    def test_if_descr_used_as_interface_name(self):
        api = _make_api()
        port_data = {"port_id": 1, "ifName": "Gi0/5", "ifDescr": "GigabitEthernet0/5", "ifVlan": ""}
        result = api.parse_port_vlan_data(port_data, interface_name_field="ifDescr")
        assert result["interface_name"] == "GigabitEthernet0/5"

    def test_string_vlan_id_normalized_to_int(self):
        """VLAN ID as string '50' must be coerced to int 50 to avoid string sort."""
        api = _make_api()
        port_data = {
            "port_id": 1,
            "ifName": "Gi0/6",
            "ifDescr": "GE",
            "ifVlan": "50",
            "vlans": [{"vlan": "50", "untagged": 1}, {"vlan": "100", "untagged": 0}],
        }
        result = api.parse_port_vlan_data(port_data)
        assert result["untagged_vlan"] == 50
        assert result["tagged_vlans"] == [100]

    def test_none_vlan_id_skipped(self):
        """Entry with missing 'vlan' key (None) must be skipped."""
        api = _make_api()
        port_data = {
            "port_id": 1,
            "ifName": "Gi0/7",
            "ifDescr": "GE",
            "ifVlan": "200",
            "vlans": [{"untagged": 0}, {"vlan": 200, "untagged": 1}],
        }
        result = api.parse_port_vlan_data(port_data)
        assert result["untagged_vlan"] == 200
        assert result["tagged_vlans"] == []

    def test_malformed_vlan_id_skipped(self):
        """Non-numeric string vlan ID is skipped gracefully."""
        api = _make_api()
        port_data = {
            "port_id": 1,
            "ifName": "Gi0/8",
            "ifDescr": "GE",
            "ifVlan": "300",
            "vlans": [{"vlan": "N/A", "untagged": 0}, {"vlan": 300, "untagged": 1}],
        }
        result = api.parse_port_vlan_data(port_data)
        assert result["untagged_vlan"] == 300
        assert result["tagged_vlans"] == []

    def test_empty_vlans_array_falls_back_to_if_vlan(self):
        """Empty vlans array should use ifVlan fallback."""
        api = _make_api()
        port_data = {"port_id": 1, "ifName": "Gi0/9", "ifDescr": "GE", "ifVlan": "10", "vlans": []}
        result = api.parse_port_vlan_data(port_data)
        assert result["untagged_vlan"] == 10


class TestGetDeviceInfoResponseFormats:
    """Tests for get_device_info with unusual API response shapes."""

    def test_null_devices_returns_false(self):
        """{"devices": null} must return (False, None), not raise TypeError."""
        from unittest.mock import MagicMock, patch

        api = _make_api()
        mock_resp = MagicMock()
        mock_resp.status_code = 200
        mock_resp.json.return_value = {"devices": None}
        with patch("requests.get", return_value=mock_resp):
            success, result = api.get_device_info(1)
        assert success is False
        assert result is None

    def test_empty_devices_list_returns_false(self):
        """{"devices": []} must return (False, None)."""
        from unittest.mock import MagicMock, patch

        api = _make_api()
        mock_resp = MagicMock()
        mock_resp.status_code = 200
        mock_resp.json.return_value = {"devices": []}
        with patch("requests.get", return_value=mock_resp):
            success, result = api.get_device_info(1)
        assert success is False
        assert result is None

    def test_non_200_returns_false(self):
        """Non-200 status code returns (False, None)."""
        from unittest.mock import MagicMock, patch

        api = _make_api()
        mock_resp = MagicMock()
        mock_resp.status_code = 404
        with patch("requests.get", return_value=mock_resp):
            success, result = api.get_device_info(1)
        assert success is False
        assert result is None

    def test_valid_device_returns_device_dict(self):
        """Normal response returns (True, device_dict)."""
        from unittest.mock import MagicMock, patch

        api = _make_api()
        device = {"device_id": 42, "hostname": "router01"}
        mock_resp = MagicMock()
        mock_resp.status_code = 200
        mock_resp.json.return_value = {"devices": [device]}
        with patch("requests.get", return_value=mock_resp):
            success, result = api.get_device_info(42)
        assert success is True
        assert result["device_id"] == 42


class TestGetPortByIdErrors:
    """Tests for get_port_by_id errors."""

    def test_request_exception_returns_false(self):
        api = _make_api()
        with patch("requests.get", side_effect=requests.exceptions.RequestException("error")):
            ok, msg = api.get_port_by_id(1)
        assert ok is False


class TestGetDeviceInventoryErrors:
    """Tests for get_device_inventory errors."""

    def test_request_exception_returns_false(self):
        api = _make_api()
        with patch("requests.get", side_effect=requests.exceptions.RequestException("error")):
            ok, msg = api.get_device_inventory(1)
        assert ok is False


class TestGetAvailableServersMultiConfig:
    """Tests for get_available_servers with multi-server config (lines 161-165)."""

    def test_multi_server_config_returns_dict(self):
        api = _make_api()
        servers_config = {
            "primary": {"display_name": "Primary Server"},
            "secondary": {"display_name": "Secondary Server"},
        }

        with patch("netbox_librenms_plugin.librenms_api.get_plugin_config") as mock_config:
            mock_config.side_effect = lambda plugin, key, default=None: servers_config if key == "servers" else None
            result = api.get_available_servers()
        assert result == {"primary": "Primary Server", "secondary": "Secondary Server"}

    def test_multi_server_config_uses_key_when_no_display_name(self):
        api = _make_api()
        servers_config = {
            "main": {},  # No display_name key
        }

        with patch("netbox_librenms_plugin.librenms_api.get_plugin_config") as mock_config:
            mock_config.side_effect = lambda plugin, key, default=None: servers_config if key == "servers" else None
            result = api.get_available_servers()
        assert result == {"main": "main"}


class TestAddDeviceWithOptionalFields:
    """Tests for add_device with optional fields (lines 405, 407, 409, 411)."""

    def _make_base_data(self):
        return {"hostname": "router01", "snmp_version": "v2c", "community": "public", "force_add": False}

    def test_add_device_with_port(self):
        api = _make_api()
        data = {**self._make_base_data(), "port": 161}
        mock_resp = MagicMock()
        mock_resp.raise_for_status.return_value = None
        mock_resp.json.return_value = {"status": "ok", "message": "Device added"}
        with patch("requests.post", return_value=mock_resp) as mock_post:
            ok, msg = api.add_device(data)
        assert ok is True
        assert "port" in mock_post.call_args[1]["json"]

    def test_add_device_with_transport(self):
        api = _make_api()
        data = {**self._make_base_data(), "transport": "udp6"}
        mock_resp = MagicMock()
        mock_resp.raise_for_status.return_value = None
        mock_resp.json.return_value = {"status": "ok", "message": "ok"}
        with patch("requests.post", return_value=mock_resp) as mock_post:
            ok, msg = api.add_device(data)
        assert ok is True
        assert "transport" in mock_post.call_args[1]["json"]

    def test_add_device_with_port_association_mode(self):
        api = _make_api()
        data = {**self._make_base_data(), "port_association_mode": "ifName"}
        mock_resp = MagicMock()
        mock_resp.raise_for_status.return_value = None
        mock_resp.json.return_value = {"status": "ok", "message": "ok"}
        with patch("requests.post", return_value=mock_resp) as mock_post:
            ok, msg = api.add_device(data)
        assert ok is True
        assert "port_association_mode" in mock_post.call_args[1]["json"]

    def test_add_device_with_poller_group(self):
        api = _make_api()
        data = {**self._make_base_data(), "poller_group": 2}
        mock_resp = MagicMock()
        mock_resp.raise_for_status.return_value = None
        mock_resp.json.return_value = {"status": "ok", "message": "ok"}
        with patch("requests.post", return_value=mock_resp) as mock_post:
            ok, msg = api.add_device(data)
        assert ok is True
        assert "poller_group" in mock_post.call_args[1]["json"]


class TestUpdateDeviceFieldUnexpected:
    """Tests for update_device_field non-ok status (line 474)."""

    def test_non_ok_status_returns_false(self):
        api = _make_api()
        mock_resp = MagicMock()
        mock_resp.raise_for_status.return_value = None
        mock_resp.json.return_value = {"status": "error", "message": "Failed"}
        with patch("requests.patch", return_value=mock_resp):
            ok, msg = api.update_device_field(1, {"field": "value"})
        assert ok is False

    def test_request_exception_with_json_response(self):
        """Lines 477-479: extract message from JSON error response."""
        api = _make_api()
        mock_response = MagicMock()
        mock_response.json.return_value = {"message": "Detailed error"}
        exc = requests.exceptions.RequestException("error")
        exc.response = mock_response
        with patch("requests.patch", side_effect=exc):
            ok, msg = api.update_device_field(1, {"field": "value"})
        assert ok is False
        assert "Detailed error" in msg


class TestGetLocationsNoLocations:
    """Tests for get_locations when no locations found (line 505)."""

    def test_no_locations_in_response(self):
        api = _make_api()
        mock_resp = MagicMock()
        mock_resp.raise_for_status.return_value = None
        mock_resp.json.return_value = {"status": "ok"}  # No 'locations' key
        with patch("requests.get", return_value=mock_resp):
            ok, msg = api.get_locations()
        assert ok is False


class TestAddLocationErrors:
    """Tests for add_location error paths (lines 542-547)."""

    def test_request_exception_returns_false(self):
        api = _make_api()
        with patch("requests.post", side_effect=requests.exceptions.RequestException("error")):
            ok, msg = api.add_location({"location": "TestSite", "lat": 0, "lng": 0})
        assert ok is False

    def test_request_exception_with_json_response(self):
        api = _make_api()
        mock_response = MagicMock()
        mock_response.json.return_value = {"message": "Detailed error"}
        exc = requests.exceptions.RequestException("error")
        exc.response = mock_response
        with patch("requests.post", side_effect=exc):
            ok, msg = api.add_location({"location": "TestSite", "lat": 0, "lng": 0})
        assert ok is False
        assert "Detailed error" in msg


class TestUpdateLocationErrors:
    """Tests for update_location error paths (lines 580-585)."""

    def test_request_exception_returns_false(self):
        api = _make_api()
        with patch("requests.patch", side_effect=requests.exceptions.RequestException("error")):
            ok, msg = api.update_location("TestSite", {"lat": 0, "lng": 0})
        assert ok is False

    def test_request_exception_with_json_response(self):
        api = _make_api()
        mock_response = MagicMock()
        mock_response.json.return_value = {"message": "Update failed"}
        exc = requests.exceptions.RequestException("error")
        exc.response = mock_response
        with patch("requests.patch", side_effect=exc):
            ok, msg = api.update_location("TestSite", {"lat": 0, "lng": 0})
        assert ok is False
        assert "Update failed" in msg


class TestGetInventoryFilteredNonOk:
    """Tests for get_inventory_filtered non-200 response (line 689 + 791, 799)."""

    def test_non_200_response_returns_false(self):
        api = _make_api()
        mock_resp = MagicMock()
        mock_resp.status_code = 404
        mock_resp.raise_for_status.return_value = None
        with patch("requests.get", return_value=mock_resp):
            ok, data = api.get_inventory_filtered(1)
        assert ok is False
        assert isinstance(data, str)  # error message, not empty list

    def test_ent_physical_contained_in_filter(self):
        """Line 791: ent_physical_contained_in filter exercised — API returns already-filtered list."""
        api = _make_api()
        mock_resp = MagicMock()
        mock_resp.status_code = 200
        mock_resp.raise_for_status.return_value = None
        # The real LibreNMS API filters server-side; mock returns only the matching item.
        inventory = [
            {"entPhysicalContainedIn": "1", "entPhysicalName": "slot1"},
        ]
        mock_resp.json.return_value = {"status": "ok", "inventory": inventory}
        with patch("requests.get", return_value=mock_resp) as mock_get:
            ok, data = api.get_inventory_filtered(1, ent_physical_contained_in="1")
        assert ok is True
        assert len(data) == 1
        mock_get.assert_called_once()
        _, call_kwargs = mock_get.call_args
        assert call_kwargs.get("params", {}).get("entPhysicalContainedIn") == "1"

    def test_empty_inventory_returns_empty(self):
        """Line 799: when response lacks status:ok (even with an empty inventory list), returns False."""
        api = _make_api()
        mock_resp = MagicMock()
        mock_resp.status_code = 200
        mock_resp.raise_for_status.return_value = None
        mock_resp.json.return_value = {"inventory": []}  # Empty inventory
        with patch("requests.get", return_value=mock_resp):
            ok, data = api.get_inventory_filtered(1)
        # No "status":"ok" in response → falls through to return False, message
        assert ok is False
        assert isinstance(data, str)  # error message, not empty list


class TestGetDeviceVlansHttpError:
    """Tests for get_device_vlans HTTP error paths (lines 918, 924)."""

    def test_http_404_returns_not_found(self):
        api = _make_api()
        mock_resp = MagicMock()
        mock_resp.status_code = 404
        exc = requests.exceptions.HTTPError(response=mock_resp)
        mock_resp.raise_for_status.side_effect = exc
        exc.response = mock_resp
        with patch("requests.get", side_effect=exc):
            ok, msg = api.get_device_vlans(1)
        assert ok is False

    def test_http_5xx_returns_error(self):
        api = _make_api()
        mock_resp = MagicMock()
        mock_resp.status_code = 500
        exc = requests.exceptions.HTTPError(response=mock_resp)
        exc.response = mock_resp
        with patch("requests.get", side_effect=exc):
            ok, msg = api.get_device_vlans(1)
        assert ok is False


class TestGetPortVlanDetailsHttpError:
    """Tests for get_port_vlan_details HTTP error paths (line 974)."""

    def test_http_non_404_returns_http_error(self):
        api = _make_api()
        mock_resp = MagicMock()
        mock_resp.status_code = 500
        exc = requests.exceptions.HTTPError(response=mock_resp)
        exc.response = mock_resp
        with patch("requests.get", side_effect=exc):
            ok, msg = api.get_port_vlan_details(1)
        assert ok is False
        assert "HTTP error" in msg


class TestGetInventoryFilteredNonOkStatus:
    """Line 689: non-200 returns False with error message."""

    def test_non_200_status(self):
        api = _make_api()
        mock_resp = MagicMock()
        mock_resp.status_code = 500
        mock_resp.raise_for_status.return_value = None
        with patch("requests.get", return_value=mock_resp):
            ok, data = api.get_inventory_filtered(1)
        assert ok is False
        assert isinstance(data, str)  # error message, not empty list


class TestGetDeviceVlansNonOkResponse:
    """Line 918: get_device_vlans when status != ok."""

    def test_vlans_response_status_not_ok(self):
        api = _make_api()
        mock_resp = MagicMock()
        mock_resp.status_code = 200
        mock_resp.raise_for_status.return_value = None
        mock_resp.json.return_value = {"status": "error", "message": "Failed to retrieve VLANs"}
        with patch("requests.get", return_value=mock_resp):
            ok, msg = api.get_device_vlans(1)
        assert ok is False
        assert "Failed" in msg


class TestGetDeviceInventoryNonOkStatus:
    """Line 689: get_device_inventory non-200 returns False, []."""

    def test_non_200_status(self):
        api = _make_api()
        mock_resp = MagicMock()
        mock_resp.status_code = 500
        mock_resp.raise_for_status.side_effect = requests.exceptions.HTTPError("500 Server Error")
        with patch("requests.get", return_value=mock_resp):
            ok, data = api.get_device_inventory(1)
        assert ok is False


class TestMalformedPayloads:
    """Tests for malformed-payload guards in API methods (inventory, devices, vlans)."""

    def _ok_resp(self, body: dict):
        mock_resp = MagicMock()
        mock_resp.status_code = 200
        mock_resp.raise_for_status.return_value = None
        mock_resp.json.return_value = body
        return mock_resp

    def test_get_device_inventory_none_inventory(self):
        """get_device_inventory: inventory=None returns (False, ...)."""
        api = _make_api()
        with patch("requests.get", return_value=self._ok_resp({"status": "ok", "inventory": None})):
            ok, msg = api.get_device_inventory(1)
        assert ok is False
        assert msg is not None

    def test_get_device_inventory_non_list_inventory(self):
        """get_device_inventory: inventory as dict returns (False, ...)."""
        api = _make_api()
        with patch("requests.get", return_value=self._ok_resp({"status": "ok", "inventory": {}})):
            ok, msg = api.get_device_inventory(1)
        assert ok is False

    def test_get_device_inventory_non_dict_inventory_item(self):
        """get_device_inventory: list containing non-dict items returns (False, ...)."""
        api = _make_api()
        body = {"status": "ok", "inventory": [None, {"entPhysicalName": "slot1"}]}
        with patch("requests.get", return_value=self._ok_resp(body)):
            ok, msg = api.get_device_inventory(1)
        assert ok is False
        assert msg is not None

    def test_get_inventory_filtered_none_inventory(self):
        """get_inventory_filtered: inventory=None in filtered path returns (False, ...) without calling get_device_inventory."""
        api = _make_api()
        with patch("requests.get", return_value=self._ok_resp({"status": "ok", "inventory": None})):
            with patch.object(api, "get_device_inventory") as mock_get_inv:
                ok, msg = api.get_inventory_filtered(1, ent_physical_class="chassis")
        assert ok is False
        assert msg is not None
        mock_get_inv.assert_not_called()

    def test_get_inventory_filtered_non_dict_inventory_item(self):
        """get_inventory_filtered: list containing non-dict items returns (False, ...) without fallback."""
        api = _make_api()
        body = {"status": "ok", "inventory": ["bad"]}
        with patch("requests.get", return_value=self._ok_resp(body)):
            with patch.object(api, "get_device_inventory") as mock_get_inv:
                ok, msg = api.get_inventory_filtered(1, ent_physical_class="chassis")
        assert ok is False
        assert msg is not None
        mock_get_inv.assert_not_called()

    def test_list_devices_none_devices(self):
        """list_devices: devices=None returns (False, ...)."""
        api = _make_api()
        with patch("requests.get", return_value=self._ok_resp({"status": "ok", "devices": None})):
            ok, msg = api.list_devices()
        assert ok is False
        assert msg is not None

    def test_list_devices_non_list_devices(self):
        """list_devices: devices as string returns (False, ...)."""
        api = _make_api()
        with patch("requests.get", return_value=self._ok_resp({"status": "ok", "devices": "bad"})):
            ok, msg = api.list_devices()
        assert ok is False

    def test_get_device_vlans_none_vlans(self):
        """get_device_vlans: vlans=None returns (False, ...)."""
        api = _make_api()
        with patch("requests.get", return_value=self._ok_resp({"status": "ok", "vlans": None})):
            ok, msg = api.get_device_vlans(1)
        assert ok is False
        assert msg is not None

    def test_get_device_vlans_skips_non_dict_items(self):
        """get_device_vlans: non-dict items in vlans list are skipped safely."""
        api = _make_api()
        vlans = [None, "bad", {"device_id": 1, "vlan_id": 10}]
        with patch("requests.get", return_value=self._ok_resp({"status": "ok", "vlans": vlans})):
            ok, data = api.get_device_vlans(1)
        assert ok is True
        assert len(data) == 1
        assert data[0]["vlan_id"] == 10

    def test_get_device_ips_none_addresses(self):
        """get_device_ips: addresses=None returns (False, ...)."""
        api = _make_api()
        with patch("requests.get", return_value=self._ok_resp({"addresses": None})):
            ok, _ = api.get_device_ips(1)
        assert ok is False
