"""Cloud internal API: JWT helpers and auth gate."""
from __future__ import annotations

from datetime import datetime, timezone
from types import SimpleNamespace
from unittest.mock import AsyncMock
from unittest.mock import MagicMock, patch
from uuid import uuid4

import pytest
from fastapi.testclient import TestClient


@patch("src.core.security.cloud_tokens.get_settings")
def test_cloud_authorization_and_grant_roundtrip(mock_gs: MagicMock) -> None:
    mock_gs.return_value = MagicMock(
        cloud_grant_jwt_secret="grant-secret-32-chars-minimum!!",
        jwt_secret="fallback",
        cloud_authorization_token_ttl_days=7,
        cloud_grant_ttl_minutes=15,
    )
    from src.core.security.cloud_tokens import (
        create_cloud_authorization_token,
        create_cloud_grant_jwt,
        decode_cloud_authorization_token,
    )

    ws = uuid4()
    pid = uuid4()
    auth_tok, jti = create_cloud_authorization_token(
        workspace_id=ws,
        project_id=pid,
        request_type="email.send",
        idempotency_key="idem-1",
    )
    claims = decode_cloud_authorization_token(auth_tok)
    assert claims is not None
    assert claims["ws"] == str(ws)
    assert claims["pid"] == str(pid)
    assert claims["jti"] == jti

    grant_tok, _exp = create_cloud_grant_jwt(
        workspace_id=ws,
        project_id=pid,
        authorization_jti=jti,
        request_type="email.send",
        policy_capability="email.send",
        policy_provider="resend",
        max_attempts=8,
        max_runtime_seconds=60,
        requires_webhook=False,
        artifact_retention_days=30,
    )
    assert grant_tok.count(".") == 2

    voice_tok, voice_exp = create_cloud_grant_jwt(
        workspace_id=ws,
        project_id=pid,
        authorization_jti=jti,
        request_type="voice.call.handle",
        policy_capability="voice.call.handle",
        policy_provider="openai_realtime",
        max_attempts=1,
        max_runtime_seconds=3600,
        requires_webhook=True,
        artifact_retention_days=30,
    )
    assert voice_tok.count(".") == 2
    assert (voice_exp - datetime.now(timezone.utc)).total_seconds() > 50 * 60


def test_cloud_routes_require_bearer(monkeypatch: pytest.MonkeyPatch) -> None:
    monkeypatch.setenv("CLOUD_INTERNAL_TOKEN", "test-internal-token")
    from src.main import create_app

    app = create_app()
    client = TestClient(app)
    r = client.post(
        "/v1/cloud/authorize",
        json={
            "workspace_id": str(uuid4()),
            "project_id": str(uuid4()),
            "client_id": "x",
            "handshake_id": "hs",
            "request_type": "email.send",
            "idempotency_key": "k",
            "request_meta": {"origin": "https://a.com", "payload_hash": "sha256:abc"},
        },
    )
    assert r.status_code == 401


@pytest.mark.asyncio
async def test_cloud_authorize_denies_unpaid_workspace() -> None:
    from src.domain.cloud.schemas import AuthorizeRequest
    from src.domain.cloud.service import CloudInternalService

    ws_id = uuid4()
    project_id = uuid4()
    project = SimpleNamespace(
        id=project_id,
        workspace_id=ws_id,
        deleted_at=None,
        status="ready",
        project_ref="projref123",
    )
    workspace = SimpleNamespace(
        id=ws_id,
        deleted_at=None,
        selected_plan_slug="pro",
        skip_plan_quota_limits=False,
    )
    settings_row = SimpleNamespace(settings={})

    service = CloudInternalService(
        project_repo=AsyncMock(get_by_id=AsyncMock(return_value=project)),
        workspace_repo=AsyncMock(get_by_id=AsyncMock(return_value=workspace)),
        settings_repo=AsyncMock(get_or_create=AsyncMock(return_value=settings_row)),
        subscription_repo=AsyncMock(get_latest_for_workspace=AsyncMock(return_value=None)),
        usage_ledger=AsyncMock(),
        storage_usage_repo=AsyncMock(),
        connector_repo=AsyncMock(),
    )

    out = await service.authorize(
        AuthorizeRequest(
            workspace_id=str(ws_id),
            project_id=str(project_id),
            client_id="cli_projref123",
            handshake_id="hs_123",
            request_type="email.send",
            idempotency_key="idem_123",
            request_meta={"origin": "https://app.p2hs.test", "payload_hash": "sha256:abc"},
        )
    )

    assert out.allowed is False
    assert out.reason_code == "billing_required"


