"""Tests for webhook server functionality."""

from __future__ import annotations

from datetime import UTC, datetime
from typing import Any
from unittest.mock import AsyncMock, MagicMock, patch

import httpx
import pytest
import respx
from fastapi.testclient import TestClient
from httpx import Response
from pydantic import ValidationError

import filtarr.webhook
from filtarr.config import (
    Config,
    ConfigurationError,
    RadarrConfig,
    SonarrConfig,
    StateConfig,
    TagConfig,
    WebhookConfig,
)
from filtarr.models.webhook import (
    RadarrWebhookPayload,
    SonarrWebhookPayload,
    WebhookResponse,
)
from filtarr.state import CheckRecord
from tests.test_utils import CreateTaskMock


@pytest.fixture
def radarr_config() -> RadarrConfig:
    """Create a test Radarr configuration."""
    return RadarrConfig(url="http://localhost:7878", api_key="radarr-api-key")


@pytest.fixture
def sonarr_config() -> SonarrConfig:
    """Create a test Sonarr configuration."""
    return SonarrConfig(url="http://127.0.0.1:8989", api_key="sonarr-api-key")


@pytest.fixture
def full_config(radarr_config: RadarrConfig, sonarr_config: SonarrConfig) -> Config:
    """Create a full test configuration."""
    return Config(
        radarr=radarr_config,
        sonarr=sonarr_config,
        tags=TagConfig(),
        webhook=WebhookConfig(host="0.0.0.0", port=8080),
    )


@pytest.fixture
def radarr_only_config(radarr_config: RadarrConfig) -> Config:
    """Create a config with only Radarr configured."""
    return Config(radarr=radarr_config, webhook=WebhookConfig())


@pytest.fixture
def sonarr_only_config(sonarr_config: SonarrConfig) -> Config:
    """Create a config with only Sonarr configured."""
    return Config(sonarr=sonarr_config, webhook=WebhookConfig())


@pytest.fixture
def test_client(full_config: Config) -> TestClient:
    """Create a test client for the webhook app."""
    app = filtarr.webhook.create_app(full_config)
    return TestClient(app)


class TestValidateApiKey:
    """Tests for the _validate_api_key helper function."""

    def test_validate_api_key_none(self, full_config: Config) -> None:
        """Should return None when api_key is None."""
        result = filtarr.webhook._validate_api_key(None, full_config)
        assert result is None

    def test_validate_api_key_empty_string(self, full_config: Config) -> None:
        """Should return None when api_key is empty string."""
        result = filtarr.webhook._validate_api_key("", full_config)
        assert result is None

    def test_validate_api_key_radarr_match(self, full_config: Config) -> None:
        """Should return 'radarr' when API key matches Radarr."""
        result = filtarr.webhook._validate_api_key("radarr-api-key", full_config)
        assert result == "radarr"

    def test_validate_api_key_sonarr_match(self, full_config: Config) -> None:
        """Should return 'sonarr' when API key matches Sonarr."""
        result = filtarr.webhook._validate_api_key("sonarr-api-key", full_config)
        assert result == "sonarr"

    def test_validate_api_key_invalid(self, full_config: Config) -> None:
        """Should return None for invalid API key."""
        result = filtarr.webhook._validate_api_key("invalid-key", full_config)
        assert result is None

    def test_validate_api_key_no_radarr_config(self, sonarr_only_config: Config) -> None:
        """Should handle missing Radarr config gracefully."""
        result = filtarr.webhook._validate_api_key("sonarr-api-key", sonarr_only_config)
        assert result == "sonarr"

    def test_validate_api_key_no_sonarr_config(self, radarr_only_config: Config) -> None:
        """Should handle missing Sonarr config gracefully."""
        result = filtarr.webhook._validate_api_key("radarr-api-key", radarr_only_config)
        assert result == "radarr"


class TestWebhookModels:
    """Tests for webhook Pydantic models."""

    def test_radarr_webhook_payload_parsing(self) -> None:
        """Should parse Radarr webhook payload correctly."""
        data = {
            "eventType": "MovieAdded",
            "movie": {
                "id": 123,
                "title": "The Matrix",
                "year": 1999,
                "tmdbId": 603,
                "imdbId": "tt0133093",
            },
        }
        payload = RadarrWebhookPayload.model_validate(data)

        assert payload.event_type == "MovieAdded"
        assert payload.movie.id == 123
        assert payload.movie.title == "The Matrix"
        assert payload.movie.year == 1999
        assert payload.movie.tmdb_id == 603
        assert payload.movie.imdb_id == "tt0133093"
        assert payload.is_movie_added() is True

    def test_radarr_webhook_non_added_event(self) -> None:
        """Should correctly identify non-MovieAdded events."""
        data = {
            "eventType": "Download",
            "movie": {"id": 123, "title": "Test Movie"},
        }
        payload = RadarrWebhookPayload.model_validate(data)

        assert payload.event_type == "Download"
        assert payload.is_movie_added() is False

    def test_sonarr_webhook_payload_parsing(self) -> None:
        """Should parse Sonarr webhook payload correctly."""
        data = {
            "eventType": "SeriesAdd",
            "series": {
                "id": 456,
                "title": "Breaking Bad",
                "year": 2008,
                "tvdbId": 81189,
                "imdbId": "tt0903747",
            },
        }
        payload = SonarrWebhookPayload.model_validate(data)

        assert payload.event_type == "SeriesAdd"
        assert payload.series.id == 456
        assert payload.series.title == "Breaking Bad"
        assert payload.series.year == 2008
        assert payload.series.tvdb_id == 81189
        assert payload.series.imdb_id == "tt0903747"
        assert payload.is_series_add() is True

    def test_sonarr_webhook_non_add_event(self) -> None:
        """Should correctly identify non-SeriesAdd events."""
        data = {
            "eventType": "Download",
            "series": {"id": 456, "title": "Test Series"},
        }
        payload = SonarrWebhookPayload.model_validate(data)

        assert payload.event_type == "Download"
        assert payload.is_series_add() is False

    def test_webhook_response_model(self) -> None:
        """Should create webhook response correctly."""
        response = WebhookResponse(
            status="accepted",
            message="4K availability check queued",
            media_id=123,
            media_title="The Matrix",
        )

        assert response.status == "accepted"
        assert response.message == "4K availability check queued"
        assert response.media_id == 123
        assert response.media_title == "The Matrix"


class TestHealthEndpoint:
    """Tests for the health check endpoint."""

    def test_health_check_returns_healthy(self, test_client: TestClient) -> None:
        """Should return healthy status."""
        response = test_client.get("/health")

        assert response.status_code == 200
        assert response.json() == {"status": "healthy"}


