# #region Test.ValidationTaskService.Crud [C:3] [TYPE Module] [SEMANTICS test,validation,crud,task,run]
# @BRIEF Tests for validation_service.py — CRUD: get/list/delete tasks, trigger runs, run history, run detail.
# @RELATION BINDS_TO -> [ValidationService]

from pathlib import Path
import sys

sys.path.insert(0, str(Path(__file__).parent.parent / "src"))

from unittest.mock import AsyncMock, MagicMock, patch
from datetime import UTC, datetime
import pytest


def make_policy(**overrides):
    """Create a mock ValidationPolicy."""
    policy = MagicMock()
    policy.id = "pol-1"
    policy.name = "Test Task"
    policy.description = None
    policy.environment_id = "env-1"
    policy.is_active = True
    policy.dashboard_ids = []
    policy.provider_id = "prov-1"
    policy.schedule_days = None
    policy.window_start = None
    policy.window_end = None
    policy.notify_owners = None
    policy.custom_channels = None
    policy.alert_condition = None
    policy.created_at = datetime(2026, 1, 1, 12, 0, 0, tzinfo=UTC)
    policy.updated_at = datetime(2026, 1, 1, 12, 0, 0, tzinfo=UTC)
    policy.sources = []
    policy.screenshot_enabled = True
    policy.logs_enabled = True
    policy.execute_chart_data = False
    policy.llm_batch_size = 1
    policy.policy_dashboard_concurrency_limit = 3
    policy.prompt_template = None
    for k, v in overrides.items():
        setattr(policy, k, v)
    return policy


def make_valid_query_mock(policies=None):
    """Build a SQLAlchemy query mock chain that works for both filtered and unfiltered paths."""
    m = MagicMock()
    policies = policies or []
    first_result = policies[0] if policies else None
    m.configure_mock(**{"count.return_value": len(policies)})
    m.configure_mock(**{"order_by.return_value.offset.return_value.limit.return_value.all.return_value": policies})
    # query.filter(...).first() — returns first policy
    m.configure_mock(**{"filter.return_value.first.return_value": first_result})
    # query.filter(...).count()
    m.configure_mock(**{"filter.return_value.count.return_value": len(policies)})
    # query.filter(...).order_by(...).first() — used by _policy_to_response for last_run
    m.configure_mock(**{"filter.return_value.order_by.return_value.first.return_value": None})
    # query.filter(...).order_by(...).offset(...).limit(...).all()
    m.configure_mock(**{"filter.return_value.order_by.return_value.offset.return_value.limit.return_value.all.return_value": policies})
    return m


class TestGetTask:
    """get_task — find by id."""

    def test_not_found_raises(self):
        from src.services.validation_service import ValidationTaskService
        db = MagicMock()
        db.query.return_value.filter.return_value.first.return_value = None
        svc = ValidationTaskService(db)
        with pytest.raises(ValueError, match="not found"):
            svc.get_task("nonexistent")

    def test_found(self):
        from src.services.validation_service import ValidationTaskService
        policy = make_policy()
        db = MagicMock()
        db.query = lambda model: make_valid_query_mock() if model.__name__ == "ValidationRun" else make_valid_query_mock([policy])
        svc = ValidationTaskService(db)
        result = svc.get_task("pol-1")
        assert result.id == "pol-1"


