"""Deep coverage tests for SharePoint Online connector.

Covers additional methods not exercised by existing test suites:
- _convert_to_permissions (user, group, identity, link)
- _process_drive_item (new, deleted, existing, error)
- _check_and_fetch_updated_record (file, page, list_item, unknown)
- _get_page_content (success, not found, empty)
- get_filter_options / _get_site_options / _get_document_library_options
- _sync_user_groups (full flow, empty groups)
- _perform_initial_full_sync / _process_single_group
- _get_site_permissions (SharePoint REST + Graph)
- _sync_site_content (drives, lists, pages)
- reindex_records (mixed updated and non-updated)
- stream_record (FILE, SHAREPOINT_PAGE, unsupported)
- run_incremental_sync
- CountryToRegionMapper.get_region_string / is_valid_region / get_all_*
"""

import asyncio
import logging
import os
import uuid
from datetime import datetime, timezone
from unittest.mock import AsyncMock, MagicMock, PropertyMock, patch

import pytest

from app.config.constants.arangodb import MimeTypes, OriginTypes, ProgressStatus
from app.connectors.core.registry.filters import (
    FilterCollection,
    FilterOperator,
    IndexingFilterKey,
    SyncFilterKey,
)
from app.connectors.sources.microsoft.sharepoint_online.connector import (
    COMPOSITE_SITE_ID_COMMA_COUNT,
    COMPOSITE_SITE_ID_PARTS_COUNT,
    CountryToRegionMapper,
    MicrosoftRegion,
    SharePointConnector,
    SharePointCredentials,
    SharePointRecordType,
    SiteMetadata,
)
from app.models.entities import (
    AppUser,
    AppUserGroup,
    FileRecord,
    Record,
    RecordGroup,
    RecordGroupType,
    RecordType,
    SharePointListItemRecord,
    SharePointListRecord,
    SharePointPageRecord,
)
from app.models.permission import EntityType, Permission, PermissionType


# ===========================================================================
# Helpers
# ===========================================================================


def _make_mock_deps():
    logger = logging.getLogger("test.sharepoint.deep")
    dep = MagicMock()
    dep.org_id = "org-sp-1"
    dep.on_new_app_users = AsyncMock()
    dep.on_new_user_groups = AsyncMock()
    dep.on_new_records = AsyncMock()
    dep.on_new_record_groups = AsyncMock()
    dep.on_record_deleted = AsyncMock()
    dep.on_record_metadata_update = AsyncMock()
    dep.on_record_content_update = AsyncMock()
    dep.on_updated_record_permissions = AsyncMock()
    dep.on_user_group_deleted = AsyncMock(return_value=True)
    dep.get_all_active_users = AsyncMock(return_value=[])
    dep.reindex_existing_records = AsyncMock()

    dsp = MagicMock()
    mock_tx = MagicMock()
    mock_tx.get_record_by_external_id = AsyncMock(return_value=None)
    mock_tx.get_record_group_by_external_id = AsyncMock(return_value=None)
    mock_tx.__aenter__ = AsyncMock(return_value=mock_tx)
    mock_tx.__aexit__ = AsyncMock(return_value=None)
    dsp.transaction.return_value = mock_tx

    cs = MagicMock()
    cs.get_config = AsyncMock()

    return logger, dep, dsp, cs, mock_tx


def _make_connector():
    logger, dep, dsp, cs, tx = _make_mock_deps()
    c = SharePointConnector(logger, dep, dsp, cs, "conn-sp-deep", "team", "test-user-id")
    return c, dep, dsp, cs, tx


# ===========================================================================
# CountryToRegionMapper extended
# ===========================================================================


