# #region Test.AsyncNetwork [C:4] [TYPE Module] [SEMANTICS test, async, network, client, httpx]
# @BRIEF Tests for core/utils/async_network.py — AsyncAPIClient.
# @RELATION BINDS_TO -> [AsyncNetworkModule]

from pathlib import Path
import sys

sys.path.insert(0, str(Path(__file__).parent.parent.parent / "src"))

import asyncio
import json
from unittest.mock import AsyncMock, MagicMock, PropertyMock, patch

import httpx
import pytest


@pytest.fixture(autouse=True)
def clear_auth_cache():
    """Clear SupersetAuthCache between tests."""
    from src.core.utils.network import SupersetAuthCache

    SupersetAuthCache._entries.clear()


class TestAsyncAPIClientInit:
    """AsyncAPIClient.__init__: config parsing, ssl handling."""

    def test_init_with_defaults(self):
        from src.core.utils.async_network import AsyncAPIClient

        client = AsyncAPIClient({"base_url": "https://superset.example.com", "auth": {"username": "admin", "password": "pass"}})
        assert client.base_url == "https://superset.example.com"
        assert client.api_base_url == "https://superset.example.com/api/v1"
        assert client._authenticated is False
        assert client._semaphore is None

    def test_init_ssl_true_creates_default_context(self):
        import ssl
        from src.core.utils.async_network import AsyncAPIClient

        real_ctx = ssl.create_default_context()
        with patch("ssl.create_default_context", return_value=real_ctx) as mock_ctx:
            client = AsyncAPIClient({"base_url": "https://test.com", "auth": {}}, verify_ssl=True)
            mock_ctx.assert_called_once()
            assert client.base_url == "https://test.com"

    def test_init_ssl_false(self):
        from src.core.utils.async_network import AsyncAPIClient

        client = AsyncAPIClient({"base_url": "https://test.com", "auth": {}}, verify_ssl=False)
        assert client.base_url == "https://test.com"
        assert client._client is not None

    def test_init_with_semaphore(self):
        from src.core.utils.async_network import AsyncAPIClient

        sem = asyncio.Semaphore(5)
        client = AsyncAPIClient({"base_url": "https://test.com", "auth": {}}, semaphore=sem)
        assert client._semaphore is sem

    def test_normalize_base_url_removes_api_v1_suffix(self):
        from src.core.utils.async_network import AsyncAPIClient

        client = AsyncAPIClient({"base_url": "https://test.com/api/v1", "auth": {}})
        assert client.base_url == "https://test.com"

    def test_normalize_base_url_strips_trailing_slash(self):
        from src.core.utils.async_network import AsyncAPIClient

        client = AsyncAPIClient({"base_url": "https://test.com/", "auth": {}})
        assert client.base_url == "https://test.com"

    def test_init_with_custom_timeout(self):
        from src.core.utils.async_network import AsyncAPIClient

        client = AsyncAPIClient({"base_url": "https://test.com", "auth": {}}, timeout=60)
        assert client.request_settings["timeout"] == 60

    def test_init_with_ssl_context(self):
        import ssl

        ctx = ssl.create_default_context()
        from src.core.utils.async_network import AsyncAPIClient

        client = AsyncAPIClient({"base_url": "https://test.com", "auth": {}}, verify_ssl=ctx)
        # The client should be initialized with the custom SSL context
        assert client.request_settings["verify_ssl"] is ctx


class TestBuildApiUrl:
    """_build_api_url: endpoint -> full URL."""

    @pytest.fixture
    def client(self):
        from src.core.utils.async_network import AsyncAPIClient

        return AsyncAPIClient({"base_url": "https://superset.example.com", "auth": {}})

    def test_relative_endpoint_adds_api_v1_prefix(self, client):
        url = client._build_api_url("/chart/")
        assert url == "https://superset.example.com/api/v1/chart/"

    def test_relative_endpoint_without_leading_slash(self, client):
        url = client._build_api_url("chart/")
        assert url == "https://superset.example.com/api/v1/chart/"

    def test_absolute_url_passthrough(self, client):
        url = client._build_api_url("https://other.com/api/v1/chart/")
        assert url == "https://other.com/api/v1/chart/"

    def test_already_has_api_v1(self, client):
        url = client._build_api_url("/api/v1/chart/")
        assert url == "https://superset.example.com/api/v1/chart/"

    def test_api_v1_root(self, client):
        url = client._build_api_url("/api/v1")
        assert url == "https://superset.example.com/api/v1"

    def test_empty_endpoint_returns_base(self, client):
        url = client._build_api_url("")
        assert url == "https://superset.example.com/api/v1/"


