"""Tests for LinkedIn sync service (US2 - Feed Integration).

Tests cover:
- Fetching notifications from LinkedIn API with pagination
- Processing notifications into Signal records
- Mapping authors to Member identities
- Status transitions during sync
- Error handling and recovery
"""

from datetime import UTC, datetime, timedelta
from unittest.mock import MagicMock, patch

import pytest
from django.utils import timezone as django_timezone

from accounts.tests.factories import WorkspaceFactory
from integrations.models import LinkedInSyncStatus
from integrations.services.linkedin_client import LinkedInNotification
from integrations.tests.factories import LinkedInPageFactory
from integrations.tests.fixtures.linkedin_notification_fixtures import (
    SAMPLE_PERSONS,
    mock_comment_notification,
    mock_like_notification,
    mock_share_mention_notification,
    mock_share_notification,
)
from members.models import Member
from messages.models import Signal
from sources.models import Source


def _create_mock_notifications(
    count: int, organization_urn: str = "urn:li:organization:12345"
) -> list[LinkedInNotification]:
    """Create a list of mock LinkedInNotification objects for tests."""
    return [
        LinkedInNotification(
            notification_id=4406000 + i,
            organization_urn=organization_urn,
            action=["SHARE_MENTION", "COMMENT", "SHARE", "LIKE"][i % 4],
            source_post_urn=f"urn:li:activity:{29292929 + i}",
            share_urn=f"urn:li:share:{343443 + i}",
            owner_urn=SAMPLE_PERSONS[list(SAMPLE_PERSONS.keys())[i % len(SAMPLE_PERSONS)]]["urn"],
            text=f"Test notification text {i}",
            landing_page_url=f"https://www.linkedin.com/feed/update/urn:li:activity:{29292929 + i}",
            media_category="NONE",
            last_modified_at=1700000000000 + i * 1000,
            subscriber_urn=SAMPLE_PERSONS[list(SAMPLE_PERSONS.keys())[i % len(SAMPLE_PERSONS)]]["urn"],
        )
        for i in range(count)
    ]


@pytest.fixture
def workspace():
    """Create a test workspace."""
    return WorkspaceFactory()


@pytest.fixture
def linkedin_page(workspace):
    """Create a connected LinkedIn page with tokens."""
    return LinkedInPageFactory(
        workspace=workspace,
        organization_urn="urn:li:organization:12345",
        sync_status=LinkedInSyncStatus.ACTIVE,
    )


@pytest.fixture
def source(workspace, linkedin_page):
    """Create a source linked to the LinkedIn page."""
    source = Source.objects.create(
        workspace=workspace,
        kind="linkedin_page",
        name=linkedin_page.name,
        external_id=linkedin_page.organization_urn,
    )
    # Link the source to the page (LinkedInPage has FK to Source)
    linkedin_page.source = source
    linkedin_page.save(update_fields=["source"])
    return source