class TestListTasks:
    """list_tasks — filtering and pagination."""

    def _make_qm(self, policies=None):
        """Build a query mock that works for both filtered and unfiltered paths."""
        return make_valid_query_mock(policies)

    def test_no_filters(self):
        from src.services.validation_service import ValidationTaskService
        policy = make_policy()
        db = MagicMock()
        db.query = lambda model: self._make_qm() if model.__name__ == "ValidationRun" else self._make_qm([policy])
        svc = ValidationTaskService(db)
        total, tasks = svc.list_tasks()
        assert total == 1
        assert len(tasks) == 1
        assert tasks[0].id == "pol-1"

    def test_filter_active(self):
        from src.services.validation_service import ValidationTaskService
        policy = make_policy()
        db = MagicMock()
        db.query = lambda model: self._make_qm() if model.__name__ == "ValidationRun" else self._make_qm([policy])
        svc = ValidationTaskService(db)
        total, tasks = svc.list_tasks(is_active=True)
        assert total == 1
        assert len(tasks) == 1

    def test_filter_environment_id(self):
        """Cover list_tasks line 398: environment_id filter."""
        from src.services.validation_service import ValidationTaskService
        policy = make_policy()
        db = MagicMock()
        db.query = lambda model: self._make_qm() if model.__name__ == "ValidationRun" else self._make_qm([policy])
        svc = ValidationTaskService(db)
        total, tasks = svc.list_tasks(environment_id="env-1")
        assert total == 1

    def test_filter_search(self):
        """Cover list_tasks line 400: search/ilike filter."""
        from src.services.validation_service import ValidationTaskService
        policy = make_policy()
        db = MagicMock()
        db.query = lambda model: self._make_qm() if model.__name__ == "ValidationRun" else self._make_qm([policy])
        svc = ValidationTaskService(db)
        total, tasks = svc.list_tasks(search="Test")
        assert total == 1


class TestDeleteTask:
    """delete_task — with/without runs."""

    def test_not_found_raises(self):
        from src.services.validation_service import ValidationTaskService
        db = MagicMock()
        db.query.return_value.filter.return_value.first.return_value = None
        svc = ValidationTaskService(db)
        with pytest.raises(ValueError, match="not found"):
            svc.delete_task("nonexistent")

    def test_delete_without_runs(self):
        from src.services.validation_service import ValidationTaskService
        policy = make_policy()
        db = MagicMock()
        db.query.return_value.filter.return_value.first.return_value = policy
        svc = ValidationTaskService(db)
        svc.delete_task("pol-1", delete_runs=False)
        db.delete.assert_called_once_with(policy)
        db.commit.assert_called_once()

    def test_delete_with_runs(self):
        from src.services.validation_service import ValidationTaskService
        policy = make_policy()
        db = MagicMock()
        db.query.return_value.filter.return_value.first.return_value = policy
        db.query.return_value.filter.return_value.all.return_value = [("run-1",)]
        svc = ValidationTaskService(db)
        svc.delete_task("pol-1", delete_runs=True)
        db.delete.assert_called_once_with(policy)
        db.commit.assert_called_once()