class TestAuthLock:
    """_get_auth_lock: class-level async lock storage."""

    def test_returns_same_lock_for_same_key(self):
        from src.core.utils.async_network import AsyncAPIClient

        lock1 = AsyncAPIClient._get_auth_lock(("url", "user", True))
        lock2 = AsyncAPIClient._get_auth_lock(("url", "user", True))
        assert lock1 is lock2

    def test_different_keys_return_different_locks(self):
        from src.core.utils.async_network import AsyncAPIClient

        lock1 = AsyncAPIClient._get_auth_lock(("url1", "user", True))
        lock2 = AsyncAPIClient._get_auth_lock(("url2", "user", True))
        assert lock1 is not lock2


class TestAuthenticate:
    """authenticate(): cache hit, cache miss, error paths."""

    @pytest.fixture
    def client(self):
        from src.core.utils.async_network import AsyncAPIClient

        c = AsyncAPIClient({"base_url": "https://superset.example.com", "auth": {"username": "admin", "password": "pass"}})
        c._client = AsyncMock()
        return c

    @pytest.mark.asyncio
    async def test_cache_hit_skips_login(self, client):
        from src.core.utils.network import SupersetAuthCache

        SupersetAuthCache.set(
            client._auth_cache_key,
            {"access_token": "cached_access", "csrf_token": "cached_csrf"},
        )
        # Mock CSRF refresh
        mock_csrf_resp = MagicMock()
        mock_csrf_resp.is_success = True
        mock_csrf_resp.json.return_value = {"result": "new_csrf"}
        client._client.get = AsyncMock(return_value=mock_csrf_resp)

        tokens = await client.authenticate()
        assert tokens["access_token"] == "cached_access"
        assert client._authenticated is True
        client._client.post.assert_not_called()

    @pytest.mark.asyncio
    async def test_cache_miss_fresh_login(self, client):
        mock_login_resp = MagicMock()
        mock_login_resp.status_code = 200
        mock_login_resp.json.return_value = {"access_token": "new_access"}
        mock_login_resp.raise_for_status = MagicMock()

        mock_csrf_resp = MagicMock()
        mock_csrf_resp.status_code = 200
        mock_csrf_resp.json.return_value = {"result": "new_csrf"}
        mock_csrf_resp.raise_for_status = MagicMock()

        client._client.post = AsyncMock(return_value=mock_login_resp)
        client._client.get = AsyncMock(return_value=mock_csrf_resp)

        tokens = await client.authenticate()
        assert tokens["access_token"] == "new_access"
        assert tokens["csrf_token"] == "new_csrf"
        assert client._authenticated is True

    @pytest.mark.asyncio
    async def test_login_http_502_raises_network_error(self, client):
        from src.core.utils.network import NetworkError

        mock_resp = MagicMock()
        mock_resp.status_code = 502
        exc = httpx.HTTPStatusError("Bad Gateway", request=MagicMock(), response=mock_resp)
        client._client.post = AsyncMock(side_effect=exc)

        with pytest.raises(NetworkError, match="Environment unavailable"):
            await client.authenticate()

    @pytest.mark.asyncio
    async def test_login_http_503_raises_network_error(self, client):
        from src.core.utils.network import NetworkError

        mock_resp = MagicMock()
        mock_resp.status_code = 503
        exc = httpx.HTTPStatusError("Service Unavailable", request=MagicMock(), response=mock_resp)
        client._client.post = AsyncMock(side_effect=exc)

        with pytest.raises(NetworkError, match="Environment unavailable"):
            await client.authenticate()

    @pytest.mark.asyncio
    async def test_login_http_401_raises_authentication_error(self, client):
        from src.core.utils.network import AuthenticationError

        mock_resp = MagicMock()
        mock_resp.status_code = 401
        exc = httpx.HTTPStatusError("Unauthorized", request=MagicMock(), response=mock_resp)
        client._client.post = AsyncMock(side_effect=exc)

        with pytest.raises(AuthenticationError):
            await client.authenticate()

    @pytest.mark.asyncio
    async def test_login_httpx_error_raises_network_error(self, client):
        from src.core.utils.network import NetworkError

        client._client.post = AsyncMock(side_effect=httpx.ConnectError("Connection refused"))

        with pytest.raises(NetworkError, match="Network or parsing error"):
            await client.authenticate()

    @pytest.mark.asyncio
    async def test_cache_refresh_csrf_failure_does_not_block(self, client):
        from src.core.utils.network import SupersetAuthCache

        SupersetAuthCache.set(
            client._auth_cache_key,
            {"access_token": "cached_access", "csrf_token": "cached_csrf"},
        )
        # CSRF refresh fails silently
        client._client.get = AsyncMock(side_effect=Exception("Connection lost"))

        tokens = await client.authenticate()
        # Should still succeed with cached tokens
        assert tokens["access_token"] == "cached_access"

    @pytest.mark.asyncio
    async def test_cache_hit_after_wait(self, client):
        """Second caller after lock should find cache populated."""
        from src.core.utils.network import SupersetAuthCache

        # Simulate first caller putting tokens in cache
        SupersetAuthCache.set(
            client._auth_cache_key,
            {"access_token": "cached_after", "csrf_token": "cached_csrf"},
        )
        mock_csrf_resp = MagicMock()
        mock_csrf_resp.is_success = False
        client._client.get = AsyncMock(return_value=mock_csrf_resp)

        tokens = await client.authenticate()  # Goes through lock path
        assert tokens["access_token"] == "cached_after"

    @pytest.mark.asyncio
    async def test_login_missing_access_token_key(self, client):
        mock_login_resp = MagicMock()
        mock_login_resp.status_code = 200
        mock_login_resp.json.return_value = {"not_access_token": "value"}
        mock_login_resp.raise_for_status = MagicMock()
        client._client.post = AsyncMock(return_value=mock_login_resp)

        from src.core.utils.network import NetworkError

        with pytest.raises(NetworkError):
            await client.authenticate()


