import time
from unittest.mock import AsyncMock, MagicMock

import pytest

from src.connectors.sdk.circuit_breaker import CircuitBreaker, CircuitState
from src.connectors.sdk.retry import RetryPolicy, with_retry
from src.connectors.sdk.context import ConnectorContext
from src.connectors.sdk.auth import BasicAuthHandler, BearerAuthHandler, APIKeyAuthHandler
from src.core.exceptions import ServiceUnavailableException


class TestCircuitBreaker:
    def test_initial_state_is_closed(self):
        cb = CircuitBreaker(name="test")
        assert cb.state == CircuitState.CLOSED

    def test_opens_after_threshold_failures(self):
        cb = CircuitBreaker(name="test", failure_threshold=3)
        for _ in range(3):
            cb._on_failure(Exception("fail"))
        assert cb._state == CircuitState.OPEN

    def test_closed_does_not_raise(self):
        cb = CircuitBreaker(name="test")

        async def _run():
            coro = cb.call(AsyncMock(return_value="ok"))
            return await coro

        import asyncio
        result = asyncio.get_event_loop().run_until_complete(_run())
        assert result == "ok"

    def test_open_raises_service_unavailable(self):
        cb = CircuitBreaker(name="test", failure_threshold=1)
        cb._on_failure(Exception("fail"))
        assert cb.state == CircuitState.OPEN

        async def _run():
            await cb.call(AsyncMock(return_value="ok"))

        import asyncio
        with pytest.raises(ServiceUnavailableException):
            asyncio.get_event_loop().run_until_complete(_run())

    def test_half_open_after_recovery_timeout(self):
        cb = CircuitBreaker(name="test", failure_threshold=1, recovery_timeout=0.01)
        cb._on_failure(Exception("fail"))
        assert cb._state == CircuitState.OPEN
        time.sleep(0.02)
        assert cb.state == CircuitState.HALF_OPEN

    def test_reset(self):
        cb = CircuitBreaker(name="test", failure_threshold=1)
        cb._on_failure(Exception("fail"))
        cb.reset()
        assert cb.state == CircuitState.CLOSED
        assert cb._failure_count == 0

    def test_success_resets_failure_count(self):
        cb = CircuitBreaker(name="test", failure_threshold=5)
        cb._on_failure(Exception("fail"))
        cb._on_failure(Exception("fail"))
        assert cb._failure_count == 2
        cb._on_success()
        assert cb._failure_count == 0

    def test_half_open_success_closes_circuit(self):
        cb = CircuitBreaker(name="test", failure_threshold=1, recovery_timeout=0.01)
        cb._on_failure(Exception("fail"))
        time.sleep(0.02)
        assert cb.state == CircuitState.HALF_OPEN
        cb._on_success()
        assert cb._state == CircuitState.CLOSED

    def test_half_open_failure_reopens(self):
        cb = CircuitBreaker(name="test", failure_threshold=1, recovery_timeout=0.01)
        cb._on_failure(Exception("fail"))
        time.sleep(0.02)
        assert cb.state == CircuitState.HALF_OPEN
        cb._on_failure(Exception("fail again"))
        assert cb._state == CircuitState.OPEN

    def test_get_status(self):
        cb = CircuitBreaker(name="test")
        status = cb.get_status()
        assert status["name"] == "test"
        assert status["state"] == "closed"


class TestRetryPolicy:
    def test_default_values(self):
        policy = RetryPolicy()
        assert policy.max_attempts == 3
        assert policy.base_delay == 1.0
        assert policy.max_delay == 30.0

    def test_custom_values(self):
        policy = RetryPolicy(max_attempts=5, base_delay=0.5, max_delay=10.0)
        assert policy.max_attempts == 5
        assert policy.base_delay == 0.5
        assert policy.max_delay == 10.0