class TestStatusEndpoint:
    """Tests for the status endpoint."""

    def test_status_without_scheduler(self, test_client: TestClient) -> None:
        """Should return status without scheduler info."""
        response = test_client.get("/status")

        assert response.status_code == 200
        data = response.json()
        assert data["status"] == "healthy"
        assert "timestamp" in data
        assert data["radarr_configured"] is True
        assert data["sonarr_configured"] is True
        assert data["scheduler"] == {"enabled": False}

    def test_status_with_scheduler_manager(self, full_config: Config) -> None:
        """Should return scheduler info when scheduler manager is set."""
        # Create mock scheduler manager
        mock_scheduler = MagicMock()
        mock_scheduler.is_running = True
        mock_scheduler.get_all_schedules.return_value = [
            MagicMock(enabled=True),
            MagicMock(enabled=True),
            MagicMock(enabled=False),
        ]
        mock_scheduler.get_running_schedules.return_value = {"test-schedule"}
        mock_scheduler.get_history.return_value = [
            MagicMock(
                schedule_name="test-schedule",
                status=MagicMock(value="completed"),
                started_at=datetime.now(UTC),
                items_processed=10,
                items_with_4k=5,
            )
        ]

        # Store original value to restore later
        original_scheduler = filtarr.webhook._scheduler_manager

        try:
            filtarr.webhook._scheduler_manager = mock_scheduler
            app = filtarr.webhook.create_app(full_config)
            client = TestClient(app)

            response = client.get("/status")

            assert response.status_code == 200
            data = response.json()
            assert data["scheduler"]["enabled"] is True
            assert data["scheduler"]["running"] is True
            assert data["scheduler"]["total_schedules"] == 3
            assert data["scheduler"]["enabled_schedules"] == 2
            assert data["scheduler"]["currently_running"] == ["test-schedule"]
            assert len(data["scheduler"]["recent_runs"]) == 1
        finally:
            filtarr.webhook._scheduler_manager = original_scheduler

    def test_status_radarr_only_config(self, radarr_only_config: Config) -> None:
        """Should show sonarr_configured as False when not configured."""
        app = filtarr.webhook.create_app(radarr_only_config)
        client = TestClient(app)

        response = client.get("/status")

        assert response.status_code == 200
        data = response.json()
        assert data["radarr_configured"] is True
        assert data["sonarr_configured"] is False

    def test_status_sonarr_only_config(self, sonarr_only_config: Config) -> None:
        """Should show radarr_configured as False when not configured."""
        app = filtarr.webhook.create_app(sonarr_only_config)
        client = TestClient(app)

        response = client.get("/status")

        assert response.status_code == 200
        data = response.json()
        assert data["radarr_configured"] is False
        assert data["sonarr_configured"] is True


class TestRadarrWebhook:
    """Tests for the Radarr webhook endpoint."""

    def test_radarr_webhook_requires_api_key(self, test_client: TestClient) -> None:
        """Should reject requests without API key."""
        response = test_client.post(
            "/webhook/radarr",
            json={
                "eventType": "MovieAdded",
                "movie": {"id": 123, "title": "Test Movie"},
            },
        )

        assert response.status_code == 401
        assert "X-Api-Key" in response.json()["detail"]

    def test_radarr_webhook_rejects_invalid_api_key(self, test_client: TestClient) -> None:
        """Should reject requests with invalid API key."""
        response = test_client.post(
            "/webhook/radarr",
            json={
                "eventType": "MovieAdded",
                "movie": {"id": 123, "title": "Test Movie"},
            },
            headers={"X-Api-Key": "wrong-key"},
        )

        assert response.status_code == 401

    def test_radarr_webhook_accepts_valid_api_key(self, full_config: Config) -> None:
        """Should accept requests with valid Radarr API key."""
        app = filtarr.webhook.create_app(full_config)
        client = TestClient(app)

        mock_create_task = CreateTaskMock()
        with patch("filtarr.webhook.asyncio.create_task", mock_create_task):
            response = client.post(
                "/webhook/radarr",
                json={
                    "eventType": "MovieAdded",
                    "movie": {"id": 123, "title": "Test Movie", "year": 2023},
                },
                headers={"X-Api-Key": "radarr-api-key"},
            )

        assert response.status_code == 200
        data = response.json()
        assert data["status"] == "accepted"
        assert data["media_id"] == 123
        assert data["media_title"] == "Test Movie"

    def test_radarr_webhook_accepts_sonarr_api_key(self, full_config: Config) -> None:
        """Should accept Radarr webhook with Sonarr API key (authentication only)."""
        app = filtarr.webhook.create_app(full_config)
        client = TestClient(app)

        mock_create_task = CreateTaskMock()
        with patch("filtarr.webhook.asyncio.create_task", mock_create_task):
            response = client.post(
                "/webhook/radarr",
                json={
                    "eventType": "MovieAdded",
                    "movie": {"id": 123, "title": "Test Movie", "year": 2023},
                },
                headers={"X-Api-Key": "sonarr-api-key"},
            )

        assert response.status_code == 200

    def test_radarr_webhook_ignores_non_movie_added_events(self, full_config: Config) -> None:
        """Should ignore events that are not MovieAdded."""
        app = filtarr.webhook.create_app(full_config)
        client = TestClient(app)

        response = client.post(
            "/webhook/radarr",
            json={
                "eventType": "Download",
                "movie": {"id": 123, "title": "Test Movie"},
            },
            headers={"X-Api-Key": "radarr-api-key"},
        )

        assert response.status_code == 200
        data = response.json()
        assert data["status"] == "ignored"
        assert "Download" in data["message"]

    def test_radarr_webhook_not_configured(self, sonarr_only_config: Config) -> None:
        """Should return 503 when Radarr is not configured."""
        app = filtarr.webhook.create_app(sonarr_only_config)
        client = TestClient(app)

        response = client.post(
            "/webhook/radarr",
            json={
                "eventType": "MovieAdded",
                "movie": {"id": 123, "title": "Test Movie"},
            },
            headers={"X-Api-Key": "sonarr-api-key"},
        )

        assert response.status_code == 503
        assert "Radarr is not configured" in response.json()["detail"]


class TestSonarrWebhook:
    """Tests for the Sonarr webhook endpoint."""

    def test_sonarr_webhook_requires_api_key(self, test_client: TestClient) -> None:
        """Should reject requests without API key."""
        response = test_client.post(
            "/webhook/sonarr",
            json={
                "eventType": "SeriesAdd",
                "series": {"id": 456, "title": "Test Series"},
            },
        )

        assert response.status_code == 401
        assert "X-Api-Key" in response.json()["detail"]

    def test_sonarr_webhook_rejects_invalid_api_key(self, test_client: TestClient) -> None:
        """Should reject requests with invalid API key."""
        response = test_client.post(
            "/webhook/sonarr",
            json={
                "eventType": "SeriesAdd",
                "series": {"id": 456, "title": "Test Series"},
            },
            headers={"X-Api-Key": "wrong-key"},
        )

        assert response.status_code == 401

    def test_sonarr_webhook_accepts_valid_api_key(self, full_config: Config) -> None:
        """Should accept requests with valid Sonarr API key."""
        app = filtarr.webhook.create_app(full_config)
        client = TestClient(app)

        mock_create_task = CreateTaskMock()
        with patch("filtarr.webhook.asyncio.create_task", mock_create_task):
            response = client.post(
                "/webhook/sonarr",
                json={
                    "eventType": "SeriesAdd",
                    "series": {"id": 456, "title": "Breaking Bad", "year": 2008},
                },
                headers={"X-Api-Key": "sonarr-api-key"},
            )

        assert response.status_code == 200
        data = response.json()
        assert data["status"] == "accepted"
        assert data["media_id"] == 456
        assert data["media_title"] == "Breaking Bad"

    def test_sonarr_webhook_ignores_non_series_add_events(self, full_config: Config) -> None:
        """Should ignore events that are not SeriesAdd."""
        app = filtarr.webhook.create_app(full_config)
        client = TestClient(app)

        response = client.post(
            "/webhook/sonarr",
            json={
                "eventType": "Download",
                "series": {"id": 456, "title": "Test Series"},
            },
            headers={"X-Api-Key": "sonarr-api-key"},
        )

        assert response.status_code == 200
        data = response.json()
        assert data["status"] == "ignored"
        assert "Download" in data["message"]

    def test_sonarr_webhook_not_configured(self, radarr_only_config: Config) -> None:
        """Should return 503 when Sonarr is not configured."""
        app = filtarr.webhook.create_app(radarr_only_config)
        client = TestClient(app)

        response = client.post(
            "/webhook/sonarr",
            json={
                "eventType": "SeriesAdd",
                "series": {"id": 456, "title": "Test Series"},
            },
            headers={"X-Api-Key": "radarr-api-key"},
        )

        assert response.status_code == 503
        assert "Sonarr is not configured" in response.json()["detail"]


