"""Tests for the Microsoft Entra retrieval connector.

Tests cover:
- MicrosoftConnectorConfig validation
- MicrosoftEntraRetrievalHandler interface conformance
- Handler construction with config and legacy settings
- list_documents and get_document with mocked HTTP responses
- PDF attachment filtering
- Delay days filtering
- Post-download email archival
"""

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

import httpx
import pytest


def _mock_response(status_code=200, json=None, content=None):
    """Create an httpx.Response with a fake request so raise_for_status works."""
    kwargs = {"status_code": status_code}
    if json is not None:
        kwargs["json"] = json
    if content is not None:
        kwargs["content"] = content
    resp = httpx.Response(**kwargs)
    resp._request = httpx.Request("GET", "https://fake")
    return resp

from entr_adapter_core.connectors.microsoft import (
    MicrosoftConnectorConfig,
    MicrosoftEntraRetrievalHandler,
)
from entr_adapter_core.handlers import RetrievalHandler
from entr_adapter_core.schemas import RetrievalPayload


# ── MicrosoftConnectorConfig tests ──────────────────────────────────────


class TestMicrosoftConnectorConfig:

    def test_valid_config(self):
        config = MicrosoftConnectorConfig(
            tenant_name="Acme",
            entra_tenant_id="tid",
            entra_client_id="cid",
            entra_client_secret="csec",
            mailbox="inbox@acme.com",
        )
        assert config.tenant_name == "Acme"
        assert config.TENANT_NAME == "Acme"
        assert config.mailbox == "inbox@acme.com"
        assert config.folder_path == ["Inbox"]
        assert config.delay_days == 0
        assert config.move_to_folder_path is None
        assert config.require_pdf_attachment is False

    def test_config_with_all_options(self):
        config = MicrosoftConnectorConfig(
            tenant_name="Acme",
            entra_tenant_id="tid",
            entra_client_id="cid",
            entra_client_secret="csec",
            mailbox="inbox@acme.com",
            folder_path=["Inbox", "Invoices"],
            delay_days=2,
            move_to_folder_path=["Inbox", "Processed"],
            require_pdf_attachment=True,
        )
        assert config.folder_path == ["Inbox", "Invoices"]
        assert config.delay_days == 2
        assert config.move_to_folder_path == ["Inbox", "Processed"]
        assert config.require_pdf_attachment is True

    def test_uppercase_aliases(self):
        config = MicrosoftConnectorConfig(
            tenant_name="T",
            entra_tenant_id="tid",
            entra_client_id="cid",
            entra_client_secret="csec",
            mailbox="m@t.com",
        )
        assert config.ENTRA_TENANT_ID == "tid"
        assert config.ENTRA_CLIENT_ID == "cid"
        assert config.ENTRA_CLIENT_SECRET == "csec"

    def test_missing_required_field(self):
        with pytest.raises(Exception):
            MicrosoftConnectorConfig(
                tenant_name="Acme",
                entra_tenant_id="tid",
                # missing client_id, client_secret, mailbox
            )

    def test_delay_days_non_negative(self):
        with pytest.raises(Exception):
            MicrosoftConnectorConfig(
                tenant_name="T",
                entra_tenant_id="tid",
                entra_client_id="cid",
                entra_client_secret="csec",
                mailbox="m@t.com",
                delay_days=-1,
            )


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


def _make_config(**overrides):
    defaults = dict(
        tenant_name="TestTenant",
        entra_tenant_id="tid-123",
        entra_client_id="cid-456",
        entra_client_secret="csec-789",
        mailbox="test@example.com",
    )
    defaults.update(overrides)
    return MicrosoftConnectorConfig(**defaults)