class TestWithRetry:
    @pytest.mark.asyncio
    async def test_success_first_try(self):
        call_count = 0

        async def _success():
            nonlocal call_count
            call_count += 1
            return "ok"

        policy = RetryPolicy(max_attempts=3)
        result = await with_retry(_success, policy=policy, connector_type="test", operation="test_op")
        assert result == "ok"
        assert call_count == 1

    @pytest.mark.asyncio
    async def test_retry_on_failure_then_success(self):
        call_count = 0

        async def _flaky():
            nonlocal call_count
            call_count += 1
            if call_count < 3:
                raise Exception("temp fail")
            return "ok"

        policy = RetryPolicy(max_attempts=3, base_delay=0.01, max_delay=0.1)
        result = await with_retry(_flaky, policy=policy, connector_type="test", operation="flaky_op")
        assert result == "ok"
        assert call_count == 3

    @pytest.mark.asyncio
    async def test_exhaust_retries(self):
        call_count = 0

        async def _always_fail():
            nonlocal call_count
            call_count += 1
            raise Exception("always fail")

        policy = RetryPolicy(max_attempts=2, base_delay=0.01, max_delay=0.1)
        with pytest.raises(Exception, match="always fail"):
            await with_retry(_always_fail, policy=policy, connector_type="test", operation="fail_op")
        assert call_count == 2


class TestConnectorContext:
    def test_cache_key(self):
        ctx = ConnectorContext(
            connector_id=MagicMock(),
            pharmacy_id=MagicMock(),
            erp_system_id=MagicMock(),
            connector_type="farmatic",
        )
        key = ctx.cache_key("stock")
        assert "farmatic" in key
        assert "stock" in key

    def test_get_config(self):
        ctx = ConnectorContext(
            connector_id=MagicMock(),
            pharmacy_id=MagicMock(),
            erp_system_id=MagicMock(),
            connector_type="mock",
            config={"base_url": "https://example.com"},
        )
        assert ctx.get_config("base_url") == "https://example.com"
        assert ctx.get_config("missing", "default") == "default"

    def test_get_secret(self):
        ctx = ConnectorContext(
            connector_id=MagicMock(),
            pharmacy_id=MagicMock(),
            erp_system_id=MagicMock(),
            connector_type="mock",
            secrets={"api_key": "secret123"},
        )
        assert ctx.get_secret("api_key") == "secret123"
        assert ctx.get_secret("missing") is None

    def test_require_secret_raises(self):
        ctx = ConnectorContext(
            connector_id=MagicMock(),
            pharmacy_id=MagicMock(),
            erp_system_id=MagicMock(),
            connector_type="mock",
        )
        with pytest.raises(ValueError, match="Required secret"):
            ctx.require_secret("missing")

    def test_require_config_raises(self):
        ctx = ConnectorContext(
            connector_id=MagicMock(),
            pharmacy_id=MagicMock(),
            erp_system_id=MagicMock(),
            connector_type="mock",
        )
        with pytest.raises(ValueError, match="Required config"):
            ctx.require_config("missing")


class TestBasicAuthHandler:
    @pytest.mark.asyncio
    async def test_apply_basic_auth(self):
        import httpx

        handler = BasicAuthHandler()
        ctx = ConnectorContext(
            connector_id=MagicMock(),
            pharmacy_id=MagicMock(),
            erp_system_id=MagicMock(),
            connector_type="test",
            config={"username": "user1", "password": "pass1"},
        )
        request = httpx.Request("GET", "https://example.com")
        result = await handler.apply(request, ctx)
        assert "Basic" in result.headers["Authorization"]


class TestBearerAuthHandler:
    @pytest.mark.asyncio
    async def test_apply_bearer_auth(self):
        import httpx

        handler = BearerAuthHandler()
        ctx = ConnectorContext(
            connector_id=MagicMock(),
            pharmacy_id=MagicMock(),
            erp_system_id=MagicMock(),
            connector_type="test",
            config={"access_token": "token123"},
        )
        request = httpx.Request("GET", "https://example.com")
        result = await handler.apply(request, ctx)
        assert result.headers["Authorization"] == "Bearer token123"


class TestAPIKeyAuthHandler:
    @pytest.mark.asyncio
    async def test_apply_api_key_auth(self):
        import httpx

        handler = APIKeyAuthHandler(header_name="X-Custom-Key")
        ctx = ConnectorContext(
            connector_id=MagicMock(),
            pharmacy_id=MagicMock(),
            erp_system_id=MagicMock(),
            connector_type="test",
            secrets={"api_key": "key123"},
        )
        request = httpx.Request("GET", "https://example.com")
        result = await handler.apply(request, ctx)
        assert result.headers["X-Custom-Key"] == "key123"
