"""
Unit tests for the per-tenant OAuth calendar_lookup tool.

Tests:
  - Returns "not configured" message when no tenant_id provided
  - Returns "not connected" message when no ChannelConnection exists for tenant
  - action="list" calls Google Calendar API and returns formatted event list
  - action="check_availability" returns free/busy summary
  - action="create" creates an event and returns confirmation
  - Token refresh write-back: updated credentials written to DB
  - All responses are natural language strings (no raw JSON)
  - API errors return human-readable messages
"""

from __future__ import annotations

import uuid
from unittest.mock import AsyncMock, MagicMock, patch


# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------

_TENANT_ID = str(uuid.uuid4())
_DATE = "2026-03-26"

# Fake encrypted token JSON stored in channel_connections.config
_FAKE_ENCRYPTED_TOKEN = "gAAAAAB..."

# Decrypted token dict (as would come from Google OAuth)
_FAKE_TOKEN_DICT = {
    "token": "ya29.test_access_token",
    "refresh_token": "1//test_refresh_token",
    "token_uri": "https://oauth2.googleapis.com/token",
    "client_id": "test-client-id",
    "client_secret": "test-client-secret",
    "scopes": ["https://www.googleapis.com/auth/calendar"],
}

# Sample Google Calendar events response
_FAKE_EVENTS = {
    "items": [
        {
            "summary": "Team Standup",
            "start": {"dateTime": "2026-03-26T09:00:00+00:00"},
            "end": {"dateTime": "2026-03-26T09:30:00+00:00"},
        },
        {
            "summary": "Sprint Planning",
            "start": {"dateTime": "2026-03-26T14:00:00+00:00"},
            "end": {"dateTime": "2026-03-26T15:00:00+00:00"},
        },
    ]
}

# Common patch targets
_PATCH_ENC = "orchestrator.tools.builtins.calendar_lookup.KeyEncryptionService"
_PATCH_CREDS = "orchestrator.tools.builtins.calendar_lookup.google_credentials_from_token"
_PATCH_BUILD = "orchestrator.tools.builtins.calendar_lookup.build"
_PATCH_SETTINGS = "orchestrator.tools.builtins.calendar_lookup.settings"


def _make_mock_session(conn_config: dict | None = None):
    """Build a mock AsyncSession that returns a ChannelConnection or None."""
    session = AsyncMock()
    mock_result = MagicMock()
    if conn_config is not None:
        mock_conn = MagicMock()
        mock_conn.id = uuid.uuid4()
        mock_conn.config = conn_config
        mock_result.scalar_one_or_none.return_value = mock_conn
    else:
        mock_result.scalar_one_or_none.return_value = None
    session.execute.return_value = mock_result
    session.commit = AsyncMock()
    return session


def _make_enc_mock():
    """Create a mock KeyEncryptionService with decrypt returning the fake token JSON."""
    import json
    mock_enc = MagicMock()
    mock_enc.decrypt.return_value = json.dumps(_FAKE_TOKEN_DICT)
    mock_enc.encrypt.return_value = "new_encrypted_token"
    return mock_enc


def _make_mock_settings():
    """Create mock settings with encryption key configured."""
    mock_settings = MagicMock()
    mock_settings.platform_encryption_key = "test-key"
    mock_settings.platform_encryption_key_previous = ""
    return mock_settings


# ---------------------------------------------------------------------------
# No tenant_id
# ---------------------------------------------------------------------------


async def test_calendar_lookup_no_tenant_id_returns_message():
    """calendar_lookup without tenant_id returns a helpful error message."""
    from orchestrator.tools.builtins.calendar_lookup import calendar_lookup

    result = await calendar_lookup(date=_DATE)
    assert "tenant" in result.lower() or "not available" in result.lower()
    assert isinstance(result, str)


# ---------------------------------------------------------------------------
# Not connected
# ---------------------------------------------------------------------------


async def test_calendar_lookup_not_connected_returns_message():
    """calendar_lookup with no ChannelConnection returns 'not connected' message."""
    from orchestrator.tools.builtins.calendar_lookup import calendar_lookup

    mock_session = _make_mock_session(conn_config=None)

    # Pass _session directly to bypass DB session creation
    result = await calendar_lookup(date=_DATE, tenant_id=_TENANT_ID, _session=mock_session)

    assert "not connected" in result.lower() or "connect" in result.lower()
    assert isinstance(result, str)


# ---------------------------------------------------------------------------
# action="list"
# ---------------------------------------------------------------------------