@pytest.mark.asyncio
async def test_cloud_authorize_denies_exhausted_cloud_wallet() -> None:
    from src.domain.cloud.schemas import AuthorizeRequest
    from src.domain.cloud.service import CloudInternalService

    ws_id = uuid4()
    project_id = uuid4()
    project = SimpleNamespace(
        id=project_id,
        workspace_id=ws_id,
        deleted_at=None,
        status="ready",
        project_ref="projref123",
    )
    workspace = SimpleNamespace(
        id=ws_id,
        deleted_at=None,
        selected_plan_slug="pro",
        skip_plan_quota_limits=False,
    )
    settings_row = SimpleNamespace(settings={})
    sub = SimpleNamespace(status="active", plan_slug="pro")
    cloud_credits = SimpleNamespace(jobs_allowed=AsyncMock(return_value=False))

    service = CloudInternalService(
        project_repo=AsyncMock(get_by_id=AsyncMock(return_value=project)),
        workspace_repo=AsyncMock(get_by_id=AsyncMock(return_value=workspace)),
        settings_repo=AsyncMock(get_or_create=AsyncMock(return_value=settings_row)),
        subscription_repo=AsyncMock(get_latest_for_workspace=AsyncMock(return_value=sub)),
        usage_ledger=AsyncMock(),
        storage_usage_repo=AsyncMock(),
        connector_repo=AsyncMock(),
        cloud_credits=cloud_credits,
    )

    out = await service.authorize(
        AuthorizeRequest(
            workspace_id=str(ws_id),
            project_id=str(project_id),
            client_id="cli_projref123",
            handshake_id="hs_123",
            request_type="email.send",
            idempotency_key="idem_cloud",
            request_meta={"origin": "https://app.p2hs.test", "payload_hash": "sha256:abc"},
        )
    )

    assert out.allowed is False
    assert out.reason_code == "cloud_credits_exhausted"


@pytest.mark.asyncio
async def test_cloud_authorize_denies_livechat_without_capability() -> None:
    from src.domain.cloud.schemas import AuthorizeRequest
    from src.domain.cloud.service import CloudInternalService

    ws_id = uuid4()
    project_id = uuid4()
    project = SimpleNamespace(
        id=project_id,
        workspace_id=ws_id,
        deleted_at=None,
        status="ready",
        project_ref="projref123",
    )
    workspace = SimpleNamespace(
        id=ws_id,
        deleted_at=None,
        selected_plan_slug="pro",
        skip_plan_quota_limits=False,
    )
    settings_row = SimpleNamespace(settings={})

    service = CloudInternalService(
        project_repo=AsyncMock(get_by_id=AsyncMock(return_value=project)),
        workspace_repo=AsyncMock(get_by_id=AsyncMock(return_value=workspace)),
        settings_repo=AsyncMock(get_or_create=AsyncMock(return_value=settings_row)),
        subscription_repo=AsyncMock(get_latest_for_workspace=AsyncMock(return_value=None)),
        usage_ledger=AsyncMock(),
        storage_usage_repo=AsyncMock(),
        connector_repo=AsyncMock(),
    )

    out = await service.authorize(
        AuthorizeRequest(
            workspace_id=str(ws_id),
            project_id=str(project_id),
            client_id="cli_projref123",
            handshake_id="hs_123",
            request_type="livechat.message",
            idempotency_key="idem_livechat",
            request_meta={"origin": "https://app.p2hs.test", "payload_hash": "sha256:abc"},
        )
    )

    assert out.allowed is False
    assert out.reason_code == "capability_not_enabled"


def test_default_cloud_request_types_include_booking_and_reminders() -> None:
    from src.domain.cloud.service import DEFAULT_CLOUD_REQUEST_TYPES, _REQUEST_TYPE_POLICY

    for request_type in ("booking.reserve", "booking.cancel", "reminder.send", "reminder.schedule"):
        assert request_type in DEFAULT_CLOUD_REQUEST_TYPES
        assert request_type in _REQUEST_TYPE_POLICY


def test_livechat_policy_snapshot_preserves_capability_bindings() -> None:
    from src.domain.cloud.schemas import PolicySnapshot

    snapshot = PolicySnapshot(
        capability="livechat.message",
        tools=[
            {
                "name": "crm.contact.lookup",
                "capability_id": "crm.contact.lookup",
                "execution": "webhook",
                "webhook_url": "https://crm.example.test/p2hs/contact-lookup",
                "parameters": {"type": "object", "properties": {"email": {"type": "string"}}},
            }
        ],
    )

    assert snapshot.tools[0]["capability_id"] == "crm.contact.lookup"
    assert snapshot.tools[0]["execution"] == "webhook"
    assert snapshot.tools[0]["webhook_url"].startswith("https://")


