from __future__ import annotations

import datetime as dt
from pathlib import Path
from types import SimpleNamespace
from unittest.mock import AsyncMock, MagicMock, patch

import httpx
import jwt
import pytest
import respx

from src.auth import HostedJWTTokenVerifier, JWTValidator, TokenProvider, bind_user_id
from src.config import initialize_settings, reset_settings
from src.errors import AuthenticationError
from src.meta_client import MetaClient, build_client_from_env


def _set_hosted_env(monkeypatch: pytest.MonkeyPatch) -> None:
    monkeypatch.setenv("HOSTED_MODE", "true")
    monkeypatch.setenv("AUTH_BROKER_URL", "https://auth.example.com")
    monkeypatch.setenv("AUTH_BROKER_API_KEY", "admin-api-key-123456")
    monkeypatch.setenv("JWT_PUBLIC_KEY", "shared-secret-0123456789abcdef01234567")
    monkeypatch.setenv("JWT_ALGORITHM", "HS256")
    monkeypatch.setenv("JWT_ISSUER", "https://auth.example.com")
    monkeypatch.setenv("JWT_AUDIENCE", "meta-ads-mcp")


def test_settings_hosted_mode_does_not_require_meta_access_token(monkeypatch: pytest.MonkeyPatch) -> None:
    reset_settings()
    _set_hosted_env(monkeypatch)
    monkeypatch.delenv("META_ACCESS_TOKEN", raising=False)
    monkeypatch.delenv("META_APP_ID", raising=False)
    monkeypatch.delenv("META_APP_SECRET", raising=False)

    settings = initialize_settings(validate=True)
    assert settings.hosted_mode is True
    reset_settings()


@pytest.mark.asyncio
@respx.mock
async def test_token_provider_fetches_meta_token_from_auth_broker() -> None:
    route = respx.get("https://auth.example.com/tokens/meta/user-1").mock(
        return_value=httpx.Response(200, json={"access_token": "meta-token-from-broker"})
    )

    provider = TokenProvider(
        auth_broker_url="https://auth.example.com",
        admin_api_key="admin-api-key",
        timeout_seconds=5.0,
        hosted_mode=True,
    )
    token = await provider.get_token("user-1")

    assert token == "meta-token-from-broker"
    assert route.called
    assert route.calls.last.request.headers["X-API-Key"] == "admin-api-key"


@pytest.mark.asyncio
@respx.mock
async def test_token_provider_reuses_cached_token_until_expiry() -> None:
    now = int(dt.datetime.now(dt.timezone.utc).timestamp())
    route = respx.get("https://auth.example.com/tokens/meta/user-1").mock(
        return_value=httpx.Response(
            200,
            json={"access_token": "meta-token-from-broker", "expires_at": now + 3600},
        )
    )

    provider = TokenProvider(
        auth_broker_url="https://auth.example.com",
        admin_api_key="admin-api-key",
        timeout_seconds=5.0,
        hosted_mode=True,
    )
    first = await provider.get_token("user-1")
    second = await provider.get_token("user-1")
    await provider.aclose()

    assert first == "meta-token-from-broker"
    assert second == "meta-token-from-broker"
    assert route.call_count == 1


@pytest.mark.asyncio
@respx.mock
async def test_token_provider_refreshes_token_when_expiring_soon() -> None:
    now = int(dt.datetime.now(dt.timezone.utc).timestamp())
    route = respx.get("https://auth.example.com/tokens/meta/user-1").mock(
        side_effect=[
            httpx.Response(200, json={"access_token": "soon-expired", "expires_at": now + 10}),
            httpx.Response(200, json={"access_token": "fresh-token", "expires_at": now + 3600}),
        ]
    )

    provider = TokenProvider(
        auth_broker_url="https://auth.example.com",
        admin_api_key="admin-api-key",
        timeout_seconds=5.0,
        hosted_mode=True,
    )
    first = await provider.get_token("user-1")
    second = await provider.get_token("user-1")
    await provider.aclose()

    assert first == "soon-expired"
    assert second == "fresh-token"
    assert route.call_count == 2