async def test_calendar_lookup_list_returns_formatted_events():
    """
    action="list" returns a natural-language event list.
    No raw JSON — results are human-readable strings.
    """
    from orchestrator.tools.builtins.calendar_lookup import calendar_lookup

    mock_session = _make_mock_session(conn_config={"token": _FAKE_ENCRYPTED_TOKEN})

    mock_creds = MagicMock()
    mock_creds.token = "ya29.test_access_token"
    mock_creds.expired = False
    mock_creds.valid = True

    mock_service = MagicMock()
    mock_events_list = MagicMock()
    mock_events_list.execute.return_value = _FAKE_EVENTS
    mock_service.events.return_value.list.return_value = mock_events_list

    with (
        patch(_PATCH_ENC) as mock_enc_cls,
        patch(_PATCH_CREDS) as mock_creds_fn,
        patch(_PATCH_BUILD) as mock_build,
        patch(_PATCH_SETTINGS, _make_mock_settings()),
    ):
        mock_enc_cls.return_value = _make_enc_mock()
        mock_creds_fn.return_value = mock_creds
        mock_build.return_value = mock_service

        result = await calendar_lookup(
            date=_DATE,
            action="list",
            tenant_id=_TENANT_ID,
            _session=mock_session,
        )

    assert isinstance(result, str)
    assert "Team Standup" in result
    assert "Sprint Planning" in result
    # No raw JSON
    assert "{" not in result or "items" not in result


async def test_calendar_lookup_list_no_events():
    """action="list" with no events returns a 'no events' message."""
    from orchestrator.tools.builtins.calendar_lookup import calendar_lookup

    mock_session = _make_mock_session(conn_config={"token": _FAKE_ENCRYPTED_TOKEN})

    mock_creds = MagicMock()
    mock_creds.expired = False
    mock_creds.valid = True

    mock_service = MagicMock()
    mock_service.events.return_value.list.return_value.execute.return_value = {"items": []}

    with (
        patch(_PATCH_ENC) as mock_enc_cls,
        patch(_PATCH_CREDS) as mock_creds_fn,
        patch(_PATCH_BUILD) as mock_build,
        patch(_PATCH_SETTINGS, _make_mock_settings()),
    ):
        mock_enc_cls.return_value = _make_enc_mock()
        mock_creds_fn.return_value = mock_creds
        mock_build.return_value = mock_service

        result = await calendar_lookup(
            date=_DATE,
            action="list",
            tenant_id=_TENANT_ID,
            _session=mock_session,
        )

    assert "no event" in result.lower() or "free" in result.lower()


# ---------------------------------------------------------------------------
# action="check_availability"
# ---------------------------------------------------------------------------


async def test_calendar_lookup_check_availability_with_events():
    """action="check_availability" returns busy slot summary when events exist."""
    from orchestrator.tools.builtins.calendar_lookup import calendar_lookup

    mock_session = _make_mock_session(conn_config={"token": _FAKE_ENCRYPTED_TOKEN})

    mock_creds = MagicMock()
    mock_creds.expired = False
    mock_creds.valid = True

    mock_service = MagicMock()
    mock_service.events.return_value.list.return_value.execute.return_value = _FAKE_EVENTS

    with (
        patch(_PATCH_ENC) as mock_enc_cls,
        patch(_PATCH_CREDS) as mock_creds_fn,
        patch(_PATCH_BUILD) as mock_build,
        patch(_PATCH_SETTINGS, _make_mock_settings()),
    ):
        mock_enc_cls.return_value = _make_enc_mock()
        mock_creds_fn.return_value = mock_creds
        mock_build.return_value = mock_service

        result = await calendar_lookup(
            date=_DATE,
            action="check_availability",
            tenant_id=_TENANT_ID,
            _session=mock_session,
        )

    assert isinstance(result, str)
    assert "busy" in result.lower() or "slot" in result.lower() or "standup" in result.lower()


async def test_calendar_lookup_check_availability_free_day():
    """action="check_availability" with no events returns 'entire day is free'."""
    from orchestrator.tools.builtins.calendar_lookup import calendar_lookup

    mock_session = _make_mock_session(conn_config={"token": _FAKE_ENCRYPTED_TOKEN})

    mock_creds = MagicMock()
    mock_creds.expired = False
    mock_creds.valid = True

    mock_service = MagicMock()
    mock_service.events.return_value.list.return_value.execute.return_value = {"items": []}

    with (
        patch(_PATCH_ENC) as mock_enc_cls,
        patch(_PATCH_CREDS) as mock_creds_fn,
        patch(_PATCH_BUILD) as mock_build,
        patch(_PATCH_SETTINGS, _make_mock_settings()),
    ):
        mock_enc_cls.return_value = _make_enc_mock()
        mock_creds_fn.return_value = mock_creds
        mock_build.return_value = mock_service

        result = await calendar_lookup(
            date=_DATE,
            action="check_availability",
            tenant_id=_TENANT_ID,
            _session=mock_session,
        )

    assert "free" in result.lower()


# ---------------------------------------------------------------------------
# action="create"
# ---------------------------------------------------------------------------