@pytest.mark.django_db
class TestLinkedInSyncServiceFetchNotifications:
    """Tests for _fetch_notifications() method."""

    @patch("integrations.services.linkedin_sync.LinkedInClient")
    def test_fetch_notifications_calls_api_with_correct_params(self, mock_client_class, linkedin_page):
        """Test that fetch calls LinkedIn API with organization URN."""
        from integrations.services.linkedin_sync import LinkedInSyncService

        mock_client = MagicMock()
        mock_client_class.return_value = mock_client
        mock_client.get_all_notifications.return_value = []

        service = LinkedInSyncService(linkedin_page)
        service._fetch_notifications()

        mock_client.get_all_notifications.assert_called_once()
        call_kwargs = mock_client.get_all_notifications.call_args.kwargs
        assert call_kwargs.get("organization_urn") == linkedin_page.organization_urn

    @patch("integrations.services.linkedin_sync.LinkedInClient")
    def test_fetch_notifications_handles_multiple_results(self, mock_client_class, linkedin_page):
        """Test that fetch handles multiple notifications from client."""
        from integrations.services.linkedin_client import LinkedInNotification
        from integrations.services.linkedin_sync import LinkedInSyncService

        mock_client = MagicMock()
        mock_client_class.return_value = mock_client

        # Create mock LinkedInNotification objects
        notifications_data = [
            LinkedInNotification(
                notification_id=i,
                organization_urn=linkedin_page.organization_urn,
                action="SHARE_MENTION",
                source_post_urn=f"urn:li:activity:{i}",
                share_urn=f"urn:li:share:{i}",
                owner_urn="urn:li:person:ABC123",
                text=f"Test notification {i}",
                landing_page_url=f"https://www.linkedin.com/feed/update/urn:li:activity:{i}",
                media_category="NONE",
                last_modified_at=1700000000000 + i * 1000,
                subscriber_urn="urn:li:person:ABC123",
            )
            for i in range(25)
        ]

        mock_client.get_all_notifications.return_value = notifications_data

        service = LinkedInSyncService(linkedin_page)
        notifications = service._fetch_notifications()

        # Should receive all notifications (pagination handled by client)
        assert len(notifications) == 25

    @patch("integrations.services.linkedin_sync.LinkedInClient")
    def test_fetch_notifications_handles_empty_response(self, mock_client_class, linkedin_page):
        """Test that fetch handles empty notification list."""
        from integrations.services.linkedin_sync import LinkedInSyncService

        mock_client = MagicMock()
        mock_client_class.return_value = mock_client
        mock_client.get_all_notifications.return_value = []

        service = LinkedInSyncService(linkedin_page)
        notifications = service._fetch_notifications()

        assert notifications == []


@pytest.mark.django_db
class TestLinkedInSyncServiceProcessNotification:
    """Tests for _process_notification() method."""

    @patch("integrations.services.linkedin_sync.LinkedInClient")
    def test_process_share_mention_creates_signal(self, mock_client_class, linkedin_page, source):
        """Test that SHARE_MENTION creates a Signal record."""
        from integrations.services.linkedin_sync import LinkedInSyncService

        notification = mock_share_mention_notification(
            notification_id=12345,
            organization_urn=linkedin_page.organization_urn,
        )

        service = LinkedInSyncService(linkedin_page)
        signal = service._process_notification(notification, source)

        assert signal is not None
        assert signal.signal_type == "mention"
        assert signal.source == source
        assert "12345" in signal.external_id
        assert "SHARE_MENTION" in signal.metadata.get("linkedin", {}).get("action", "")

    @patch("integrations.services.linkedin_sync.LinkedInClient")
    def test_process_comment_creates_signal(self, mock_client_class, linkedin_page, source):
        """Test that COMMENT creates a Signal record."""
        from integrations.services.linkedin_sync import LinkedInSyncService

        notification = mock_comment_notification(
            notification_id=12346,
            organization_urn=linkedin_page.organization_urn,
            comment_text="Great content!",
        )

        service = LinkedInSyncService(linkedin_page)
        signal = service._process_notification(notification, source)

        assert signal is not None
        assert signal.signal_type == "mention"
        assert "Great content!" in signal.body

    @patch("integrations.services.linkedin_sync.LinkedInClient")
    def test_process_share_creates_signal(self, mock_client_class, linkedin_page, source):
        """Test that SHARE creates a Signal record."""
        from integrations.services.linkedin_sync import LinkedInSyncService

        notification = mock_share_notification(
            notification_id=12347,
            organization_urn=linkedin_page.organization_urn,
        )

        service = LinkedInSyncService(linkedin_page)
        signal = service._process_notification(notification, source)

        assert signal is not None
        assert signal.metadata.get("linkedin", {}).get("action") == "SHARE"

    @patch("integrations.services.linkedin_sync.LinkedInClient")
    def test_process_skips_duplicate_notification(self, mock_client_class, linkedin_page, source):
        """Test that duplicate notifications are skipped."""
        from integrations.services.linkedin_sync import LinkedInSyncService

        notification = mock_share_mention_notification(
            notification_id=12348,
            organization_urn=linkedin_page.organization_urn,
        )

        service = LinkedInSyncService(linkedin_page)

        # Process first time
        signal1 = service._process_notification(notification, source)
        assert signal1 is not None

        # Process same notification again
        signal2 = service._process_notification(notification, source)
        assert signal2 is None  # Should skip duplicate

        # Only one signal should exist
        assert Signal.objects.filter(source=source).count() == 1

    @patch("integrations.services.linkedin_sync.LinkedInClient")
    def test_process_extracts_author_from_urn(self, mock_client_class, linkedin_page, source):
        """Test that author info is extracted from person URN."""
        from integrations.services.linkedin_sync import LinkedInSyncService

        notification = mock_share_mention_notification(
            notification_id=12349,
            organization_urn=linkedin_page.organization_urn,
            person_key="john_doe",
        )

        service = LinkedInSyncService(linkedin_page)
        signal = service._process_notification(notification, source)

        # Metadata should contain author URN
        linkedin_meta = signal.metadata.get("linkedin", {})
        assert linkedin_meta.get("author_urn") == SAMPLE_PERSONS["john_doe"]["urn"]


