# #region Test.ValidationTaskService.Helpers [C:3] [TYPE Module] [SEMANTICS test,validation,helpers,utilities]
# @BRIEF Tests for validation_service.py — helper functions, provider/environment validation, run/record conversion, source resolution, time fields.
# @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, time
import pytest


class TestHelperFunctions:
    """_extract_screenshot_paths, _parse_time, _format_time."""

    def test_extract_screenshot_paths_none(self):
        from src.services.validation_service import _extract_screenshot_paths
        assert _extract_screenshot_paths(None) == []

    def test_extract_screenshot_paths_valid(self):
        from src.services.validation_service import _extract_screenshot_paths
        result = _extract_screenshot_paths('{"screenshot_paths": ["/path/1.png", "/path/2.png"]}')
        assert result == ["/path/1.png", "/path/2.png"]

    def test_extract_screenshot_paths_empty_json(self):
        from src.services.validation_service import _extract_screenshot_paths
        result = _extract_screenshot_paths('{"other": "data"}')
        assert result == []

    def test_extract_screenshot_paths_invalid_json(self):
        from src.services.validation_service import _extract_screenshot_paths
        result = _extract_screenshot_paths("not-json")
        assert result == []

    def test_parse_time_none(self):
        from src.services.validation_service import _parse_time
        assert _parse_time(None) is None

    def test_parse_time_valid(self):
        from src.services.validation_service import _parse_time
        result = _parse_time("09:30")
        assert result == time(9, 30)

    def test_parse_time_invalid(self):
        from src.services.validation_service import _parse_time
        assert _parse_time("abc") is None

    def test_format_time_none(self):
        from src.services.validation_service import _format_time
        assert _format_time(None) is None

    def test_format_time_valid(self):
        from src.services.validation_service import _format_time
        assert _format_time(time(14, 5)) == "14:05"


class TestValidateProvider:
    """_validate_provider — existence, active, multimodal checks."""

    @pytest.fixture
    def service(self):
        from src.services.validation_service import ValidationTaskService
        db = MagicMock()
        return ValidationTaskService(db, config_manager=None)

    def test_empty_provider_id_raises(self, service):
        with pytest.raises(ValueError, match="provider_id is required"):
            service._validate_provider("")

    def test_none_provider_id_raises(self, service):
        with pytest.raises(ValueError, match="provider_id is required"):
            service._validate_provider("  ")

    def test_not_found_raises(self, service):
        with patch("src.services.validation_service.LLMProviderService") as mock_cls:
            mock_svc = MagicMock()
            mock_svc.get_provider.return_value = None
            mock_cls.return_value = mock_svc
            with pytest.raises(ValueError, match="not found"):
                service._validate_provider("prov-1")

    def test_inactive_raises(self, service):
        provider = MagicMock()
        provider.is_active = False
        provider.name = "GPT-4"
        with patch("src.services.validation_service.LLMProviderService") as mock_cls:
            mock_svc = MagicMock()
            mock_svc.get_provider.return_value = provider
            mock_cls.return_value = mock_svc
            with pytest.raises(ValueError, match="not active"):
                service._validate_provider("prov-1")

    def test_non_multimodal_raises(self, service):
        provider = MagicMock()
        provider.is_active = True
        provider.is_multimodal = False
        provider.name = "GPT-4"
        with patch("src.services.validation_service.LLMProviderService") as mock_cls:
            mock_svc = MagicMock()
            mock_svc.get_provider.return_value = provider
            mock_cls.return_value = mock_svc
            with pytest.raises(ValueError, match="not multimodal"):
                service._validate_provider("prov-1", require_multimodal=True)

    def test_multimodal_not_required(self, service):
        provider = MagicMock()
        provider.is_active = True
        provider.is_multimodal = False
        with patch("src.services.validation_service.LLMProviderService") as mock_cls:
            mock_svc = MagicMock()
            mock_svc.get_provider.return_value = provider
            mock_cls.return_value = mock_svc
            result = service._validate_provider("prov-1", require_multimodal=False)
            assert result is provider

    def test_valid_multimodal(self, service):
        provider = MagicMock()
        provider.is_active = True
        provider.is_multimodal = True
        with patch("src.services.validation_service.LLMProviderService") as mock_cls:
            mock_svc = MagicMock()
            mock_svc.get_provider.return_value = provider
            mock_cls.return_value = mock_svc
            result = service._validate_provider("prov-1")
            assert result is provider