class TestGetHeaders:
    """get_headers(): returns auth headers, may trigger auth."""

    @pytest.mark.asyncio
    async def test_unauthenticated_calls_authenticate(self):
        from src.core.utils.async_network import AsyncAPIClient

        client = AsyncAPIClient({"base_url": "https://test.com", "auth": {}})
        # Simulate what authenticate sets
        async def fake_auth():
            client._authenticated = True
            client._tokens = {"access_token": "acc", "csrf_token": "csrf"}
            return client._tokens
        client.authenticate = fake_auth
        client._authenticated = False

        headers = await client.get_headers()
        assert "Authorization" in headers
        assert headers["Authorization"] == "Bearer acc"
        assert headers["X-CSRFToken"] == "csrf"

    @pytest.mark.asyncio
    async def test_authenticated_returns_headers(self):
        from src.core.utils.async_network import AsyncAPIClient

        client = AsyncAPIClient({"base_url": "https://test.com", "auth": {}})
        client._authenticated = True
        client._tokens = {"access_token": "acc", "csrf_token": "csrf"}
        client.authenticate = AsyncMock()

        headers = await client.get_headers()
        client.authenticate.assert_not_called()
        assert headers["Referer"] == "https://test.com"


class TestRequest:
    """request(): main HTTP request method."""

    @pytest.fixture
    def client(self):
        from src.core.utils.async_network import AsyncAPIClient

        c = AsyncAPIClient({"base_url": "https://test.com", "auth": {}})
        c._client = AsyncMock()
        c._authenticated = True
        c._tokens = {"access_token": "acc", "csrf_token": "csrf"}
        return c

    @pytest.mark.asyncio
    async def test_get_request(self, client):
        mock_resp = MagicMock()
        mock_resp.status_code = 200
        mock_resp.json.return_value = {"result": "ok"}
        client._client.request = AsyncMock(return_value=mock_resp)

        result = await client.request(method="GET", endpoint="/chart/")
        assert result == {"result": "ok"}

    @pytest.mark.asyncio
    async def test_post_with_dict_data(self, client):
        mock_resp = MagicMock()
        mock_resp.status_code = 200
        mock_resp.json.return_value = {"id": 1}
        client._client.request = AsyncMock(return_value=mock_resp)

        result = await client.request(method="POST", endpoint="/chart/", data={"key": "val"})
        assert result == {"id": 1}
        call_kwargs = client._client.request.call_args[1]
        assert call_kwargs["json"] == {"key": "val"}

    @pytest.mark.asyncio
    async def test_post_with_pre_serialized_string(self, client):
        mock_resp = MagicMock()
        mock_resp.status_code = 200
        mock_resp.json.return_value = {"id": 1}
        client._client.request = AsyncMock(return_value=mock_resp)

        result = await client.request(method="POST", endpoint="/chart/", data='{"key":"val"}')
        assert result == {"id": 1}
        call_kwargs = client._client.request.call_args[1]
        assert call_kwargs["content"] == b'{"key":"val"}'
        assert "json" not in call_kwargs

    @pytest.mark.asyncio
    async def test_raw_response(self, client):
        mock_resp = MagicMock()
        mock_resp.status_code = 200
        client._client.request = AsyncMock(return_value=mock_resp)

        result = await client.request(method="GET", endpoint="/chart/1", raw_response=True)
        assert result is mock_resp

    @pytest.mark.asyncio
    async def test_401_retry_for_get(self, client):
        # Pre-authenticate so 401 retry works without hitting login flow.
        # We need authenticate() to succeed quickly when called in the retry path.
        client._authenticated = True
        client._tokens = {"access_token": "acc", "csrf_token": "csrf"}

        # Cache tokens so SupersetAuthCache.get returns something
        from src.core.utils.network import SupersetAuthCache
        SupersetAuthCache.set(
            client._auth_cache_key,
            {"access_token": "retry_token", "csrf_token": "retry_csrf"},
        )
        # Un-authenticate so get_headers() calls authenticate() (which will hit cache)
        client._authenticated = False

        first_resp = MagicMock()
        first_resp.status_code = 401
        second_resp = MagicMock()
        second_resp.status_code = 200
        second_resp.json.return_value = {"result": "retried"}
        client._client.request = AsyncMock(side_effect=[first_resp, second_resp])
        # Mock CSRF refresh during auth
        mock_csrf_resp = MagicMock()
        mock_csrf_resp.is_success = True
        mock_csrf_resp.json.return_value = {"result": "csrf_new"}
        client._client.get = AsyncMock(return_value=mock_csrf_resp)
        # Mock login POST for the retry path — after cache invalidation,
        # authenticate() falls through to the login endpoint (line 182).
        # Without this mock, await self._client.post(...) returns a raw AsyncMock
        # which causes response.json() to return a coroutine instead of a dict.
        mock_login_resp = MagicMock()
        mock_login_resp.status_code = 200
        mock_login_resp.json.return_value = {"access_token": "new_token"}
        mock_login_resp.raise_for_status = MagicMock()
        client._client.post = AsyncMock(return_value=mock_login_resp)

        result = await client.request(method="GET", endpoint="/chart/")
        assert result == {"result": "retried"}
        assert client._client.request.call_count == 2

    @pytest.mark.asyncio
    async def test_401_raises_for_non_get(self, client):
        from src.core.utils.network import AuthenticationError

        mock_resp = MagicMock()
        mock_resp.status_code = 401
        client._client.request = AsyncMock(return_value=mock_resp)

        with pytest.raises(AuthenticationError):
            await client.request(method="POST", endpoint="/chart/")

    @pytest.mark.asyncio
    async def test_json_parse_error(self, client):
        from src.core.utils.network import SupersetAPIError

        mock_resp = MagicMock()
        mock_resp.status_code = 200
        mock_resp.json.side_effect = ValueError("No JSON")
        mock_resp.text = "not json"
        client._client.request = AsyncMock(return_value=mock_resp)

        with pytest.raises(SupersetAPIError, match="Failed to parse response JSON"):
            await client.request(method="GET", endpoint="/chart/")

    @pytest.mark.asyncio
    async def test_semaphore_acquire_release(self, client):
        sem = asyncio.Semaphore(1)
        client._semaphore = sem
        mock_resp = MagicMock()
        mock_resp.status_code = 200
        mock_resp.json.return_value = {"ok": True}
        client._client.request = AsyncMock(return_value=mock_resp)

        result = await client.request(method="GET", endpoint="/test")
        assert result == {"ok": True}
        # Semaphore released implies no deadlock

    @pytest.mark.asyncio
    async def test_http_status_error_404_dashboard(self, client):
        from src.core.utils.network import DashboardNotFoundError

        mock_resp = MagicMock()
        mock_resp.status_code = 404
        exc = httpx.HTTPStatusError("Not Found", request=MagicMock(), response=mock_resp)
        client._client.request = AsyncMock(side_effect=exc)
        client._is_dashboard_endpoint = MagicMock(return_value=True)

        with pytest.raises(DashboardNotFoundError):
            await client.request(method="GET", endpoint="/dashboard/42")

    @pytest.mark.asyncio
    async def test_http_status_error_404_generic(self, client):
        from src.core.utils.network import SupersetAPIError

        mock_resp = MagicMock()
        mock_resp.status_code = 404
        exc = httpx.HTTPStatusError("Not Found", request=MagicMock(), response=mock_resp)
        client._client.request = AsyncMock(side_effect=exc)
        client._is_dashboard_endpoint = MagicMock(return_value=False)

        with pytest.raises(SupersetAPIError, match="API resource not found"):
            await client.request(method="GET", endpoint="/chart/99")

    @pytest.mark.asyncio
    async def test_http_status_error_403(self, client):
        from src.core.utils.network import PermissionDeniedError

        mock_resp = MagicMock()
        mock_resp.status_code = 403
        exc = httpx.HTTPStatusError("Forbidden", request=MagicMock(), response=mock_resp)
        client._client.request = AsyncMock(side_effect=exc)

        with pytest.raises(PermissionDeniedError):
            await client.request(method="GET", endpoint="/chart/")

    @pytest.mark.asyncio
    async def test_http_status_error_502(self, client):
        from src.core.utils.network import NetworkError

        mock_resp = MagicMock()
        mock_resp.status_code = 502
        exc = httpx.HTTPStatusError("Bad Gateway", request=MagicMock(), response=mock_resp)
        client._client.request = AsyncMock(side_effect=exc)

        with pytest.raises(NetworkError, match="Environment unavailable"):
            await client.request(method="GET", endpoint="/chart/")

    @pytest.mark.asyncio
    async def test_http_status_error_generic(self, client):
        from src.core.utils.network import SupersetAPIError

        mock_resp = MagicMock()
        mock_resp.status_code = 500
        mock_resp.text = "Internal error"
        exc = httpx.HTTPStatusError("Server Error", request=MagicMock(), response=mock_resp)
        client._client.request = AsyncMock(side_effect=exc)

        with pytest.raises(SupersetAPIError, match="API Error 500"):
            await client.request(method="GET", endpoint="/chart/")

    @pytest.mark.asyncio
    async def test_httpx_timeout_error(self, client):
        from src.core.utils.network import NetworkError

        client._client.request = AsyncMock(side_effect=httpx.TimeoutException("Timed out"))

        with pytest.raises(NetworkError, match="Request timeout"):
            await client.request(method="GET", endpoint="/chart/")

    @pytest.mark.asyncio
    async def test_httpx_connect_error(self, client):
        from src.core.utils.network import NetworkError

        client._client.request = AsyncMock(side_effect=httpx.ConnectError("Connection refused"))

        with pytest.raises(NetworkError, match="Connection error"):
            await client.request(method="GET", endpoint="/chart/")

    @pytest.mark.asyncio
    async def test_httpx_generic_error(self, client):
        from src.core.utils.network import NetworkError

        client._client.request = AsyncMock(side_effect=httpx.HTTPError("Generic error"))

        with pytest.raises(NetworkError, match="Unknown network error"):
            await client.request(method="GET", endpoint="/chart/")