@pytest.mark.asyncio
async def test_cloud_authorize_email_send_uses_platform_cloud_email(monkeypatch: pytest.MonkeyPatch) -> None:
    from src.domain.cloud.schemas import AuthorizeRequest
    from src.domain.cloud.service import CloudInternalService

    ws_id = uuid4()
    project_id = uuid4()
    project = SimpleNamespace(
        id=project_id,
        workspace_id=ws_id,
        deleted_at=None,
        status="ready",
        project_ref="projref123",
        name="RF Consultant",
    )
    workspace = SimpleNamespace(
        id=ws_id,
        deleted_at=None,
        selected_plan_slug="pro",
        skip_plan_quota_limits=True,
    )
    settings_row = SimpleNamespace(settings={})
    sub = SimpleNamespace(status="active", plan_slug="pro")
    cloud_settings = SimpleNamespace(
        quota_exempt_workspace_id_set=set(),
        cloud_email_provider="ses",
        cloud_email_from="apps@p2hs.app",
        cloud_email_from_name="P2HS Apps",
        cloud_email_reply_to="",
        home_url="https://p2hs.com",
        cloud_resend_api_key="",
        cloud_mailgun_api_key="",
        cloud_mailgun_domain="",
        cloud_ses_access_key_id="AKIACLOUD",
        cloud_ses_secret_access_key="cloud-secret",
        cloud_grant_jwt_secret="grant-secret-32-chars-minimum!!",
        jwt_secret="fallback",
        cloud_authorization_token_ttl_days=7,
        cloud_grant_ttl_minutes=15,
    )
    monkeypatch.setattr("src.domain.cloud.service.get_settings", lambda: cloud_settings)
    monkeypatch.setattr("src.core.security.cloud_tokens.get_settings", lambda: cloud_settings)
    monkeypatch.setattr(
        "src.integrations.email_provider.get_settings", lambda: cloud_settings
    )

    connector_repo = AsyncMock()
    connector_repo.list_active_email_connections = AsyncMock(return_value=[])

    service = CloudInternalService(
        project_repo=AsyncMock(get_by_id=AsyncMock(return_value=project)),
        workspace_repo=AsyncMock(get_by_id=AsyncMock(return_value=workspace)),
        settings_repo=AsyncMock(get_or_create=AsyncMock(return_value=settings_row)),
        subscription_repo=AsyncMock(get_latest_for_workspace=AsyncMock(return_value=sub)),
        usage_ledger=AsyncMock(),
        storage_usage_repo=AsyncMock(),
        connector_repo=connector_repo,
    )

    out = await service.authorize(
        AuthorizeRequest(
            workspace_id=str(ws_id),
            project_id=str(project_id),
            client_id="cli_projref123",
            handshake_id="hs_123",
            request_type="email.send",
            idempotency_key="idem_email_default",
            request_meta={"origin": "https://app.p2hs.test", "payload_hash": "sha256:abc"},
        )
    )

    assert out.allowed is True
    assert out.policy_snapshot.delivery_scope == "internal"
    assert out.policy_snapshot.provider == "ses"
    assert out.policy_snapshot.connection_id is None
    assert out.policy_snapshot.email_from == "RF Consultant <apps@p2hs.app>"
    assert out.policy_snapshot.email_footer_text
    assert "Want to build your own site?" in out.policy_snapshot.email_footer_text
    assert "https://p2hs.com" in (out.policy_snapshot.email_footer_html or "")


