"""Unit tests for the SQLite media index service."""

from typing import Any

import pytest

from media_library_viewer_api.services.media_index import (
    MediaIndex,
    MediaIndexBuildCancelled,
    build_media_index,
)


@pytest.fixture
def index(tmp_path):
    """Create a temporary media index for testing."""
    db_path = tmp_path / "test_index.sqlite"
    return MediaIndex(db_path)


@pytest.fixture
def populated_index(index):
    """Index with sample rows inserted."""
    rows = [
        {
            "id": "m1",
            "title": "The Matrix",
            "series": "",
            "season": "",
            "season_number": None,
            "episode": None,
            "type": "Movie",
            "year": 1999,
            "runtime_ticks": 81600000000,
            "runtime_min": 136,
            "size_bytes": 15_000_000_000,
            "bitrate_bps": 15_000_000,
            "hdr": 0,
            "video": "h264",
            "width": 1920,
            "height": 1080,
            "resolution": "1920x1080",
            "date_added": "2024-01-15",
            "date_added_ts": 1705276800,
            "path": "/media/movies/The Matrix (1999)/file.mkv",
            "library_id": "lib1",
            "library_name": "Movies",
            "size": "15.0 GB",
            "bitrate": "15.0 Mbps",
        },
        {
            "id": "m2",
            "title": "Dune Part Two",
            "series": "",
            "season": "",
            "season_number": None,
            "episode": None,
            "type": "Movie",
            "year": 2024,
            "runtime_ticks": 99600000000,
            "runtime_min": 166,
            "size_bytes": 45_000_000_000,
            "bitrate_bps": 40_000_000,
            "hdr": 1,
            "video": "hevc",
            "width": 3840,
            "height": 2160,
            "resolution": "3840x2160",
            "date_added": "2024-06-01",
            "date_added_ts": 1717200000,
            "path": "/media/movies/Dune Part Two (2024)/file.mkv",
            "library_id": "lib1",
            "library_name": "Movies",
            "size": "45.0 GB",
            "bitrate": "40.0 Mbps",
        },
        {
            "id": "e1",
            "title": "Pilot",
            "series": "Breaking Bad",
            "season": "S01",
            "season_number": 1,
            "episode": 1,
            "type": "Episode",
            "year": 2008,
            "runtime_ticks": 35000000000,
            "runtime_min": 58,
            "size_bytes": 3_000_000_000,
            "bitrate_bps": 8_000_000,
            "hdr": 0,
            "video": "h264",
            "width": 1920,
            "height": 1080,
            "resolution": "1920x1080",
            "date_added": "2024-03-01",
            "date_added_ts": 1709251200,
            "path": "/media/shows/Breaking Bad/S01E01.mkv",
            "library_id": "lib2",
            "library_name": "TV Shows",
            "size": "3.0 GB",
            "bitrate": "8.0 Mbps",
        },
    ]
    index.replace_items(rows)
    return index


class TestMediaIndexStatus:
    def test_nonexistent(self, tmp_path):
        idx = MediaIndex(tmp_path / "nonexistent.sqlite")
        status = idx.status()
        assert status.exists is False
        assert status.item_count == 0

    def test_empty_index(self, index):
        index.init_schema()
        status = index.status()
        assert status.exists is True
        assert status.item_count == 0

    def test_populated(self, populated_index):
        status = populated_index.status()
        assert status.exists is True
        assert status.item_count == 3


