"""Tests for the Gmail mailbox retrieval connector.

Tests cover:
- GmailConnectorConfig validation (correct/incorrect inputs)
- EntrMailboxRetrievalHandler interface conformance
- Handler construction with config and legacy settings objects
- Lazy import guard behaviour
- list_documents and get_document with mocked HTTP responses
"""

from types import SimpleNamespace
from unittest.mock import AsyncMock, patch

import httpx
import pytest
import pytest_asyncio

from entr_adapter_core.connectors.gmail import (
    EntrMailboxRetrievalHandler,
    GmailConnectorConfig,
)
from entr_adapter_core.handlers import RetrievalHandler
from datetime import date

from entr_adapter_core.schemas import RetrievalPayload


# ── GmailConnectorConfig tests ──────────────────────────────────────────


class TestGmailConnectorConfig:
    """Validate the Pydantic config model for the Gmail connector."""

    def test_valid_config(self):
        config = GmailConnectorConfig(
            tenant_name="Acme",
            gmail_client_id="id-123",
            gmail_client_secret="secret-456",
            gmail_refresh_token="refresh-789",
        )
        assert config.tenant_name == "Acme"
        assert config.TENANT_NAME == "Acme"
        assert config.gmail_client_id == "id-123"
        assert config.GMAIL_CLIENT_ID == "id-123"
        assert config.label_override is None
        assert config.max_results_per_page == 500

    def test_config_with_label_override(self):
        config = GmailConnectorConfig(
            tenant_name="Acme",
            gmail_client_id="id",
            gmail_client_secret="secret",
            gmail_refresh_token="refresh",
            label_override="CUSTOM/Label",
        )
        assert config.label_override == "CUSTOM/Label"

    def test_config_with_custom_max_results(self):
        config = GmailConnectorConfig(
            tenant_name="Acme",
            gmail_client_id="id",
            gmail_client_secret="secret",
            gmail_refresh_token="refresh",
            max_results_per_page=100,
        )
        assert config.max_results_per_page == 100

    def test_config_max_results_upper_bound(self):
        with pytest.raises(Exception):
            GmailConnectorConfig(
                tenant_name="Acme",
                gmail_client_id="id",
                gmail_client_secret="secret",
                gmail_refresh_token="refresh",
                max_results_per_page=501,
            )

    def test_config_max_results_lower_bound(self):
        with pytest.raises(Exception):
            GmailConnectorConfig(
                tenant_name="Acme",
                gmail_client_id="id",
                gmail_client_secret="secret",
                gmail_refresh_token="refresh",
                max_results_per_page=0,
            )

    def test_missing_required_field(self):
        with pytest.raises(Exception):
            GmailConnectorConfig(
                tenant_name="Acme",
                gmail_client_id="id",
                # missing gmail_client_secret and gmail_refresh_token
            )

    def test_uppercase_aliases_match_lowercase(self):
        config = GmailConnectorConfig(
            tenant_name="Test",
            gmail_client_id="cid",
            gmail_client_secret="csec",
            gmail_refresh_token="rtok",
        )
        assert config.TENANT_NAME == config.tenant_name
        assert config.GMAIL_CLIENT_ID == config.gmail_client_id
        assert config.GMAIL_CLIENT_SECRET == config.gmail_client_secret
        assert config.GMAIL_REFRESH_TOKEN == config.gmail_refresh_token


# ── Handler interface conformance ────────────────────────────────────────