async def test_calendar_lookup_create_event():
    """action="create" inserts an event and returns confirmation."""
    from orchestrator.tools.builtins.calendar_lookup import calendar_lookup

    mock_session = _make_mock_session(conn_config={"token": _FAKE_ENCRYPTED_TOKEN})

    mock_creds = MagicMock()
    mock_creds.expired = False
    mock_creds.valid = True

    created_event = {
        "id": "abc123",
        "summary": "Product Demo",
        "start": {"dateTime": "2026-03-26T10:00:00+00:00"},
        "end": {"dateTime": "2026-03-26T11:00:00+00:00"},
    }
    mock_service = MagicMock()
    mock_service.events.return_value.insert.return_value.execute.return_value = created_event

    with (
        patch(_PATCH_ENC) as mock_enc_cls,
        patch(_PATCH_CREDS) as mock_creds_fn,
        patch(_PATCH_BUILD) as mock_build,
        patch(_PATCH_SETTINGS, _make_mock_settings()),
    ):
        mock_enc_cls.return_value = _make_enc_mock()
        mock_creds_fn.return_value = mock_creds
        mock_build.return_value = mock_service

        result = await calendar_lookup(
            date=_DATE,
            action="create",
            event_summary="Product Demo",
            event_start="2026-03-26T10:00:00+00:00",
            event_end="2026-03-26T11:00:00+00:00",
            tenant_id=_TENANT_ID,
            _session=mock_session,
        )

    assert isinstance(result, str)
    assert "created" in result.lower() or "product demo" in result.lower()


async def test_calendar_lookup_create_missing_fields():
    """action="create" without event_summary returns an error message."""
    from orchestrator.tools.builtins.calendar_lookup import calendar_lookup

    mock_session = _make_mock_session(conn_config={"token": _FAKE_ENCRYPTED_TOKEN})

    mock_creds = MagicMock()
    mock_creds.expired = False
    mock_creds.valid = True

    mock_service = MagicMock()

    with (
        patch(_PATCH_ENC) as mock_enc_cls,
        patch(_PATCH_CREDS) as mock_creds_fn,
        patch(_PATCH_BUILD) as mock_build,
        patch(_PATCH_SETTINGS, _make_mock_settings()),
    ):
        mock_enc_cls.return_value = _make_enc_mock()
        mock_creds_fn.return_value = mock_creds
        mock_build.return_value = mock_service

        result = await calendar_lookup(
            date=_DATE,
            action="create",
            # No event_summary, event_start, event_end
            tenant_id=_TENANT_ID,
            _session=mock_session,
        )

    assert isinstance(result, str)
    assert "error" in result.lower() or "required" in result.lower() or "missing" in result.lower()


# ---------------------------------------------------------------------------
# Token refresh write-back
# ---------------------------------------------------------------------------


async def test_calendar_lookup_token_refresh_writeback():
    """
    When credentials.token changes after an API call (refresh occurred),
    the updated token should be encrypted and written back to channel_connections.
    """
    from orchestrator.tools.builtins.calendar_lookup import calendar_lookup

    conn_id = uuid.uuid4()
    mock_session = _make_mock_session(conn_config={"token": _FAKE_ENCRYPTED_TOKEN})
    # Get the mock connection to track updates
    mock_conn = mock_session.execute.return_value.scalar_one_or_none.return_value
    mock_conn.id = conn_id
    mock_conn.config = {"token": _FAKE_ENCRYPTED_TOKEN}

    # Credentials that change token after API call (simulating refresh)
    original_token = "ya29.original_token"
    refreshed_token = "ya29.refreshed_token"

    mock_creds = MagicMock()
    mock_creds.token = original_token
    mock_creds.refresh_token = "1//refresh_token"
    mock_creds.expired = False
    mock_creds.valid = True

    def simulate_api_call_that_refreshes():
        """Simulate the side effect of token refresh during API call."""
        mock_creds.token = refreshed_token
        return {"items": []}

    mock_service = MagicMock()
    mock_service.events.return_value.list.return_value.execute.side_effect = simulate_api_call_that_refreshes

    mock_enc = _make_enc_mock()

    with (
        patch(_PATCH_ENC) as mock_enc_cls,
        patch(_PATCH_CREDS) as mock_creds_fn,
        patch(_PATCH_BUILD) as mock_build,
        patch(_PATCH_SETTINGS, _make_mock_settings()),
    ):
        mock_enc_cls.return_value = mock_enc
        mock_creds_fn.return_value = mock_creds
        mock_build.return_value = mock_service

        await calendar_lookup(
            date=_DATE,
            action="list",
            tenant_id=_TENANT_ID,
            _session=mock_session,
        )

    # encrypt should have been called for write-back
    mock_enc.encrypt.assert_called()
    # session.commit should have been called to persist the updated token
    mock_session.commit.assert_called()