class TestBackgroundProcessing:
    """Tests for background task processing."""

    @pytest.mark.asyncio
    async def test_movie_check_background_task_created(self, full_config: Config) -> None:
        """Should create background task for movie check on MovieAdded event."""
        app = filtarr.webhook.create_app(full_config)
        client = TestClient(app)

        mock_create_task = CreateTaskMock()
        with patch("filtarr.webhook.asyncio.create_task", mock_create_task):
            response = client.post(
                "/webhook/radarr",
                json={
                    "eventType": "MovieAdded",
                    "movie": {"id": 123, "title": "Test Movie", "year": 2023},
                },
                headers={"X-Api-Key": "radarr-api-key"},
            )

            assert response.status_code == 200
            mock_create_task.assert_called_once()

    @pytest.mark.asyncio
    async def test_series_check_background_task_created(self, full_config: Config) -> None:
        """Should create background task for series check on SeriesAdd event."""
        app = filtarr.webhook.create_app(full_config)
        client = TestClient(app)

        mock_create_task = CreateTaskMock()
        with patch("filtarr.webhook.asyncio.create_task", mock_create_task):
            response = client.post(
                "/webhook/sonarr",
                json={
                    "eventType": "SeriesAdd",
                    "series": {"id": 456, "title": "Test Series", "year": 2020},
                },
                headers={"X-Api-Key": "sonarr-api-key"},
            )

            assert response.status_code == 200
            mock_create_task.assert_called_once()


class TestProcessMovieCheck:
    """Tests for _process_movie_check background task."""

    @respx.mock
    @pytest.mark.asyncio
    async def test_process_movie_check_success(self, full_config: Config) -> None:
        """Should successfully check movie for 4K availability."""
        # Mock movie info endpoint
        respx.get("http://localhost:7878/api/v3/movie/123").mock(
            return_value=Response(
                200,
                json={"id": 123, "title": "Test Movie", "year": 2024, "tags": []},
            )
        )
        # Mock releases endpoint with 4K release
        respx.get("http://localhost:7878/api/v3/release", params={"movieId": "123"}).mock(
            return_value=Response(
                200,
                json=[
                    {
                        "guid": "rel1",
                        "title": "Movie.2160p.BluRay",
                        "indexer": "Test",
                        "size": 5000,
                        "quality": {"quality": {"id": 19, "name": "WEBDL-2160p"}},
                    }
                ],
            )
        )
        # Mock tags endpoint
        respx.get("http://localhost:7878/api/v3/tag").mock(
            return_value=Response(200, json=[{"id": 1, "label": "4k-available"}])
        )
        # Mock update movie
        respx.put("http://localhost:7878/api/v3/movie/123").mock(
            return_value=Response(
                200,
                json={"id": 123, "title": "Test Movie", "year": 2024, "tags": [1]},
            )
        )

        # Should complete without error
        await filtarr.webhook._process_movie_check(123, "Test Movie", full_config)

    @pytest.mark.asyncio
    async def test_process_movie_check_handles_exception(self, full_config: Config) -> None:
        """Should handle exceptions gracefully in background task."""
        # Mock ReleaseChecker to raise an exception
        with patch("filtarr.webhook.ReleaseChecker") as mock_checker_class:
            mock_checker = MagicMock()
            mock_checker.check_movie = AsyncMock(side_effect=Exception("API error"))
            mock_checker_class.return_value = mock_checker

            # Should not raise, just log the error
            await filtarr.webhook._process_movie_check(123, "Test Movie", full_config)


class TestProcessSeriesCheck:
    """Tests for _process_series_check background task."""

    @respx.mock
    @pytest.mark.asyncio
    async def test_process_series_check_success(self, full_config: Config) -> None:
        """Should successfully check series for 4K availability."""
        # Mock series info endpoint
        respx.get("http://127.0.0.1:8989/api/v3/series/456").mock(
            return_value=Response(
                200,
                json={"id": 456, "title": "Test Series", "year": 2020, "seasons": [], "tags": []},
            )
        )
        # Mock episodes endpoint
        respx.get("http://127.0.0.1:8989/api/v3/episode", params={"seriesId": "456"}).mock(
            return_value=Response(
                200,
                json=[
                    {
                        "id": 1001,
                        "seriesId": 456,
                        "seasonNumber": 1,
                        "episodeNumber": 1,
                        "airDate": "2020-01-01",
                        "monitored": True,
                    }
                ],
            )
        )
        # Mock releases endpoint
        respx.get("http://127.0.0.1:8989/api/v3/release", params={"episodeId": "1001"}).mock(
            return_value=Response(
                200,
                json=[
                    {
                        "guid": "rel1",
                        "title": "Series.S01E01.2160p",
                        "indexer": "Test",
                        "size": 3000,
                        "quality": {"quality": {"id": 19, "name": "WEBDL-2160p"}},
                    }
                ],
            )
        )
        # Mock tags endpoint
        respx.get("http://127.0.0.1:8989/api/v3/tag").mock(
            return_value=Response(200, json=[{"id": 1, "label": "4k-available"}])
        )
        # Mock update series
        respx.put("http://127.0.0.1:8989/api/v3/series/456").mock(
            return_value=Response(
                200,
                json={"id": 456, "title": "Test Series", "year": 2020, "tags": [1]},
            )
        )

        # Should complete without error
        await filtarr.webhook._process_series_check(456, "Test Series", full_config)

    @pytest.mark.asyncio
    async def test_process_series_check_handles_exception(self, full_config: Config) -> None:
        """Should handle exceptions gracefully in background task."""
        # Mock ReleaseChecker to raise an exception
        with patch("filtarr.webhook.ReleaseChecker") as mock_checker_class:
            mock_checker = MagicMock()
            mock_checker.check_series = AsyncMock(side_effect=Exception("API error"))
            mock_checker_class.return_value = mock_checker

            # Should not raise, just log the error
            await filtarr.webhook._process_series_check(456, "Test Series", full_config)


