"""Integration test: add 'Sacrificial Princess And The King Of Beasts' and verify NFO completeness.

Simulates the production scenario where this anime is added and validates
that the generated tvshow.nfo contains plot, outline, and all other required
information. Also tests the repair path for an incomplete NFO.
"""

from pathlib import Path
from unittest.mock import AsyncMock, patch

import pytest
from lxml import etree

from src.core.services.nfo_repair_service import (
    NfoRepairService,
    _read_tmdb_id,
    find_missing_tags,
    nfo_needs_repair,
)
from src.core.services.nfo_service import NFOService

# ---------------------------------------------------------------------------
# TMDB mock data matching production responses for this anime
# ---------------------------------------------------------------------------
SERIES_KEY = "sacrificial-princess-and-the-king-of-beasts"
SERIES_NAME = "Sacrificial Princess And The King Of Beasts"
SERIES_FOLDER = "Sacrificial Princess And The King Of Beasts (2023)"
TMDB_ID = 222093

MOCK_TMDB_DETAILS = {
    "id": TMDB_ID,
    "name": "Sacrificial Princess and the King of Beasts",
    "original_name": "贄姫と獣の王",
    "overview": (
        "On the outskirts of the Demon King's realm lies a small village of "
        "humans who offer a sacrifice to the beast king every year. Sariphi, "
        "the latest sacrificial girl, expects to be devoured — but instead "
        "her fearless nature catches the king's attention and she becomes "
        "his unlikely companion."
    ),
    "tagline": "A tale of love between a sacrifice and a beast king.",
    "first_air_date": "2023-04-20",
    "last_air_date": "2023-09-28",
    "vote_average": 7.5,
    "vote_count": 150,
    "status": "Ended",
    "episode_run_time": [24],
    "number_of_seasons": 1,
    "number_of_episodes": 24,
    "genres": [
        {"id": 16, "name": "Animation"},
        {"id": 10749, "name": "Romance"},
        {"id": 10765, "name": "Sci-Fi & Fantasy"},
    ],
    "networks": [{"id": 160, "name": "TBS"}],
    "production_companies": [{"id": 291, "name": "J.C.Staff"}],
    "origin_country": ["JP"],
    "poster_path": "/sacrificial_poster.jpg",
    "backdrop_path": "/sacrificial_backdrop.jpg",
    "external_ids": {"imdb_id": "tt19896734", "tvdb_id": 421737},
    "credits": {
        "cast": [
            {
                "id": 2072089,
                "name": "Kana Hanazawa",
                "character": "Sariphi",
                "profile_path": "/hanazawa.jpg",
                "order": 0,
            },
            {
                "id": 1254783,
                "name": "Satoshi Hino",
                "character": "Leonhart",
                "profile_path": "/hino.jpg",
                "order": 1,
            },
        ]
    },
    "images": {"logos": [{"file_path": "/sacrificial_logo.png"}]},
    "seasons": [
        {"season_number": 0, "name": "Specials"},
        {"season_number": 1, "name": "Season 1"},
    ],
}

MOCK_CONTENT_RATINGS = {
    "results": [
        {"iso_3166_1": "DE", "rating": "12"},
        {"iso_3166_1": "US", "rating": "TV-14"},
    ]
}

MOCK_SEARCH_RESULTS = {
    "results": [
        {
            "id": TMDB_ID,
            "name": "Sacrificial Princess and the King of Beasts",
            "first_air_date": "2023-04-20",
            "overview": (
                "On the outskirts of the Demon King's realm lies a small village "
                "of humans who offer a sacrifice to the beast king every year."
            ),
        }
    ]
}

# ---------------------------------------------------------------------------
# Tags that MUST be present and non-empty in a complete NFO
# ---------------------------------------------------------------------------
REQUIRED_TAGS = [
    "title",
    "originaltitle",
    "year",
    "plot",
    "outline",
    "runtime",
    "premiered",
    "status",
    "tmdbid",
    "imdbid",
    "genre",
    "studio",
    "country",
    "watched",
]


@pytest.fixture
def anime_dir(tmp_path: Path) -> Path:
    """Temporary anime directory."""
    d = tmp_path / "anime"
    d.mkdir()
    return d


@pytest.fixture
def nfo_service(anime_dir: Path) -> NFOService:
    """NFOService configured for the temp directory."""
    return NFOService(
        tmdb_api_key="test_api_key",
        anime_directory=str(anime_dir),
        image_size="w500",
        auto_create=True,
    )


def _mock_tmdb_calls(nfo_service: NFOService):
    """Context manager that patches all TMDB calls with mock data."""
    return _PatchContext(nfo_service)