class TestHandlerInterface:

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

    def test_instantiation_with_config(self):
        config = _make_config()
        handler = MicrosoftEntraRetrievalHandler(config)
        assert handler.settings is config
        assert handler.mailbox == "test@example.com"
        assert handler.folder_path == ["Inbox"]

    def test_instantiation_with_legacy_settings(self):
        settings = SimpleNamespace(
            TENANT_NAME="Legacy",
            ENTRA_TENANT_ID="tid",
            ENTRA_CLIENT_ID="cid",
            ENTRA_CLIENT_SECRET="csec",
        )
        handler = MicrosoftEntraRetrievalHandler(settings, mailbox="m@t.com")
        assert handler.mailbox == "m@t.com"

    def test_config_options_propagate(self):
        config = _make_config(
            folder_path=["Inbox", "Sub"],
            delay_days=3,
            move_to_folder_path=["Archive"],
            require_pdf_attachment=True,
        )
        handler = MicrosoftEntraRetrievalHandler(config)
        assert handler.folder_path == ["Inbox", "Sub"]
        assert handler.delay_days == 3
        assert handler.move_to_folder_path == ["Archive"]
        assert handler.require_pdf_attachment is True

    def test_missing_mailbox_raises(self):
        settings = SimpleNamespace(
            TENANT_NAME="T",
            ENTRA_TENANT_ID="tid",
            ENTRA_CLIENT_ID="cid",
            ENTRA_CLIENT_SECRET="csec",
        )
        with pytest.raises(ValueError, match="mailbox must be provided"):
            MicrosoftEntraRetrievalHandler(settings)

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

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


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


class TestListDocuments:

    @pytest.mark.asyncio
    async def test_list_documents_success(self):
        handler = MicrosoftEntraRetrievalHandler(_make_config())

        token_resp = _mock_response(200, json={"access_token": "tok", "expires_in": 3600})
        folders_resp = _mock_response(200, json={
            "value": [{"id": "folder-1", "displayName": "Inbox"}]
        })
        messages_resp = _mock_response(200, json={
            "value": [
                {"id": "msg-1", "subject": "Invoice 001"},
                {"id": "msg-2", "subject": "Invoice 002"},
            ]
        })

        with patch("entr_adapter_core.connectors.microsoft.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 = [folders_resp, messages_resp]

            payload = RetrievalPayload(source_identifier="exchange", 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].filename == "Invoice 001.eml"

    @pytest.mark.asyncio
    async def test_list_documents_folder_not_found(self):
        handler = MicrosoftEntraRetrievalHandler(_make_config())

        token_resp = _mock_response(200, json={"access_token": "tok", "expires_in": 3600})
        folders_resp = _mock_response(200, json={
            "value": [{"id": "folder-1", "displayName": "Drafts"}]
        })

        with patch("entr_adapter_core.connectors.microsoft.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 = folders_resp

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

        assert response.status.value == "failed"
        assert "Could not find folder" in response.message

    @pytest.mark.asyncio
    async def test_list_documents_empty(self):
        handler = MicrosoftEntraRetrievalHandler(_make_config())

        token_resp = _mock_response(200, json={"access_token": "tok", "expires_in": 3600})
        folders_resp = _mock_response(200, json={
            "value": [{"id": "f1", "displayName": "Inbox"}]
        })
        messages_resp = _mock_response(200, json={"value": []})

        with patch("entr_adapter_core.connectors.microsoft.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 = [folders_resp, messages_resp]

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

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


class TestGetDocument:

    @pytest.mark.asyncio
    async def test_get_document_success(self):
        handler = MicrosoftEntraRetrievalHandler(_make_config())

        raw_email = b"From: test@example.com\r\nSubject: Test Invoice\r\n\r\nBody"
        token_resp = _mock_response(200, json={"access_token": "tok", "expires_in": 3600})
        mime_resp = _mock_response(200, content=raw_email)

        with patch("entr_adapter_core.connectors.microsoft.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 = mime_resp

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

        assert response.document_identifier == "Test Invoice.eml"
        assert response.adapter_document_id == "msg-123"
        import base64
        assert base64.b64decode(response.content_b64) == raw_email


class TestFilenameGeneration:

    def test_subject_to_filename(self):
        handler = MicrosoftEntraRetrievalHandler(_make_config())
        msg = {"id": "abc123", "subject": "RE: Invoice #42"}
        assert handler._generate_filename_from_message(msg) == "RE_ Invoice #42.eml"

    def test_no_subject_fallback(self):
        handler = MicrosoftEntraRetrievalHandler(_make_config())
        msg = {"id": "abc123def456ghi789jkl", "subject": ""}
        assert handler._generate_filename_from_message(msg) == "abc123def456ghi789jk_email.eml"

    def test_special_characters_cleaned(self):
        handler = MicrosoftEntraRetrievalHandler(_make_config())
        msg = {"id": "x", "subject": 'Test <file> "name" path/to\\file'}
        filename = handler._generate_filename_from_message(msg)
        assert "<" not in filename
        assert '"' not in filename
        assert "\\" not in filename