class TestHandlerInterface:
    """Verify the handler conforms to the RetrievalHandler interface."""

    def _make_config(self, **overrides):
        defaults = dict(
            tenant_name="TestTenant",
            gmail_client_id="id",
            gmail_client_secret="secret",
            gmail_refresh_token="refresh",
        )
        defaults.update(overrides)
        return GmailConnectorConfig(**defaults)

    def test_extends_retrieval_handler(self):
        assert issubclass(EntrMailboxRetrievalHandler, RetrievalHandler)

    def test_instantiation_with_config(self):
        config = self._make_config()
        handler = EntrMailboxRetrievalHandler(config)
        assert handler.settings is config

    def test_instantiation_with_legacy_settings(self):
        settings = SimpleNamespace(
            TENANT_NAME="Legacy",
            GMAIL_CLIENT_ID="id",
            GMAIL_CLIENT_SECRET="secret",
            GMAIL_REFRESH_TOKEN="refresh",
        )
        handler = EntrMailboxRetrievalHandler(settings)
        assert handler.settings.TENANT_NAME == "Legacy"

    def test_label_override_from_config(self):
        config = self._make_config(label_override="CUSTOM/Label")
        handler = EntrMailboxRetrievalHandler(config)
        assert handler._build_label_name() == "CUSTOM/Label"

    def test_label_override_from_constructor(self):
        config = self._make_config()
        handler = EntrMailboxRetrievalHandler(config, label_override="ARG/Label")
        # Config has no label_override, so constructor arg is used
        assert handler._build_label_name() == "ARG/Label"

    def test_config_label_override_takes_precedence(self):
        config = self._make_config(label_override="CONFIG/Label")
        handler = EntrMailboxRetrievalHandler(config, label_override="ARG/Label")
        # Config label_override should win
        assert handler._build_label_name() == "CONFIG/Label"

    def test_default_label_from_tenant_name(self):
        config = self._make_config(tenant_name="Acme")
        handler = EntrMailboxRetrievalHandler(config)
        assert handler._build_label_name() == "TENANT/Acme"

    def test_has_list_documents_method(self):
        assert callable(getattr(EntrMailboxRetrievalHandler, "list_documents", None))

    def test_has_get_document_method(self):
        assert callable(getattr(EntrMailboxRetrievalHandler, "get_document", None))

    def test_no_hardcoded_credentials(self):
        """Verify the handler has no hardcoded tenant credentials in source."""
        import inspect

        source = inspect.getsource(EntrMailboxRetrievalHandler)
        # Should not contain any hardcoded API keys or tokens
        assert "AKIA" not in source  # AWS key prefix
        assert "1//0" not in source  # Google refresh token prefix
        # Note: .apps.googleusercontent.com appears in docstring examples, not as credentials


# ── Mocked HTTP tests ───────────────────────────────────────────────────


def _make_handler(**overrides):
    defaults = dict(
        tenant_name="TestTenant",
        gmail_client_id="id",
        gmail_client_secret="secret",
        gmail_refresh_token="refresh",
    )
    defaults.update(overrides)
    config = GmailConnectorConfig(**defaults)
    return EntrMailboxRetrievalHandler(config)


class TestListDocuments:
    """Test list_documents with mocked Gmail API responses."""

    @pytest.mark.asyncio
    async def test_list_documents_returns_messages(self):
        handler = _make_handler()

        # Mock token response
        token_resp = httpx.Response(200, json={"access_token": "tok123"})
        # Mock labels response
        labels_resp = httpx.Response(200, json={
            "labels": [{"id": "Label_1", "name": "TENANT/TestTenant"}]
        })
        # Mock messages response
        messages_resp = httpx.Response(200, json={
            "messages": [
                {"id": "msg-1", "threadId": "t1"},
                {"id": "msg-2", "threadId": "t2"},
            ]
        })

        with patch("entr_adapter_core.connectors.gmail.httpx.AsyncClient") as mock_cls:
            mock_client = AsyncMock()
            mock_cls.return_value.__aenter__ = AsyncMock(return_value=mock_client)
            mock_cls.return_value.__aexit__ = AsyncMock(return_value=False)

            mock_client.post.return_value = token_resp
            mock_client.get.side_effect = [labels_resp, messages_resp]

            payload = RetrievalPayload(source_identifier="gmail", date_from=date(2026, 1, 1))
            response = await handler.list_documents(payload)

        assert response.status.value == "success"
        assert len(response.documents) == 2
        assert response.documents[0].document_identifier == "msg-1"
        assert response.documents[0].filename == "msg-1.eml"
        assert response.documents[1].document_identifier == "msg-2"

    @pytest.mark.asyncio
    async def test_list_documents_empty_inbox(self):
        handler = _make_handler()

        token_resp = httpx.Response(200, json={"access_token": "tok"})
        labels_resp = httpx.Response(200, json={
            "labels": [{"id": "Label_1", "name": "TENANT/TestTenant"}]
        })
        messages_resp = httpx.Response(200, json={})

        with patch("entr_adapter_core.connectors.gmail.httpx.AsyncClient") as mock_cls:
            mock_client = AsyncMock()
            mock_cls.return_value.__aenter__ = AsyncMock(return_value=mock_client)
            mock_cls.return_value.__aexit__ = AsyncMock(return_value=False)

            mock_client.post.return_value = token_resp
            mock_client.get.side_effect = [labels_resp, messages_resp]

            payload = RetrievalPayload(source_identifier="gmail", date_from=date(2026, 1, 1))
            response = await handler.list_documents(payload)

        assert response.status.value == "success"
        assert len(response.documents) == 0

    @pytest.mark.asyncio
    async def test_list_documents_label_not_found(self):
        handler = _make_handler()

        token_resp = httpx.Response(200, json={"access_token": "tok"})
        labels_resp = httpx.Response(200, json={
            "labels": [{"id": "Label_1", "name": "TENANT/OtherTenant"}]
        })

        with patch("entr_adapter_core.connectors.gmail.httpx.AsyncClient") as mock_cls:
            mock_client = AsyncMock()
            mock_cls.return_value.__aenter__ = AsyncMock(return_value=mock_client)
            mock_cls.return_value.__aexit__ = AsyncMock(return_value=False)

            mock_client.post.return_value = token_resp
            mock_client.get.return_value = labels_resp

            payload = RetrievalPayload(source_identifier="gmail", date_from=date(2026, 1, 1))
            with pytest.raises(RuntimeError, match="not found"):
                await handler.list_documents(payload)

    @pytest.mark.asyncio
    async def test_list_documents_token_refresh_failure(self):
        handler = _make_handler()

        token_resp = httpx.Response(401, json={"error": "invalid_grant"})

        with patch("entr_adapter_core.connectors.gmail.httpx.AsyncClient") as mock_cls:
            mock_client = AsyncMock()
            mock_cls.return_value.__aenter__ = AsyncMock(return_value=mock_client)
            mock_cls.return_value.__aexit__ = AsyncMock(return_value=False)

            mock_client.post.return_value = token_resp

            payload = RetrievalPayload(source_identifier="gmail", date_from=date(2026, 1, 1))
            with pytest.raises(RuntimeError, match="OAuth token refresh failed"):
                await handler.list_documents(payload)