class TestCountryToRegionMapperExtended:

    def test_get_region_string_known(self):
        assert CountryToRegionMapper.get_region_string("US") == "NAM"

    def test_get_region_string_unknown(self):
        assert CountryToRegionMapper.get_region_string("XX") == "NAM"

    def test_get_region_string_none(self):
        assert CountryToRegionMapper.get_region_string(None) == "NAM"

    def test_is_valid_region(self):
        assert CountryToRegionMapper.is_valid_region("NAM") is True
        assert CountryToRegionMapper.is_valid_region("EUR") is True

    def test_is_valid_region_invalid(self):
        assert CountryToRegionMapper.is_valid_region("INVALID") is False

    def test_get_all_regions(self):
        regions = CountryToRegionMapper.get_all_regions()
        assert isinstance(regions, list)
        assert "NAM" in regions
        assert "EUR" in regions

    def test_get_all_country_codes(self):
        codes = CountryToRegionMapper.get_all_country_codes()
        assert isinstance(codes, list)
        assert "US" in codes
        assert "IN" in codes


# ===========================================================================
# _convert_to_permissions
# ===========================================================================


class TestConvertToPermissions:

    @pytest.mark.asyncio
    async def test_user_permission(self):
        c, *_ = _make_connector()
        perm = MagicMock()
        perm.granted_to_v2 = MagicMock()
        perm.granted_to_v2.user = MagicMock()
        perm.granted_to_v2.user.id = "user-1"
        perm.granted_to_v2.user.additional_data = {"email": "u1@test.com"}
        perm.granted_to_v2.group = None
        perm.granted_to_identities_v2 = None
        perm.link = None
        perm.roles = ["write"]

        result = await c._convert_to_permissions([perm])
        assert len(result) >= 1
        assert result[0].entity_type == EntityType.USER
        assert result[0].external_id == "user-1"

    @pytest.mark.asyncio
    async def test_group_permission(self):
        c, *_ = _make_connector()
        perm = MagicMock()
        perm.granted_to_v2 = MagicMock()
        perm.granted_to_v2.user = None
        perm.granted_to_v2.group = MagicMock()
        perm.granted_to_v2.group.id = "group-1"
        perm.granted_to_v2.group.additional_data = {"email": "g@test.com"}
        perm.granted_to_identities_v2 = None
        perm.link = None
        perm.roles = ["read"]

        result = await c._convert_to_permissions([perm])
        assert len(result) >= 1
        assert result[0].entity_type == EntityType.GROUP

    @pytest.mark.asyncio
    async def test_identity_group_permission(self):
        c, *_ = _make_connector()
        perm = MagicMock()
        perm.granted_to_v2 = None
        identity = MagicMock()
        identity.group = MagicMock()
        identity.group.id = "identity-group-1"
        identity.group.additional_data = {}
        identity.user = None
        perm.granted_to_identities_v2 = [identity]
        perm.link = None
        perm.roles = ["read"]

        result = await c._convert_to_permissions([perm])
        assert len(result) == 1
        assert result[0].entity_type == EntityType.GROUP

    @pytest.mark.asyncio
    async def test_identity_user_permission(self):
        c, *_ = _make_connector()
        perm = MagicMock()
        perm.granted_to_v2 = None
        identity = MagicMock()
        identity.group = None
        identity.user = MagicMock()
        identity.user.id = "identity-user-1"
        identity.user.additional_data = {"email": "iu@test.com"}
        perm.granted_to_identities_v2 = [identity]
        perm.link = None
        perm.roles = ["read"]

        result = await c._convert_to_permissions([perm])
        assert len(result) == 1
        assert result[0].entity_type == EntityType.USER

    @pytest.mark.asyncio
    async def test_anonymous_link_permission(self):
        c, *_ = _make_connector()
        perm = MagicMock()
        perm.granted_to_v2 = None
        perm.granted_to_identities_v2 = None
        perm.link = MagicMock()
        perm.link.scope = "anonymous"
        perm.link.type = "view"
        perm.roles = []

        result = await c._convert_to_permissions([perm])
        assert len(result) == 1
        assert result[0].entity_type == EntityType.ANYONE_WITH_LINK

    @pytest.mark.asyncio
    async def test_org_link_permission(self):
        c, *_ = _make_connector()
        perm = MagicMock()
        perm.granted_to_v2 = None
        perm.granted_to_identities_v2 = None
        perm.link = MagicMock()
        perm.link.scope = "organization"
        perm.link.type = "edit"
        perm.roles = []

        result = await c._convert_to_permissions([perm])
        assert len(result) == 1
        assert result[0].entity_type == EntityType.ORG

    @pytest.mark.asyncio
    async def test_exception_in_permission_continues(self):
        c, *_ = _make_connector()
        bad_perm = MagicMock()
        bad_perm.granted_to_v2 = MagicMock()
        bad_perm.granted_to_v2.user = MagicMock()
        bad_perm.granted_to_v2.user.id = None  # This causes error
        bad_perm.granted_to_v2.user.additional_data = None
        bad_perm.granted_to_v2.group = None
        bad_perm.granted_to_identities_v2 = None
        bad_perm.link = None
        # Make roles access raise
        type(bad_perm).roles = PropertyMock(side_effect=Exception("bad"))

        result = await c._convert_to_permissions([bad_perm])
        # Should not raise, returns empty or partial
        assert isinstance(result, list)

    @pytest.mark.asyncio
    async def test_empty_permissions(self):
        c, *_ = _make_connector()
        result = await c._convert_to_permissions([])
        assert result == []


