"""Contract tests for F5 BIG-IP read-only tools."""

import json

import requests


# ---------------------------------------------------------------------------
# f5_list
# ---------------------------------------------------------------------------
class TestF5ListSuccess:
    """Contract tests for successful f5_list responses."""

    def test_returns_success_response_structure(self, mock_f5_credentials, mock_f5_requests):
        """Successful list returns JSON matching F5Response schema."""
        from mcp_network_jcd.tools.f5 import f5_list

        result = f5_list(host="192.168.1.245", object_type="pool", object_name="mypool")

        assert isinstance(result, dict)
        assert result["success"] is True
        assert result["host"] == "192.168.1.245"
        assert "data" in result
        assert "duration_ms" in result
        assert "timestamp" in result

    def test_list_all_objects_without_name(self, mock_f5_credentials, mock_f5_requests):
        """Listing all objects of a type works when object_name is empty."""
        from mcp_network_jcd.tools.f5 import f5_list

        result = f5_list(host="192.168.1.245", object_type="virtual")

        assert result["success"] is True

    def test_response_serializable_to_json(self, mock_f5_credentials, mock_f5_requests):
        """Response can be serialized to valid JSON."""
        from mcp_network_jcd.tools.f5 import f5_list

        result = f5_list(host="192.168.1.245", object_type="pool", object_name="mypool")

        json_str = json.dumps(result)
        parsed = json.loads(json_str)
        assert parsed["success"] is True

    def test_response_includes_timestamp(self, mock_f5_credentials, mock_f5_requests):
        """Response includes ISO 8601 timestamp."""
        from mcp_network_jcd.tools.f5 import f5_list

        result = f5_list(host="192.168.1.245", object_type="pool", object_name="mypool")

        assert "T" in result["timestamp"]
        assert result["timestamp"].endswith("Z")


class TestF5ListValidationError:
    """Contract tests for VALIDATION_ERROR on f5_list."""

    def test_invalid_object_type(self, mock_f5_credentials):
        """Invalid object_type returns VALIDATION_ERROR."""
        from mcp_network_jcd.tools.f5 import f5_list

        result = f5_list(host="192.168.1.245", object_type="node", object_name="mynode")

        assert result["success"] is False
        assert result["error"]["code"] == "VALIDATION_ERROR"
        assert "node" in result["error"]["message"]


class TestF5ListSubCollectionSuccess:
    """Contract tests for successful f5_list sub-collection responses."""

    def test_pool_members_returns_success(self, mock_f5_credentials, mock_f5_requests):
        """Querying pool members sub-collection returns success."""
        from mcp_network_jcd.tools.f5 import f5_list

        result = f5_list(
            host="192.168.1.245",
            object_type="pool",
            object_name="mypool",
            sub_collection="members",
        )

        assert result["success"] is True

    def test_virtual_policies_returns_success(self, mock_f5_credentials, mock_f5_requests):
        """Querying virtual policies sub-collection returns success."""
        from mcp_network_jcd.tools.f5 import f5_list

        result = f5_list(
            host="192.168.1.245",
            object_type="virtual",
            object_name="myvip",
            sub_collection="policies",
        )

        assert result["success"] is True


class TestF5ListSubCollectionValidationError:
    """Contract tests for VALIDATION_ERROR on f5_list sub-collections."""

    def test_subcollection_without_object_name(self, mock_f5_credentials):
        """sub_collection without object_name returns VALIDATION_ERROR."""
        from mcp_network_jcd.tools.f5 import f5_list

        result = f5_list(
            host="192.168.1.245",
            object_type="pool",
            sub_collection="members",
        )

        assert result["success"] is False
        assert result["error"]["code"] == "VALIDATION_ERROR"
        assert "object_name" in result["error"]["message"]

    def test_invalid_subcollection_for_type(self, mock_f5_credentials):
        """Invalid sub_collection for object_type returns VALIDATION_ERROR."""
        from mcp_network_jcd.tools.f5 import f5_list

        result = f5_list(
            host="192.168.1.245",
            object_type="virtual",
            object_name="myvip",
            sub_collection="members",
        )

        assert result["success"] is False
        assert result["error"]["code"] == "VALIDATION_ERROR"
        assert "members" in result["error"]["message"]

    def test_subcollection_on_unsupported_type(self, mock_f5_credentials):
        """sub_collection on type with no sub-collections returns VALIDATION_ERROR."""
        from mcp_network_jcd.tools.f5 import f5_list

        result = f5_list(
            host="192.168.1.245",
            object_type="irule",
            object_name="myrule",
            sub_collection="members",
        )

        assert result["success"] is False
        assert result["error"]["code"] == "VALIDATION_ERROR"
        assert "irule" in result["error"]["message"]


class TestF5ListConfigError:
    """Contract tests for CONFIG_ERROR on f5_list."""

    def test_missing_credentials(self, mocker):
        """Missing credentials returns CONFIG_ERROR."""
        mocker.patch.dict("os.environ", {}, clear=True)

        from mcp_network_jcd.tools.f5 import f5_list

        result = f5_list(host="192.168.1.245", object_type="pool", object_name="mypool")

        assert result["success"] is False
        assert result["error"]["code"] == "CONFIG_ERROR"
        assert "F5_USERNAME" in result["error"]["message"]


