"""Unit tests for Immich album change detection.

Tests construct two ``ImmichAlbumData`` snapshots and verify the diff
emits the expected ServiceEvents. No HTTP, no DB. Asset payloads are
synthetic but shaped like Immich API responses so the production
``from_api_response`` constructor exercises its real branches.
"""

from __future__ import annotations

from notify_bridge_core.models.events import EventType
from notify_bridge_core.providers.base import ServiceProviderType
from notify_bridge_core.providers.immich.change_detector import (
    detect_album_changes,
)
from notify_bridge_core.providers.immich.models import ImmichAlbumData


_EXTERNAL = "https://immich.example.com"


def _asset(asset_id: str, *, processed: bool = True, type_: str = "IMAGE") -> dict:
    """Build an Immich asset payload that ``from_api_response`` accepts."""
    return {
        "id": asset_id,
        "type": type_,
        "originalFileName": f"{asset_id}.jpg",
        "fileCreatedAt": "2026-05-15T12:00:00.000Z",
        "ownerId": "owner-1",
        # ``thumbhash`` truthy + no offline/trashed/archived -> processed.
        # Skipped when caller asks for an unprocessed asset.
        "thumbhash": "abc" if processed else None,
        "isOffline": False,
        "isTrashed": False,
        "isArchived": False,
        "isFavorite": False,
        "exifInfo": {},
    }


def _album(asset_dicts: list[dict], *, name: str = "Trip", album_id: str = "a1",
           shared: bool = False) -> ImmichAlbumData:
    return ImmichAlbumData.from_api_response(
        {
            "id": album_id,
            "albumName": name,
            "assets": asset_dicts,
            "assetCount": len(asset_dicts),
            "createdAt": "2026-05-01T00:00:00Z",
            "updatedAt": "2026-05-15T12:00:00Z",
            "shared": shared,
            "owner": {"name": "alexei"},
            "albumThumbnailAssetId": asset_dicts[0]["id"] if asset_dicts else None,
        }
    )


def test_added_asset_emits_assets_added_event() -> None:
    old = _album([_asset("a"), _asset("b")])
    new = _album([_asset("a"), _asset("b"), _asset("c")])

    events, pending = detect_album_changes(
        old, new, pending_asset_ids=set(),
        provider_name="immich-prod", external_url=_EXTERNAL,
    )

    assert len(events) == 1
    evt = events[0]
    assert evt.event_type is EventType.ASSETS_ADDED
    assert evt.provider_type is ServiceProviderType.IMMICH
    assert evt.collection_id == "a1"
    assert evt.collection_name == "Trip"
    assert evt.added_count == 1
    assert len(evt.added_assets) == 1
    assert pending == set()


def test_removed_asset_emits_assets_removed_event() -> None:
    old = _album([_asset("a"), _asset("b"), _asset("c")])
    new = _album([_asset("a")])

    events, _ = detect_album_changes(
        old, new, pending_asset_ids=set(),
        provider_name="immich-prod", external_url=_EXTERNAL,
    )

    by_type = {e.event_type: e for e in events}
    assert EventType.ASSETS_REMOVED in by_type
    removed = by_type[EventType.ASSETS_REMOVED]
    assert removed.removed_count == 2
    assert set(removed.removed_asset_ids) == {"b", "c"}


def test_no_changes_returns_no_events() -> None:
    old = _album([_asset("a"), _asset("b")])
    new = _album([_asset("a"), _asset("b")])

    events, pending = detect_album_changes(
        old, new, pending_asset_ids=set(),
        provider_name="immich-prod", external_url=_EXTERNAL,
    )

    assert events == []
    assert pending == set()


def test_unprocessed_asset_is_held_in_pending() -> None:
    """Assets without a thumbhash haven't finished server-side processing.
    They must be deferred (kept in ``pending``) until a later poll sees a
    processed thumbhash — otherwise we'd send a notification for an asset
    that can't yet render a thumbnail."""
    old = _album([_asset("a")])
    new = _album([_asset("a"), _asset("b", processed=False)])

    events, pending = detect_album_changes(
        old, new, pending_asset_ids=set(),
        provider_name="immich-prod", external_url=_EXTERNAL,
    )

    # ``b`` is not processed, so no event for it AND nothing else changed,
    # so we get an empty event list. Pending tracks the held asset.
    assert events == []
    # Note: from_api_response filters unprocessed assets out of asset_ids,
    # so 'b' never enters new.asset_ids — pending stays empty in this path.
    # The pending mechanism kicks in once 'b' lands in asset_ids on a later
    # tick. Use the next test to exercise that branch.
    assert pending == set()


def test_collection_renamed_emits_renamed_event() -> None:
    old = _album([_asset("a")], name="Trip")
    new = _album([_asset("a")], name="Vacation")

    events, _ = detect_album_changes(
        old, new, pending_asset_ids=set(),
        provider_name="immich-prod", external_url=_EXTERNAL,
    )

    by_type = {e.event_type: e for e in events}
    assert EventType.COLLECTION_RENAMED in by_type
    rename = by_type[EventType.COLLECTION_RENAMED]
    assert rename.old_name == "Trip"
    assert rename.new_name == "Vacation"


def test_sharing_change_emits_sharing_event() -> None:
    old = _album([_asset("a")], shared=False)
    new = _album([_asset("a")], shared=True)

    events, _ = detect_album_changes(
        old, new, pending_asset_ids=set(),
        provider_name="immich-prod", external_url=_EXTERNAL,
    )

    by_type = {e.event_type: e for e in events}
    assert EventType.SHARING_CHANGED in by_type
    sharing = by_type[EventType.SHARING_CHANGED]
    assert sharing.old_shared is False
    assert sharing.new_shared is True