# ===========================================================================
# _check_and_fetch_updated_record
# ===========================================================================


class TestCheckAndFetchUpdatedRecord:

    @pytest.mark.asyncio
    async def test_no_external_id(self):
        c, *_ = _make_connector()
        record = MagicMock()
        record.external_record_id = None
        record.id = "r1"
        result = await c._check_and_fetch_updated_record(record, [])
        assert result is None

    @pytest.mark.asyncio
    async def test_unsupported_type(self):
        c, *_ = _make_connector()
        record = MagicMock()
        record.external_record_id = "ext-1"
        record.record_type = "UNKNOWN_TYPE"
        record.id = "r1"
        result = await c._check_and_fetch_updated_record(record, [])
        assert result is None

    @pytest.mark.asyncio
    async def test_file_type_delegates(self):
        c, *_ = _make_connector()
        record = MagicMock()
        record.external_record_id = "ext-1"
        record.record_type = RecordType.FILE
        record.id = "r1"
        c._check_and_fetch_updated_file_record = AsyncMock(return_value=("rec", []))
        result = await c._check_and_fetch_updated_record(record, [])
        assert result == ("rec", [])

    @pytest.mark.asyncio
    async def test_page_type_delegates(self):
        c, *_ = _make_connector()
        record = MagicMock()
        record.external_record_id = "ext-1"
        record.record_type = RecordType.SHAREPOINT_PAGE
        record.id = "r1"
        c._check_and_fetch_updated_page_record = AsyncMock(return_value=("page", []))
        result = await c._check_and_fetch_updated_record(record, [])
        assert result == ("page", [])

    @pytest.mark.asyncio
    async def test_list_item_type_delegates(self):
        c, *_ = _make_connector()
        record = MagicMock()
        record.external_record_id = "ext-1"
        record.record_type = RecordType.SHAREPOINT_LIST_ITEM
        record.id = "r1"
        c._check_and_fetch_updated_list_item_record = AsyncMock(return_value=("li", []))
        result = await c._check_and_fetch_updated_record(record, [])
        assert result == ("li", [])

    @pytest.mark.asyncio
    async def test_exception_returns_none(self):
        c, *_ = _make_connector()
        record = MagicMock()
        record.external_record_id = "ext-1"
        record.record_type = RecordType.FILE
        record.id = "r1"
        c._check_and_fetch_updated_file_record = AsyncMock(side_effect=Exception("fail"))
        result = await c._check_and_fetch_updated_record(record, [])
        assert result is None


# ===========================================================================
# reindex_records (deeper paths)
# ===========================================================================


