"""Tests for anime API endpoints."""
import asyncio
from unittest.mock import AsyncMock, MagicMock, patch

import pytest
from httpx import ASGITransport, AsyncClient

from src.server.api import anime as anime_module
from src.server.fastapi_app import app
from src.server.services.auth_service import auth_service


class FakeSerie:
    """Mock Serie object for testing.
    
    Note on identifiers:
    - key: Provider-assigned URL-safe identifier (e.g., 'attack-on-titan')
    - folder: Filesystem folder name for metadata only (e.g., 'Attack on Titan (2013)')
    
    The 'key' is the primary identifier used for all lookups and operations.
    The 'folder' is metadata only, not used for identification.
    """

    def __init__(self, key, name, folder, episodeDict=None):
        """Initialize fake serie.
        
        Args:
            key: Provider-assigned URL-safe key (primary identifier)
            name: Display name for the series
            folder: Filesystem folder name (metadata only)
            episodeDict: Dictionary of missing episodes
        """
        self.key = key
        self.name = name
        self.folder = folder
        self.episodeDict = episodeDict or {}
        self.site = "aniworld.to"  # Add site attribute


class FakeSeriesApp:
    """Mock SeriesApp for testing."""

    def __init__(self):
        """Initialize fake series app."""
        self.list = self  # Changed from self.List to self.list
        self.serie_scanner = FakeScanner()  # Add fake scanner (matches SeriesApp attribute name)
        self.directory = "/tmp/fake_anime"
        self.keyDict = {}  # Add keyDict for direct access
        self._items = [
            # Using realistic key values (URL-safe, lowercase, hyphenated)
            FakeSerie("test-show-key", "Test Show", "Test Show (2023)", {1: [1, 2]}),
            FakeSerie("complete-show-key", "Complete Show", "Complete Show (2022)", {}),
        ]
        # Populate keyDict
        for item in self._items:
            self.keyDict[item.key] = item

    def GetMissingEpisode(self):
        """Return series with missing episodes."""
        return [s for s in self._items if s.episodeDict]

    def GetList(self):
        """Return all series."""
        return self._items

    def ReScan(self, callback):
        """Trigger rescan with callback."""
        callback()

    def add(self, serie, use_sanitized_folder=True):
        """Add a serie to the list.
        
        Args:
            serie: The Serie instance to add
            use_sanitized_folder: Whether to use sanitized folder name
        
        Returns:
            str: The folder path (fake path for testing)
        """
        # Check if already exists
        if not any(s.key == serie.key for s in self._items):
            self._items.append(serie)
            self.keyDict[serie.key] = serie
        return f"/tmp/fake_anime/{serie.folder}"

    async def search(self, query):
        """Search for series (async)."""
        # Return mock search results
        return [
            {
                "key": "test-result",
                "name": "Test Search Result",
                "site": "aniworld.to",
                "folder": "test-result",
                "link": "https://aniworld.to/anime/test",
                "missing_episodes": {},
            }
        ]

    def refresh_series_list(self):
        """Refresh series list."""
        pass


class FakeScanner:
    """Mock SerieScanner for testing."""
    
    def scan_single_series(self, key, folder):
        """Mock scan that returns some fake missing episodes."""
        return {1: [1, 2, 3], 2: [1, 2]}


@pytest.fixture(autouse=True)
def reset_auth_state():
    """Reset auth service state before each test."""
    if hasattr(auth_service, '_failed'):
        auth_service._failed.clear()
    yield
    if hasattr(auth_service, '_failed'):
        auth_service._failed.clear()


@pytest.fixture(autouse=True)
def mock_series_app_dependency():
    """Override the series_app dependency with FakeSeriesApp."""
    from src.server.services.background_loader_service import (
        get_background_loader_service,
    )
    from src.server.utils.dependencies import get_series_app
    
    fake_app = FakeSeriesApp()
    app.dependency_overrides[get_series_app] = lambda: fake_app
    
    # Mock background loader service
    mock_background_loader = AsyncMock()
    mock_background_loader.add_series_loading_task = AsyncMock()
    app.dependency_overrides[get_background_loader_service] = lambda: mock_background_loader
    
    yield fake_app
    
    # Clean up
    app.dependency_overrides.clear()