class TestValidateEnvironment:
    """_validate_environment — existence check."""

    def test_no_config_manager_skips(self):
        from src.services.validation_service import ValidationTaskService
        svc = ValidationTaskService(MagicMock(), config_manager=None)
        svc._validate_environment("env-1")

    def test_environment_found(self):
        from src.services.validation_service import ValidationTaskService
        cm = MagicMock()
        cm.get_environment.return_value = MagicMock()
        svc = ValidationTaskService(MagicMock(), config_manager=cm)
        svc._validate_environment("env-1")

    def test_environment_not_found(self):
        from src.services.validation_service import ValidationTaskService
        cm = MagicMock()
        cm.get_environment.return_value = None
        svc = ValidationTaskService(MagicMock(), config_manager=cm)
        with pytest.raises(ValueError, match="not found"):
            svc._validate_environment("env-unknown")


class TestRunToDict:
    """_run_to_dict — conversion with duration_ms."""

    def make_run(self, **overrides):
        run = MagicMock()
        run.id = "run-1"
        run.policy_id = "pol-1"
        run.task_id = "task-1"
        run.status = "completed"
        run.trigger = "manual"
        run.dashboard_count = 5
        run.pass_count = 3
        run.warn_count = 1
        run.fail_count = 1
        run.unknown_count = 0
        run.started_at = None
        run.finished_at = None
        run.created_at = datetime(2026, 1, 1, 12, 0, 0, tzinfo=UTC)
        for k, v in overrides.items():
            setattr(run, k, v)
        return run

    def test_no_dates(self):
        from src.services.validation_service import ValidationTaskService
        result = ValidationTaskService(MagicMock())._run_to_dict(self.make_run())
        assert result["duration_ms"] is None
        assert result["status"] == "completed"

    def test_with_dates_computes_duration(self):
        from src.services.validation_service import ValidationTaskService
        run = self.make_run(
            started_at=datetime(2026, 1, 1, 12, 0, 0, tzinfo=UTC),
            finished_at=datetime(2026, 1, 1, 12, 3, 30, tzinfo=UTC),
        )
        result = ValidationTaskService(MagicMock())._run_to_dict(run)
        assert result["duration_ms"] == 210000

    def test_naive_datetime_utc(self):
        from src.services.validation_service import ValidationTaskService
        run = self.make_run(
            started_at=datetime(2026, 1, 1, 12, 0, 0),
            finished_at=datetime(2026, 1, 1, 12, 1, 0),
        )
        result = ValidationTaskService(MagicMock())._run_to_dict(run)
        assert result["duration_ms"] == 60000


class TestRecordToDict:
    """_record_to_dict — conversion with title resolution."""

    def make_record(self, **overrides):
        from src.models.llm import ValidationRecord as VR
        record = MagicMock(spec=VR)
        record.id = "rec-1"
        record.run_id = "run-1"
        record.policy_id = "pol-1"
        record.dashboard_id = "42"
        record.environment_id = "env-1"
        record.status = "PASS"
        record.summary = "OK"
        record.issues = []
        record.raw_response = None
        record.screenshot_path = None
        record.screenshot_paths = None
        record.execution_path = None
        record.dataset_health = None
        record.chart_data_results = []
        record.tab_screenshots = []
        record.logs_sent_to_llm = []
        record.token_usage = None
        record.timings = None
        record.timestamp = datetime(2026, 1, 1, 12, 0, 0, tzinfo=UTC)
        for k, v in overrides.items():
            setattr(record, k, v)
        return record

    async def test_basic(self):
        from src.services.validation_service import ValidationTaskService
        db = MagicMock()
        db.query.return_value.filter.return_value.first.return_value = None
        svc = ValidationTaskService(db)
        result = await svc._record_to_dict(self.make_record())
        assert result["status"] == "PASS"
        assert result["screenshot_paths"] == []

    async def test_with_source_title(self):
        from src.services.validation_service import ValidationTaskService
        source = MagicMock()
        source.title = "Sales Dashboard"
        db = MagicMock()
        db.query.return_value.filter.return_value.first.return_value = source
        svc = ValidationTaskService(db)
        result = await svc._record_to_dict(self.make_record())
        assert result["dashboard_title"] == "Sales Dashboard"

    async def test_screenshot_paths_fallback(self):
        from src.services.validation_service import ValidationTaskService
        db = MagicMock()
        db.query.return_value.filter.return_value.first.return_value = None
        svc = ValidationTaskService(db)
        result = await svc._record_to_dict(self.make_record(
            raw_response='{"screenshot_paths": ["/path/img.png"]}',
        ))
        assert result["screenshot_paths"] == ["/path/img.png"]