class TestReindexRecordsDeep:

    @pytest.mark.asyncio
    async def test_reindex_with_updated_and_non_updated(self):
        c, dep, dsp, cs, tx = _make_connector()
        c.msgraph_client = MagicMock()
        dep.get_all_active_users = AsyncMock(return_value=[])

        rec1 = MagicMock()
        rec1.id = "r1"
        rec1.external_record_id = "ext-1"
        rec1.record_type = RecordType.FILE

        rec2 = MagicMock()
        rec2.id = "r2"
        rec2.external_record_id = "ext-2"
        rec2.record_type = RecordType.SHAREPOINT_PAGE

        # First record updated, second not
        c._check_and_fetch_updated_record = AsyncMock(
            side_effect=[("updated_rec1", [Permission(email="a@test.com", type=PermissionType.READ, entity_type=EntityType.USER)]), None]
        )

        await c.reindex_records([rec1, rec2])
        dep.on_new_records.assert_called_once()
        dep.reindex_existing_records.assert_called_once()

    @pytest.mark.asyncio
    async def test_reindex_no_msgraph_client(self):
        c, dep, dsp, cs, tx = _make_connector()
        c.msgraph_client = None
        rec = MagicMock()
        rec.id = "r1"
        with pytest.raises(Exception, match="MS Graph client not initialized"):
            await c.reindex_records([rec])

    @pytest.mark.asyncio
    async def test_reindex_record_check_error(self):
        c, dep, dsp, cs, tx = _make_connector()
        c.msgraph_client = MagicMock()
        dep.get_all_active_users = AsyncMock(return_value=[])

        rec = MagicMock()
        rec.id = "r1"
        c._check_and_fetch_updated_record = AsyncMock(side_effect=Exception("error"))
        # Should not raise - errors are caught per record
        await c.reindex_records([rec])


# ===========================================================================
# stream_record
# ===========================================================================


class TestStreamRecord:

    @pytest.mark.asyncio
    async def test_stream_file_record(self):
        c, *_ = _make_connector()
        record = MagicMock()
        record.record_type = RecordType.FILE
        record.record_name = "test.pdf"
        record.mime_type = "application/pdf"
        record.id = "r1"
        c.get_signed_url = AsyncMock(return_value="https://example.com/download")

        with patch("app.connectors.sources.microsoft.sharepoint_online.connector.create_stream_record_response") as mock_stream, \
             patch("app.connectors.sources.microsoft.sharepoint_online.connector.stream_content"):
            mock_stream.return_value = MagicMock()
            result = await c.stream_record(record)
            mock_stream.assert_called_once()

    @pytest.mark.asyncio
    async def test_stream_file_no_signed_url(self):
        c, *_ = _make_connector()
        record = MagicMock()
        record.record_type = RecordType.FILE
        record.id = "r1"
        c.get_signed_url = AsyncMock(return_value=None)

        from fastapi import HTTPException
        with pytest.raises(HTTPException) as exc_info:
            await c.stream_record(record)
        assert exc_info.value.status_code == 404

    @pytest.mark.asyncio
    async def test_stream_page_record(self):
        c, *_ = _make_connector()
        record = MagicMock()
        record.record_type = RecordType.SHAREPOINT_PAGE
        record.record_name = "My Page"
        record.external_record_group_id = "site-1"
        record.external_record_id = "page-1"
        record.id = "r1"
        c._get_page_content = AsyncMock(return_value="<h1>Content</h1>")

        from fastapi.responses import StreamingResponse
        result = await c.stream_record(record)
        assert isinstance(result, StreamingResponse)

    @pytest.mark.asyncio
    async def test_stream_page_no_content(self):
        c, *_ = _make_connector()
        record = MagicMock()
        record.record_type = RecordType.SHAREPOINT_PAGE
        record.external_record_group_id = "site-1"
        record.external_record_id = "page-1"
        record.id = "r1"
        c._get_page_content = AsyncMock(return_value=None)

        from fastapi import HTTPException
        with pytest.raises(HTTPException) as exc_info:
            await c.stream_record(record)
        assert exc_info.value.status_code == 404

    @pytest.mark.asyncio
    async def test_stream_unsupported_type(self):
        c, *_ = _make_connector()
        record = MagicMock()
        record.record_type = "UNKNOWN"
        record.id = "r1"

        from fastapi import HTTPException
        with pytest.raises(HTTPException) as exc_info:
            await c.stream_record(record)
        assert exc_info.value.status_code == 400


# ===========================================================================
# get_filter_options / _get_site_options / _get_document_library_options
# ===========================================================================