class TestProcessMovieCheckExceptions:
    """Tests for _process_movie_check exception handling."""

    @pytest.mark.asyncio
    async def test_handles_configuration_error(self, full_config: Config) -> None:
        """Should handle ConfigurationError from config.require_radarr()."""
        with patch.object(
            full_config, "require_radarr", side_effect=ConfigurationError("Radarr not configured")
        ):
            # Should not raise, just log the error
            await filtarr.webhook._process_movie_check(123, "Test Movie", full_config)

    @pytest.mark.asyncio
    async def test_handles_http_status_error(self, full_config: Config) -> None:
        """Should handle HTTPStatusError from checker."""
        mock_response = MagicMock()
        mock_response.status_code = 500
        mock_response.reason_phrase = "Internal Server Error"
        mock_request = MagicMock()

        with patch("filtarr.webhook.ReleaseChecker") as mock_checker_class:
            mock_checker = MagicMock()
            mock_checker.check_movie = AsyncMock(
                side_effect=httpx.HTTPStatusError(
                    "Server error", request=mock_request, response=mock_response
                )
            )
            mock_checker_class.return_value = mock_checker

            # Should not raise, just log the error
            await filtarr.webhook._process_movie_check(123, "Test Movie", full_config)

    @pytest.mark.asyncio
    async def test_handles_connect_error(self, full_config: Config) -> None:
        """Should handle ConnectError from checker."""
        with patch("filtarr.webhook.ReleaseChecker") as mock_checker_class:
            mock_checker = MagicMock()
            mock_checker.check_movie = AsyncMock(
                side_effect=httpx.ConnectError("Connection refused")
            )
            mock_checker_class.return_value = mock_checker

            # Should not raise, just log the error
            await filtarr.webhook._process_movie_check(123, "Test Movie", full_config)

    @pytest.mark.asyncio
    async def test_handles_timeout_exception(self, full_config: Config) -> None:
        """Should handle TimeoutException from checker."""
        with patch("filtarr.webhook.ReleaseChecker") as mock_checker_class:
            mock_checker = MagicMock()
            mock_checker.check_movie = AsyncMock(side_effect=httpx.TimeoutException("Timed out"))
            mock_checker_class.return_value = mock_checker

            # Should not raise, just log the error
            await filtarr.webhook._process_movie_check(123, "Test Movie", full_config)

    @pytest.mark.asyncio
    async def test_handles_validation_error(self, full_config: Config) -> None:
        """Should handle ValidationError from checker."""
        with patch("filtarr.webhook.ReleaseChecker") as mock_checker_class:
            mock_checker = MagicMock()
            # Create a ValidationError with proper structure
            mock_checker.check_movie = AsyncMock(
                side_effect=ValidationError.from_exception_data(
                    "TestModel", [{"type": "missing", "loc": ("field",), "input": {}}]
                )
            )
            mock_checker_class.return_value = mock_checker

            # Should not raise, just log the error
            await filtarr.webhook._process_movie_check(123, "Test Movie", full_config)

    @pytest.mark.asyncio
    async def test_handles_generic_exception(self, full_config: Config) -> None:
        """Should handle generic Exception from checker."""
        with patch("filtarr.webhook.ReleaseChecker") as mock_checker_class:
            mock_checker = MagicMock()
            mock_checker.check_movie = AsyncMock(side_effect=RuntimeError("Unexpected error"))
            mock_checker_class.return_value = mock_checker

            # Should not raise, just log the error
            await filtarr.webhook._process_movie_check(123, "Test Movie", full_config)


class TestProcessSeriesCheckExceptions:
    """Tests for _process_series_check exception handling."""

    @pytest.mark.asyncio
    async def test_handles_configuration_error(self, full_config: Config) -> None:
        """Should handle ConfigurationError from config.require_sonarr()."""
        with patch.object(
            full_config, "require_sonarr", side_effect=ConfigurationError("Sonarr not configured")
        ):
            # Should not raise, just log the error
            await filtarr.webhook._process_series_check(456, "Test Series", full_config)

    @pytest.mark.asyncio
    async def test_handles_http_status_error(self, full_config: Config) -> None:
        """Should handle HTTPStatusError from checker."""
        mock_response = MagicMock()
        mock_response.status_code = 500
        mock_response.reason_phrase = "Internal Server Error"
        mock_request = MagicMock()

        with patch("filtarr.webhook.ReleaseChecker") as mock_checker_class:
            mock_checker = MagicMock()
            mock_checker.check_series = AsyncMock(
                side_effect=httpx.HTTPStatusError(
                    "Server error", request=mock_request, response=mock_response
                )
            )
            mock_checker_class.return_value = mock_checker

            # Should not raise, just log the error
            await filtarr.webhook._process_series_check(456, "Test Series", full_config)

    @pytest.mark.asyncio
    async def test_handles_connect_error(self, full_config: Config) -> None:
        """Should handle ConnectError from checker."""
        with patch("filtarr.webhook.ReleaseChecker") as mock_checker_class:
            mock_checker = MagicMock()
            mock_checker.check_series = AsyncMock(
                side_effect=httpx.ConnectError("Connection refused")
            )
            mock_checker_class.return_value = mock_checker

            # Should not raise, just log the error
            await filtarr.webhook._process_series_check(456, "Test Series", full_config)

    @pytest.mark.asyncio
    async def test_handles_timeout_exception(self, full_config: Config) -> None:
        """Should handle TimeoutException from checker."""
        with patch("filtarr.webhook.ReleaseChecker") as mock_checker_class:
            mock_checker = MagicMock()
            mock_checker.check_series = AsyncMock(side_effect=httpx.TimeoutException("Timed out"))
            mock_checker_class.return_value = mock_checker

            # Should not raise, just log the error
            await filtarr.webhook._process_series_check(456, "Test Series", full_config)

    @pytest.mark.asyncio
    async def test_handles_validation_error(self, full_config: Config) -> None:
        """Should handle ValidationError from checker."""
        with patch("filtarr.webhook.ReleaseChecker") as mock_checker_class:
            mock_checker = MagicMock()
            # Create a ValidationError with proper structure
            mock_checker.check_series = AsyncMock(
                side_effect=ValidationError.from_exception_data(
                    "TestModel", [{"type": "missing", "loc": ("field",), "input": {}}]
                )
            )
            mock_checker_class.return_value = mock_checker

            # Should not raise, just log the error
            await filtarr.webhook._process_series_check(456, "Test Series", full_config)

    @pytest.mark.asyncio
    async def test_handles_generic_exception(self, full_config: Config) -> None:
        """Should handle generic Exception from checker."""
        with patch("filtarr.webhook.ReleaseChecker") as mock_checker_class:
            mock_checker = MagicMock()
            mock_checker.check_series = AsyncMock(side_effect=RuntimeError("Unexpected error"))
            mock_checker_class.return_value = mock_checker

            # Should not raise, just log the error
            await filtarr.webhook._process_series_check(456, "Test Series", full_config)


class TestExceptionHandler:
    """Tests for the global exception handler."""

    def test_global_exception_handler(self, full_config: Config) -> None:
        """Should return 500 for unhandled exceptions."""
        app = filtarr.webhook.create_app(full_config)

        # Add a route that raises an exception
        @app.get("/raise-error")  # type: ignore[untyped-decorator]
        async def raise_error() -> None:
            raise RuntimeError("Test exception")

        client = TestClient(app, raise_server_exceptions=False)
        response = client.get("/raise-error")

        assert response.status_code == 500
        data = response.json()
        assert data["status"] == "error"
        assert data["message"] == "Internal server error"
        assert data["media_id"] is None
        assert data["media_title"] is None


class TestWebhookConfig:
    """Tests for WebhookConfig in the Config class."""

    def test_default_webhook_config(self) -> None:
        """Should have default webhook configuration."""
        config = Config()

        assert config.webhook.host == "0.0.0.0"
        assert config.webhook.port == 8080

    def test_custom_webhook_config(self) -> None:
        """Should accept custom webhook configuration."""
        config = Config(webhook=WebhookConfig(host="127.0.0.1", port=9000))

        assert config.webhook.host == "127.0.0.1"
        assert config.webhook.port == 9000


class TestCreateAppWithoutConfig:
    """Tests for create_app when config is not provided."""

    def test_create_app_loads_default_config(self) -> None:
        """Should load default config when none provided."""
        # Mock Config.load() to return a test config
        with patch("filtarr.webhook.Config.load") as mock_load:
            mock_config = Config(
                radarr=RadarrConfig(url="http://localhost:7878", api_key="test-key"),
            )
            mock_load.return_value = mock_config

            app = filtarr.webhook.create_app()
            assert app is not None
            mock_load.assert_called_once()