@pytest.fixture
async def authenticated_client():
    """Create authenticated async client."""
    if not auth_service.is_configured():
        auth_service.setup_master_password("TestPass123!")

    transport = ASGITransport(app=app)
    async with AsyncClient(transport=transport, base_url="http://test") as client:
        # Login to get token
        r = await client.post(
            "/api/auth/login", json={"password": "TestPass123!"}
        )
        if r.status_code == 200:
            token = r.json()["access_token"]
            client.headers["Authorization"] = f"Bearer {token}"
        yield client


def test_get_anime_detail_direct_call():
    """Test get_anime function directly.
    
    Uses the series key (test-show-key) for lookup, not the folder name.
    """
    fake = FakeSeriesApp()
    # Use the series key (primary identifier) for lookup
    result = asyncio.run(
        anime_module.get_anime("test-show-key", series_app=fake)
    )
    assert result.title == "Test Show"
    assert "1-1" in result.episodes


def test_rescan_direct_call():
    """Test trigger_rescan function directly."""
    from unittest.mock import AsyncMock

    from src.server.services.anime_service import AnimeService

    # Create a mock anime service
    mock_anime_service = AsyncMock(spec=AnimeService)
    mock_anime_service.rescan = AsyncMock()
    
    result = asyncio.run(
        anime_module.trigger_rescan(anime_service=mock_anime_service)
    )
    assert result["success"] is True
    mock_anime_service.rescan.assert_called_once()


@pytest.mark.asyncio
async def test_list_anime_endpoint_unauthorized():
    """Test GET /api/anime without authentication.
    
    Should return 401 since authentication is required.
    """
    transport = ASGITransport(app=app)
    async with AsyncClient(transport=transport, base_url="http://test") as client:
        response = await client.get("/api/anime/")
        # Should return 401 since this endpoint requires authentication
        assert response.status_code == 401


@pytest.mark.asyncio
async def test_rescan_endpoint_unauthorized():
    """Test POST /api/anime/rescan without authentication."""
    transport = ASGITransport(app=app)
    async with AsyncClient(transport=transport, base_url="http://test") as client:
        response = await client.post("/api/anime/rescan")
        # Should require auth
        assert response.status_code == 401


@pytest.mark.asyncio
async def test_search_anime_endpoint_unauthorized():
    """Test GET /api/anime/search without authentication.
    
    This endpoint is intentionally public for read-only access.
    """
    transport = ASGITransport(app=app)
    async with AsyncClient(
        transport=transport, base_url="http://test"
    ) as client:
        response = await client.get(
            "/api/anime/search", params={"query": "test"}
        )
        # Should return 200 since this is a public endpoint
        assert response.status_code == 200
        assert isinstance(response.json(), list)


@pytest.mark.asyncio
async def test_get_anime_detail_endpoint_unauthorized():
    """Test GET /api/v1/anime/{id} without authentication."""
    transport = ASGITransport(app=app)
    async with AsyncClient(transport=transport, base_url="http://test") as client:
        response = await client.get("/api/v1/anime/1")
        # Should work or require auth
        assert response.status_code in (200, 401, 404, 503)


@pytest.mark.asyncio
async def test_add_series_endpoint_unauthorized():
    """Test POST /api/anime/add without authentication."""
    transport = ASGITransport(app=app)
    async with AsyncClient(transport=transport, base_url="http://test") as client:
        response = await client.post(
            "/api/anime/add",
            json={"link": "test-link", "name": "Test Anime"}
        )
        # Should require auth
        assert response.status_code == 401


@pytest.mark.asyncio
async def test_add_series_endpoint_authenticated(authenticated_client):
    """Test POST /api/anime/add with authentication."""
    response = await authenticated_client.post(
        "/api/anime/add",
        json={"link": "test-anime-link", "name": "Test New Anime"}
    )
    
    # The endpoint should succeed with 202 Accepted (async operation)
    assert response.status_code == 202
    data = response.json()
    assert data["status"] == "success"
    assert "Test New Anime" in data["message"]


@pytest.mark.asyncio
async def test_add_series_endpoint_empty_name(authenticated_client):
    """Test POST /api/anime/add with empty name."""
    response = await authenticated_client.post(
        "/api/anime/add",
        json={"link": "test-link", "name": ""}
    )
    
    # Should return 400 for empty name
    assert response.status_code == 400
    data = response.json()
    assert "name" in data["detail"].lower()


