"""Tests for scheduler API endpoints.

This module tests all scheduler management REST API endpoints including
configuration retrieval, updates, and manual rescan triggering.
"""
from unittest.mock import AsyncMock, Mock, patch

import pytest
from httpx import ASGITransport, AsyncClient

from src.server.fastapi_app import app
from src.server.models.config import SchedulerConfig
from src.server.services.auth_service import auth_service


@pytest.fixture(autouse=True)
def reset_auth_state():
    """Reset auth service state before each test."""
    auth_service._hash = None
    auth_service._failed.clear()
    if not auth_service.is_configured():
        auth_service.setup_master_password("TestPass123!")
    yield
    auth_service._hash = None
    auth_service._failed.clear()


@pytest.fixture
async def client():
    """Create an async test client."""
    transport = ASGITransport(app=app)
    async with AsyncClient(transport=transport, base_url="http://test") as ac:
        yield ac


@pytest.fixture
async def authenticated_client(client):
    """Create an authenticated test client with token."""
    response = await client.post(
        "/api/auth/login",
        json={"password": "TestPass123!"}
    )
    assert response.status_code == 200
    token = response.json()["access_token"]
    client.headers.update({"Authorization": f"Bearer {token}"})
    yield client


@pytest.fixture
def mock_config_service():
    """Create mock configuration service with default SchedulerConfig."""
    service = Mock()
    config = Mock()
    config.scheduler = SchedulerConfig(
        enabled=True,
        interval_minutes=60,
        schedule_time="03:00",
        schedule_days=["mon", "tue", "wed", "thu", "fri", "sat", "sun"],
        auto_download_after_rescan=False,
    )

    def save_config_side_effect(new_config):
        config.scheduler = new_config.scheduler

    service.load_config = Mock(return_value=config)
    service.save_config = Mock(side_effect=save_config_side_effect)
    return service


@pytest.fixture
def mock_scheduler_service():
    """Create a mock scheduler service returning a basic status."""
    svc = Mock()
    svc.get_status = Mock(return_value={
        "is_running": True,
        "next_run": None,
        "last_run": None,
        "scan_in_progress": False,
    })
    svc.reload_config = Mock()
    return svc


# ---------------------------------------------------------------------------
# GET /api/scheduler/config
# ---------------------------------------------------------------------------
class TestGetSchedulerConfig:
    """Tests for GET /api/scheduler/config endpoint."""

    @pytest.mark.asyncio
    async def test_returns_success_envelope(
        self, authenticated_client, mock_config_service, mock_scheduler_service
    ):
        """Response carries the top-level success/config/status envelope."""
        with patch("src.server.api.scheduler.get_config_service", return_value=mock_config_service), \
             patch("src.server.api.scheduler.get_scheduler_service", return_value=mock_scheduler_service):
            response = await authenticated_client.get("/api/scheduler/config")

        assert response.status_code == 200
        data = response.json()
        assert data["success"] is True
        assert "config" in data
        assert "status" in data

    @pytest.mark.asyncio
    async def test_config_contains_all_fields(
        self, authenticated_client, mock_config_service, mock_scheduler_service
    ):
        """Config block includes all SchedulerConfig fields."""
        with patch("src.server.api.scheduler.get_config_service", return_value=mock_config_service), \
             patch("src.server.api.scheduler.get_scheduler_service", return_value=mock_scheduler_service):
            response = await authenticated_client.get("/api/scheduler/config")

        cfg = response.json()["config"]
        assert cfg["enabled"] is True
        assert cfg["interval_minutes"] == 60
        assert cfg["schedule_time"] == "03:00"
        assert cfg["schedule_days"] == ["mon", "tue", "wed", "thu", "fri", "sat", "sun"]
        assert cfg["auto_download_after_rescan"] is False

    @pytest.mark.asyncio
    async def test_status_block_present(
        self, authenticated_client, mock_config_service, mock_scheduler_service
    ):
        """Status block includes runtime keys."""
        with patch("src.server.api.scheduler.get_config_service", return_value=mock_config_service), \
             patch("src.server.api.scheduler.get_scheduler_service", return_value=mock_scheduler_service):
            response = await authenticated_client.get("/api/scheduler/config")

        st = response.json()["status"]
        for key in ("is_running", "next_run", "last_run", "scan_in_progress"):
            assert key in st

    @pytest.mark.asyncio
    async def test_unauthorized(self, client):
        """GET without auth token returns 401."""
        response = await client.get("/api/scheduler/config")
        assert response.status_code == 401

    @pytest.mark.asyncio
    async def test_config_load_failure_returns_500(
        self, authenticated_client, mock_config_service, mock_scheduler_service
    ):
        """500 when config_service.load_config raises ConfigServiceError."""
        from src.server.services.config_service import ConfigServiceError

        mock_config_service.load_config.side_effect = ConfigServiceError("disk error")

        with patch("src.server.api.scheduler.get_config_service", return_value=mock_config_service), \
             patch("src.server.api.scheduler.get_scheduler_service", return_value=mock_scheduler_service):
            response = await authenticated_client.get("/api/scheduler/config")

        assert response.status_code == 500
        assert "Failed to load scheduler configuration" in response.text