class TestResolveSources:
    """_resolve_sources — source resolution from input."""

    async def test_no_sources(self):
        from src.services.validation_service import ValidationTaskService
        svc = ValidationTaskService(MagicMock())
        svc._parse_superset_link = AsyncMock()

    async def test_dashboard_id_sources(self):
        from src.services.validation_service import ValidationTaskService
        svc = ValidationTaskService(MagicMock())
        svc._resolve_dashboard_title = AsyncMock(return_value="Sales Dashboard")
        result = await svc._resolve_sources(None, ["42", "99"], "pol-1", "env-1")
        assert len(result) == 2
        assert result[0].type == "dashboard_id"
        assert result[0].value == "42"
        assert result[0].title == "Sales Dashboard"
        assert result[0].status == "valid"

    async def test_dashboard_url_sources(self):
        from src.services.validation_service import ValidationTaskService
        svc = ValidationTaskService(MagicMock())
        svc._parse_superset_link = AsyncMock(return_value={
            "dashboard_id": "42", "dataset_id": "ds-1",
            "partial_recovery": False, "source_url": "http://superset/d/42",
        })
        svc._resolve_dashboard_title = AsyncMock(return_value="Sales Dashboard")
        from src.schemas.validation import SourceInput
        sources_input = [SourceInput(type="dashboard_url", value="http://superset/d/42")]
        result = await svc._resolve_sources(sources_input, None, "pol-1", "env-1")
        assert len(result) == 1
        assert result[0].type == "dashboard_url"
        assert result[0].resolved_dashboard_id == "42"

    async def test_partial_recovery_status(self):
        from src.services.validation_service import ValidationTaskService
        svc = ValidationTaskService(MagicMock())
        svc._parse_superset_link = AsyncMock(return_value={
            "dashboard_id": "42", "partial_recovery": True,
        })
        svc._resolve_dashboard_title = AsyncMock(return_value=None)
        from src.schemas.validation import SourceInput
        sources_input = [SourceInput(type="dashboard_url", value="http://superset/d/42")]
        result = await svc._resolve_sources(sources_input, None, "pol-1", "env-1")
        assert result[0].status == "partial_recovery"

    async def test_dashboard_id_type_source_input(self):
        """Cover _resolve_sources line 199-200: s.type == 'dashboard_id'."""
        from src.services.validation_service import ValidationTaskService
        svc = ValidationTaskService(MagicMock())
        svc._resolve_dashboard_title = AsyncMock(return_value="Dashboard 42")
        from src.schemas.validation import SourceInput
        sources_input = [SourceInput(type="dashboard_id", value="42")]
        result = await svc._resolve_sources(sources_input, None, "pol-1", "env-1")
        assert len(result) == 1
        assert result[0].type == "dashboard_id"
        assert result[0].resolved_dashboard_id == "42"
        assert result[0].status == "valid"

    def test_policy_to_response_with_sources(self):
        """Cover _policy_to_response lines 242-243: iterating sources."""
        from src.services.validation_service import ValidationTaskService
        svc = ValidationTaskService(MagicMock())
        # Create mock policy with sources
        policy_mock = MagicMock()
        policy_mock.id = "pol-1"
        policy_mock.name = "Test"
        policy_mock.description = None
        policy_mock.environment_id = "env-1"
        policy_mock.is_active = True
        policy_mock.dashboard_ids = []
        policy_mock.provider_id = "prov-1"
        policy_mock.schedule_days = []
        policy_mock.window_start = None
        policy_mock.window_end = None
        policy_mock.notify_owners = None
        policy_mock.custom_channels = None
        policy_mock.alert_condition = "FAIL_ONLY"
        policy_mock.created_at = datetime(2026, 1, 1, 12, 0, 0, tzinfo=UTC)
        policy_mock.updated_at = datetime(2026, 1, 1, 12, 0, 0, tzinfo=UTC)
        policy_mock.screenshot_enabled = True
        policy_mock.logs_enabled = True
        policy_mock.execute_chart_data = False
        policy_mock.llm_batch_size = 1
        policy_mock.policy_dashboard_concurrency_limit = 3
        policy_mock.prompt_template = None

        source = MagicMock()
        source.id = "src-1"
        source.type = "dashboard_id"
        source.value = "42"
        source.status = "valid"
        source.title = "Sales Dashboard"
        source.resolved_dashboard_id = "42"
        source.last_error = None
        source.last_checked_at = None
        source.created_at = datetime(2026, 1, 1, 12, 0, 0, tzinfo=UTC)
        policy_mock.sources = [source]

        # Mock last_run query
        db = MagicMock()
        db.query.return_value.filter.return_value.order_by.return_value.first.return_value = None

        svc.db = db
        result = svc._policy_to_response(policy_mock)
        assert result.id == "pol-1"
        assert len(result.sources) == 1

    async def test_record_to_dict_title_fallback(self):
        """Cover _record_to_dict lines 346-350: resolve title when source has no title."""
        from src.services.validation_service import ValidationTaskService
        from src.models.llm import ValidationRecord as VR

        record = MagicMock(spec=VR)
        record.id = "rec-1"
        record.run_id = "run-1"
        record.policy_id = "pol-1"
        record.dashboard_id = "42"
        record.environment_id = "env-1"
        record.status = "PASS"
        record.summary = "OK"
        record.issues = []
        record.raw_response = None
        record.screenshot_path = None
        record.screenshot_paths = None
        record.execution_path = None
        record.dataset_health = None
        record.chart_data_results = []
        record.tab_screenshots = []
        record.logs_sent_to_llm = []
        record.token_usage = None
        record.timings = None
        record.timestamp = datetime(2026, 1, 1, 12, 0, 0, tzinfo=UTC)

        # Source exists but without title → triggers fallback
        source = MagicMock()
        source.title = None
        source.resolved_dashboard_id = "42"

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

        svc = ValidationTaskService(db)
        svc._resolve_dashboard_title = AsyncMock(return_value="Resolved Dashboard")
        result = await svc._record_to_dict(record)
        assert result["dashboard_title"] == "Resolved Dashboard"
        svc._resolve_dashboard_title.assert_called_once_with("env-1", "42")