@pytest.mark.asyncio
async def test_add_series_endpoint_empty_link(authenticated_client):
    """Test POST /api/anime/add with empty link."""
    response = await authenticated_client.post(
        "/api/anime/add",
        json={"link": "", "name": "Test Anime"}
    )
    
    # Should return 400 for empty link
    assert response.status_code == 400
    data = response.json()
    assert "link" in data["detail"].lower()


@pytest.mark.asyncio
async def test_add_series_extracts_key_from_full_url(authenticated_client):
    """Test that add_series extracts key from full URL."""
    response = await authenticated_client.post(
        "/api/anime/add",
        json={
            "link": "https://aniworld.to/anime/stream/attack-on-titan",
            "name": "Attack on Titan"
        }
    )
    
    assert response.status_code == 202
    data = response.json()
    assert data["key"] == "attack-on-titan"


@pytest.mark.asyncio
async def test_add_series_sanitizes_folder_name(authenticated_client):
    """Test that add_series creates sanitized folder name."""
    response = await authenticated_client.post(
        "/api/anime/add",
        json={
            "link": "https://aniworld.to/anime/stream/rezero",
            "name": "Re:Zero - Starting Life in Another World?"
        }
    )
    
    assert response.status_code == 202
    data = response.json()
    
    # Folder should not contain invalid characters
    folder = data["folder"]
    assert ":" not in folder
    assert "?" not in folder


@pytest.mark.asyncio
async def test_add_series_does_not_duplicate_year(authenticated_client):
    """Test that add_series doesn't duplicate year when name already contains it."""
    response = await authenticated_client.post(
        "/api/anime/add",
        json={
            "link": "https://aniworld.to/anime/stream/eighty-six",
            "name": "86 Eighty Six (2021)"
        }
    )
    
    assert response.status_code == 202
    data = response.json()
    
    # Folder should contain year only once
    folder = data["folder"]
    assert folder.count("(2021)") == 1


@pytest.mark.asyncio
async def test_add_series_returns_missing_episodes(authenticated_client):
    """Test that add_series returns loading progress info."""
    response = await authenticated_client.post(
        "/api/anime/add",
        json={
            "link": "https://aniworld.to/anime/stream/test-anime",
            "name": "Test Anime"
        }
    )
    
    assert response.status_code == 202
    data = response.json()
    
    # Response should contain loading_progress fields (async endpoint)
    assert "loading_status" in data
    assert "loading_progress" in data
    assert isinstance(data["loading_progress"], dict)


@pytest.mark.asyncio
async def test_add_series_response_structure(authenticated_client):
    """Test the full response structure of add_series."""
    response = await authenticated_client.post(
        "/api/anime/add",
        json={
            "link": "https://aniworld.to/anime/stream/new-anime",
            "name": "New Anime Series"
        }
    )
    
    assert response.status_code == 202
    data = response.json()
    
    # Verify all expected fields are present
    assert "status" in data
    assert "message" in data
    assert "key" in data
    assert "folder" in data
    assert "loading_status" in data
    assert "loading_progress" in data
    
    # Status should be success or exists
    assert data["status"] in ("success", "exists")


@pytest.mark.asyncio
async def test_add_series_special_characters_in_name(authenticated_client):
    """Test adding series with various special characters in name."""
    test_cases = [
        ("86: Eighty-Six", "86-eighty-six"),
        ("Fate/Stay Night", "fate-stay-night"),
        ("What If...?", "what-if"),
        ("Steins;Gate", "steins-gate"),
    ]
    
    for name, key in test_cases:
        response = await authenticated_client.post(
            "/api/anime/add",
            json={
                "link": f"https://aniworld.to/anime/stream/{key}",
                "name": name
            }
        )
        
        assert response.status_code == 202
        data = response.json()
        
        # Get just the folder name (last part of path)
        folder_path = data["folder"]
        # Handle both full paths and just folder names
        if "/" in folder_path:
            folder_name = folder_path.rstrip("/").split("/")[-1]
        else:
            folder_name = folder_path
        
        # Folder name should not contain invalid filesystem characters
        invalid_chars = [':', '\\', '?', '*', '<', '>', '|', '"']
        for char in invalid_chars:
            assert char not in folder_name, f"Found '{char}' in folder name for {name}"