# ---------------------------------------------------------------------------
# POST /api/scheduler/config
# ---------------------------------------------------------------------------
class TestUpdateSchedulerConfig:
    """Tests for POST /api/scheduler/config endpoint."""

    @pytest.mark.asyncio
    async def test_update_returns_success_envelope(
        self, authenticated_client, mock_config_service, mock_scheduler_service
    ):
        """POST returns success envelope with saved values."""
        payload = {
            "enabled": False,
            "interval_minutes": 120,
            "schedule_time": "06:30",
            "schedule_days": ["mon", "wed", "fri"],
            "auto_download_after_rescan": True,
        }

        with patch("src.server.api.scheduler.get_config_service", return_value=mock_config_service), \
             patch("src.server.api.scheduler.get_scheduler_service", return_value=mock_scheduler_service):
            response = await authenticated_client.post("/api/scheduler/config", json=payload)

        assert response.status_code == 200
        data = response.json()
        assert data["success"] is True
        assert data["config"]["enabled"] is False
        assert data["config"]["schedule_time"] == "06:30"
        assert data["config"]["schedule_days"] == ["mon", "wed", "fri"]
        assert data["config"]["auto_download_after_rescan"] is True

    @pytest.mark.asyncio
    async def test_update_persists_to_config_service(
        self, authenticated_client, mock_config_service, mock_scheduler_service
    ):
        """POST calls save_config exactly once."""
        payload = {"enabled": True, "interval_minutes": 30}

        with patch("src.server.api.scheduler.get_config_service", return_value=mock_config_service), \
             patch("src.server.api.scheduler.get_scheduler_service", return_value=mock_scheduler_service):
            response = await authenticated_client.post("/api/scheduler/config", json=payload)

        assert response.status_code == 200
        mock_config_service.save_config.assert_called_once()

    @pytest.mark.asyncio
    async def test_reload_config_called_after_save(
        self, authenticated_client, mock_config_service, mock_scheduler_service
    ):
        """POST calls scheduler_service.reload_config(SchedulerConfig) after save."""
        payload = {"enabled": True, "schedule_time": "10:00"}

        with patch("src.server.api.scheduler.get_config_service", return_value=mock_config_service), \
             patch("src.server.api.scheduler.get_scheduler_service", return_value=mock_scheduler_service):
            await authenticated_client.post("/api/scheduler/config", json=payload)

        mock_scheduler_service.reload_config.assert_called_once()
        call_arg = mock_scheduler_service.reload_config.call_args[0][0]
        assert isinstance(call_arg, SchedulerConfig)
        assert call_arg.schedule_time == "10:00"

    @pytest.mark.asyncio
    async def test_update_unauthorized(self, client):
        """POST without auth token returns 401."""
        response = await client.post(
            "/api/scheduler/config",
            json={"enabled": False, "interval_minutes": 120},
        )
        assert response.status_code == 401

    @pytest.mark.asyncio
    async def test_invalid_interval_returns_422(self, authenticated_client):
        """interval_minutes < 1 triggers Pydantic validation error (422)."""
        response = await authenticated_client.post(
            "/api/scheduler/config",
            json={"enabled": True, "interval_minutes": 0},
        )
        assert response.status_code == 422

    @pytest.mark.asyncio
    async def test_invalid_schedule_time_returns_422(self, authenticated_client):
        """Bad schedule_time format triggers validation error (422)."""
        response = await authenticated_client.post(
            "/api/scheduler/config",
            json={"enabled": True, "schedule_time": "25:00"},
        )
        assert response.status_code == 422

    @pytest.mark.asyncio
    async def test_invalid_schedule_days_returns_422(self, authenticated_client):
        """Unknown day abbreviation triggers validation error (422)."""
        response = await authenticated_client.post(
            "/api/scheduler/config",
            json={"enabled": True, "schedule_days": ["monday"]},
        )
        assert response.status_code == 422

    @pytest.mark.asyncio
    async def test_empty_schedule_days_accepted(
        self, authenticated_client, mock_config_service, mock_scheduler_service
    ):
        """Empty schedule_days list is valid (disables the cron job)."""
        payload = {"enabled": True, "schedule_days": []}

        with patch("src.server.api.scheduler.get_config_service", return_value=mock_config_service), \
             patch("src.server.api.scheduler.get_scheduler_service", return_value=mock_scheduler_service):
            response = await authenticated_client.post("/api/scheduler/config", json=payload)

        assert response.status_code == 200
        assert response.json()["config"]["schedule_days"] == []

    @pytest.mark.asyncio
    async def test_update_enable_disable_toggle(
        self, authenticated_client, mock_config_service, mock_scheduler_service
    ):
        """Toggling enabled is reflected in the returned config."""
        with patch("src.server.api.scheduler.get_config_service", return_value=mock_config_service), \
             patch("src.server.api.scheduler.get_scheduler_service", return_value=mock_scheduler_service):
            r1 = await authenticated_client.post(
                "/api/scheduler/config",
                json={"enabled": True, "interval_minutes": 60},
            )
            assert r1.json()["config"]["enabled"] is True

            r2 = await authenticated_client.post(
                "/api/scheduler/config",
                json={"enabled": False, "interval_minutes": 60},
            )
            assert r2.json()["config"]["enabled"] is False

    @pytest.mark.asyncio
    async def test_save_failure_returns_500(
        self, authenticated_client, mock_config_service, mock_scheduler_service
    ):
        """500 when config_service.save_config raises ConfigServiceError."""
        from src.server.services.config_service import ConfigServiceError

        mock_config_service.save_config.side_effect = ConfigServiceError("disk full")

        with patch("src.server.api.scheduler.get_config_service", return_value=mock_config_service), \
             patch("src.server.api.scheduler.get_scheduler_service", return_value=mock_scheduler_service):
            response = await authenticated_client.post(
                "/api/scheduler/config",
                json={"enabled": False},
            )

        assert response.status_code == 500
        assert "Failed to update scheduler configuration" in response.text

    @pytest.mark.asyncio
    async def test_backward_compat_minimal_payload(
        self, authenticated_client, mock_config_service, mock_scheduler_service
    ):
        """Payload with only legacy fields fills new fields with model defaults."""
        payload = {"enabled": True, "interval_minutes": 60}

        with patch("src.server.api.scheduler.get_config_service", return_value=mock_config_service), \
             patch("src.server.api.scheduler.get_scheduler_service", return_value=mock_scheduler_service):
            response = await authenticated_client.post("/api/scheduler/config", json=payload)

        assert response.status_code == 200
        cfg = response.json()["config"]
        assert cfg["schedule_time"] == "03:00"
        assert cfg["auto_download_after_rescan"] is False
        assert len(cfg["schedule_days"]) == 7

    @pytest.mark.asyncio
    async def test_interval_boundary_values(
        self, authenticated_client, mock_config_service, mock_scheduler_service
    ):
        """interval_minutes = 1 and 10080 (1 week) are both valid."""
        with patch("src.server.api.scheduler.get_config_service", return_value=mock_config_service), \
             patch("src.server.api.scheduler.get_scheduler_service", return_value=mock_scheduler_service):
            for minutes in (1, 10080):
                r = await authenticated_client.post(
                    "/api/scheduler/config",
                    json={"enabled": True, "interval_minutes": minutes},
                )
                assert r.status_code == 200