class TestFastAPIImportError:
    """Tests for FastAPI import error handling."""

    def test_create_app_raises_import_error(self) -> None:
        """Should raise ImportError when FastAPI is not installed."""
        with (
            patch.dict("sys.modules", {"fastapi": None}),
            patch("builtins.__import__", side_effect=ImportError("No module")),
        ):
            # This is tricky to test since FastAPI is installed
            # We verify the error message format instead
            pass  # The import error path is covered by the module structure


class TestRunServer:
    """Tests for the run_server function."""

    def test_run_server_imports_uvicorn(self) -> None:
        """Should import uvicorn when running server."""
        # Verify run_server is callable
        assert callable(filtarr.webhook.run_server)

    def test_run_server_with_scheduler_disabled(self, full_config: Config) -> None:
        """Should start server without scheduler when disabled."""
        import uvicorn

        from filtarr.config import SchedulerConfig

        config = Config(
            radarr=full_config.radarr,
            sonarr=full_config.sonarr,
            scheduler=SchedulerConfig(enabled=False),
        )

        with patch.object(uvicorn, "run") as mock_uvicorn_run:
            filtarr.webhook.run_server(config=config, scheduler_enabled=False)

            mock_uvicorn_run.assert_called_once()

    def test_run_server_loads_default_config(self) -> None:
        """Should load default config when none provided."""
        import uvicorn

        mock_config = Config()

        with (
            patch.object(uvicorn, "run"),
            patch("filtarr.webhook.Config.load") as mock_load,
        ):
            mock_load.return_value = mock_config

            filtarr.webhook.run_server(scheduler_enabled=False)

            mock_load.assert_called_once()


class TestBackgroundTaskManagement:
    """Tests for background task lifecycle management."""

    def test_background_task_added_to_set(self, full_config: Config) -> None:
        """Should add background tasks to set for GC protection."""
        app = filtarr.webhook.create_app(full_config)
        client = TestClient(app)

        mock_create_task = CreateTaskMock()
        with patch("filtarr.webhook.asyncio.create_task", mock_create_task):
            response = client.post(
                "/webhook/radarr",
                json={
                    "eventType": "MovieAdded",
                    "movie": {"id": 123, "title": "Test Movie", "year": 2023},
                },
                headers={"X-Api-Key": "radarr-api-key"},
            )

            assert response.status_code == 200
            # Verify task was created and done callback was added
            mock_create_task.assert_called_once()
            mock_task = mock_create_task.last_task
            mock_task.add_done_callback.assert_called_once()


class TestImportErrors:
    """Tests for import error handling in webhook module."""

    def test_create_app_fastapi_import_error(self, full_config: Config) -> None:
        """Should raise ImportError with helpful message when FastAPI not installed."""
        import sys

        # Save and remove fastapi from sys.modules to force re-import attempt
        saved_modules: dict[str, Any] = {}
        fastapi_modules = [
            key
            for key in list(sys.modules.keys())
            if key == "fastapi" or key.startswith("fastapi.")
        ]
        for mod in fastapi_modules:
            saved_modules[mod] = sys.modules.pop(mod)

        try:
            # Mock builtins.__import__ to fail for fastapi
            import builtins

            original_import = builtins.__import__

            def mock_import(name: str, *args: Any, **kwargs: Any) -> Any:
                if name == "fastapi" or name.startswith("fastapi."):
                    raise ImportError("No module named 'fastapi'")
                return original_import(name, *args, **kwargs)

            with (
                patch.object(builtins, "__import__", side_effect=mock_import),
                pytest.raises(ImportError) as exc_info,
            ):
                # Call create_app which has a local import of fastapi
                filtarr.webhook.create_app(full_config)

        finally:
            # Restore all saved modules
            sys.modules.update(saved_modules)

        assert "filtarr[webhook]" in str(exc_info.value)
        assert "FastAPI is required" in str(exc_info.value)

    def test_run_server_uvicorn_import_error(self, full_config: Config) -> None:
        """Should raise ImportError with helpful message when uvicorn not installed."""
        import builtins

        original_import = builtins.__import__

        def mock_import(name: str, *args, **kwargs):  # type: ignore[no-untyped-def]
            if name == "uvicorn":
                raise ImportError("No module named 'uvicorn'")
            return original_import(name, *args, **kwargs)

        with (
            patch.object(builtins, "__import__", side_effect=mock_import),
            pytest.raises(ImportError) as exc_info,
        ):
            filtarr.webhook.run_server(config=full_config, scheduler_enabled=False)

        assert "filtarr[webhook]" in str(exc_info.value)
        assert "uvicorn is required" in str(exc_info.value)