@pytest.mark.django_db
class TestLinkedInSyncServiceMapAuthorToMember:
    """Tests for _map_author_to_member() method."""

    @patch("integrations.services.linkedin_sync.LinkedInClient")
    def test_map_author_resolves_existing_identity(self, mock_client_class, linkedin_page, source, workspace):
        """Test that existing Member is found via Identity."""
        from integrations.services.linkedin_sync import LinkedInSyncService
        from members.models import Identity

        # Create existing member with LinkedIn identity
        member = Member.objects.create(
            workspace=workspace,
            display_name="John Doe",
            email="john@example.com",
        )
        Identity.objects.create(
            member=member,
            provider="linkedin",
            external_id=SAMPLE_PERSONS["john_doe"]["urn"],
            handle="johndoe",
        )

        service = LinkedInSyncService(linkedin_page)
        resolved_member = service._map_author_to_member(
            person_urn=SAMPLE_PERSONS["john_doe"]["urn"],
            workspace=workspace,
        )

        assert resolved_member == member

    @patch("integrations.services.linkedin_sync.LinkedInClient")
    def test_map_author_creates_new_member_if_not_found(self, mock_client_class, linkedin_page, workspace):
        """Test that new Member is created if no existing identity."""
        from integrations.services.linkedin_sync import LinkedInSyncService

        person_urn = "urn:li:person:NEWPERSON123"

        service = LinkedInSyncService(linkedin_page)
        member = service._map_author_to_member(
            person_urn=person_urn,
            workspace=workspace,
            person_name="New Person",
        )

        assert member is not None
        assert member.workspace == workspace
        # Should create identity
        from members.models import Identity

        assert Identity.objects.filter(
            member=member,
            provider="linkedin",
            external_id=person_urn,
        ).exists()