class TestHandleHttpError:
    """_handle_http_error: translates HTTP status codes to exceptions."""

    @pytest.fixture
    def client(self):
        from src.core.utils.async_network import AsyncAPIClient

        return AsyncAPIClient({"base_url": "https://test.com", "auth": {}})

    def test_502_raises_network_error(self, client):
        from src.core.utils.network import NetworkError

        mock_resp = MagicMock()
        mock_resp.status_code = 502
        exc = httpx.HTTPStatusError("", request=MagicMock(), response=mock_resp)
        with pytest.raises(NetworkError, match="Environment unavailable"):
            client._handle_http_error(exc, "/chart/")

    def test_503_raises_network_error(self, client):
        from src.core.utils.network import NetworkError

        mock_resp = MagicMock()
        mock_resp.status_code = 503
        exc = httpx.HTTPStatusError("", request=MagicMock(), response=mock_resp)
        with pytest.raises(NetworkError, match="Environment unavailable"):
            client._handle_http_error(exc, "/chart/")

    def test_404_dashboard(self, client):
        from src.core.utils.network import DashboardNotFoundError

        mock_resp = MagicMock()
        mock_resp.status_code = 404
        exc = httpx.HTTPStatusError("", request=MagicMock(), response=mock_resp)
        with patch.object(client, '_is_dashboard_endpoint', return_value=True):
            with pytest.raises(DashboardNotFoundError):
                client._handle_http_error(exc, "/dashboard/5")

    def test_404_generic(self, client):
        from src.core.utils.network import SupersetAPIError

        mock_resp = MagicMock()
        mock_resp.status_code = 404
        exc = httpx.HTTPStatusError("", request=MagicMock(), response=mock_resp)
        with patch.object(client, '_is_dashboard_endpoint', return_value=False):
            with pytest.raises(SupersetAPIError, match="API resource not found"):
                client._handle_http_error(exc, "/chart/5")

    def test_403_raises_permission_denied(self, client):
        from src.core.utils.network import PermissionDeniedError

        mock_resp = MagicMock()
        mock_resp.status_code = 403
        exc = httpx.HTTPStatusError("", request=MagicMock(), response=mock_resp)
        with pytest.raises(PermissionDeniedError):
            client._handle_http_error(exc, "/chart/")

    def test_401_raises_authentication_error(self, client):
        from src.core.utils.network import AuthenticationError

        mock_resp = MagicMock()
        mock_resp.status_code = 401
        exc = httpx.HTTPStatusError("", request=MagicMock(), response=mock_resp)
        with pytest.raises(AuthenticationError):
            client._handle_http_error(exc, "/chart/")

    def test_generic_500(self, client):
        from src.core.utils.network import SupersetAPIError

        mock_resp = MagicMock()
        mock_resp.status_code = 500
        mock_resp.text = "Server broke"
        exc = httpx.HTTPStatusError("", request=MagicMock(), response=mock_resp)
        with pytest.raises(SupersetAPIError, match="API Error 500"):
            client._handle_http_error(exc, "/chart/")