class TestTTLCaching:
    """Tests for TTL caching functionality in webhook processing."""

    @pytest.mark.asyncio
    async def test_movie_check_skips_when_ttl_cache_hit(
        self, full_config: Config, caplog: pytest.LogCaptureFixture
    ) -> None:
        """Should skip processing when movie was recently checked (TTL hit)."""
        import logging

        # Configure TTL to 24 hours
        config = Config(
            radarr=full_config.radarr,
            sonarr=full_config.sonarr,
            tags=full_config.tags,
            state=StateConfig(ttl_hours=24),
        )

        # Create a mock state manager with cached result
        mock_state_manager = MagicMock()
        cached_record = CheckRecord(
            last_checked=datetime.now(UTC),
            result="available",
            tag_applied="4k-available",
        )
        mock_state_manager.get_cached_result.return_value = cached_record

        # Store original value to restore later
        original_state_manager = filtarr.webhook._state_manager

        try:
            filtarr.webhook._state_manager = mock_state_manager

            with caplog.at_level(logging.INFO):
                await filtarr.webhook._process_movie_check(123, "Test Movie", config)

            # Verify state manager was queried for cached result
            mock_state_manager.get_cached_result.assert_called_once_with("movie", 123, 24)

            # Verify log message about using cached result
            assert any(
                "Using cached result" in record.message and "Test Movie" in record.message
                for record in caplog.records
            )
            assert any("result=available" in record.message for record in caplog.records)
        finally:
            filtarr.webhook._state_manager = original_state_manager

    @pytest.mark.asyncio
    async def test_movie_check_proceeds_when_ttl_cache_miss(self, full_config: Config) -> None:
        """Should proceed with check when movie TTL cache misses."""
        # Configure TTL to 24 hours
        config = Config(
            radarr=full_config.radarr,
            sonarr=full_config.sonarr,
            tags=full_config.tags,
            state=StateConfig(ttl_hours=24),
        )

        # Create a mock state manager with no cached result
        mock_state_manager = MagicMock()
        mock_state_manager.get_cached_result.return_value = None

        original_state_manager = filtarr.webhook._state_manager

        try:
            filtarr.webhook._state_manager = mock_state_manager

            # Mock ReleaseChecker to avoid actual API calls
            with patch("filtarr.webhook.ReleaseChecker") as mock_checker_class:
                mock_checker = MagicMock()
                mock_result = MagicMock()
                mock_result.has_match = True
                mock_result.matched_releases = []
                mock_result.releases = []
                mock_result.tag_result = MagicMock()
                mock_result.tag_result.tag_applied = "4k-available"
                mock_checker.check_movie = AsyncMock(return_value=mock_result)
                mock_checker_class.return_value = mock_checker

                await filtarr.webhook._process_movie_check(123, "Test Movie", config)

                # Verify ReleaseChecker was created and used
                mock_checker_class.assert_called_once()
                mock_checker.check_movie.assert_called_once_with(123, apply_tags=True)
        finally:
            filtarr.webhook._state_manager = original_state_manager

    @pytest.mark.asyncio
    async def test_movie_check_skips_ttl_when_disabled(self, full_config: Config) -> None:
        """Should skip TTL check when ttl_hours is 0 (disabled)."""
        # Configure TTL to 0 (disabled)
        config = Config(
            radarr=full_config.radarr,
            sonarr=full_config.sonarr,
            tags=full_config.tags,
            state=StateConfig(ttl_hours=0),
        )

        mock_state_manager = MagicMock()

        original_state_manager = filtarr.webhook._state_manager

        try:
            filtarr.webhook._state_manager = mock_state_manager

            with patch("filtarr.webhook.ReleaseChecker") as mock_checker_class:
                mock_checker = MagicMock()
                mock_result = MagicMock()
                mock_result.has_match = False
                mock_result.matched_releases = []
                mock_result.releases = []
                mock_result.tag_result = MagicMock()
                mock_result.tag_result.tag_applied = None
                mock_checker.check_movie = AsyncMock(return_value=mock_result)
                mock_checker_class.return_value = mock_checker

                await filtarr.webhook._process_movie_check(123, "Test Movie", config)

                # TTL check should not be called when disabled
                mock_state_manager.get_cached_result.assert_not_called()
        finally:
            filtarr.webhook._state_manager = original_state_manager

    @pytest.mark.asyncio
    async def test_series_check_skips_when_ttl_cache_hit(
        self, full_config: Config, caplog: pytest.LogCaptureFixture
    ) -> None:
        """Should skip processing when series was recently checked (TTL hit)."""
        import logging

        # Configure TTL to 24 hours
        config = Config(
            radarr=full_config.radarr,
            sonarr=full_config.sonarr,
            tags=full_config.tags,
            state=StateConfig(ttl_hours=24),
        )

        # Create a mock state manager with cached result
        mock_state_manager = MagicMock()
        cached_record = CheckRecord(
            last_checked=datetime.now(UTC),
            result="unavailable",
            tag_applied="4k-unavailable",
        )
        mock_state_manager.get_cached_result.return_value = cached_record

        original_state_manager = filtarr.webhook._state_manager

        try:
            filtarr.webhook._state_manager = mock_state_manager

            with caplog.at_level(logging.INFO):
                await filtarr.webhook._process_series_check(456, "Breaking Bad", config)

            # Verify state manager was queried for cached result
            mock_state_manager.get_cached_result.assert_called_once_with("series", 456, 24)

            # Verify log message about using cached result
            assert any(
                "Using cached result" in record.message and "Breaking Bad" in record.message
                for record in caplog.records
            )
            assert any("result=unavailable" in record.message for record in caplog.records)
        finally:
            filtarr.webhook._state_manager = original_state_manager

    @pytest.mark.asyncio
    async def test_series_check_proceeds_when_ttl_cache_miss(self, full_config: Config) -> None:
        """Should proceed with check when series TTL cache misses."""
        # Configure TTL to 24 hours
        config = Config(
            radarr=full_config.radarr,
            sonarr=full_config.sonarr,
            tags=full_config.tags,
            state=StateConfig(ttl_hours=24),
        )

        # Create a mock state manager with no cached result
        mock_state_manager = MagicMock()
        mock_state_manager.get_cached_result.return_value = None

        original_state_manager = filtarr.webhook._state_manager

        try:
            filtarr.webhook._state_manager = mock_state_manager

            # Mock ReleaseChecker to avoid actual API calls
            with patch("filtarr.webhook.ReleaseChecker") as mock_checker_class:
                mock_checker = MagicMock()
                mock_result = MagicMock()
                mock_result.has_match = True
                mock_result.matched_releases = []
                mock_result.releases = []
                mock_result.tag_result = MagicMock()
                mock_result.tag_result.tag_applied = "4k-available"
                mock_checker.check_series = AsyncMock(return_value=mock_result)
                mock_checker_class.return_value = mock_checker

                await filtarr.webhook._process_series_check(456, "Breaking Bad", config)

                # Verify ReleaseChecker was created and used
                mock_checker_class.assert_called_once()
                mock_checker.check_series.assert_called_once_with(456, apply_tags=True)
        finally:
            filtarr.webhook._state_manager = original_state_manager

    @pytest.mark.asyncio
    async def test_series_check_skips_ttl_when_disabled(self, full_config: Config) -> None:
        """Should skip TTL check when ttl_hours is 0 (disabled)."""
        # Configure TTL to 0 (disabled)
        config = Config(
            radarr=full_config.radarr,
            sonarr=full_config.sonarr,
            tags=full_config.tags,
            state=StateConfig(ttl_hours=0),
        )

        mock_state_manager = MagicMock()

        original_state_manager = filtarr.webhook._state_manager

        try:
            filtarr.webhook._state_manager = mock_state_manager

            with patch("filtarr.webhook.ReleaseChecker") as mock_checker_class:
                mock_checker = MagicMock()
                mock_result = MagicMock()
                mock_result.has_match = False
                mock_result.matched_releases = []
                mock_result.releases = []
                mock_result.tag_result = MagicMock()
                mock_result.tag_result.tag_applied = None
                mock_checker.check_series = AsyncMock(return_value=mock_result)
                mock_checker_class.return_value = mock_checker

                await filtarr.webhook._process_series_check(456, "Breaking Bad", config)

                # TTL check should not be called when disabled
                mock_state_manager.get_cached_result.assert_not_called()
        finally:
            filtarr.webhook._state_manager = original_state_manager


class TestStateRecording:
    """Tests for state recording after successful checks."""

    @pytest.mark.asyncio
    async def test_movie_check_records_result_in_state(
        self, full_config: Config, caplog: pytest.LogCaptureFixture
    ) -> None:
        """Should record check result in state file after successful movie check."""
        import logging

        config = Config(
            radarr=full_config.radarr,
            sonarr=full_config.sonarr,
            tags=full_config.tags,
            state=StateConfig(ttl_hours=24),
        )

        mock_state_manager = MagicMock()
        mock_state_manager.get_cached_result.return_value = None

        original_state_manager = filtarr.webhook._state_manager

        try:
            filtarr.webhook._state_manager = mock_state_manager

            with (
                patch("filtarr.webhook.ReleaseChecker") as mock_checker_class,
                caplog.at_level(logging.INFO),
            ):
                mock_checker = MagicMock()
                mock_result = MagicMock()
                mock_result.has_match = True
                mock_result.matched_releases = [MagicMock()]
                mock_result.releases = [MagicMock()]
                mock_result.tag_result = MagicMock()
                mock_result.tag_result.tag_applied = "4k-available"
                mock_checker.check_movie = AsyncMock(return_value=mock_result)
                mock_checker_class.return_value = mock_checker

                await filtarr.webhook._process_movie_check(123, "Test Movie", config)

                # Verify state was recorded
                mock_state_manager.record_check.assert_called_once_with(
                    "movie",
                    123,
                    True,
                    "4k-available",
                )

                # Verify log message includes tag info
                assert any(
                    "Check complete" in record.message
                    and "available" in record.message
                    and "tag=4k-available" in record.message
                    for record in caplog.records
                )
        finally:
            filtarr.webhook._state_manager = original_state_manager

    @pytest.mark.asyncio
    async def test_series_check_records_result_in_state(
        self, full_config: Config, caplog: pytest.LogCaptureFixture
    ) -> None:
        """Should record check result in state file after successful series check."""
        import logging

        config = Config(
            radarr=full_config.radarr,
            sonarr=full_config.sonarr,
            tags=full_config.tags,
            state=StateConfig(ttl_hours=24),
        )

        mock_state_manager = MagicMock()
        mock_state_manager.get_cached_result.return_value = None

        original_state_manager = filtarr.webhook._state_manager

        try:
            filtarr.webhook._state_manager = mock_state_manager

            with (
                patch("filtarr.webhook.ReleaseChecker") as mock_checker_class,
                caplog.at_level(logging.INFO),
            ):
                mock_checker = MagicMock()
                mock_result = MagicMock()
                mock_result.has_match = False
                mock_result.matched_releases = []
                mock_result.releases = [MagicMock(), MagicMock()]
                mock_result.tag_result = MagicMock()
                mock_result.tag_result.tag_applied = "4k-unavailable"
                mock_checker.check_series = AsyncMock(return_value=mock_result)
                mock_checker_class.return_value = mock_checker

                await filtarr.webhook._process_series_check(456, "Breaking Bad", config)

                # Verify state was recorded
                mock_state_manager.record_check.assert_called_once_with(
                    "series",
                    456,
                    False,
                    "4k-unavailable",
                )

                # Verify log message includes tag info
                assert any(
                    "Check complete" in record.message
                    and "unavailable" in record.message
                    and "tag=4k-unavailable" in record.message
                    for record in caplog.records
                )
        finally:
            filtarr.webhook._state_manager = original_state_manager