class TestF5ListAuthFailed:
    """Contract tests for AUTH_FAILED on f5_list."""

    def test_http_401_on_login_returns_auth_failed(self, mock_f5_credentials, mock_f5_requests_401):
        """HTTP 401 on login returns AUTH_FAILED."""
        from mcp_network_jcd.tools.f5 import f5_list

        result = f5_list(host="192.168.1.245", object_type="pool", object_name="mypool")

        assert result["success"] is False
        assert result["error"]["code"] == "AUTH_FAILED"


class TestF5ListConnectionTimeout:
    """Contract tests for CONNECTION_TIMEOUT on f5_list."""

    def test_timeout_on_login_returns_connection_timeout(self, mock_f5_credentials, mocker):
        """requests.Timeout on login returns CONNECTION_TIMEOUT."""
        mocker.patch(
            "mcp_network_jcd.tools.f5.requests.post",
            side_effect=requests.exceptions.Timeout("timed out"),
        )

        from mcp_network_jcd.tools.f5 import f5_list

        result = f5_list(host="192.168.1.245", object_type="pool", object_name="mypool")

        assert result["success"] is False
        assert result["error"]["code"] == "CONNECTION_TIMEOUT"


class TestF5ListConnectionRefused:
    """Contract tests for CONNECTION_REFUSED on f5_list."""

    def test_connection_error_on_login_returns_refused(self, mock_f5_credentials, mocker):
        """requests.ConnectionError on login returns CONNECTION_REFUSED."""
        mocker.patch(
            "mcp_network_jcd.tools.f5.requests.post",
            side_effect=requests.exceptions.ConnectionError("refused"),
        )

        from mcp_network_jcd.tools.f5 import f5_list

        result = f5_list(host="192.168.1.245", object_type="pool", object_name="mypool")

        assert result["success"] is False
        assert result["error"]["code"] == "CONNECTION_REFUSED"


class TestF5ListHttpError:
    """Contract tests for HTTP_ERROR on f5_list."""

    def test_http_404_returns_http_error(self, mock_f5_credentials, mock_f5_requests_404):
        """HTTP 404 on data request returns HTTP_ERROR."""
        from mcp_network_jcd.tools.f5 import f5_list

        result = f5_list(host="192.168.1.245", object_type="pool", object_name="nonexistent")

        assert result["success"] is False
        assert result["error"]["code"] == "HTTP_ERROR"
        assert "404" in result["error"]["message"]


# ---------------------------------------------------------------------------
# f5_stats
# ---------------------------------------------------------------------------
class TestF5StatsSuccess:
    """Contract tests for successful f5_stats responses."""

    def test_returns_success_response_structure(self, mock_f5_credentials, mock_f5_requests):
        """Successful stats returns JSON matching F5Response schema."""
        from mcp_network_jcd.tools.f5 import f5_stats

        result = f5_stats(host="192.168.1.245", object_type="virtual", object_name="myvip")

        assert result["success"] is True
        assert result["host"] == "192.168.1.245"
        assert "data" in result
        assert "duration_ms" in result


class TestF5StatsValidationError:
    """Contract tests for VALIDATION_ERROR on f5_stats."""

    def test_invalid_object_type(self, mock_f5_credentials):
        """Invalid object_type returns VALIDATION_ERROR."""
        from mcp_network_jcd.tools.f5 import f5_stats

        result = f5_stats(host="192.168.1.245", object_type="monitor", object_name="http")

        assert result["success"] is False
        assert result["error"]["code"] == "VALIDATION_ERROR"


class TestF5StatsConfigError:
    """Contract tests for CONFIG_ERROR on f5_stats."""

    def test_missing_credentials(self, mocker):
        """Missing credentials returns CONFIG_ERROR."""
        mocker.patch.dict("os.environ", {}, clear=True)

        from mcp_network_jcd.tools.f5 import f5_stats

        result = f5_stats(host="192.168.1.245", object_type="pool", object_name="mypool")

        assert result["success"] is False
        assert result["error"]["code"] == "CONFIG_ERROR"


class TestF5StatsAuthFailed:
    """Contract tests for AUTH_FAILED on f5_stats."""

    def test_http_401_on_login_returns_auth_failed(
        self, mock_f5_credentials, mock_f5_requests_401
    ):
        """HTTP 401 on login returns AUTH_FAILED."""
        from mcp_network_jcd.tools.f5 import f5_stats

        result = f5_stats(host="192.168.1.245", object_type="pool", object_name="mypool")

        assert result["success"] is False
        assert result["error"]["code"] == "AUTH_FAILED"


