"""Tests for network_utils — fetch_with_curl, circuit breaker, domain fail cache."""
import time
import pytest
from unittest.mock import patch, MagicMock
from services.network_utils import fetch_with_curl, _circuit_breaker, _domain_fail_cache, _cb_lock, _DummyResponse


class TestDummyResponse:
    """Tests for the minimal response object used as curl fallback."""

    def test_status_code_and_text(self):
        resp = _DummyResponse(200, '{"ok": true}')
        assert resp.status_code == 200
        assert resp.text == '{"ok": true}'

    def test_json_parsing(self):
        resp = _DummyResponse(200, '{"key": "value", "num": 42}')
        data = resp.json()
        assert data["key"] == "value"
        assert data["num"] == 42

    def test_content_bytes(self):
        resp = _DummyResponse(200, "hello")
        assert resp.content == b"hello"

    def test_raise_for_status_ok(self):
        resp = _DummyResponse(200, "ok")
        resp.raise_for_status()  # Should not raise

    def test_raise_for_status_error(self):
        resp = _DummyResponse(500, "server error")
        with pytest.raises(Exception, match="HTTP 500"):
            resp.raise_for_status()

    def test_raise_for_status_404(self):
        resp = _DummyResponse(404, "not found")
        with pytest.raises(Exception, match="HTTP 404"):
            resp.raise_for_status()


class TestCircuitBreaker:
    """Tests for the circuit breaker and domain fail cache."""

    def setup_method(self):
        """Clear caches before each test."""
        with _cb_lock:
            _circuit_breaker.clear()
            _domain_fail_cache.clear()

    def test_circuit_breaker_blocks_request(self):
        """If a domain is in circuit breaker, fetch_with_curl should fail fast."""
        with _cb_lock:
            _circuit_breaker["example.com"] = time.time()

        with pytest.raises(Exception, match="Circuit breaker open"):
            fetch_with_curl("https://example.com/test")

    def test_circuit_breaker_expires_after_ttl(self):
        """Circuit breaker entries older than TTL should be ignored."""
        with _cb_lock:
            _circuit_breaker["expired.com"] = time.time() - 200  # > 120s TTL

        # Should not raise — circuit breaker expired
        # Will fail for other reasons (network) but won't raise circuit breaker
        mock_resp = MagicMock()
        mock_resp.status_code = 200
        mock_resp.text = "ok"
        mock_resp.raise_for_status = MagicMock()

        with patch("services.network_utils._session") as mock_session:
            mock_session.get.return_value = mock_resp
            result = fetch_with_curl("https://expired.com/test")
            assert result.status_code == 200

    def test_domain_fail_cache_skips_to_curl(self):
        """If a domain recently failed with requests, skip straight to curl."""
        with _cb_lock:
            _domain_fail_cache["skip-to-curl.com"] = time.time()

        # Mock subprocess to simulate curl success
        mock_result = MagicMock()
        mock_result.returncode = 0
        mock_result.stdout = '{"data": true}\n200'
        mock_result.stderr = ''

        with patch("subprocess.run", return_value=mock_result) as mock_run:
            result = fetch_with_curl("https://skip-to-curl.com/api")
            assert result.status_code == 200
            assert result.json()["data"] is True
            # Verify subprocess.run was called (curl fallback)
            mock_run.assert_called_once()

    def test_successful_request_clears_caches(self):
        """Successful requests should clear both domain_fail_cache and circuit_breaker."""
        domain = "success-clears.com"
        with _cb_lock:
            _domain_fail_cache[domain] = time.time() - 400  # Expired, won't skip
            _circuit_breaker[domain] = time.time() - 200  # Expired, won't block

        mock_resp = MagicMock()
        mock_resp.status_code = 200
        mock_resp.text = "ok"
        mock_resp.raise_for_status = MagicMock()

        with patch("services.network_utils._session") as mock_session:
            mock_session.get.return_value = mock_resp
            fetch_with_curl(f"https://{domain}/test")

        with _cb_lock:
            assert domain not in _domain_fail_cache
            assert domain not in _circuit_breaker


class TestFetchWithCurl:
    """Tests for the primary fetch_with_curl function."""

    def setup_method(self):
        with _cb_lock:
            _circuit_breaker.clear()
            _domain_fail_cache.clear()

    def test_successful_get_returns_response(self):
        mock_resp = MagicMock()
        mock_resp.status_code = 200
        mock_resp.text = '{"result": 42}'
        mock_resp.raise_for_status = MagicMock()

        with patch("services.network_utils._session") as mock_session:
            mock_session.get.return_value = mock_resp
            result = fetch_with_curl("https://api.example.com/data")
            assert result.status_code == 200

    def test_post_with_json_data(self):
        mock_resp = MagicMock()
        mock_resp.status_code = 200
        mock_resp.text = '{"created": true}'
        mock_resp.raise_for_status = MagicMock()

        with patch("services.network_utils._session") as mock_session:
            mock_session.post.return_value = mock_resp
            result = fetch_with_curl("https://api.example.com/create",
                                     method="POST", json_data={"name": "test"})
            assert result.status_code == 200
            mock_session.post.assert_called_once()

    def test_custom_headers_merged(self):
        mock_resp = MagicMock()
        mock_resp.status_code = 200
        mock_resp.text = "ok"
        mock_resp.raise_for_status = MagicMock()

        with patch("services.network_utils._session") as mock_session:
            mock_session.get.return_value = mock_resp
            fetch_with_curl("https://api.example.com/data",
                           headers={"Authorization": "Bearer token123"})
            call_args = mock_session.get.call_args
            headers = call_args.kwargs.get("headers", {})
            assert "Authorization" in headers
            assert headers["Authorization"] == "Bearer token123"