class _PatchContext:
    """Helper to patch TMDB calls on an NFOService instance."""

    def __init__(self, svc: NFOService):
        self._svc = svc
        self._patches = []

    def __enter__(self):
        p1 = patch.object(
            self._svc.tmdb_client, "search_tv_show", new_callable=AsyncMock
        )
        p2 = patch.object(
            self._svc.tmdb_client, "get_tv_show_details", new_callable=AsyncMock
        )
        p3 = patch.object(
            self._svc.tmdb_client, "get_tv_show_content_ratings", new_callable=AsyncMock
        )
        p4 = patch.object(
            self._svc.image_downloader, "download_all_media", new_callable=AsyncMock
        )
        p5 = patch.object(
            self._svc.tmdb_client, "_ensure_session", new_callable=AsyncMock
        )
        p6 = patch.object(
            self._svc.tmdb_client, "close", new_callable=AsyncMock
        )

        self._patches = [p1, p2, p3, p4, p5, p6]
        mocks = [p.start() for p in self._patches]

        mocks[0].return_value = MOCK_SEARCH_RESULTS
        mocks[1].return_value = MOCK_TMDB_DETAILS
        mocks[2].return_value = MOCK_CONTENT_RATINGS
        mocks[3].return_value = {"poster": True, "logo": True, "fanart": True}

        return self

    def __exit__(self, *args):
        for p in self._patches:
            p.stop()