class TestMediaIndexQuery:
    def test_query_all(self, populated_index):
        rows, total = populated_index.query(
            library_ids=["lib1", "lib2"],
            media_types=["Movie", "Episode"],
        )
        assert total == 3
        assert len(rows) == 3

    def test_filter_by_library(self, populated_index):
        rows, total = populated_index.query(
            library_ids=["lib1"],
            media_types=["Movie", "Episode"],
        )
        assert total == 2
        # Only movies from lib1
        assert all(r["library"] == "Movies" for r in rows)

    def test_filter_by_type(self, populated_index):
        rows, total = populated_index.query(
            library_ids=["lib1", "lib2"],
            media_types=["Episode"],
        )
        assert total == 1
        assert rows[0]["title"] == "Pilot"

    def test_search(self, populated_index):
        rows, total = populated_index.query(
            library_ids=["lib1", "lib2"],
            media_types=["Movie", "Episode"],
            search="matrix",
        )
        assert total == 1
        assert rows[0]["title"] == "The Matrix"

    def test_hdr_filter(self, populated_index):
        rows, total = populated_index.query(
            library_ids=["lib1", "lib2"],
            media_types=["Movie", "Episode"],
            hdr_filter="HDR only",
        )
        assert total == 1
        assert rows[0]["title"] == "Dune Part Two"

    def test_sdr_filter(self, populated_index):
        rows, total = populated_index.query(
            library_ids=["lib1", "lib2"],
            media_types=["Movie", "Episode"],
            hdr_filter="SDR/unknown only",
        )
        assert total == 2

    def test_sort_by_size_desc(self, populated_index):
        rows, total = populated_index.query(
            library_ids=["lib1", "lib2"],
            media_types=["Movie", "Episode"],
            sort_key="size",
            sort_order="Descending",
        )
        assert rows[0]["title"] == "Dune Part Two"  # largest

    def test_sort_by_year_asc(self, populated_index):
        rows, total = populated_index.query(
            library_ids=["lib1", "lib2"],
            media_types=["Movie", "Episode"],
            sort_key="year",
            sort_order="Ascending",
        )
        assert rows[0]["title"] == "The Matrix"  # 1999

    def test_pagination(self, populated_index):
        rows, total = populated_index.query(
            library_ids=["lib1", "lib2"],
            media_types=["Movie", "Episode"],
            limit=2,
            offset=0,
        )
        assert total == 3
        assert len(rows) == 2

        rows2, total2 = populated_index.query(
            library_ids=["lib1", "lib2"],
            media_types=["Movie", "Episode"],
            limit=2,
            offset=2,
        )
        assert total2 == 3
        assert len(rows2) == 1

    def test_display_row_format(self, populated_index):
        rows, _ = populated_index.query(
            library_ids=["lib1"],
            media_types=["Movie"],
        )
        for row in rows:
            assert "title" in row
            assert "hdr" in row
            assert row["hdr"] in ("yes", "no")
            assert "library" in row
            assert "path" in row


class TestMediaIndexReplace:
    def test_replace_clears_old(self, index):
        index.replace_items(
            [
                {"id": "x", "title": "Old", "type": "Movie", "library_id": "l1", "library_name": "L1"},
            ]
        )
        status = index.status()
        assert status.item_count == 1

        index.replace_items(
            [
                {"id": "y", "title": "New1", "type": "Movie", "library_id": "l1", "library_name": "L1"},
                {"id": "z", "title": "New2", "type": "Movie", "library_id": "l1", "library_name": "L1"},
            ]
        )
        status = index.status()
        assert status.item_count == 2

    def test_replace_scoped_by_service_id_preserves_other_services(self, index):
        """Regression test: building for one Jellyfin must not wipe another's rows.

        Before the migration, ``replace_items`` did ``DELETE FROM media_items``
        (global clear). This test locks in the fix: a scoped replace preserves
        rows belonging to a different service_id.
        """
        rows_a = [
            {"id": "a1", "title": "Alpha Movie", "type": "Movie", "library_id": "l1", "library_name": "L1"},
            {"id": "a2", "title": "Alpha Show", "type": "Episode", "library_id": "l2", "library_name": "L2"},
        ]
        rows_b = [
            {"id": "b1", "title": "Beta Movie", "type": "Movie", "library_id": "l1", "library_name": "L1"},
        ]

        index.replace_items(rows_a, service_id="svc-a")
        assert index.status().item_count == 2

        # Replacing for svc-b must NOT wipe svc-a's rows.
        index.replace_items(rows_b, service_id="svc-b")
        assert index.status().item_count == 3  # 2 from svc-a + 1 from svc-b

        # Querying svc-a returns only its rows.
        rows_a_result, total_a = index.query(
            library_ids=["l1", "l2"],
            media_types=["Movie", "Episode"],
            service_id="svc-a",
        )
        assert total_a == 2
        assert {r["id"] for r in rows_a_result} == {"a1", "a2"}

        # Querying svc-b returns only its rows.
        rows_b_result, total_b = index.query(
            library_ids=["l1"],
            media_types=["Movie"],
            service_id="svc-b",
        )
        assert total_b == 1
        assert rows_b_result[0]["id"] == "b1"

        # Querying with no service_id returns all rows (backward-compat).
        _, total_all = index.query(
            library_ids=["l1", "l2"],
            media_types=["Movie", "Episode"],
        )
        assert total_all == 3