class TestIsDashboardEndpoint:
    """_is_dashboard_endpoint: recognizes dashboard endpoints."""

    @pytest.fixture
    def client(self):
        from src.core.utils.async_network import AsyncAPIClient

        return AsyncAPIClient({"base_url": "https://test.com", "auth": {}})

    def test_dashboard_endpoint(self, client):
        assert client._is_dashboard_endpoint("/dashboard/42") is True

    def test_dashboard_list(self, client):
        assert client._is_dashboard_endpoint("/dashboard") is True

    def test_chart_endpoint(self, client):
        assert client._is_dashboard_endpoint("/chart/42") is False

    def test_absolute_url_dashboard(self, client):
        assert client._is_dashboard_endpoint("https://superset.example.com/api/v1/dashboard/42") is True

    def test_absolute_url_chart(self, client):
        assert client._is_dashboard_endpoint("https://superset.example.com/api/v1/chart/42") is False

    def test_empty_endpoint(self, client):
        assert client._is_dashboard_endpoint("") is False

    def test_none_endpoint(self, client):
        assert client._is_dashboard_endpoint(None) is False

    def test_dashboard_with_api_v1_prefix(self, client):
        assert client._is_dashboard_endpoint("/api/v1/dashboard/1") is True

    def test_absolute_url_bad_format(self, client):
        # URL with /api/v1 not present
        assert client._is_dashboard_endpoint("https://other.com/not-api/dashboard/1") is False