# ---------------------------------------------------------------------------
# POST /api/scheduler/trigger-rescan
# ---------------------------------------------------------------------------
class TestTriggerRescan:
    """Tests for POST /api/scheduler/trigger-rescan endpoint."""

    @pytest.mark.asyncio
    async def test_trigger_rescan_success(self, authenticated_client):
        """Successful trigger returns 200 with a message."""
        mock_trigger = AsyncMock(return_value={"message": "Rescan triggered"})
        mock_series_app = Mock()

        with patch("src.server.utils.dependencies.get_series_app", return_value=mock_series_app), \
             patch("src.server.api.anime.trigger_rescan", mock_trigger):
            response = await authenticated_client.post("/api/scheduler/trigger-rescan")

        assert response.status_code == 200
        assert "message" in response.json()
        mock_trigger.assert_called_once()

    @pytest.mark.asyncio
    async def test_trigger_rescan_unauthorized(self, client):
        """Trigger without auth token returns 401."""
        response = await client.post("/api/scheduler/trigger-rescan")
        assert response.status_code == 401

    @pytest.mark.asyncio
    async def test_trigger_rescan_series_app_not_initialized(
        self, authenticated_client
    ):
        """503 when SeriesApp is not yet initialised."""
        with patch("src.server.utils.dependencies.get_series_app", return_value=None):
            response = await authenticated_client.post("/api/scheduler/trigger-rescan")

        assert response.status_code == 503
        assert "SeriesApp not initialized" in response.text

    @pytest.mark.asyncio
    async def test_trigger_rescan_failure(self, authenticated_client):
        """500 when underlying rescan call raises an exception."""
        mock_trigger = AsyncMock(side_effect=Exception("Rescan failed"))
        mock_series_app = Mock()

        with patch("src.server.utils.dependencies.get_series_app", return_value=mock_series_app), \
             patch("src.server.api.anime.trigger_rescan", mock_trigger):
            response = await authenticated_client.post("/api/scheduler/trigger-rescan")

        assert response.status_code == 500
        assert "Failed to trigger rescan" in response.text