# ---------------------------------------------------------------------------
# New tests: get_anime_status
# ---------------------------------------------------------------------------


class TestGetAnimeStatusEndpoint:
    """Tests for GET /api/anime/status."""

    @pytest.mark.asyncio
    async def test_status_unauthorized(self):
        """Status endpoint should require authentication."""
        transport = ASGITransport(app=app)
        async with AsyncClient(transport=transport, base_url="http://test") as client:
            response = await client.get("/api/anime/status")
            assert response.status_code == 401

    @pytest.mark.asyncio
    async def test_status_authenticated(self, authenticated_client):
        """Authenticated request returns directory and series count."""
        response = await authenticated_client.get("/api/anime/status")
        assert response.status_code == 200
        data = response.json()
        assert "directory" in data
        assert "series_count" in data
        assert isinstance(data["series_count"], int)

    def test_status_direct_no_series_app(self):
        """When series_app is None, returns empty directory and 0 count."""
        result = asyncio.run(anime_module.get_anime_status(series_app=None))
        assert result["directory"] == ""
        assert result["series_count"] == 0


# ---------------------------------------------------------------------------
# New tests: list_anime authenticated
# ---------------------------------------------------------------------------


class TestListAnimeAuthenticated:
    """Tests for GET /api/anime/ with authentication."""

    @pytest.mark.asyncio
    async def test_list_anime_returns_summaries(self, authenticated_client):
        """Authenticated list returns anime summaries."""
        response = await authenticated_client.get("/api/anime/")
        assert response.status_code == 200
        data = response.json()
        assert isinstance(data, list)

    @pytest.mark.asyncio
    async def test_list_anime_invalid_page(self, authenticated_client):
        """Negative page number returns validation error."""
        response = await authenticated_client.get(
            "/api/anime/", params={"page": -1}
        )
        assert response.status_code == 422

    @pytest.mark.asyncio
    async def test_list_anime_per_page_too_large(self, authenticated_client):
        """Per page > 1000 returns validation error."""
        response = await authenticated_client.get(
            "/api/anime/", params={"per_page": 5000}
        )
        assert response.status_code == 422

    @pytest.mark.asyncio
    async def test_list_anime_invalid_sort_by(self, authenticated_client):
        """Invalid sort_by parameter returns validation error."""
        response = await authenticated_client.get(
            "/api/anime/", params={"sort_by": "injection_attempt"}
        )
        assert response.status_code == 422

    @pytest.mark.asyncio
    async def test_list_anime_valid_sort_by(self, authenticated_client):
        """Valid sort_by parameter is accepted."""
        response = await authenticated_client.get(
            "/api/anime/", params={"sort_by": "title"}
        )
        assert response.status_code == 200

    @pytest.mark.asyncio
    async def test_list_anime_invalid_filter(self, authenticated_client):
        """Invalid filter value returns validation error."""
        response = await authenticated_client.get(
            "/api/anime/", params={"filter": "hacked"}
        )
        assert response.status_code == 422


# ---------------------------------------------------------------------------
# New tests: get_scan_status
# ---------------------------------------------------------------------------


class TestGetScanStatusEndpoint:
    """Tests for GET /api/anime/scan/status."""

    @pytest.mark.asyncio
    async def test_scan_status_unauthorized(self):
        """Scan status endpoint should require authentication."""
        transport = ASGITransport(app=app)
        async with AsyncClient(transport=transport, base_url="http://test") as client:
            response = await client.get("/api/anime/scan/status")
            assert response.status_code == 401

    @pytest.mark.asyncio
    async def test_scan_status_authenticated(self, authenticated_client):
        """Authenticated request returns scan status dict."""
        response = await authenticated_client.get("/api/anime/scan/status")
        assert response.status_code == 200
        data = response.json()
        assert isinstance(data, dict)


# ---------------------------------------------------------------------------
# New tests: _validate_search_query_extended
# ---------------------------------------------------------------------------