@pytest.mark.django_db
class TestLinkedInSyncServiceSyncPage:
    """Tests for sync_page() main entry point."""

    @patch("integrations.services.linkedin_sync.LinkedInClient")
    def test_sync_page_updates_last_sync_at_on_success(self, mock_client_class, linkedin_page, source):
        """Test that last_sync_at is updated after successful sync."""
        from integrations.services.linkedin_sync import LinkedInSyncService

        mock_client = MagicMock()
        mock_client_class.return_value = mock_client
        mock_client.get_all_notifications.return_value = _create_mock_notifications(2)

        original_last_sync = linkedin_page.last_sync_at

        service = LinkedInSyncService(linkedin_page)
        service.sync_page()

        linkedin_page.refresh_from_db()
        assert linkedin_page.last_sync_at is not None
        if original_last_sync:
            assert linkedin_page.last_sync_at > original_last_sync

    @patch("integrations.services.linkedin_sync.LinkedInClient")
    def test_sync_page_sets_status_to_error_on_failure(self, mock_client_class, linkedin_page, source):
        """Test that status is set to ERROR on API failure."""
        from integrations.exceptions import LinkedInAPIError
        from integrations.services.linkedin_sync import LinkedInSyncService

        mock_client = MagicMock()
        mock_client_class.return_value = mock_client
        mock_client.get_all_notifications.side_effect = LinkedInAPIError("API Error")

        service = LinkedInSyncService(linkedin_page)

        with pytest.raises(LinkedInAPIError):
            service.sync_page()

        linkedin_page.refresh_from_db()
        assert linkedin_page.sync_status == LinkedInSyncStatus.ERROR

    @patch("integrations.services.linkedin_sync.LinkedInClient")
    def test_sync_page_stores_sync_error_message(self, mock_client_class, linkedin_page, source):
        """Test that error message is stored on failure."""
        from integrations.exceptions import LinkedInAPIError
        from integrations.services.linkedin_sync import LinkedInSyncService

        mock_client = MagicMock()
        mock_client_class.return_value = mock_client
        mock_client.get_all_notifications.side_effect = LinkedInAPIError("Rate limit exceeded")

        service = LinkedInSyncService(linkedin_page)

        with pytest.raises(LinkedInAPIError):
            service.sync_page()

        linkedin_page.refresh_from_db()
        assert "Rate limit exceeded" in (linkedin_page.sync_error or "")

    @patch("integrations.services.linkedin_sync.LinkedInClient")
    def test_sync_page_creates_signals_from_notifications(self, mock_client_class, linkedin_page, source):
        """Test that signals are created from fetched notifications."""
        from integrations.services.linkedin_sync import LinkedInSyncService

        mock_client = MagicMock()
        mock_client_class.return_value = mock_client
        mock_client.get_all_notifications.return_value = _create_mock_notifications(
            5, organization_urn=linkedin_page.organization_urn
        )

        service = LinkedInSyncService(linkedin_page)
        stats = service.sync_page()

        assert Signal.objects.filter(source=source).count() == 5
        assert stats["signals_created"] == 5

    @patch("integrations.services.linkedin_sync.LinkedInClient")
    def test_sync_page_sets_status_to_syncing_during_sync(self, mock_client_class, linkedin_page, source):
        """Test that status is SYNCING during sync operation."""
        from integrations.services.linkedin_sync import LinkedInSyncService

        mock_client = MagicMock()
        mock_client_class.return_value = mock_client

        status_during_sync = None

        def capture_status(*args, **kwargs):
            nonlocal status_during_sync
            linkedin_page.refresh_from_db()
            status_during_sync = linkedin_page.sync_status
            return []  # Return empty list for get_all_notifications

        mock_client.get_all_notifications.side_effect = capture_status

        service = LinkedInSyncService(linkedin_page)
        service.sync_page()

        assert status_during_sync == LinkedInSyncStatus.SYNCING

    @patch("integrations.services.linkedin_sync.LinkedInClient")
    def test_sync_page_restores_active_status_after_success(self, mock_client_class, linkedin_page, source):
        """Test that status returns to ACTIVE after successful sync."""
        from integrations.services.linkedin_sync import LinkedInSyncService

        mock_client = MagicMock()
        mock_client_class.return_value = mock_client
        mock_client.get_all_notifications.return_value = []

        service = LinkedInSyncService(linkedin_page)
        service.sync_page()

        linkedin_page.refresh_from_db()
        assert linkedin_page.sync_status == LinkedInSyncStatus.ACTIVE