@pytest.mark.asyncio
async def test_hosted_jwt_token_verifier_maps_subject_to_client_id() -> None:
    secret = "shared-secret-0123456789abcdef01234567"
    now = dt.datetime.now(dt.timezone.utc)
    raw_token = jwt.encode(
        {
            "sub": "user-123",
            "iss": "https://auth.example.com",
            "aud": "meta-ads-mcp",
            "iat": int(now.timestamp()),
            "exp": int((now + dt.timedelta(hours=1)).timestamp()),
            "scope": "ads.read ads.write",
        },
        secret,
        algorithm="HS256",
    )

    validator = JWTValidator(
        public_key=secret,
        issuer="https://auth.example.com",
        audience="meta-ads-mcp",
        algorithm="HS256",
    )
    verifier = HostedJWTTokenVerifier(validator)
    access_token = await verifier.verify_token(raw_token)

    assert access_token is not None
    assert access_token.client_id == "user-123"
    assert "ads.read" in access_token.scopes


@pytest.mark.asyncio
async def test_meta_client_uses_request_user_context_for_broker_token() -> None:
    token_provider = AsyncMock()
    token_provider.get_token = AsyncMock(return_value="broker-token")
    transport = AsyncMock(spec=httpx.AsyncBaseTransport)

    client = MetaClient(
        access_token="fallback-token",
        token_provider=token_provider,
        app_secret=None,
        transport=transport,
        enable_caching=False,
        enable_rate_limiting=False,
    )

    mock_response = MagicMock()
    mock_response.is_success = True
    mock_response.status_code = 200
    mock_response.content = b'{"ok": true}'
    mock_response.json.return_value = {"ok": True}
    mock_response.headers = {}

    with patch.object(client._client, "request", new_callable=AsyncMock) as mock_request:
        mock_request.return_value = mock_response
        with bind_user_id("user-context"):
            result = await client.get("me")

    assert result == {"ok": True}
    token_provider.get_token.assert_awaited_once_with("user-context")
    assert mock_request.call_args.kwargs["params"]["access_token"] == "broker-token"


@pytest.mark.asyncio
async def test_meta_client_requires_user_context_in_hosted_mode() -> None:
    token_provider = AsyncMock()
    token_provider.get_token = AsyncMock(return_value="broker-token")
    transport = AsyncMock(spec=httpx.AsyncBaseTransport)

    client = MetaClient(
        access_token="fallback-token",
        token_provider=token_provider,
        app_secret=None,
        transport=transport,
        enable_caching=False,
        enable_rate_limiting=False,
    )

    with pytest.raises(AuthenticationError):
        await client.get("me")


@pytest.mark.asyncio
async def test_build_client_from_env_supports_hosted_mode_without_meta_token(
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    reset_settings()
    _set_hosted_env(monkeypatch)
    monkeypatch.delenv("META_ACCESS_TOKEN", raising=False)
    monkeypatch.delenv("META_APP_ID", raising=False)
    monkeypatch.delenv("META_APP_SECRET", raising=False)

    settings = initialize_settings(validate=True)
    client = await build_client_from_env(settings)

    assert client.token_provider is not None
    assert client.access_token == "hosted-mode-placeholder-token"
    await client.aclose()
    reset_settings()


@pytest.mark.asyncio
async def test_build_client_from_env_passes_settings_token_cache_path() -> None:
    settings = SimpleNamespace(
        hosted_mode=False,
        meta_app_secret=None,
        meta_api_timeout_seconds=30.0,
        meta_max_concurrency=10,
        meta_api_version="v24.0",
        meta_graph_base_url="https://graph.facebook.com",
        cache_enabled=False,
        rate_limiting_enabled=False,
        auth_broker_url=None,
        auth_broker_api_key=None,
        auth_broker_timeout_seconds=10.0,
        meta_token_cache_path="~/custom/meta-token-cache.json",
    )
    expected_cache_path = Path.home() / "custom" / "meta-token-cache.json"

    with patch("src.meta_client.ensure_access_token", new_callable=AsyncMock) as ensure_mock:
        ensure_mock.return_value = "settings-token"
        client = await build_client_from_env(settings)

    ensure_mock.assert_awaited_once_with("v24.0", cache_path=expected_cache_path)
    await client.aclose()