class TestStateManagerInitialization:
    """Tests for state manager initialization in run_server."""

    def test_state_manager_initialized_at_startup(
        self, full_config: Config, caplog: pytest.LogCaptureFixture
    ) -> None:
        """Should initialize state manager at server startup."""
        import logging

        import uvicorn

        mock_state_manager = MagicMock()

        original_state_manager = filtarr.webhook._state_manager

        try:
            with (
                patch(
                    "filtarr.state.StateManager", return_value=mock_state_manager
                ) as mock_state_class,
                patch.object(uvicorn, "run") as mock_uvicorn_run,
                caplog.at_level(logging.INFO),
            ):
                filtarr.webhook.run_server(config=full_config, scheduler_enabled=False)

                # Verify StateManager was created with correct path
                mock_state_class.assert_called_once_with(full_config.state.path)

                # Verify ensure_initialized was called
                mock_state_manager.ensure_initialized.assert_called_once()

                # Server should be started
                mock_uvicorn_run.assert_called_once()

                # Verify log message about state initialization
                assert any("State file initialized" in record.message for record in caplog.records)
        finally:
            filtarr.webhook._state_manager = original_state_manager

    def test_state_ttl_disabled_logging(
        self, full_config: Config, caplog: pytest.LogCaptureFixture
    ) -> None:
        """Should log 'TTL: disabled' when ttl_hours is 0."""
        import logging

        import uvicorn

        # Configure TTL to 0 (disabled)
        config = Config(
            radarr=full_config.radarr,
            sonarr=full_config.sonarr,
            state=StateConfig(ttl_hours=0),
        )

        mock_state_manager = MagicMock()

        original_state_manager = filtarr.webhook._state_manager

        try:
            with (
                patch("filtarr.state.StateManager", return_value=mock_state_manager),
                patch.object(uvicorn, "run"),
                caplog.at_level(logging.INFO),
            ):
                filtarr.webhook.run_server(config=config, scheduler_enabled=False)

                # Verify log message about TTL being disabled
                assert any("State TTL: disabled" in record.message for record in caplog.records)
        finally:
            filtarr.webhook._state_manager = original_state_manager

    def test_state_ttl_enabled_logging(
        self, full_config: Config, caplog: pytest.LogCaptureFixture
    ) -> None:
        """Should log TTL hours when ttl_hours is greater than 0."""
        import logging

        import uvicorn

        # Configure TTL to 48 hours
        config = Config(
            radarr=full_config.radarr,
            sonarr=full_config.sonarr,
            state=StateConfig(ttl_hours=48),
        )

        mock_state_manager = MagicMock()

        original_state_manager = filtarr.webhook._state_manager

        try:
            with (
                patch("filtarr.state.StateManager", return_value=mock_state_manager),
                patch.object(uvicorn, "run"),
                caplog.at_level(logging.INFO),
            ):
                filtarr.webhook.run_server(config=config, scheduler_enabled=False)

                # Verify log message about TTL hours
                assert any("State TTL: 48 hours" in record.message for record in caplog.records)
        finally:
            filtarr.webhook._state_manager = original_state_manager