@pytest.mark.django_db
class TestLinkedInSyncServiceMetadata:
    """Tests for Thread/Signal metadata structure (T055)."""

    @patch("integrations.services.linkedin_sync.LinkedInClient")
    def test_signal_metadata_contains_linkedin_fields(self, mock_client_class, linkedin_page, source):
        """Test that Signal metadata contains correct linkedin fields."""
        from integrations.services.linkedin_sync import LinkedInSyncService

        notification = mock_share_mention_notification(
            notification_id=99001,
            organization_urn=linkedin_page.organization_urn,
        )

        service = LinkedInSyncService(linkedin_page)
        signal = service._process_notification(notification, source)

        linkedin_meta = signal.metadata.get("linkedin", {})

        # Required fields
        assert "action" in linkedin_meta
        assert "author_urn" in linkedin_meta
        assert "notification_id" in linkedin_meta
        assert "source_post" in linkedin_meta

    @patch("integrations.services.linkedin_sync.LinkedInClient")
    def test_linkedin_url_is_properly_formatted(self, mock_client_class, linkedin_page, source):
        """Test that external_url is a valid LinkedIn URL."""
        from integrations.services.linkedin_sync import LinkedInSyncService

        notification = mock_share_mention_notification(
            notification_id=99002,
            organization_urn=linkedin_page.organization_urn,
        )

        service = LinkedInSyncService(linkedin_page)
        signal = service._process_notification(notification, source)

        assert signal.external_url.startswith("https://www.linkedin.com/")

    @patch("integrations.services.linkedin_sync.LinkedInClient")
    def test_action_type_is_preserved_in_metadata(self, mock_client_class, linkedin_page, source):
        """Test that action type is preserved in metadata."""
        from integrations.services.linkedin_sync import LinkedInSyncService

        for action_type, mock_func in [
            ("SHARE_MENTION", mock_share_mention_notification),
            ("COMMENT", mock_comment_notification),
            ("SHARE", mock_share_notification),
            ("LIKE", mock_like_notification),
        ]:
            notification = mock_func(
                notification_id=99000 + hash(action_type) % 1000,
                organization_urn=linkedin_page.organization_urn,
            )

            service = LinkedInSyncService(linkedin_page)
            signal = service._process_notification(notification, source)

            if signal:  # May be None if duplicate
                linkedin_meta = signal.metadata.get("linkedin", {})
                assert linkedin_meta.get("action") == action_type