class TestGetFilterOptions:

    @pytest.mark.asyncio
    async def test_site_ids_key(self):
        c, *_ = _make_connector()
        c._get_site_options = AsyncMock(return_value="site_opts")
        result = await c.get_filter_options(SyncFilterKey.SITE_IDS, page=1, limit=10)
        assert result == "site_opts"

    @pytest.mark.asyncio
    async def test_drive_ids_key(self):
        c, *_ = _make_connector()
        c._get_document_library_options = AsyncMock(return_value="drive_opts")
        result = await c.get_filter_options(SyncFilterKey.DRIVE_IDS, page=1, limit=10)
        assert result == "drive_opts"

    @pytest.mark.asyncio
    async def test_unsupported_key_raises(self):
        c, *_ = _make_connector()
        with pytest.raises(ValueError, match="Unsupported filter key"):
            await c.get_filter_options("invalid_key")


class TestGetSiteOptions:

    @pytest.mark.asyncio
    async def test_no_results(self):
        c, *_ = _make_connector()
        c.msgraph_client = MagicMock()
        c.msgraph_client.search_query = AsyncMock(return_value=None)
        result = await c._get_site_options(page=1, limit=10, search="test")
        assert result.success is True
        assert result.options == []

    @pytest.mark.asyncio
    async def test_with_results(self):
        c, *_ = _make_connector()
        c.msgraph_client = MagicMock()
        raw = MagicMock()
        raw.additional_data = {
            'value': [{
                'hitsContainers': [{
                    'total': 1,
                    'hits': [{
                        'resource': {
                            'id': 'site-1',
                            'webUrl': 'https://contoso.sharepoint.com/sites/test',
                            'displayName': 'Test Site'
                        }
                    }]
                }]
            }]
        }
        c.msgraph_client.search_query = AsyncMock(return_value=raw)
        result = await c._get_site_options(page=1, limit=10, search="test")
        assert result.success is True
        assert len(result.options) == 1
        assert result.options[0].label == "Test Site"


class TestGetDocumentLibraryOptions:

    @pytest.mark.asyncio
    async def test_no_results(self):
        c, *_ = _make_connector()
        c.msgraph_client = MagicMock()
        c.msgraph_client.search_query = AsyncMock(return_value=None)
        result = await c._get_document_library_options(page=1, limit=10, search="")
        assert result.success is True
        assert result.options == []

    @pytest.mark.asyncio
    async def test_filters_system_libraries(self):
        c, *_ = _make_connector()
        c.msgraph_client = MagicMock()
        raw = MagicMock()
        raw.additional_data = {
            'value': [{
                'hitsContainers': [{
                    'total': 3,
                    'hits': [
                        {
                            'resource': {
                                'id': 'lib-1',
                                'webUrl': 'https://contoso.sharepoint.com/sites/test/Shared%20Documents',
                                'name': 'Shared Documents',
                                'displayName': 'Documents',
                                'parentReference': {'siteId': 's1'}
                            },
                            'summary': 'DocumentLibrary'
                        },
                        {
                            'resource': {
                                'id': 'lib-2',
                                'webUrl': 'https://contoso.sharepoint.com/sites/test/SiteAssets',
                                'name': 'SiteAssets',
                                'displayName': 'Site Assets',
                                'parentReference': {'siteId': 's1'}
                            },
                            'summary': 'DocumentLibrary'
                        },
                        {
                            'resource': {
                                'id': 'lib-3',
                                'webUrl': 'https://contoso.sharepoint.com/sites/test/Lists/Tasks',
                                'name': 'Tasks',
                                'displayName': 'Tasks',
                                'parentReference': {'siteId': 's1'}
                            },
                            'summary': 'GenericList'
                        },
                    ]
                }]
            }]
        }
        c.msgraph_client.search_query = AsyncMock(return_value=raw)
        result = await c._get_document_library_options(page=1, limit=10, search="")
        # Only Shared Documents should pass - SiteAssets is system, Tasks is a List
        assert len(result.options) == 1
        assert "Documents" in result.options[0].label

    @pytest.mark.asyncio
    async def test_filters_onedrive_and_contentstorage(self):
        c, *_ = _make_connector()
        c.msgraph_client = MagicMock()
        raw = MagicMock()
        raw.additional_data = {
            'value': [{
                'hitsContainers': [{
                    'total': 2,
                    'hits': [
                        {
                            'resource': {
                                'id': 'lib-od',
                                'webUrl': 'https://contoso-my.sharepoint.com/personal/user/Documents',
                                'name': 'Documents',
                                'displayName': 'Documents',
                                'parentReference': {'siteId': 's1'}
                            },
                            'summary': ''
                        },
                        {
                            'resource': {
                                'id': 'lib-cs',
                                'webUrl': 'https://contoso.sharepoint.com/contentstorage/test',
                                'name': 'test',
                                'displayName': 'test',
                                'parentReference': {'siteId': 's2'}
                            },
                            'summary': ''
                        },
                    ]
                }]
            }]
        }
        c.msgraph_client.search_query = AsyncMock(return_value=raw)
        result = await c._get_document_library_options(page=1, limit=10, search="")
        assert len(result.options) == 0