class TestFetchPaginatedCount:
    """fetch_paginated_count: gets total count."""

    @pytest.mark.asyncio
    async def test_success(self):
        from src.core.utils.async_network import AsyncAPIClient

        client = AsyncAPIClient({"base_url": "https://test.com", "auth": {}})
        client.request = AsyncMock(return_value={"count": 42})

        result = await client.fetch_paginated_count("/chart/", {"page": 0, "page_size": 1})
        assert result == 42

    @pytest.mark.asyncio
    async def test_non_dict_response(self):
        from src.core.utils.async_network import AsyncAPIClient

        client = AsyncAPIClient({"base_url": "https://test.com", "auth": {}})
        client.request = AsyncMock(return_value="not_a_dict")

        result = await client.fetch_paginated_count("/chart/", {})
        assert result == 0

    @pytest.mark.asyncio
    async def test_missing_count_field(self):
        from src.core.utils.async_network import AsyncAPIClient

        client = AsyncAPIClient({"base_url": "https://test.com", "auth": {}})
        client.request = AsyncMock(return_value={"other": 10})

        result = await client.fetch_paginated_count("/chart/", {})
        assert result == 0


class TestFetchPaginatedData:
    """fetch_paginated_data: page iteration."""

    @pytest.mark.asyncio
    async def test_single_page(self):
        from src.core.utils.async_network import AsyncAPIClient

        client = AsyncAPIClient({"base_url": "https://test.com", "auth": {}})
        client.request = AsyncMock(
            return_value={"result": [{"id": 1}, {"id": 2}], "count": 2}
        )

        result = await client.fetch_paginated_data(
            "/chart/",
            {"base_query": {"page_size": 100}, "results_field": "result"},
        )
        assert len(result) == 2

    @pytest.mark.asyncio
    async def test_multiple_pages(self):
        from src.core.utils.async_network import AsyncAPIClient

        client = AsyncAPIClient({"base_url": "https://test.com", "auth": {}})
        client.request = AsyncMock(
            side_effect=[
                {"result": [{"id": 1}, {"id": 2}], "count": 5},
                {"result": [{"id": 3}, {"id": 4}]},
                {"result": [{"id": 5}]},
            ]
        )

        result = await client.fetch_paginated_data(
            "/chart/",
            {"base_query": {"page_size": 2}, "results_field": "result"},
        )
        assert len(result) == 5
        assert client.request.call_count == 3

    @pytest.mark.asyncio
    async def test_total_count_from_options(self):
        from src.core.utils.async_network import AsyncAPIClient

        client = AsyncAPIClient({"base_url": "https://test.com", "auth": {}})
        client.request = AsyncMock(
            side_effect=[
                {"result": [{"id": 1}], "count": 1},
                {"result": [{"id": 2}]},
            ]
        )

        result = await client.fetch_paginated_data(
            "/chart/",
            {"base_query": {"page_size": 1}, "results_field": "result", "total_count": 2},
        )
        assert len(result) == 2
        assert client.request.call_count == 2

    @pytest.mark.asyncio
    async def test_non_dict_first_page_returns_empty(self):
        from src.core.utils.async_network import AsyncAPIClient

        client = AsyncAPIClient({"base_url": "https://test.com", "auth": {}})
        client.request = AsyncMock(return_value="not_a_dict")

        result = await client.fetch_paginated_data(
            "/chart/",
            {"base_query": {"page_size": 100}, "results_field": "result"},
        )
        assert result == []

    @pytest.mark.asyncio
    async def test_non_dict_subsequent_page_skipped(self):
        from src.core.utils.async_network import AsyncAPIClient

        client = AsyncAPIClient({"base_url": "https://test.com", "auth": {}})
        client.request = AsyncMock(
            side_effect=[
                {"result": [{"id": 1}], "count": 3},
                "not_a_dict",
                {"result": [{"id": 3}]},
            ]
        )

        result = await client.fetch_paginated_data(
            "/chart/",
            {"base_query": {"page_size": 1}, "results_field": "result"},
        )
        # First page has 1, second page is skipped, third page has 1
        assert len(result) == 2


