# #region Test.AsyncNetworkExtra [C:2] [TYPE Module] [SEMANTICS test, async, network, edge, upload]
# @BRIEF Extra edge-case tests for async_network.py and network.py — auth CSRF refresh, expired cache, upload 201.
# @RELATION BINDS_TO -> [AsyncNetworkModule]

from __future__ import annotations

import json
import sys
import time
from pathlib import Path
from unittest.mock import AsyncMock, MagicMock, patch

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

import httpx
import pytest

from src.core.utils.network import SupersetAuthCache


# ── SupersetAuthCache: expired token path —─


class TestSupersetAuthCacheExpired:
    """SupersetAuthCache.get — expired token returns None (network.py lines 115-116)."""

    def clear(self):
        SupersetAuthCache._entries.clear()

    def test_expired_entry_returns_none(self):
        """Entry with past expires_at returns None and is removed."""
        self.clear()
        key = ("exp_url", "exp_user", False)
        past_time = time.time() - 100
        with SupersetAuthCache._lock:
            SupersetAuthCache._entries[key] = {
                "tokens": {"access_token": "old", "csrf_token": "old_csrf"},
                "expires_at": past_time,
            }
        result = SupersetAuthCache.get(key)
        assert result is None

    def test_expired_after_wait_returns_none(self):
        """Entry that expired between check and get returns None."""
        self.clear()
        key = ("exp2_url", "exp2_user", False)
        past_time = time.time() - 10
        with SupersetAuthCache._lock:
            SupersetAuthCache._entries[key] = {
                "tokens": {"access_token": "old", "csrf_token": "old_csrf"},
                "expires_at": past_time,
            }
        result = SupersetAuthCache.get(key)
        assert result is None
        # Entry should be removed
        assert key not in SupersetAuthCache._entries


# ── AsyncAPIClient.authenticate: cache-hit-after-wait CSRF refresh ──


class TestAuthenticateCacheHitAfterWaitCSRF:
    """authenticate — cache-hit-after-wait CSRF refresh paths (lines 161-177).

    To hit the lock path, we need:
      - First cache check (line 153) → miss (no cache)
      - Second cache check (line 159, inside lock) → hit

    We achieve this by patching SupersetAuthCache.get to return different
    values on successive calls.
    """

    @pytest.fixture(autouse=True)
    def clear_cache(self):
        SupersetAuthCache._entries.clear()
        yield

    def _make_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_csrf_refresh_success_path(self):
        """Cache hit after wait: CSRF refresh succeeds and updates token."""
        client = self._make_client()
        mock_csrf_resp = MagicMock()
        mock_csrf_resp.is_success = True
        mock_csrf_resp.json.return_value = {"result": "new_csrf_token"}
        client._client.get = AsyncMock(return_value=mock_csrf_resp)

        call_count = 0
        _tokens_cache = {"access_token": "cached_at", "csrf_token": "refresh_me"}

        def _fake_get(key):
            nonlocal call_count
            call_count += 1
            if call_count == 1:
                return None  # first check — miss
            return _tokens_cache  # second check — hit

        with patch.object(SupersetAuthCache, "get", side_effect=_fake_get):
            tokens = await client.authenticate()
        assert tokens["access_token"] == "cached_at"
        assert tokens["csrf_token"] == "new_csrf_token"
        assert client._authenticated is True

    @pytest.mark.asyncio
    async def test_csrf_refresh_exception_swallowed(self):
        """Cache hit after wait: CSRF refresh exception is swallowed (lines 175-176)."""
        client = self._make_client()
        mock_csrf_resp = MagicMock()
        mock_csrf_resp.is_success = True
        mock_csrf_resp.json.side_effect = ValueError("Bad JSON")
        client._client.get = AsyncMock(return_value=mock_csrf_resp)

        call_count = 0
        _tokens_cache = {"access_token": "cached_at", "csrf_token": "old_csrf"}

        def _fake_get(key):
            nonlocal call_count
            call_count += 1
            if call_count == 1:
                return None
            return _tokens_cache

        with patch.object(SupersetAuthCache, "get", side_effect=_fake_get):
            tokens = await client.authenticate()
        assert tokens["access_token"] == "cached_at"
        # CSRF token stays as cached (silently failed)
        assert tokens["csrf_token"] == "old_csrf"

    @pytest.mark.asyncio
    async def test_csrf_refresh_connection_error_swallowed(self):
        """Cache hit after wait: CSRF connection error is swallowed."""
        client = self._make_client()
        client._client.get = AsyncMock(side_effect=httpx.ConnectError("No route"))

        call_count = 0
        _tokens_cache = {"access_token": "cached_at", "csrf_token": "old_csrf"}

        def _fake_get(key):
            nonlocal call_count
            call_count += 1
            if call_count == 1:
                return None
            return _tokens_cache

        with patch.object(SupersetAuthCache, "get", side_effect=_fake_get):
            tokens = await client.authenticate()
        assert tokens["access_token"] == "cached_at"

    @pytest.mark.asyncio
    async def test_cache_hit_stale_csrf_refreshed(self):
        """Cache hit after wait: CSRF token gets refreshed."""
        client = self._make_client()
        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)

        call_count = 0
        _tokens_cache = {"access_token": "cached_at", "csrf_token": "stale_csrf"}

        def _fake_get(key):
            nonlocal call_count
            call_count += 1
            if call_count == 1:
                return None
            return _tokens_cache

        with patch.object(SupersetAuthCache, "get", side_effect=_fake_get):
            tokens = await client.authenticate()
        assert tokens["access_token"] == "cached_at"
        assert tokens["csrf_token"] == "new_csrf"


# ── AsyncAPIClient.upload_file: non-200 success status ──


class TestUploadFileNon200:
    """upload_file — response with non-200 success status like 201 (line 494)."""

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

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

        mock_resp = MagicMock()
        mock_resp.status_code = 201  # Created — NOT 200
        mock_resp.json.return_value = {"result": "created"}
        client._client.post = AsyncMock(return_value=mock_resp)

        import io

        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": "created"}

    @pytest.mark.asyncio
    async def test_upload_204_no_content(self):
        """204 response with no body — raise_for_status passes, but json() may fail."""
        from src.core.utils.async_network import AsyncAPIClient
        from src.core.utils.network import SupersetAPIError

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

        mock_resp = MagicMock()
        mock_resp.status_code = 200
        # Empty text means the first response.json() inside try succeeds
        # We want to test the case where it fails
        mock_resp.json.side_effect = ValueError("No JSON")
        client._client.post = AsyncMock(return_value=mock_resp)

        import io

        buf = io.BytesIO(b"zip content")
        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"},
            )


# ── AsyncAPIClient._build_api_url with strip ──


class TestBuildApiUrlEdge:
    """_build_api_url — 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_endpoint_with_query(self, client):
        """Endpoint with query params."""
        url = client._build_api_url("/chart/?page=1")
        assert "page=1" in url

    def test_endpoint_with_hash(self, client):
        """Endpoint with hash fragment."""
        url = client._build_api_url("/chart/#section")
        assert "#section" in url

    def test_api_v1_with_query_does_not_dedup(self, client):
        """Endpoint with api/v1 and query doesn't double-add api/v1."""
        url = client._build_api_url("/api/v1/chart/?page=1")
        assert url.count("api/v1") == 1
# #endregion Test.AsyncNetworkExtra