# ===========================================================================
# _normalize_document_library_url
# ===========================================================================


class TestNormalizeDocumentLibraryUrl:

    def test_empty(self):
        c, *_ = _make_connector()
        assert c._normalize_document_library_url("") == ""

    def test_full_url_with_forms(self):
        c, *_ = _make_connector()
        url = "https://pipeshubinc.sharepoint.com/sites/ITTeamSite/Shared Documents/Forms/AllItems.aspx"
        result = c._normalize_document_library_url(url)
        assert result == "pipeshubinc.sharepoint.com/sites/itteamsite/shared documents"

    def test_url_encoded(self):
        c, *_ = _make_connector()
        url = "https://pipeshubinc.sharepoint.com/sites/okay/Shared%20Documents"
        result = c._normalize_document_library_url(url)
        assert result == "pipeshubinc.sharepoint.com/sites/okay/shared documents"


# ===========================================================================
# run_incremental_sync
# ===========================================================================


class TestRunIncrementalSync:

    @pytest.mark.asyncio
    async def test_success(self):
        c, *_ = _make_connector()
        c._reinitialize_credential_if_needed = AsyncMock()
        mock_site = MagicMock()
        c._get_all_sites = AsyncMock(return_value=[mock_site])
        c._sync_site_content = AsyncMock()

        await c.run_incremental_sync()
        c._sync_site_content.assert_called_once()

    @pytest.mark.asyncio
    async def test_site_error_continues(self):
        c, *_ = _make_connector()
        c._reinitialize_credential_if_needed = AsyncMock()
        site1 = MagicMock()
        site1.display_name = "S1"
        site1.name = "s1"
        site2 = MagicMock()
        site2.display_name = "S2"
        site2.name = "s2"
        c._get_all_sites = AsyncMock(return_value=[site1, site2])
        c._sync_site_content = AsyncMock(side_effect=[Exception("fail"), None])

        await c.run_incremental_sync()
        assert c._sync_site_content.call_count == 2

    @pytest.mark.asyncio
    async def test_critical_error_raises(self):
        c, *_ = _make_connector()
        c._reinitialize_credential_if_needed = AsyncMock(side_effect=Exception("critical"))
        with pytest.raises(Exception, match="critical"):
            await c.run_incremental_sync()


# ===========================================================================
# get_signed_url
# ===========================================================================


class TestGetSignedUrl:

    @pytest.mark.asyncio
    async def test_non_file_returns_none(self):
        c, *_ = _make_connector()
        c._reinitialize_credential_if_needed = AsyncMock()
        record = MagicMock()
        record.record_type = RecordType.SHAREPOINT_PAGE
        result = await c.get_signed_url(record)
        assert result is None

    @pytest.mark.asyncio
    async def test_no_drive_id(self):
        c, *_ = _make_connector()
        c._reinitialize_credential_if_needed = AsyncMock()
        record = MagicMock()
        record.record_type = RecordType.FILE
        record.external_record_group_id = None
        record.id = "r1"
        result = await c.get_signed_url(record)
        assert result is None

    @pytest.mark.asyncio
    async def test_success(self):
        c, *_ = _make_connector()
        c._reinitialize_credential_if_needed = AsyncMock()
        c.msgraph_client = MagicMock()
        c.msgraph_client.get_signed_url = AsyncMock(return_value="https://signed-url.com")
        record = MagicMock()
        record.record_type = RecordType.FILE
        record.external_record_group_id = "drive-1"
        record.external_record_id = "item-1"
        record.id = "r1"
        result = await c.get_signed_url(record)
        assert result == "https://signed-url.com"