class TestGetDocument:
    """Test get_document with mocked Gmail API responses."""

    @pytest.mark.asyncio
    async def test_get_document_returns_eml(self):
        handler = _make_handler()

        # Raw RFC822 content, base64url-encoded (Gmail format)
        import base64
        raw_email = b"From: test@example.com\r\nSubject: Test\r\n\r\nHello"
        b64url = base64.urlsafe_b64encode(raw_email).decode().rstrip("=")

        token_resp = httpx.Response(200, json={"access_token": "tok"})
        message_resp = httpx.Response(200, json={"raw": b64url})

        with patch("entr_adapter_core.connectors.gmail.httpx.AsyncClient") as mock_cls:
            mock_client = AsyncMock()
            mock_cls.return_value.__aenter__ = AsyncMock(return_value=mock_client)
            mock_cls.return_value.__aexit__ = AsyncMock(return_value=False)

            mock_client.post.return_value = token_resp
            mock_client.get.return_value = message_resp

            response = await handler.get_document("msg-123")

        assert response.document_identifier == "msg-123.eml"
        assert response.adapter_document_id == "msg-123"
        # Verify the content decodes back to the original email
        decoded = base64.b64decode(response.content_b64)
        assert decoded == raw_email

    @pytest.mark.asyncio
    async def test_get_document_missing_raw_content(self):
        handler = _make_handler()

        token_resp = httpx.Response(200, json={"access_token": "tok"})
        message_resp = httpx.Response(200, json={})  # No "raw" field

        with patch("entr_adapter_core.connectors.gmail.httpx.AsyncClient") as mock_cls:
            mock_client = AsyncMock()
            mock_cls.return_value.__aenter__ = AsyncMock(return_value=mock_client)
            mock_cls.return_value.__aexit__ = AsyncMock(return_value=False)

            mock_client.post.return_value = token_resp
            mock_client.get.return_value = message_resp

            with pytest.raises(RuntimeError, match="No raw content"):
                await handler.get_document("msg-bad")


# ── Pagination test ──────────────────────────────────────────────────────


class TestPagination:
    """Test that list_documents handles multi-page Gmail responses."""

    @pytest.mark.asyncio
    async def test_list_documents_handles_pagination(self):
        handler = _make_handler()

        token_resp = httpx.Response(200, json={"access_token": "tok"})
        labels_resp = httpx.Response(200, json={
            "labels": [{"id": "L1", "name": "TENANT/TestTenant"}]
        })
        # Page 1 with nextPageToken
        page1_resp = httpx.Response(200, json={
            "messages": [{"id": "msg-1", "threadId": "t1"}],
            "nextPageToken": "page2token",
        })
        # Page 2 without nextPageToken (last page)
        page2_resp = httpx.Response(200, json={
            "messages": [{"id": "msg-2", "threadId": "t2"}],
        })

        with patch("entr_adapter_core.connectors.gmail.httpx.AsyncClient") as mock_cls:
            mock_client = AsyncMock()
            mock_cls.return_value.__aenter__ = AsyncMock(return_value=mock_client)
            mock_cls.return_value.__aexit__ = AsyncMock(return_value=False)

            mock_client.post.return_value = token_resp
            mock_client.get.side_effect = [labels_resp, page1_resp, page2_resp]

            payload = RetrievalPayload(source_identifier="gmail", date_from=date(2026, 1, 1))
            response = await handler.list_documents(payload)

        assert len(response.documents) == 2
        assert response.documents[0].document_identifier == "msg-1"
        assert response.documents[1].document_identifier == "msg-2"