@pytest.mark.django_db
class TestLinkedInSyncServiceBackfill:
    """Tests for historical backfill functionality (US3 - T069)."""

    @patch("integrations.services.linkedin_sync.LinkedInClient")
    def test_sync_page_full_sync_fetches_without_changed_since(self, mock_client_class, linkedin_page, source):
        """Test that full_sync=True does not send changed_since parameter."""
        from integrations.services.linkedin_sync import LinkedInSyncService

        mock_client = MagicMock()
        mock_client_class.return_value = mock_client
        mock_client.get_all_notifications.return_value = []

        # Set a last_sync_at to ensure it would normally use changed_since
        linkedin_page.last_sync_at = django_timezone.now() - timedelta(hours=1)
        linkedin_page.save(update_fields=["last_sync_at"])

        service = LinkedInSyncService(linkedin_page)
        service.sync_page(full_sync=True)

        # Verify changed_since was NOT passed (should be None for full sync)
        call_kwargs = mock_client.get_all_notifications.call_args.kwargs
        assert call_kwargs.get("changed_since") is None

    @patch("integrations.services.linkedin_sync.LinkedInClient")
    def test_sync_page_incremental_uses_changed_since(self, mock_client_class, linkedin_page, source):
        """Test that incremental sync (full_sync=False) uses changed_since."""
        from integrations.services.linkedin_sync import LinkedInSyncService

        mock_client = MagicMock()
        mock_client_class.return_value = mock_client
        mock_client.get_all_notifications.return_value = []

        # Set last_sync_at
        sync_time = django_timezone.now() - timedelta(hours=1)
        linkedin_page.last_sync_at = sync_time
        linkedin_page.save(update_fields=["last_sync_at"])

        service = LinkedInSyncService(linkedin_page)
        service.sync_page(full_sync=False)

        # Verify changed_since was passed
        call_kwargs = mock_client.get_all_notifications.call_args.kwargs
        assert call_kwargs.get("changed_since") is not None
        # Should be approximately the sync_time in milliseconds
        expected_ms = int(sync_time.timestamp() * 1000)
        assert abs(call_kwargs["changed_since"] - expected_ms) < 1000  # Allow 1 second tolerance

    @patch("integrations.services.linkedin_sync.LinkedInClient")
    def test_backfill_handles_large_result_set(self, mock_client_class, linkedin_page, source):
        """Test that backfill handles 100+ notifications (pagination)."""
        from integrations.services.linkedin_sync import LinkedInSyncService

        mock_client = MagicMock()
        mock_client_class.return_value = mock_client
        # Create 150 mock notifications to simulate large backfill
        mock_client.get_all_notifications.return_value = _create_mock_notifications(
            150, organization_urn=linkedin_page.organization_urn
        )

        service = LinkedInSyncService(linkedin_page)
        stats = service.sync_page(full_sync=True)

        # All 150 should be created
        assert stats["signals_created"] == 150
        assert Signal.objects.filter(source=source).count() == 150

    @patch("integrations.services.linkedin_sync.LinkedInClient")
    def test_backfill_creates_signals_with_historical_timestamps(self, mock_client_class, linkedin_page, source):
        """Test that backfilled Signals have correct historical timestamps."""
        from integrations.services.linkedin_sync import LinkedInSyncService

        mock_client = MagicMock()
        mock_client_class.return_value = mock_client

        # Create notification with known historical timestamp (30 days ago)
        historical_ts_ms = int((datetime.now(UTC) - timedelta(days=30)).timestamp() * 1000)
        notification = LinkedInNotification(
            notification_id=99999,
            organization_urn=linkedin_page.organization_urn,
            action="SHARE_MENTION",
            source_post_urn="urn:li:activity:12345",
            share_urn="urn:li:share:67890",
            owner_urn="urn:li:person:ABC123",
            text="Historical mention",
            landing_page_url="https://www.linkedin.com/feed/update/urn:li:activity:12345",
            media_category="NONE",
            last_modified_at=historical_ts_ms,
            subscriber_urn="urn:li:person:ABC123",
        )
        mock_client.get_all_notifications.return_value = [notification]

        service = LinkedInSyncService(linkedin_page)
        service.sync_page(full_sync=True)

        signal = Signal.objects.get(source=source)
        # occurred_at should be approximately 30 days ago
        expected_time = datetime.fromtimestamp(historical_ts_ms / 1000, tz=UTC)
        assert abs((signal.occurred_at - expected_time).total_seconds()) < 1

    @patch("integrations.services.linkedin_sync.LinkedInClient")
    def test_backfill_deduplicates_existing_notifications(self, mock_client_class, linkedin_page, source):
        """Test that backfill skips notifications that already exist."""
        from integrations.services.linkedin_sync import LinkedInSyncService

        mock_client = MagicMock()
        mock_client_class.return_value = mock_client
        mock_client.get_all_notifications.return_value = _create_mock_notifications(
            10, organization_urn=linkedin_page.organization_urn
        )

        service = LinkedInSyncService(linkedin_page)

        # First sync - creates all 10
        stats1 = service.sync_page(full_sync=True)
        assert stats1["signals_created"] == 10

        # Second sync with same notifications - should skip all
        stats2 = service.sync_page(full_sync=True)
        assert stats2["signals_created"] == 0
        assert stats2["signals_skipped"] == 10

        # Total signals should still be 10
        assert Signal.objects.filter(source=source).count() == 10

    @patch("integrations.services.linkedin_sync.LinkedInClient")
    def test_backfill_handles_mixed_new_and_existing(self, mock_client_class, linkedin_page, source):
        """Test that backfill creates only new notifications when some exist."""
        from integrations.services.linkedin_sync import LinkedInSyncService

        mock_client = MagicMock()
        mock_client_class.return_value = mock_client

        # First sync with 5 notifications
        mock_client.get_all_notifications.return_value = _create_mock_notifications(
            5, organization_urn=linkedin_page.organization_urn
        )

        service = LinkedInSyncService(linkedin_page)
        stats1 = service.sync_page(full_sync=True)
        assert stats1["signals_created"] == 5

        # Second sync with 10 notifications (includes the first 5)
        mock_client.get_all_notifications.return_value = _create_mock_notifications(
            10, organization_urn=linkedin_page.organization_urn
        )

        stats2 = service.sync_page(full_sync=True)
        assert stats2["signals_created"] == 5  # Only 5 new ones
        assert stats2["signals_skipped"] == 5  # First 5 already exist

        # Total should be 10
        assert Signal.objects.filter(source=source).count() == 10