class TestSchedulerLifecycle:
    """Tests for scheduler lifecycle management in run_server."""

    def test_scheduler_initialization_when_enabled(self, full_config: Config) -> None:
        """Should create scheduler manager when scheduler is enabled in config."""
        from filtarr.config import SchedulerConfig

        config = Config(
            radarr=full_config.radarr,
            sonarr=full_config.sonarr,
            scheduler=SchedulerConfig(enabled=True),
        )

        mock_scheduler_manager = MagicMock()
        mock_scheduler_manager.start = AsyncMock()
        mock_scheduler_manager.stop = AsyncMock()

        mock_state_manager = MagicMock()

        # Store original scheduler manager
        original_scheduler = filtarr.webhook._scheduler_manager

        try:
            with (
                patch(
                    "filtarr.scheduler.SchedulerManager", return_value=mock_scheduler_manager
                ) as mock_scheduler_class,
                patch(
                    "filtarr.state.StateManager", return_value=mock_state_manager
                ) as mock_state_class,
                patch("filtarr.webhook.create_app"),
                patch("uvicorn.Config"),
                patch("uvicorn.Server") as mock_server_class,
            ):
                # Mock server.serve() to not actually run
                mock_server = MagicMock()
                mock_server.serve = AsyncMock()
                mock_server_class.return_value = mock_server

                # Run the server briefly
                filtarr.webhook.run_server(config=config, scheduler_enabled=True)

                # Verify scheduler was initialized
                mock_state_class.assert_called_once()
                mock_scheduler_class.assert_called_once_with(config, mock_state_manager)
                mock_scheduler_manager.start.assert_called_once()

        finally:
            filtarr.webhook._scheduler_manager = original_scheduler

    def test_scheduler_import_error_continues_without_scheduler(
        self, full_config: Config, caplog: pytest.LogCaptureFixture
    ) -> None:
        """Should log warning and continue without scheduler when imports fail."""
        import builtins
        import logging

        import uvicorn

        from filtarr.config import SchedulerConfig

        config = Config(
            radarr=full_config.radarr,
            sonarr=full_config.sonarr,
            scheduler=SchedulerConfig(enabled=True),
        )

        original_scheduler = filtarr.webhook._scheduler_manager
        original_import = builtins.__import__

        def mock_import(
            name: str,
            globals_: dict[str, object] | None = None,
            locals_: dict[str, object] | None = None,
            fromlist: tuple[str, ...] = (),
            level: int = 0,
        ) -> object:
            # Only block scheduler imports - state is a core dependency
            if name == "filtarr.scheduler":
                raise ImportError(f"No module named '{name}'")
            return original_import(name, globals_, locals_, fromlist, level)

        try:
            with (
                patch.object(builtins, "__import__", side_effect=mock_import),
                patch.object(uvicorn, "run") as mock_uvicorn_run,
                caplog.at_level(logging.WARNING),
            ):
                filtarr.webhook.run_server(config=config, scheduler_enabled=True)

                # Server should still be started (without scheduler)
                mock_uvicorn_run.assert_called_once()

                # Should have logged warning about scheduler dependencies
                assert any(
                    "Scheduler dependencies not installed" in record.message
                    for record in caplog.records
                )

        finally:
            filtarr.webhook._scheduler_manager = original_scheduler

    def test_scheduler_start_import_error(
        self, full_config: Config, caplog: pytest.LogCaptureFixture
    ) -> None:
        """Should log error when scheduler.start() raises ImportError."""
        import logging

        from filtarr.config import SchedulerConfig

        config = Config(
            radarr=full_config.radarr,
            sonarr=full_config.sonarr,
            scheduler=SchedulerConfig(enabled=True),
        )

        mock_scheduler_manager = MagicMock()
        mock_scheduler_manager.start = AsyncMock(
            side_effect=ImportError("APScheduler not installed")
        )
        mock_scheduler_manager.stop = AsyncMock()

        mock_state_manager = MagicMock()

        original_scheduler = filtarr.webhook._scheduler_manager

        try:
            with (
                patch("filtarr.scheduler.SchedulerManager", return_value=mock_scheduler_manager),
                patch("filtarr.state.StateManager", return_value=mock_state_manager),
                patch("filtarr.webhook.create_app"),
                patch("uvicorn.Config"),
                patch("uvicorn.Server") as mock_server_class,
                caplog.at_level(logging.ERROR),
            ):
                mock_server = MagicMock()
                mock_server.serve = AsyncMock()
                mock_server_class.return_value = mock_server

                filtarr.webhook.run_server(config=config, scheduler_enabled=True)

                # Scheduler start should have been attempted
                mock_scheduler_manager.start.assert_called_once()

                # Error should be logged
                assert any(
                    "Failed to start scheduler" in record.message for record in caplog.records
                )

        finally:
            filtarr.webhook._scheduler_manager = original_scheduler

    def test_scheduler_start_value_error(
        self, full_config: Config, caplog: pytest.LogCaptureFixture
    ) -> None:
        """Should log error when scheduler.start() raises ValueError."""
        import logging

        from filtarr.config import SchedulerConfig

        config = Config(
            radarr=full_config.radarr,
            sonarr=full_config.sonarr,
            scheduler=SchedulerConfig(enabled=True),
        )

        mock_scheduler_manager = MagicMock()
        mock_scheduler_manager.start = AsyncMock(
            side_effect=ValueError("Invalid schedule configuration")
        )
        mock_scheduler_manager.stop = AsyncMock()

        mock_state_manager = MagicMock()

        original_scheduler = filtarr.webhook._scheduler_manager

        try:
            with (
                patch("filtarr.scheduler.SchedulerManager", return_value=mock_scheduler_manager),
                patch("filtarr.state.StateManager", return_value=mock_state_manager),
                patch("filtarr.webhook.create_app"),
                patch("uvicorn.Config"),
                patch("uvicorn.Server") as mock_server_class,
                caplog.at_level(logging.ERROR),
            ):
                mock_server = MagicMock()
                mock_server.serve = AsyncMock()
                mock_server_class.return_value = mock_server

                filtarr.webhook.run_server(config=config, scheduler_enabled=True)

                # Error should be logged
                assert any(
                    "Failed to start scheduler" in record.message for record in caplog.records
                )

        finally:
            filtarr.webhook._scheduler_manager = original_scheduler

    def test_scheduler_start_runtime_error(
        self, full_config: Config, caplog: pytest.LogCaptureFixture
    ) -> None:
        """Should log error when scheduler.start() raises RuntimeError."""
        import logging

        from filtarr.config import SchedulerConfig

        config = Config(
            radarr=full_config.radarr,
            sonarr=full_config.sonarr,
            scheduler=SchedulerConfig(enabled=True),
        )

        mock_scheduler_manager = MagicMock()
        mock_scheduler_manager.start = AsyncMock(
            side_effect=RuntimeError("Scheduler already running")
        )
        mock_scheduler_manager.stop = AsyncMock()

        mock_state_manager = MagicMock()

        original_scheduler = filtarr.webhook._scheduler_manager

        try:
            with (
                patch("filtarr.scheduler.SchedulerManager", return_value=mock_scheduler_manager),
                patch("filtarr.state.StateManager", return_value=mock_state_manager),
                patch("filtarr.webhook.create_app"),
                patch("uvicorn.Config"),
                patch("uvicorn.Server") as mock_server_class,
                caplog.at_level(logging.ERROR),
            ):
                mock_server = MagicMock()
                mock_server.serve = AsyncMock()
                mock_server_class.return_value = mock_server

                filtarr.webhook.run_server(config=config, scheduler_enabled=True)

                # Error should be logged (RuntimeError uses different message)
                assert any("Scheduler runtime error" in record.message for record in caplog.records)

        finally:
            filtarr.webhook._scheduler_manager = original_scheduler

    def test_scheduler_shutdown_cancelled_error(
        self, full_config: Config, caplog: pytest.LogCaptureFixture
    ) -> None:
        """Should log debug message when scheduler.stop() raises CancelledError."""
        import asyncio
        import logging

        from filtarr.config import SchedulerConfig

        config = Config(
            radarr=full_config.radarr,
            sonarr=full_config.sonarr,
            scheduler=SchedulerConfig(enabled=True),
        )

        mock_scheduler_manager = MagicMock()
        mock_scheduler_manager.start = AsyncMock()
        mock_scheduler_manager.stop = AsyncMock(side_effect=asyncio.CancelledError())

        mock_state_manager = MagicMock()

        original_scheduler = filtarr.webhook._scheduler_manager

        try:
            with (
                patch("filtarr.scheduler.SchedulerManager", return_value=mock_scheduler_manager),
                patch("filtarr.state.StateManager", return_value=mock_state_manager),
                patch("filtarr.webhook.create_app"),
                patch("uvicorn.Config"),
                patch("uvicorn.Server") as mock_server_class,
                caplog.at_level(logging.DEBUG),
            ):
                mock_server = MagicMock()
                mock_server.serve = AsyncMock()
                mock_server_class.return_value = mock_server

                filtarr.webhook.run_server(config=config, scheduler_enabled=True)

                # Debug message should be logged
                assert any(
                    "Scheduler stop cancelled during shutdown" in record.message
                    for record in caplog.records
                )

        finally:
            filtarr.webhook._scheduler_manager = original_scheduler

    def test_scheduler_shutdown_runtime_error(
        self, full_config: Config, caplog: pytest.LogCaptureFixture
    ) -> None:
        """Should log warning when scheduler.stop() raises RuntimeError."""
        import logging

        from filtarr.config import SchedulerConfig

        config = Config(
            radarr=full_config.radarr,
            sonarr=full_config.sonarr,
            scheduler=SchedulerConfig(enabled=True),
        )

        mock_scheduler_manager = MagicMock()
        mock_scheduler_manager.start = AsyncMock()
        mock_scheduler_manager.stop = AsyncMock(
            side_effect=RuntimeError("Scheduler state issue during shutdown")
        )

        mock_state_manager = MagicMock()

        original_scheduler = filtarr.webhook._scheduler_manager

        try:
            with (
                patch("filtarr.scheduler.SchedulerManager", return_value=mock_scheduler_manager),
                patch("filtarr.state.StateManager", return_value=mock_state_manager),
                patch("filtarr.webhook.create_app"),
                patch("uvicorn.Config"),
                patch("uvicorn.Server") as mock_server_class,
                caplog.at_level(logging.WARNING),
            ):
                mock_server = MagicMock()
                mock_server.serve = AsyncMock()
                mock_server_class.return_value = mock_server

                filtarr.webhook.run_server(config=config, scheduler_enabled=True)

                # Warning should be logged
                assert any(
                    "Error during scheduler shutdown" in record.message for record in caplog.records
                )

        finally:
            filtarr.webhook._scheduler_manager = original_scheduler