class TestUploadFile:
    """upload_file: multipart file upload."""

    @pytest.mark.asyncio
    async def test_upload_with_path_string(self):
        from src.core.utils.async_network import AsyncAPIClient

        client = AsyncAPIClient({"base_url": "https://test.com", "auth": {}})
        client._authenticated = True
        client._tokens = {"access_token": "acc", "csrf_token": "csrf"}
        client._client = AsyncMock()

        mock_resp = MagicMock()
        mock_resp.status_code = 200
        mock_resp.json.return_value = {"result": "imported"}
        client._client.post = AsyncMock(return_value=mock_resp)

        with patch("pathlib.Path.exists", return_value=True):
            with patch("pathlib.Path.read_bytes", return_value=b"zip content"):
                result = await client.upload_file(
                    endpoint="/dashboard/import/",
                    file_info={
                        "file_obj": "/tmp/test.zip",
                        "file_name": "test.zip",
                        "form_field": "formData",
                    },
                    extra_data={"overwrite": "true"},
                )
        assert result == {"result": "imported"}

    @pytest.mark.asyncio
    async def test_upload_with_bytesio(self):
        import io

        from src.core.utils.async_network import AsyncAPIClient

        client = AsyncAPIClient({"base_url": "https://test.com", "auth": {}})
        client._authenticated = True
        client._tokens = {"access_token": "acc", "csrf_token": "csrf"}
        client._client = AsyncMock()

        mock_resp = MagicMock()
        mock_resp.status_code = 200
        mock_resp.json.return_value = {"result": "ok"}
        client._client.post = AsyncMock(return_value=mock_resp)

        buf = io.BytesIO(b"zip content")
        result = await client.upload_file(
            endpoint="/dashboard/import/",
            file_info={"file_obj": buf, "file_name": "test.zip"},
        )
        assert result == {"result": "ok"}

    @pytest.mark.asyncio
    async def test_upload_file_not_found(self):
        from src.core.utils.async_network import AsyncAPIClient

        client = AsyncAPIClient({"base_url": "https://test.com", "auth": {}})
        client._authenticated = True
        client._tokens = {"access_token": "acc", "csrf_token": "csrf"}
        client._client = AsyncMock()

        with patch("pathlib.Path.exists", return_value=False):
            with pytest.raises(FileNotFoundError, match="does not exist"):
                await client.upload_file(
                    endpoint="/dashboard/import/",
                    file_info={"file_obj": "/nonexistent.zip", "file_name": "nope.zip"},
                )

    @pytest.mark.asyncio
    async def test_upload_unsupported_type(self):
        from src.core.utils.async_network import AsyncAPIClient

        client = AsyncAPIClient({"base_url": "https://test.com", "auth": {}})
        client._authenticated = True
        client._tokens = {"access_token": "acc", "csrf_token": "csrf"}
        client._client = AsyncMock()

        with pytest.raises(TypeError, match="Unsupported file_obj"):
            await client.upload_file(
                endpoint="/dashboard/import/",
                file_info={"file_obj": 42, "file_name": "nope.zip"},
            )

    @pytest.mark.asyncio
    async def test_upload_http_error(self):
        from src.core.utils.async_network import AsyncAPIClient
        from src.core.utils.network import SupersetAPIError

        client = AsyncAPIClient({"base_url": "https://test.com", "auth": {}})
        client._authenticated = True
        client._tokens = {"access_token": "acc", "csrf_token": "csrf"}
        client._client = AsyncMock()

        exc = httpx.HTTPStatusError("Error", request=MagicMock(), response=MagicMock())
        exc.response.text = "Bad request"
        client._client.post = AsyncMock(side_effect=exc)

        with patch("pathlib.Path.exists", return_value=True):
            with patch("pathlib.Path.read_bytes", return_value=b"zip"):
                with pytest.raises(SupersetAPIError, match="API error during upload"):
                    await client.upload_file(
                        endpoint="/dashboard/import/",
                        file_info={"file_obj": "/tmp/test.zip", "file_name": "test.zip"},
                    )

    @pytest.mark.asyncio
    async def test_upload_network_error(self):
        from src.core.utils.async_network import AsyncAPIClient
        from src.core.utils.network import NetworkError

        client = AsyncAPIClient({"base_url": "https://test.com", "auth": {}})
        client._authenticated = True
        client._tokens = {"access_token": "acc", "csrf_token": "csrf"}
        client._client = AsyncMock()
        client._client.post = AsyncMock(side_effect=httpx.HTTPError("Network error"))

        with patch("pathlib.Path.exists", return_value=True):
            with patch("pathlib.Path.read_bytes", return_value=b"zip"):
                with pytest.raises(NetworkError, match="Network error during upload"):
                    await client.upload_file(
                        endpoint="/dashboard/import/",
                        file_info={"file_obj": "/tmp/test.zip", "file_name": "test.zip"},
                    )

    @pytest.mark.asyncio
    async def test_upload_non_200_json_error(self):
        import io

        from src.core.utils.async_network import AsyncAPIClient
        from src.core.utils.network import SupersetAPIError

        client = AsyncAPIClient({"base_url": "https://test.com", "auth": {}})
        client._authenticated = True
        client._tokens = {"access_token": "acc", "csrf_token": "csrf"}
        client._client = AsyncMock()

        mock_resp = MagicMock()
        mock_resp.status_code = 200
        mock_resp.json.side_effect = ValueError("Not JSON")
        client._client.post = AsyncMock(return_value=mock_resp)

        buf = io.BytesIO(b"zip")
        with pytest.raises(SupersetAPIError, match="API error during upload"):
            await client.upload_file(
                endpoint="/dashboard/import/",
                file_info={"file_obj": buf, "file_name": "test.zip"},
            )