class TestCreateTask:
    """create_task — full CRUD with provider/environment validation."""

    async def test_creates_policy_with_sources(self):
        from src.services.validation_service import ValidationTaskService
        from src.schemas.validation import ValidationTaskCreate, SourceInput

        db = MagicMock()
        db.add = MagicMock()
        db.flush = MagicMock()
        db.commit = MagicMock()
        db.refresh = MagicMock()

        with patch("src.services.validation_service.LLMProviderService") as mock_cls:
            mock_svc = MagicMock()
            provider = MagicMock()
            provider.is_active = True
            provider.is_multimodal = True
            mock_svc.get_provider.return_value = provider
            mock_cls.return_value = mock_svc

            cm = MagicMock()
            cm.get_environment.return_value = MagicMock()

            svc = ValidationTaskService(db, config_manager=cm)
            svc._resolve_sources = AsyncMock(return_value=[
                MagicMock(type="dashboard_id", value="42",
                          resolved_dashboard_id="42", status="valid", title=None),
            ])
            # Mock _policy_to_response to avoid ORM serialization issues
            svc._policy_to_response = MagicMock(return_value=MagicMock())

            payload = MagicMock(spec=ValidationTaskCreate)
            payload.name = "New Task"
            payload.description = "Test desc"
            payload.environment_id = "env-1"
            payload.is_active = True
            payload.dashboard_ids = ["42"]
            payload.provider_id = "prov-1"
            payload.schedule_days = ["MON", "WED", "FRI"]
            payload.window_start = None
            payload.window_end = None
            payload.notify_owners = True
            payload.custom_channels = None
            payload.alert_condition = "FAIL_ONLY"
            payload.screenshot_enabled = True
            payload.logs_enabled = True
            payload.execute_chart_data = False
            payload.llm_batch_size = 1
            payload.policy_dashboard_concurrency_limit = 3
            payload.prompt_template = None
            payload.sources = None

            result = await svc.create_task(payload)

        assert db.commit.called
        svc._resolve_sources.assert_called_once()

    async def test_creates_with_dashboard_url_sources(self):
        from src.services.validation_service import ValidationTaskService
        from src.schemas.validation import ValidationTaskCreate

        db = MagicMock()
        db.add = MagicMock()
        db.flush = MagicMock()
        db.commit = MagicMock()
        db.refresh = MagicMock()

        with patch("src.services.validation_service.LLMProviderService") as mock_cls:
            mock_svc = MagicMock()
            provider = MagicMock()
            provider.is_active = True
            provider.is_multimodal = False  # screenshot_enabled=False
            mock_svc.get_provider.return_value = provider
            mock_cls.return_value = mock_svc

            cm = MagicMock()
            cm.get_environment.return_value = MagicMock()

            svc = ValidationTaskService(db, config_manager=cm)
            svc._resolve_sources = AsyncMock(return_value=[
                MagicMock(type="dashboard_url", value="http://superset/d/42",
                          resolved_dashboard_id="42", status="valid", title=None),
            ])
            svc._policy_to_response = MagicMock(return_value=MagicMock())

            payload = MagicMock(spec=ValidationTaskCreate)
            payload.name = "URL Task"
            payload.description = None
            payload.environment_id = "env-1"
            payload.is_active = True
            payload.dashboard_ids = None
            payload.provider_id = "prov-1"
            payload.schedule_days = []
            payload.window_start = "09:00"
            payload.window_end = "18:00"
            payload.notify_owners = None
            payload.custom_channels = [{"type": "SLACK", "target": "#alerts"}]
            payload.alert_condition = "ALWAYS"
            payload.screenshot_enabled = False
            payload.logs_enabled = True
            payload.execute_chart_data = True
            payload.llm_batch_size = 3
            payload.policy_dashboard_concurrency_limit = 5
            payload.prompt_template = "Check {{dashboard}}"
            payload.sources = None

            result = await svc.create_task(payload)

        assert db.commit.called