class TestSacrificialPrincessNFO:
    """Tests for 'Sacrificial Princess And The King Of Beasts' NFO generation."""

    @pytest.mark.asyncio
    async def test_add_anime_creates_complete_nfo(
        self, nfo_service: NFOService, anime_dir: Path
    ) -> None:
        """Adding the anime produces an NFO with all required tags filled."""
        series_path = anime_dir / SERIES_FOLDER
        series_path.mkdir()

        with _PatchContext(nfo_service):
            nfo_path = await nfo_service.create_tvshow_nfo(
                serie_name=SERIES_NAME,
                serie_folder=SERIES_FOLDER,
                year=2023,
                download_poster=True,
                download_logo=True,
                download_fanart=True,
            )

        assert nfo_path.exists(), f"NFO not created at {nfo_path}"

        root = etree.parse(str(nfo_path)).getroot()
        missing = []
        for tag in REQUIRED_TAGS:
            elems = root.findall(f".//{tag}")
            if not elems or not any((e.text or "").strip() for e in elems):
                missing.append(tag)

        # Actor check
        actors = root.findall(".//actor/name")
        if not actors or not any((a.text or "").strip() for a in actors):
            missing.append("actor/name")

        assert not missing, (
            f"Missing or empty tags in NFO for '{SERIES_NAME}':\n"
            f"  {', '.join(missing)}\n\n"
            f"NFO content:\n{nfo_path.read_text(encoding='utf-8')}"
        )

    @pytest.mark.asyncio
    async def test_nfo_plot_and_outline_are_meaningful(
        self, nfo_service: NFOService, anime_dir: Path
    ) -> None:
        """Plot and outline must contain substantial descriptive text."""
        series_path = anime_dir / SERIES_FOLDER
        series_path.mkdir()

        with _PatchContext(nfo_service):
            nfo_path = await nfo_service.create_tvshow_nfo(
                serie_name=SERIES_NAME,
                serie_folder=SERIES_FOLDER,
                year=2023,
                download_poster=False,
                download_logo=False,
                download_fanart=False,
            )

        root = etree.parse(str(nfo_path)).getroot()

        plot = (root.findtext(".//plot") or "").strip()
        outline = (root.findtext(".//outline") or "").strip()

        assert len(plot) >= 20, f"Plot too short ({len(plot)} chars): {plot!r}"
        assert len(outline) >= 20, f"Outline too short ({len(outline)} chars): {outline!r}"

        # Should mention relevant keywords from the series
        combined = (plot + outline).lower()
        assert any(
            kw in combined for kw in ("sacrifice", "beast", "king", "sariphi")
        ), f"Plot/outline missing expected content:\n  plot={plot!r}\n  outline={outline!r}"

    @pytest.mark.asyncio
    async def test_nfo_specific_values(
        self, nfo_service: NFOService, anime_dir: Path
    ) -> None:
        """Verify specific metadata values match the anime."""
        series_path = anime_dir / SERIES_FOLDER
        series_path.mkdir()

        with _PatchContext(nfo_service):
            nfo_path = await nfo_service.create_tvshow_nfo(
                serie_name=SERIES_NAME,
                serie_folder=SERIES_FOLDER,
                year=2023,
                download_poster=False,
                download_logo=False,
                download_fanart=False,
            )

        root = etree.parse(str(nfo_path)).getroot()

        assert root.findtext(".//year") == "2023"
        assert root.findtext(".//status") == "Ended"
        assert root.findtext(".//tmdbid") == str(TMDB_ID)
        assert root.findtext(".//imdbid") == "tt19896734"
        assert root.findtext(".//watched") == "false"
        assert root.findtext(".//premiered") == "2023-04-20"

        genres = [g.text for g in root.findall(".//genre") if g.text]
        assert "Animation" in genres

    @pytest.mark.asyncio
    async def test_incomplete_nfo_detected_as_needing_repair(
        self, anime_dir: Path
    ) -> None:
        """An NFO with only a <title> tag is detected as incomplete."""
        series_path = anime_dir / SERIES_FOLDER
        series_path.mkdir()
        nfo_path = series_path / "tvshow.nfo"

        # Simulate production state: minimal NFO with only title
        nfo_path.write_text(
            '<?xml version="1.0" encoding="UTF-8"?>\n'
            "<tvshow>\n"
            f"  <title>{SERIES_NAME}</title>\n"
            "</tvshow>\n",
            encoding="utf-8",
        )

        assert nfo_needs_repair(nfo_path) is True

        missing = find_missing_tags(nfo_path)
        # All these should be detected as missing
        for tag_label in ["plot", "year", "runtime", "premiered", "genre", "studio"]:
            assert tag_label in missing, f"'{tag_label}' not detected as missing"

    @pytest.mark.asyncio
    async def test_repair_fixes_incomplete_nfo(
        self, nfo_service: NFOService, anime_dir: Path
    ) -> None:
        """NfoRepairService re-fetches and creates a complete NFO from an incomplete one."""
        series_path = anime_dir / SERIES_FOLDER
        series_path.mkdir()
        nfo_path = series_path / "tvshow.nfo"

        # Write an incomplete NFO with a tmdbid so update_tvshow_nfo can work
        nfo_path.write_text(
            '<?xml version="1.0" encoding="UTF-8"?>\n'
            "<tvshow>\n"
            f"  <title>{SERIES_NAME}</title>\n"
            f"  <tmdbid>{TMDB_ID}</tmdbid>\n"
            "</tvshow>\n",
            encoding="utf-8",
        )

        assert nfo_needs_repair(nfo_path) is True

        # Patch TMDB calls for the update path
        with patch.object(
            nfo_service.tmdb_client, "_ensure_session", new_callable=AsyncMock
        ), patch.object(
            nfo_service.tmdb_client, "get_tv_show_details", new_callable=AsyncMock
        ) as mock_details, patch.object(
            nfo_service.tmdb_client, "get_tv_show_content_ratings", new_callable=AsyncMock
        ) as mock_ratings, patch.object(
            nfo_service.tmdb_client, "close", new_callable=AsyncMock
        ):
            mock_details.return_value = MOCK_TMDB_DETAILS
            mock_ratings.return_value = MOCK_CONTENT_RATINGS

            repair_service = NfoRepairService(nfo_service)
            repaired = await repair_service.repair_series(series_path, SERIES_FOLDER)

        assert repaired is True

        # After repair, NFO should be complete
        assert nfo_needs_repair(nfo_path) is False

        # Verify content
        root = etree.parse(str(nfo_path)).getroot()
        plot = (root.findtext(".//plot") or "").strip()
        assert len(plot) >= 20, f"Plot still incomplete after repair: {plot!r}"

    @pytest.mark.asyncio
    async def test_repair_recreates_nfo_without_tmdb_id(
        self, nfo_service: NFOService, anime_dir: Path
    ) -> None:
        """If the NFO has no <tmdbid>, repair falls back to create_tvshow_nfo."""
        series_path = anime_dir / SERIES_FOLDER
        series_path.mkdir()
        nfo_path = series_path / "tvshow.nfo"

        # Simulate the production worst-case: only a title, no TMDB ID
        nfo_path.write_text(
            '<?xml version="1.0" encoding="UTF-8"?>\n'
            "<tvshow>\n"
            f"  <title>{SERIES_NAME}</title>\n"
            "</tvshow>\n",
            encoding="utf-8",
        )

        assert _read_tmdb_id(nfo_path) is None
        assert nfo_needs_repair(nfo_path) is True

        with _PatchContext(nfo_service):
            repair_service = NfoRepairService(nfo_service)
            repaired = await repair_service.repair_series(series_path, SERIES_FOLDER)

        assert repaired is True
        assert nfo_path.exists()
        assert nfo_needs_repair(nfo_path) is False

        root = etree.parse(str(nfo_path)).getroot()
        plot = (root.findtext(".//plot") or "").strip()
        assert len(plot) >= 20, f"Plot incomplete after recreate: {plot!r}"
        assert root.findtext(".//tmdbid") == str(TMDB_ID)

    @pytest.mark.asyncio
    async def test_complete_nfo_not_repaired(
        self, nfo_service: NFOService, anime_dir: Path
    ) -> None:
        """A complete NFO should not trigger a repair."""
        series_path = anime_dir / SERIES_FOLDER
        series_path.mkdir()

        # First create a complete NFO
        with _PatchContext(nfo_service):
            await nfo_service.create_tvshow_nfo(
                serie_name=SERIES_NAME,
                serie_folder=SERIES_FOLDER,
                year=2023,
                download_poster=False,
                download_logo=False,
                download_fanart=False,
            )

        nfo_path = series_path / "tvshow.nfo"
        assert nfo_path.exists()
        assert nfo_needs_repair(nfo_path) is False

        # Repair should be skipped
        repair_service = NfoRepairService(nfo_service)
        repaired = await repair_service.repair_series(series_path, SERIES_FOLDER)
        assert repaired is False