class TestApplyTimeField:
    """_apply_time_field — parse and set time fields."""

    def test_field_not_in_update(self):
        from src.services.validation_service import ValidationTaskService
        svc = ValidationTaskService(MagicMock())
        policy = MagicMock()
        svc._apply_time_field({}, policy, "window_start")

    def test_field_in_update(self):
        from src.services.validation_service import ValidationTaskService
        svc = ValidationTaskService(MagicMock())
        policy = MagicMock()
        svc._apply_time_field({"window_start": "10:00"}, policy, "window_start")
        assert policy.window_start == time(10, 0)

class TestParseSupersetLink:
    """_parse_superset_link — Superset URL parsing."""

    async def test_no_config_manager_raises(self):
        from src.services.validation_service import ValidationTaskService
        svc = ValidationTaskService(MagicMock(), config_manager=None)
        with pytest.raises(ValueError, match="Config manager not available"):
            await svc._parse_superset_link("http://superset/d/1", "env-1")

    async def test_environment_not_found_raises(self):
        from src.services.validation_service import ValidationTaskService
        cm = MagicMock()
        cm.get_environment.return_value = None
        svc = ValidationTaskService(MagicMock(), config_manager=cm)
        with pytest.raises(ValueError, match="not found"):
            await svc._parse_superset_link("http://superset/d/1", "env-unknown")

    async def test_successful_parse(self):
        """Cover lines 129-133: happy path with valid extractor result."""
        from src.services.validation_service import ValidationTaskService
        cm = MagicMock()
        env = MagicMock()
        cm.get_environment.return_value = env
        svc = ValidationTaskService(MagicMock(), config_manager=cm)

        with patch("src.core.utils.superset_context_extractor.SupersetContextExtractor") as MockExtractor:
            mock_extractor = MagicMock()
            parsed_result = MagicMock()
            parsed_result.source_url = "http://superset/d/42"
            parsed_result.dataset_ref = "dataset1"
            parsed_result.dataset_id = "ds-1"
            parsed_result.dashboard_id = "42"
            parsed_result.chart_id = None
            parsed_result.resource_type = "dashboard"
            parsed_result.partial_recovery = False
            parsed_result.unresolved_references = set()
            mock_extractor.parse_superset_link = AsyncMock(return_value=parsed_result)
            MockExtractor.return_value = mock_extractor

            result = await svc._parse_superset_link("http://superset/d/42", "env-1")

            assert result["source_url"] == "http://superset/d/42"
            assert result["dashboard_id"] == "42"
            assert result["partial_recovery"] is False