class TestUpdateTask:
    """update_task — partial update with re-validation."""

    async def test_not_found_raises(self):
        from src.services.validation_service import ValidationTaskService
        from src.schemas.validation import ValidationTaskUpdate
        db = MagicMock()
        db.query.return_value.filter.return_value.first.return_value = None
        svc = ValidationTaskService(db)
        payload = MagicMock(spec=ValidationTaskUpdate)
        payload.model_dump.return_value = {"name": "Updated"}
        with pytest.raises(ValueError, match="not found"):
            await svc.update_task("nonexistent", payload)

    async def test_sources_update_with_none_is_skipped(self):
        """Cover _apply_sources_update line 502: sources_data is None."""
        from src.services.validation_service import ValidationTaskService
        from src.schemas.validation import ValidationTaskUpdate
        policy = make_policy()

        db = MagicMock()
        db.query.return_value.filter.return_value.first.return_value = policy

        svc = ValidationTaskService(db, config_manager=MagicMock())

        payload = MagicMock(spec=ValidationTaskUpdate)
        payload.model_dump.return_value = {"sources": None}

        result = await svc.update_task("pol-1", payload)
        assert db.commit.called

    async def test_updates_name_only(self):
        from src.services.validation_service import ValidationTaskService
        from src.schemas.validation import ValidationTaskUpdate
        policy = make_policy()

        db = MagicMock()
        db.query.return_value.filter.return_value.first.return_value = policy

        svc = ValidationTaskService(db, config_manager=MagicMock())

        payload = MagicMock(spec=ValidationTaskUpdate)
        payload.model_dump.return_value = {"name": "Updated Name"}

        result = await svc.update_task("pol-1", payload)
        assert db.commit.called

    async def test_updates_provider_and_environment(self):
        from src.services.validation_service import ValidationTaskService
        from src.schemas.validation import ValidationTaskUpdate
        policy = make_policy()

        db = MagicMock()
        db.query.return_value.filter.return_value.first.return_value = policy

        cm = MagicMock()
        cm.get_environment.return_value = MagicMock()

        svc = ValidationTaskService(db, config_manager=cm)

        with patch("src.services.validation_service.LLMProviderService") as mock_cls:
            mock_svc = MagicMock()
            provider = MagicMock()
            provider.is_active = True
            provider.is_multimodal = True
            mock_svc.get_provider.return_value = provider
            mock_cls.return_value = mock_svc

            payload = MagicMock(spec=ValidationTaskUpdate)
            payload.model_dump.return_value = {
                "provider_id": "prov-2",
                "environment_id": "env-2",
                "screenshot_enabled": True,
                "window_start": "10:00",
                "window_end": "20:00",
            }

            result = await svc.update_task("pol-1", payload)
        assert db.commit.called

    async def test_updates_with_sources(self):
        from src.services.validation_service import ValidationTaskService
        from src.schemas.validation import ValidationTaskUpdate, SourceInput
        policy = make_policy()

        db = MagicMock()
        db.query.return_value.filter.return_value.first.return_value = policy

        svc = ValidationTaskService(db, config_manager=MagicMock())
        svc._resolve_sources = AsyncMock(return_value=[
            MagicMock(type="dashboard_id", value="99",
                      resolved_dashboard_id="99", status="valid", title="New"),
        ])

        payload = MagicMock(spec=ValidationTaskUpdate)
        payload.model_dump.return_value = {
            "name": "Updated with sources",
            "sources": [SourceInput(type="dashboard_id", value="99")],
        }

        result = await svc.update_task("pol-1", payload)
        assert db.commit.called
        svc._resolve_sources.assert_called_once()


class TestGetRunHistory:
    """get_run_history — paginated run list for a policy."""

    def test_empty(self):
        from src.services.validation_service import ValidationTaskService
        from src.models.llm import ValidationRun
        db = MagicMock()
        m = MagicMock()
        m.configure_mock(**{"count.return_value": 0})
        m.configure_mock(**{"order_by.return_value.offset.return_value.limit.return_value.all.return_value": []})
        db.query.return_value.filter.return_value = m
        svc = ValidationTaskService(db)
        total, runs = svc.get_run_history("pol-1")
        assert total == 0
        assert runs == []

    def test_with_runs(self):
        from src.services.validation_service import ValidationTaskService
        run = MagicMock()
        run.id = "run-1"
        run.policy_id = "pol-1"
        run.task_id = "task-1"
        run.status = "completed"
        run.trigger = "manual"
        run.started_at = None
        run.finished_at = None
        run.dashboard_count = 3
        run.pass_count = 2
        run.warn_count = 1
        run.fail_count = 0
        run.unknown_count = 0
        run.created_at = datetime(2026, 1, 1, 12, 0, 0, tzinfo=UTC)

        db = MagicMock()
        m = MagicMock()
        m.configure_mock(**{"count.return_value": 1})
        m.configure_mock(**{"order_by.return_value.offset.return_value.limit.return_value.all.return_value": [run]})
        db.query.return_value.filter.return_value = m
        svc = ValidationTaskService(db)
        total, runs = svc.get_run_history("pol-1")
        assert total == 1
        assert len(runs) == 1
        assert runs[0]["status"] == "completed"
# #endregion Test.ValidationTaskService.Crud