# ---------------------------------------------------------------------------
# Multi-step integration tests
# ---------------------------------------------------------------------------
class TestSchedulerEndpointsIntegration:
    """Multi-step integration tests for scheduler endpoints."""

    @pytest.mark.asyncio
    async def test_full_config_workflow(
        self, authenticated_client, mock_config_service, mock_scheduler_service
    ):
        """GET → POST → verify save called and response consistent."""
        with patch("src.server.api.scheduler.get_config_service", return_value=mock_config_service), \
             patch("src.server.api.scheduler.get_scheduler_service", return_value=mock_scheduler_service):
            r = await authenticated_client.get("/api/scheduler/config")
            assert r.status_code == 200
            assert r.json()["config"]["enabled"] is True

            r = await authenticated_client.post(
                "/api/scheduler/config",
                json={
                    "enabled": False,
                    "interval_minutes": 30,
                    "schedule_time": "12:00",
                    "schedule_days": ["mon", "fri"],
                },
            )
            assert r.status_code == 200
            cfg = r.json()["config"]
            assert cfg["enabled"] is False
            assert cfg["interval_minutes"] == 30
            assert cfg["schedule_time"] == "12:00"
            assert cfg["schedule_days"] == ["mon", "fri"]

        mock_config_service.save_config.assert_called_once()

    @pytest.mark.asyncio
    async def test_trigger_rescan_after_config_update(
        self, authenticated_client, mock_config_service, mock_scheduler_service
    ):
        """POST config then POST trigger-rescan both succeed."""
        mock_trigger = AsyncMock(return_value={"message": "Rescan triggered"})
        mock_series_app = Mock()

        with patch("src.server.api.scheduler.get_config_service", return_value=mock_config_service), \
             patch("src.server.api.scheduler.get_scheduler_service", return_value=mock_scheduler_service), \
             patch("src.server.utils.dependencies.get_series_app", return_value=mock_series_app), \
             patch("src.server.api.anime.trigger_rescan", mock_trigger):
            r = await authenticated_client.post(
                "/api/scheduler/config",
                json={"enabled": True, "interval_minutes": 360},
            )
            assert r.status_code == 200

            r = await authenticated_client.post("/api/scheduler/trigger-rescan")
            assert r.status_code == 200
            mock_trigger.assert_called_once()