class TestResolveDashboardTitle:
    """_resolve_dashboard_title — fetch title from Superset API."""

    async def test_no_config_manager_returns_none(self):
        from src.services.validation_service import ValidationTaskService
        svc = ValidationTaskService(MagicMock(), config_manager=None)
        result = await svc._resolve_dashboard_title("env-1", "42")
        assert result is None

    async def test_environment_not_found_returns_none(self):
        from src.services.validation_service import ValidationTaskService
        cm = MagicMock()
        cm.get_environment.return_value = None
        svc = ValidationTaskService(MagicMock(), config_manager=cm)
        result = await svc._resolve_dashboard_title("env-1", "42")
        assert result is None

    async def test_api_exception_returns_none(self):
        from src.services.validation_service import ValidationTaskService
        cm = MagicMock()
        cm.get_environment.return_value = MagicMock()
        svc = ValidationTaskService(MagicMock(), config_manager=cm)
        with patch("src.core.superset_client.SupersetClient") as MockClient:
            mock_client = MagicMock()
            mock_client.get_dashboard = AsyncMock(side_effect=Exception("API error"))
            MockClient.return_value = mock_client
            result = await svc._resolve_dashboard_title("env-1", "42")
            assert result is None

    async def test_result_with_dashboard_title(self):
        from src.services.validation_service import ValidationTaskService
        cm = MagicMock()
        cm.get_environment.return_value = MagicMock()
        svc = ValidationTaskService(MagicMock(), config_manager=cm)
        with patch("src.core.superset_client.SupersetClient") as MockClient:
            mock_client = MagicMock()
            mock_client.get_dashboard = AsyncMock(return_value={
                "result": {"dashboard_title": "Sales Dashboard"},
            })
            MockClient.return_value = mock_client
            result = await svc._resolve_dashboard_title("env-1", "42")
            assert result == "Sales Dashboard"

    async def test_result_with_title_field(self):
        from src.services.validation_service import ValidationTaskService
        cm = MagicMock()
        cm.get_environment.return_value = MagicMock()
        svc = ValidationTaskService(MagicMock(), config_manager=cm)
        with patch("src.core.superset_client.SupersetClient") as MockClient:
            mock_client = MagicMock()
            mock_client.get_dashboard = AsyncMock(return_value={
                "result": {"title": "Revenue Dashboard"},
            })
            MockClient.return_value = mock_client
            result = await svc._resolve_dashboard_title("env-1", "42")
            assert result == "Revenue Dashboard"

    async def test_result_no_title_returns_none(self):
        from src.services.validation_service import ValidationTaskService
        cm = MagicMock()
        cm.get_environment.return_value = MagicMock()
        svc = ValidationTaskService(MagicMock(), config_manager=cm)
        with patch("src.core.superset_client.SupersetClient") as MockClient:
            mock_client = MagicMock()
            mock_client.get_dashboard = AsyncMock(return_value={"result": {}})
            MockClient.return_value = mock_client
            result = await svc._resolve_dashboard_title("env-1", "42")
            assert result is None


class TestRecordToDictExceptionHandler:
    """_record_to_dict — exception handler at lines 351-352."""

    async def test_exception_in_title_resolution_safe(self):
        """Cover lines 351-352: except Exception: pass."""
        from src.services.validation_service import ValidationTaskService
        from src.models.llm import ValidationRecord as VR

        record = MagicMock(spec=VR)
        record.id = "rec-1"
        record.run_id = "run-1"
        record.policy_id = "pol-1"
        record.dashboard_id = "42"
        record.environment_id = "env-1"
        record.status = "PASS"
        record.summary = "OK"
        record.issues = []
        record.raw_response = None
        record.screenshot_path = None
        record.screenshot_paths = []
        record.execution_path = None
        record.dataset_health = None
        record.chart_data_results = []
        record.tab_screenshots = []
        record.logs_sent_to_llm = []
        record.token_usage = None
        record.timings = None
        record.timestamp = datetime(2026, 1, 1, 12, 0, 0, tzinfo=UTC)

        db = MagicMock()
        # Make the source query throw an exception to trigger except block
        db.query.return_value.filter.return_value.first.side_effect = Exception("DB error")
        svc = ValidationTaskService(db)
        result = await svc._record_to_dict(record)
        assert result["status"] == "PASS"
        assert result["dashboard_title"] == "42"  # falls back to dashboard_id
# #endregion Test.ValidationTaskService.Helpers