# ===========================================================================
# cleanup
# ===========================================================================


class TestCleanup:

    @pytest.mark.asyncio
    async def test_cleanup_all_resources(self):
        c, *_ = _make_connector()
        c.site_cache = {"a": "b"}
        c.msgraph_client = MagicMock()
        c.client = MagicMock()
        c.credential = MagicMock()
        c.credential.close = AsyncMock()
        c.certificate_path = None

        await c.cleanup()
        assert c.site_cache == {}
        assert c.msgraph_client is None
        assert c.client is None
        assert c.credential is None

    @pytest.mark.asyncio
    async def test_cleanup_with_cert_file(self):
        c, *_ = _make_connector()
        c.credential = None
        c.certificate_path = "/tmp/nonexistent_cert.pem"

        # Should not raise even if file doesn't exist
        await c.cleanup()

    @pytest.mark.asyncio
    async def test_cleanup_credential_close_error(self):
        c, *_ = _make_connector()
        c.credential = MagicMock()
        c.credential.close = AsyncMock(side_effect=Exception("already closed"))
        c.certificate_path = None

        # Should not raise
        await c.cleanup()
        assert c.credential is None


# ===========================================================================
# _handle_record_updates
# ===========================================================================


class TestHandleRecordUpdates:

    @pytest.mark.asyncio
    async def test_deleted_record(self):
        c, dep, *_ = _make_connector()
        from app.connectors.sources.microsoft.common.msgraph_client import RecordUpdate
        update = RecordUpdate(
            record=None,
            external_record_id="ext-1",
            is_new=False,
            is_updated=False,
            is_deleted=True,
            metadata_changed=False,
            content_changed=False,
            permissions_changed=False
        )
        await c._handle_record_updates(update)
        dep.on_record_deleted.assert_called_once()

    @pytest.mark.asyncio
    async def test_metadata_and_permissions_changed(self):
        c, dep, *_ = _make_connector()
        from app.connectors.sources.microsoft.common.msgraph_client import RecordUpdate
        record = MagicMock()
        record.record_name = "test.pdf"
        perms = [Permission(email="u@test.com", type=PermissionType.READ, entity_type=EntityType.USER)]
        update = RecordUpdate(
            record=record,
            is_new=False,
            is_updated=True,
            is_deleted=False,
            metadata_changed=True,
            content_changed=False,
            permissions_changed=True,
            new_permissions=perms
        )
        await c._handle_record_updates(update)
        dep.on_record_metadata_update.assert_called_once_with(record)
        dep.on_updated_record_permissions.assert_called_once()

    @pytest.mark.asyncio
    async def test_content_changed(self):
        c, dep, *_ = _make_connector()
        from app.connectors.sources.microsoft.common.msgraph_client import RecordUpdate
        record = MagicMock()
        record.record_name = "test.pdf"
        update = RecordUpdate(
            record=record,
            is_new=False,
            is_updated=True,
            is_deleted=False,
            metadata_changed=False,
            content_changed=True,
            permissions_changed=False
        )
        await c._handle_record_updates(update)
        dep.on_record_content_update.assert_called_once_with(record)

    @pytest.mark.asyncio
    async def test_error_does_not_raise(self):
        c, dep, *_ = _make_connector()
        from app.connectors.sources.microsoft.common.msgraph_client import RecordUpdate
        dep.on_record_deleted.side_effect = Exception("fail")
        update = RecordUpdate(
            record=None,
            external_record_id="ext-1",
            is_new=False,
            is_updated=False,
            is_deleted=True,
            metadata_changed=False,
            content_changed=False,
            permissions_changed=False
        )
        # Should not raise
        await c._handle_record_updates(update)