@pytest.mark.asyncio
async def test_cloud_authorize_email_send_denied_without_default_or_connector(
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    from src.domain.cloud.schemas import AuthorizeRequest
    from src.domain.cloud.service import CloudInternalService

    ws_id = uuid4()
    project_id = uuid4()
    project = SimpleNamespace(
        id=project_id,
        workspace_id=ws_id,
        deleted_at=None,
        status="ready",
        project_ref="projref123",
    )
    workspace = SimpleNamespace(
        id=ws_id,
        deleted_at=None,
        selected_plan_slug="pro",
        skip_plan_quota_limits=True,
    )
    settings_row = SimpleNamespace(settings={})
    sub = SimpleNamespace(status="active", plan_slug="pro")
    cloud_settings = SimpleNamespace(
        quota_exempt_workspace_id_set=set(),
        cloud_email_provider="resend",
        cloud_email_from="apps@p2hs.app",
        cloud_resend_api_key="",
        cloud_mailgun_api_key="",
        cloud_mailgun_domain="",
        cloud_ses_access_key_id="",
        cloud_ses_secret_access_key="",
        cloud_grant_jwt_secret="grant-secret-32-chars-minimum!!",
        jwt_secret="fallback",
        cloud_authorization_token_ttl_days=7,
        cloud_grant_ttl_minutes=15,
    )
    monkeypatch.setattr("src.domain.cloud.service.get_settings", lambda: cloud_settings)
    monkeypatch.setattr("src.integrations.email_provider.get_settings", lambda: cloud_settings)

    connector_repo = AsyncMock()
    connector_repo.list_active_email_connections = AsyncMock(return_value=[])

    service = CloudInternalService(
        project_repo=AsyncMock(get_by_id=AsyncMock(return_value=project)),
        workspace_repo=AsyncMock(get_by_id=AsyncMock(return_value=workspace)),
        settings_repo=AsyncMock(get_or_create=AsyncMock(return_value=settings_row)),
        subscription_repo=AsyncMock(get_latest_for_workspace=AsyncMock(return_value=sub)),
        usage_ledger=AsyncMock(),
        storage_usage_repo=AsyncMock(),
        connector_repo=connector_repo,
    )

    out = await service.authorize(
        AuthorizeRequest(
            workspace_id=str(ws_id),
            project_id=str(project_id),
            client_id="cli_projref123",
            handshake_id="hs_123",
            request_type="email.send",
            idempotency_key="idem_email_none",
            request_meta={"origin": "https://app.p2hs.test", "payload_hash": "sha256:abc"},
        )
    )

    assert out.allowed is False
    assert out.reason_code == "email_provider_not_configured"


@pytest.mark.asyncio
async def test_system_grant_auth_email_forces_platform_internal_delivery(
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    from src.domain.cloud.schemas import SystemGrantRequest
    from src.domain.cloud.service import CloudInternalService

    ws_id = uuid4()
    project_id = uuid4()
    project = SimpleNamespace(
        id=project_id,
        workspace_id=ws_id,
        deleted_at=None,
        status="ready",
        project_ref="projref123",
        name="RF Consultant",
    )
    workspace = SimpleNamespace(
        id=ws_id,
        deleted_at=None,
        selected_plan_slug="pro",
        skip_plan_quota_limits=True,
    )
    settings_row = SimpleNamespace(settings={})
    sub = SimpleNamespace(status="active", plan_slug="pro")
    cloud_settings = SimpleNamespace(
        quota_exempt_workspace_id_set=set(),
        cloud_email_provider="ses",
        cloud_email_from="apps@p2hs.app",
        cloud_email_from_name="P2HS Apps",
        cloud_email_reply_to="",
        home_url="https://p2hs.com",
        cloud_resend_api_key="",
        cloud_mailgun_api_key="",
        cloud_mailgun_domain="",
        cloud_ses_access_key_id="AKIACLOUD",
        cloud_ses_secret_access_key="cloud-secret",
        cloud_grant_jwt_secret="grant-secret-32-chars-minimum!!",
        jwt_secret="fallback",
        cloud_authorization_token_ttl_days=7,
        cloud_grant_ttl_minutes=15,
    )
    monkeypatch.setattr("src.domain.cloud.service.get_settings", lambda: cloud_settings)
    monkeypatch.setattr("src.core.security.cloud_tokens.get_settings", lambda: cloud_settings)
    monkeypatch.setattr(
        "src.integrations.email_provider.get_settings", lambda: cloud_settings
    )

    connector_repo = AsyncMock()
    connector_repo.list_active_email_connections = AsyncMock(
        side_effect=AssertionError("auth mail must not look up project connectors")
    )

    service = CloudInternalService(
        project_repo=AsyncMock(get_by_id=AsyncMock(return_value=project)),
        workspace_repo=AsyncMock(get_by_id=AsyncMock(return_value=workspace)),
        settings_repo=AsyncMock(get_or_create=AsyncMock(return_value=settings_row)),
        subscription_repo=AsyncMock(get_latest_for_workspace=AsyncMock(return_value=sub)),
        usage_ledger=AsyncMock(),
        storage_usage_repo=AsyncMock(),
        connector_repo=connector_repo,
    )

    out = await service.authorize_system_job(
        SystemGrantRequest(
            project_id=str(project_id),
            request_type="email.send",
            idempotency_key="auth-reset:token",
            delivery_scope="internal",
        )
    )

    assert out.allowed is True
    assert out.policy_snapshot.delivery_scope == "internal"
    assert out.policy_snapshot.provider == "ses"
    assert out.policy_snapshot.connection_id is None