class TestAclose:
    """aclose: closes underlying httpx client."""

    @pytest.mark.asyncio
    async def test_closes_client(self):
        from src.core.utils.async_network import AsyncAPIClient

        client = AsyncAPIClient({"base_url": "https://test.com", "auth": {}})
        client._client = AsyncMock()
        client._client.aclose = AsyncMock()

        await client.aclose()
        client._client.aclose.assert_awaited_once()


class TestNormalizeBaseUrl:
    """_normalize_base_url: URL normalization edge cases."""

    @pytest.fixture
    def client(self):
        from src.core.utils.async_network import AsyncAPIClient

        return AsyncAPIClient({"base_url": "https://test.com", "auth": {}})

    def test_empty_url(self, client):
        assert client._normalize_base_url("") == ""

    def test_none_url(self, client):
        assert client._normalize_base_url(None) == ""

    def test_api_v1_suffix(self, client):
        assert client._normalize_base_url("https://test.com/api/v1") == "https://test.com"

    def test_trailing_slash(self, client):
        assert client._normalize_base_url("https://test.com/") == "https://test.com"

    def test_whitespace_stripped(self, client):
        assert client._normalize_base_url("  https://test.com  ") == "https://test.com"

    def test_api_v1_with_trailing_slash(self, client):
        assert client._normalize_base_url("https://test.com/api/v1/") == "https://test.com"
# #endregion Test.AsyncNetwork