@pytest.mark.django_db
class TestLinkedInSyncServiceBackfillIntegration:
    """Integration tests for backfill with large datasets (US3 - T070)."""

    @patch("integrations.services.linkedin_sync.LinkedInClient")
    def test_backfill_100_plus_notifications_complete(self, mock_client_class, linkedin_page, source):
        """Integration test: backfill with 100+ notifications completes successfully."""
        from integrations.services.linkedin_sync import LinkedInSyncService

        mock_client = MagicMock()
        mock_client_class.return_value = mock_client

        # Create 120 notifications with varying timestamps over 60 days
        notifications = []
        for i in range(120):
            days_ago = i * 0.5  # Spread over ~60 days
            ts_ms = int((datetime.now(UTC) - timedelta(days=days_ago)).timestamp() * 1000)
            notification = LinkedInNotification(
                notification_id=100000 + i,
                organization_urn=linkedin_page.organization_urn,
                action=["SHARE_MENTION", "COMMENT", "SHARE", "LIKE"][i % 4],
                source_post_urn=f"urn:li:activity:{3000000 + i}",
                share_urn=f"urn:li:share:{4000000 + i}",
                owner_urn=SAMPLE_PERSONS[list(SAMPLE_PERSONS.keys())[i % len(SAMPLE_PERSONS)]]["urn"],
                text=f"Backfill test notification {i}",
                landing_page_url=f"https://www.linkedin.com/feed/update/urn:li:activity:{3000000 + i}",
                media_category="NONE",
                last_modified_at=ts_ms,
                subscriber_urn=SAMPLE_PERSONS[list(SAMPLE_PERSONS.keys())[i % len(SAMPLE_PERSONS)]]["urn"],
            )
            notifications.append(notification)

        mock_client.get_all_notifications.return_value = notifications

        service = LinkedInSyncService(linkedin_page)
        stats = service.sync_page(full_sync=True)

        # All 120 should be created
        assert stats["signals_created"] == 120
        assert stats["notifications_fetched"] == 120
        assert stats["signals_skipped"] == 0
        assert len(stats["errors"]) == 0

        # Verify all signals exist with correct data
        signals = Signal.objects.filter(source=source).order_by("occurred_at")
        assert signals.count() == 120

        # Verify timestamps span ~60 days
        oldest = signals.first()
        newest = signals.last()
        time_span = newest.occurred_at - oldest.occurred_at
        assert time_span.days >= 55  # At least 55 days of spread

        # Verify action types are distributed
        action_counts = {}
        for signal in signals:
            action = signal.metadata.get("linkedin", {}).get("action")
            action_counts[action] = action_counts.get(action, 0) + 1

        # Each action type should have ~30 signals (120/4)
        assert set(action_counts.keys()) == {"SHARE_MENTION", "COMMENT", "SHARE", "LIKE"}
        for count in action_counts.values():
            assert count == 30