class TestValidateSearchQueryExtended:
    """Tests for the internal _validate_search_query_extended function."""

    def test_empty_query_raises(self):
        """Empty string raises 422."""
        from fastapi import HTTPException

        with pytest.raises(HTTPException) as exc_info:
            anime_module._validate_search_query_extended("")
        assert exc_info.value.status_code == 422

    def test_whitespace_only_raises(self):
        """Whitespace-only raises 422."""
        from fastapi import HTTPException

        with pytest.raises(HTTPException) as exc_info:
            anime_module._validate_search_query_extended("   ")
        assert exc_info.value.status_code == 422

    def test_null_bytes_raise(self):
        """Null bytes in query raise 400."""
        from fastapi import HTTPException

        with pytest.raises(HTTPException) as exc_info:
            anime_module._validate_search_query_extended("test\x00query")
        assert exc_info.value.status_code == 400

    def test_too_long_query_raises(self):
        """Query exceeding 200 chars raises 422."""
        from fastapi import HTTPException

        with pytest.raises(HTTPException) as exc_info:
            anime_module._validate_search_query_extended("a" * 201)
        assert exc_info.value.status_code == 422

    def test_valid_query_returns_string(self):
        """Valid query is returned (possibly normalised)."""
        result = anime_module._validate_search_query_extended("Naruto")
        assert isinstance(result, str)
        assert len(result) > 0


# ---------------------------------------------------------------------------
# New tests: search_anime_post
# ---------------------------------------------------------------------------


class TestSearchAnimePost:
    """Tests for POST /api/anime/search."""

    @pytest.mark.asyncio
    async def test_search_post_returns_results(self):
        """POST search with valid query returns results."""
        transport = ASGITransport(app=app)
        async with AsyncClient(transport=transport, base_url="http://test") as client:
            response = await client.post(
                "/api/anime/search",
                json={"query": "test"},
            )
            assert response.status_code == 200
            assert isinstance(response.json(), list)

    @pytest.mark.asyncio
    async def test_search_post_empty_query_rejected(self):
        """POST search with empty query returns 422."""
        transport = ASGITransport(app=app)
        async with AsyncClient(transport=transport, base_url="http://test") as client:
            response = await client.post(
                "/api/anime/search",
                json={"query": ""},
            )
            assert response.status_code == 422


# ---------------------------------------------------------------------------
# New tests: _perform_search
# ---------------------------------------------------------------------------


class TestPerformSearch:
    """Tests for the internal _perform_search function."""

    @pytest.mark.asyncio
    async def test_search_no_series_app(self):
        """When series_app is None return empty list."""
        result = await anime_module._perform_search("test", None)
        assert result == []

    @pytest.mark.asyncio
    async def test_search_dict_results(self):
        """Dict-format results are converted to AnimeSummary."""
        mock_app = AsyncMock()
        mock_app.search = AsyncMock(
            return_value=[
                {
                    "key": "k1",
                    "title": "Title One",
                    "site": "aniworld.to",
                    "folder": "f1",
                    "link": "https://aniworld.to/anime/stream/k1",
                    "missing_episodes": {},
                }
            ]
        )
        results = await anime_module._perform_search("query", mock_app)
        assert len(results) == 1
        assert results[0].key == "k1"
        assert results[0].name == "Title One"

    @pytest.mark.asyncio
    async def test_search_object_results(self):
        """Object-format results (with attributes) are handled."""
        match = MagicMock(spec=[])
        match.key = "obj-key"
        match.id = ""
        match.title = "Object Title"
        match.name = "Object Title"
        match.site = "aniworld.to"
        match.folder = "Object Folder"
        match.link = ""
        match.url = ""
        match.missing_episodes = {}
        mock_app = AsyncMock()
        mock_app.search = AsyncMock(return_value=[match])
        results = await anime_module._perform_search("query", mock_app)
        assert len(results) == 1
        assert results[0].key == "obj-key"

    @pytest.mark.asyncio
    async def test_search_key_extracted_from_link(self):
        """When key is empty, extract from link URL."""
        mock_app = AsyncMock()
        mock_app.search = AsyncMock(
            return_value=[
                {
                    "key": "",
                    "name": "No Key",
                    "site": "",
                    "folder": "",
                    "link": "https://aniworld.to/anime/stream/extracted-key",
                    "missing_episodes": {},
                }
            ]
        )
        results = await anime_module._perform_search("q", mock_app)
        assert results[0].key == "extracted-key"

    @pytest.mark.asyncio
    async def test_search_exception_raises_500(self):
        """Non-HTTP exception in search raises 500."""
        from fastapi import HTTPException

        mock_app = AsyncMock()
        mock_app.search = AsyncMock(side_effect=RuntimeError("boom"))
        with pytest.raises(HTTPException) as exc_info:
            await anime_module._perform_search("q", mock_app)
        assert exc_info.value.status_code == 500