class TestMediaIndexMetadata:
    def test_set_and_read_metadata(self, index):
        index.set_metadata("build_duration_seconds", "12.5")
        status = index.status()
        assert status.build_duration_seconds == 12.5


class TestMediaIndexBuildPaths:
    def test_build_media_index_patches_remote_media_root(self, tmp_path):
        class FakeClient:
            def items(self, **kwargs):
                return {
                    "Items": [
                        {
                            "Id": "m1",
                            "Name": "Movie One",
                            "Type": "Movie",
                            "Path": "/media/movies/Movie One/file.mkv",
                        }
                    ],
                    "TotalRecordCount": 1,
                }

        index = MediaIndex(tmp_path / "index.sqlite")
        count = build_media_index(
            FakeClient(),
            "user1",
            [{"Id": "lib1", "Name": "Movies"}],
            index,
            media_root="/srv/media",
            fallback_prefix="",
        )
        assert count == 1
        rows, total = index.query(library_ids=["lib1"], media_types=["Movie"])
        assert total == 1
        assert rows[0]["path"] == "/srv/media/movies/Movie One/file.mkv"

    def test_build_media_index_reports_progress(self, tmp_path):
        class FakeClient:
            def __init__(self):
                self.calls = []

            def items(self, **kwargs):
                self.calls.append(kwargs.get("start_index", 0))
                if kwargs.get("start_index", 0) == 0:
                    return {
                        "Items": [{"Id": "m1", "Name": "Movie One", "Type": "Movie", "Path": "/media/a.mkv"}],
                        "TotalRecordCount": 2,
                    }
                return {
                    "Items": [{"Id": "m2", "Name": "Movie Two", "Type": "Movie", "Path": "/media/b.mkv"}],
                    "TotalRecordCount": 2,
                }

        events: list[dict[str, Any]] = []
        index = MediaIndex(tmp_path / "index.sqlite")
        count = build_media_index(
            FakeClient(),
            "user1",
            [{"Id": "lib1", "Name": "Movies"}],
            index,
            page_size=1,
            progress_callback=events.append,
        )
        assert count == 2
        assert events[0]["stage"] == "starting"
        assert events[0]["progress"] is None
        assert any(event["stage"] == "building" for event in events)
        building_event = next(event for event in events if event["stage"] == "building")
        assert building_event["library"] == "Movies"
        assert building_event["library_progress"] in (0.5, 1.0)
        assert "elapsed_seconds" in building_event
        assert "eta_seconds" in building_event
        assert events[-1]["stage"] == "completed"
        assert events[-1]["progress"] == 1.0
        assert events[-1]["library_progress"] == 1.0

    def test_build_media_index_can_be_cancelled(self, tmp_path):
        class FakeClient:
            def __init__(self):
                self.calls = []

            def items(self, **kwargs):
                self.calls.append(kwargs.get("start_index", 0))
                if kwargs.get("start_index", 0) == 0:
                    return {
                        "Items": [{"Id": "m1", "Name": "Movie One", "Type": "Movie", "Path": "/media/a.mkv"}],
                        "TotalRecordCount": 2,
                    }
                return {
                    "Items": [{"Id": "m2", "Name": "Movie Two", "Type": "Movie", "Path": "/media/b.mkv"}],
                    "TotalRecordCount": 2,
                }

        events: list[dict[str, Any]] = []
        cancel_after_building = [False]

        def progress_callback(state: dict[str, Any]) -> None:
            events.append(state)
            if state["stage"] == "building":
                cancel_after_building[0] = True

        def should_cancel() -> bool:
            return cancel_after_building[0]

        index = MediaIndex(tmp_path / "index.sqlite")
        client = FakeClient()
        with pytest.raises(MediaIndexBuildCancelled):
            build_media_index(
                client,
                "user1",
                [{"Id": "lib1", "Name": "Movies"}],
                index,
                page_size=1,
                progress_callback=progress_callback,
                should_cancel=should_cancel,
            )
        assert any(event["stage"] == "building" for event in events)
        assert events[-1]["stage"] == "building"
        assert client.calls == [0]