# ---------------------------------------------------------------------------
# f5_list name_filter
# ---------------------------------------------------------------------------
class TestF5ListNameFilter:
    """Contract tests for f5_list name_filter parameter."""

    def test_filter_by_name_pattern(self, mock_f5_credentials, mock_f5_requests_with_items):
        """Filtering by name pattern returns only matching items."""
        from mcp_network_jcd.tools.f5 import f5_list

        result = f5_list(host="192.168.1.245", object_type="virtual", name_filter="THOR")

        assert result["success"] is True
        items = result["data"]["items"]
        assert len(items) == 2
        assert all("THOR" in item["name"] for item in items)

    def test_filter_by_destination_ip(self, mock_f5_credentials, mock_f5_requests_with_items):
        """Filtering by destination IP returns matching VIPs."""
        from mcp_network_jcd.tools.f5 import f5_list

        result = f5_list(host="192.168.1.245", object_type="virtual", name_filter="10.231.25.247")

        assert result["success"] is True
        items = result["data"]["items"]
        assert len(items) == 2
        assert all("10.231.25.247" in item["destination"] for item in items)

    def test_filter_case_insensitive(self, mock_f5_credentials, mock_f5_requests_with_items):
        """Filtering is case-insensitive."""
        from mcp_network_jcd.tools.f5 import f5_list

        result = f5_list(host="192.168.1.245", object_type="virtual", name_filter="thor")

        assert result["success"] is True
        items = result["data"]["items"]
        assert len(items) == 2

    def test_filter_no_match_returns_empty_items(
        self, mock_f5_credentials, mock_f5_requests_with_items
    ):
        """Filter with no matches returns empty items list, still success."""
        from mcp_network_jcd.tools.f5 import f5_list

        result = f5_list(host="192.168.1.245", object_type="virtual", name_filter="NONEXISTENT")

        assert result["success"] is True
        assert result["data"]["items"] == []

    def test_no_filter_returns_all_items(self, mock_f5_credentials, mock_f5_requests_with_items):
        """Empty filter returns all items unchanged."""
        from mcp_network_jcd.tools.f5 import f5_list

        result = f5_list(host="192.168.1.245", object_type="virtual")

        assert result["success"] is True
        assert len(result["data"]["items"]) == 3


# ---------------------------------------------------------------------------
# f5_list / f5_stats object_name encoding (fullPath → URL-safe)
# ---------------------------------------------------------------------------
class TestF5ObjectNameEncoding:
    """Contract tests for automatic /Common/ → ~Common~ encoding in object_name."""

    def test_f5_list_encodes_fullpath_slashes_to_tildes(
        self, mock_f5_credentials, mock_f5_requests
    ):
        """f5_list converts /Common/name to ~Common~name in URL."""
        from mcp_network_jcd.tools.f5 import f5_list

        f5_list(host="192.168.1.245", object_type="virtual", object_name="/Common/GIT-HTTPS_PRD")

        mock_f5_requests.assert_called_once()
        url = mock_f5_requests.call_args[1].get("url", mock_f5_requests.call_args[0][1])
        assert "~Common~GIT-HTTPS_PRD" in url
        assert "/Common/GIT-HTTPS_PRD" not in url

    def test_f5_list_subcollection_encodes_fullpath(
        self, mock_f5_credentials, mock_f5_requests
    ):
        """f5_list with sub_collection correctly encodes object_name in URL."""
        from mcp_network_jcd.tools.f5 import f5_list

        f5_list(
            host="192.168.1.245",
            object_type="pool",
            object_name="/Common/POOL_EXAMPLE",
            sub_collection="members",
        )

        mock_f5_requests.assert_called_once()
        url = mock_f5_requests.call_args[1].get("url", mock_f5_requests.call_args[0][1])
        assert "~Common~POOL_EXAMPLE" in url
        assert "/members" in url

    def test_f5_stats_encodes_fullpath_slashes_to_tildes(
        self, mock_f5_credentials, mock_f5_requests
    ):
        """f5_stats converts /Common/name to ~Common~name in URL."""
        from mcp_network_jcd.tools.f5 import f5_stats

        f5_stats(
            host="192.168.1.245",
            object_type="virtual",
            object_name="/Common/THOR-PRD-443_PLA",
        )

        mock_f5_requests.assert_called_once()
        url = mock_f5_requests.call_args[1].get("url", mock_f5_requests.call_args[0][1])
        assert "~Common~THOR-PRD-443_PLA" in url
        assert "/Common/THOR-PRD-443_PLA" not in url

    def test_plain_name_unchanged(self, mock_f5_credentials, mock_f5_requests):
        """Plain object names (no partition path) are left unchanged."""
        from mcp_network_jcd.tools.f5 import f5_list

        f5_list(host="192.168.1.245", object_type="pool", object_name="mypool")

        mock_f5_requests.assert_called_once()
        url = mock_f5_requests.call_args[1].get("url", mock_f5_requests.call_args[0][1])
        assert url.endswith("/mypool")

    def test_already_encoded_name_unchanged(self, mock_f5_credentials, mock_f5_requests):
        """Already tilde-encoded names are not double-encoded."""
        from mcp_network_jcd.tools.f5 import f5_list

        f5_list(host="192.168.1.245", object_type="virtual", object_name="~Common~GIT-HTTPS_PRD")

        mock_f5_requests.assert_called_once()
        url = mock_f5_requests.call_args[1].get("url", mock_f5_requests.call_args[0][1])
        assert "~Common~GIT-HTTPS_PRD" in url