# ---------------------------------------------------------------------------
# New tests: get_loading_status
# ---------------------------------------------------------------------------


class TestGetLoadingStatusEndpoint:
    """Tests for GET /api/anime/{key}/loading-status."""

    @pytest.mark.asyncio
    async def test_loading_status_unauthorized(self):
        """Loading status requires authentication."""
        transport = ASGITransport(app=app)
        async with AsyncClient(transport=transport, base_url="http://test") as client:
            response = await client.get("/api/anime/some-key/loading-status")
            assert response.status_code == 401

    @pytest.mark.asyncio
    async def test_loading_status_no_db(self, authenticated_client):
        """Without database session returns 503."""
        response = await authenticated_client.get(
            "/api/anime/some-key/loading-status"
        )
        # get_optional_database_session may return None → 503
        assert response.status_code in (503, 404, 500)

    def test_loading_status_direct_no_db(self):
        """Direct call with db=None raises 503."""
        from fastapi import HTTPException

        with pytest.raises(HTTPException) as exc_info:
            asyncio.run(anime_module.get_loading_status("key", db=None))
        assert exc_info.value.status_code == 503

    def test_loading_status_not_found(self):
        """Direct call with unknown key raises 404."""
        from fastapi import HTTPException

        mock_db = AsyncMock()

        async def _run():
            with patch(
                "src.server.database.service.AnimeSeriesService"
            ) as mock_svc:
                mock_svc.get_by_key = AsyncMock(return_value=None)
                return await anime_module.get_loading_status(
                    "missing-key", db=mock_db
                )

        with pytest.raises(HTTPException) as exc_info:
            asyncio.run(_run())
        assert exc_info.value.status_code == 404

    def test_loading_status_pending(self):
        """Direct call returns correct pending status payload."""
        mock_db = AsyncMock()
        series_row = MagicMock()
        series_row.key = "test-key"
        series_row.loading_status = "pending"
        series_row.episodes_loaded = False
        series_row.has_nfo = False
        series_row.logo_loaded = False
        series_row.images_loaded = False
        series_row.loading_started_at = None
        series_row.loading_completed_at = None
        series_row.loading_error = None

        async def _run():
            with patch(
                "src.server.database.service.AnimeSeriesService"
            ) as mock_svc:
                mock_svc.get_by_key = AsyncMock(return_value=series_row)
                return await anime_module.get_loading_status(
                    "test-key", db=mock_db
                )

        result = asyncio.run(_run())
        assert result["key"] == "test-key"
        assert result["loading_status"] == "pending"
        assert "Queued" in result["message"]
        assert result["progress"]["episodes"] is False

    def test_loading_status_completed(self):
        """Completed status returns correct message."""
        from datetime import datetime

        mock_db = AsyncMock()
        series_row = MagicMock()
        series_row.key = "done-key"
        series_row.loading_status = "completed"
        series_row.episodes_loaded = True
        series_row.has_nfo = True
        series_row.logo_loaded = True
        series_row.images_loaded = True
        series_row.loading_started_at = datetime(2025, 1, 1)
        series_row.loading_completed_at = datetime(2025, 1, 1, 0, 5)
        series_row.loading_error = None

        async def _run():
            with patch(
                "src.server.database.service.AnimeSeriesService"
            ) as mock_svc:
                mock_svc.get_by_key = AsyncMock(return_value=series_row)
                return await anime_module.get_loading_status(
                    "done-key", db=mock_db
                )

        result = asyncio.run(_run())
        assert result["loading_status"] == "completed"
        assert "successfully" in result["message"]
        assert result["progress"]["episodes"] is True
        assert result["completed_at"] is not None


# ---------------------------------------------------------------------------
# New tests: get_anime detail
# ---------------------------------------------------------------------------


class TestGetAnimeDetail:
    """Tests for GET /api/anime/{anime_id} detail endpoint."""

    def test_get_anime_by_key(self):
        """Primary lookup by key returns correct detail."""
        fake = FakeSeriesApp()
        result = asyncio.run(
            anime_module.get_anime("test-show-key", series_app=fake)
        )
        assert result.key == "test-show-key"
        assert result.title == "Test Show"

    def test_get_anime_by_folder_fallback(self):
        """Folder-based lookup works as deprecated fallback."""
        fake = FakeSeriesApp()
        result = asyncio.run(
            anime_module.get_anime("Test Show (2023)", series_app=fake)
        )
        assert result.key == "test-show-key"

    def test_get_anime_not_found(self):
        """Unknown anime_id raises 404."""
        from fastapi import HTTPException

        fake = FakeSeriesApp()
        with pytest.raises(HTTPException) as exc_info:
            asyncio.run(
                anime_module.get_anime("nonexistent", series_app=fake)
            )
        assert exc_info.value.status_code == 404

    def test_get_anime_no_series_app(self):
        """None series_app raises 404."""
        from fastapi import HTTPException

        with pytest.raises(HTTPException) as exc_info:
            asyncio.run(
                anime_module.get_anime("any-id", series_app=None)
            )
        assert exc_info.value.status_code == 404

    def test_get_anime_episodes_formatted(self):
        """Episode dict is converted to season-episode strings."""
        fake = FakeSeriesApp()
        result = asyncio.run(
            anime_module.get_anime("test-show-key", series_app=fake)
        )
        assert "1-1" in result.episodes
        assert "1-2" in result.episodes

    def test_get_anime_complete_show_no_episodes(self):
        """Complete show with empty episodeDict returns empty episodes list."""
        fake = FakeSeriesApp()
        result = asyncio.run(
            anime_module.get_anime("complete-show-key", series_app=fake)
        )
        assert result.episodes == []


# ---------------------------------------------------------------------------
# New tests: trigger_rescan authenticated
# ---------------------------------------------------------------------------


class TestTriggerRescanAuthenticated:
    """Tests for POST /api/anime/rescan with authentication."""

    @pytest.mark.asyncio
    async def test_rescan_authenticated(self, authenticated_client):
        """Authenticated rescan returns success."""
        from src.server.services.anime_service import AnimeService
        from src.server.utils.dependencies import get_anime_service

        mock_service = AsyncMock(spec=AnimeService)
        mock_service.rescan = AsyncMock()
        app.dependency_overrides[get_anime_service] = lambda: mock_service
        try:
            response = await authenticated_client.post("/api/anime/rescan")
            assert response.status_code == 200
            data = response.json()
            assert data["success"] is True
            mock_service.rescan.assert_called_once()
        finally:
            app.dependency_overrides.pop(get_anime_service, None)

    def test_rescan_service_error(self):
        """AnimeServiceError is converted to ServerError."""
        from src.server.services.anime_service import AnimeServiceError

        mock_service = AsyncMock()
        mock_service.rescan = AsyncMock(
            side_effect=AnimeServiceError("scan failed")
        )

        from src.server.exceptions import ServerError

        with pytest.raises(ServerError):
            asyncio.run(
                anime_module.trigger_rescan(anime_service=mock_service)
            )


# ---------------------------------------------------------------------------
# New tests: search_anime_get additional
# ---------------------------------------------------------------------------


class TestSearchAnimeGetAdditional:
    """Additional tests for GET /api/anime/search."""

    @pytest.mark.asyncio
    async def test_search_get_with_query(self):
        """Search GET with valid query returns list."""
        transport = ASGITransport(app=app)
        async with AsyncClient(
            transport=transport, base_url="http://test"
        ) as client:
            response = await client.get(
                "/api/anime/search", params={"query": "naruto"}
            )
            assert response.status_code == 200
            assert isinstance(response.json(), list)

    @pytest.mark.asyncio
    async def test_search_get_null_byte_query(self):
        """Search GET with null byte in query returns 400."""
        transport = ASGITransport(app=app)
        async with AsyncClient(
            transport=transport, base_url="http://test"
        ) as client:
            response = await client.get(
                "/api/anime/search", params={"query": "test\x00bad"}
            )
            assert response.status_code == 400
