# #region Test.ResourceService [C:3] [TYPE Module] [SEMANTICS test,resource,git,task,dashboard,dataset]
# # @BRIEF Tests for resource_service.py — dashboard/dataset enrichment with git status and task status.
# @RELATION BINDS_TO -> [ResourceServiceModule]

from pathlib import Path
import sys

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

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


# ── Autouse fixture: prevent GitService from touching real filesystem ──
# When run in the full suite, a prior test writes global config to the DB
# (AppConfigRecord with storage.root_path), causing ResourceService.__init__
# → GitService.__init__ → _resolve_base_path → _ensure_base_path_exists
# → mkdir /app/storage/repositories → PermissionError.
# This fixture patches GitService so every test gets an isolated Mock.
# See ADR-0013 and tests/api/ contamination issue for context.

@pytest.fixture(autouse=True)
def _patch_git_service(monkeypatch: pytest.MonkeyPatch):
    """Replace GitService with MagicMock to prevent filesystem access."""
    import src.services.resource_service as rs_mod
    original = rs_mod.GitService
    rs_mod.GitService = MagicMock  # type: ignore[misc]
    yield
    rs_mod.GitService = original


def make_task(**overrides):
    """Create a mock Task object with configurable attributes."""
    task = MagicMock()
    defaults = {
        "id": "task-1",
        "plugin_id": "llm_dashboard_validation",
        "status": "COMPLETED",
        "params": {"dashboard_id": "42", "environment_id": "env-1"},
        "result": {"status": "PASS"},
        "started_at": datetime(2026, 1, 1, 12, 0, 0, tzinfo=UTC),
        "finished_at": datetime(2026, 1, 1, 12, 5, 0, tzinfo=UTC),
        "created_at": datetime(2026, 1, 1, 11, 0, 0, tzinfo=UTC),
    }
    for k, v in {**defaults, **overrides}.items():
        setattr(task, k, v)
    return task


class TestNormalizeTaskStatus:
    """_normalize_task_status — uppercase, strip enum prefix."""

    def test_none_returns_empty(self):
        from src.services.resource_service import ResourceService
        svc = ResourceService()
        assert svc._normalize_task_status(None) == ""

    def test_plain_string(self):
        from src.services.resource_service import ResourceService
        svc = ResourceService()
        assert svc._normalize_task_status("completed") == "COMPLETED"

    def test_enum_value(self):
        from src.services.resource_service import ResourceService
        svc = ResourceService()
        mock_enum = MagicMock()
        mock_enum.value = "RUNNING"
        assert svc._normalize_task_status(mock_enum) == "RUNNING"

    def test_with_enum_prefix(self):
        from src.services.resource_service import ResourceService
        svc = ResourceService()
        assert svc._normalize_task_status("TaskStatus.RUNNING") == "RUNNING"


class TestNormalizeValidationStatus:
    """_normalize_validation_status — PASS/FAIL/WARN/UNKNOWN."""

    def test_none_returns_none(self):
        from src.services.resource_service import ResourceService
        svc = ResourceService()
        assert svc._normalize_validation_status(None) is None

    def test_pass(self):
        from src.services.resource_service import ResourceService
        svc = ResourceService()
        assert svc._normalize_validation_status("PASS") == "PASS"

    def test_fail(self):
        from src.services.resource_service import ResourceService
        svc = ResourceService()
        assert svc._normalize_validation_status("FAIL") == "FAIL"

    def test_warn(self):
        from src.services.resource_service import ResourceService
        svc = ResourceService()
        assert svc._normalize_validation_status("warn") == "WARN"

    def test_unknown(self):
        from src.services.resource_service import ResourceService
        svc = ResourceService()
        assert svc._normalize_validation_status("UNKNOWN") == "UNKNOWN"

    def test_other_defaults_unknown(self):
        from src.services.resource_service import ResourceService
        svc = ResourceService()
        assert svc._normalize_validation_status("invalid_status") == "UNKNOWN"


class TestNormalizeDatetime:
    """_normalize_datetime_for_compare — UTC-aware comparison."""

    def test_naive_converted_to_utc(self):
        from src.services.resource_service import ResourceService
        svc = ResourceService()
        dt = datetime(2026, 1, 1, 12, 0, 0)
        result = svc._normalize_datetime_for_compare(dt)
        assert result.tzinfo is not None
        assert result == dt.replace(tzinfo=UTC)

    def test_aware_converted_to_utc(self):
        from src.services.resource_service import ResourceService
        svc = ResourceService()
        from datetime import timezone
        dt = datetime(2026, 1, 1, 12, 0, 0, tzinfo=timezone.utc)
        result = svc._normalize_datetime_for_compare(dt)
        assert result == dt

    def test_non_datetime_returns_min(self):
        from src.services.resource_service import ResourceService
        svc = ResourceService()
        result = svc._normalize_datetime_for_compare("not-a-date")
        assert result == datetime.min.replace(tzinfo=UTC)


class TestExtractFromTask:
    """_extract_resource_name_from_task and _extract_resource_type_from_task."""

    def test_name_from_params(self):
        from src.services.resource_service import ResourceService
        svc = ResourceService()
        task = make_task(params={"resource_name": "Sales Dashboard"})
        assert svc._extract_resource_name_from_task(task) == "Sales Dashboard"

    def test_name_fallback(self):
        from src.services.resource_service import ResourceService
        svc = ResourceService()
        task = make_task(params={})
        task.id = "task-99"
        assert svc._extract_resource_name_from_task(task) == "Task task-99"

    def test_type_from_params(self):
        from src.services.resource_service import ResourceService
        svc = ResourceService()
        task = make_task(params={"resource_type": "dashboard"})
        assert svc._extract_resource_type_from_task(task) == "dashboard"

    def test_type_default(self):
        from src.services.resource_service import ResourceService
        svc = ResourceService()
        task = make_task(params={})
        assert svc._extract_resource_type_from_task(task) == "unknown"


class TestGetLastTaskForResource:
    """_get_last_task_for_resource — most recent task by created_at."""

    def test_no_tasks_returns_none(self):
        from src.services.resource_service import ResourceService
        svc = ResourceService()
        assert svc._get_last_task_for_resource("dataset-42", None) is None

    def test_empty_list_returns_none(self):
        from src.services.resource_service import ResourceService
        svc = ResourceService()
        assert svc._get_last_task_for_resource("dataset-42", []) is None

    def test_no_matching_tasks(self):
        from src.services.resource_service import ResourceService
        svc = ResourceService()
        task = make_task(params={"resource_id": "dataset-other"})
        assert svc._get_last_task_for_resource("dataset-42", [task]) is None

    def test_returns_latest_task(self):
        from src.services.resource_service import ResourceService
        svc = ResourceService()
        old = make_task(
            id="task-old",
            params={"resource_id": "dataset-42"},
            created_at=datetime(2026, 1, 1, 10, 0, 0, tzinfo=UTC),
        )
        new = make_task(
            id="task-new",
            params={"resource_id": "dataset-42"},
            created_at=datetime(2026, 1, 2, 10, 0, 0, tzinfo=UTC),
        )
        result = svc._get_last_task_for_resource("dataset-42", [old, new])
        assert result is not None
        assert result["task_id"] == "task-new"

    def test_task_without_created_at_uses_min(self):
        from src.services.resource_service import ResourceService
        svc = ResourceService()
        task_nodate = make_task(
            id="task-nodate",
            params={"resource_id": "dataset-42"},
        )
        del task_nodate.created_at  # remove created_at
        task_nodate.started_at = None
        task_nodate.finished_at = None
        task_dated = make_task(
            id="task-dated",
            params={"resource_id": "dataset-42"},
            created_at=datetime(2026, 1, 1, 10, 0, 0, tzinfo=UTC),
        )
        result = svc._get_last_task_for_resource("dataset-42", [task_nodate, task_dated])
        assert result["task_id"] == "task-dated"


class TestGetActivitySummary:
    """get_activity_summary — active count and recent tasks."""

    def test_empty_tasks(self):
        from src.services.resource_service import ResourceService
        svc = ResourceService()
        result = svc.get_activity_summary([])
        assert result["active_count"] == 0
        assert result["recent_tasks"] == []

    def test_active_task_count(self):
        from src.services.resource_service import ResourceService
        svc = ResourceService()
        running = make_task(id="t1", params={}, status="RUNNING")
        waiting = make_task(id="t2", params={}, status="WAITING_INPUT")
        completed = make_task(id="t3", params={}, status="COMPLETED")
        result = svc.get_activity_summary([running, waiting, completed])
        assert result["active_count"] == 2

    def test_recent_tasks_limit(self):
        from src.services.resource_service import ResourceService
        svc = ResourceService()
        tasks = [
            make_task(id=f"t{i}", params={}, status="COMPLETED",
                      created_at=datetime(2026, 1, i + 1, 0, 0, 0, tzinfo=UTC))
            for i in range(10)
        ]
        result = svc.get_activity_summary(tasks)
        assert len(result["recent_tasks"]) == 5

    def test_recent_tasks_sorted(self):
        from src.services.resource_service import ResourceService
        svc = ResourceService()
        old = make_task(
            id="t-old", params={"resource_name": "Old", "resource_type": "dashboard"},
            status="COMPLETED",
            created_at=datetime(2026, 1, 1, 0, 0, 0, tzinfo=UTC),
        )
        new = make_task(
            id="t-new", params={"resource_name": "New", "resource_type": "chart"},
            status="FAILED",
            created_at=datetime(2026, 1, 5, 0, 0, 0, tzinfo=UTC),
        )
        result = svc.get_activity_summary([old, new])
        assert result["recent_tasks"][0]["task_id"] == "t-new"


class TestGetLastLlmTaskForDashboard:
    """_get_last_llm_task_for_dashboard — filter by plugin_id, dashboard_id, env."""

    def test_no_tasks_returns_none(self):
        from src.services.resource_service import ResourceService
        svc = ResourceService()
        assert svc._get_last_llm_task_for_dashboard(42, "env-1", None) is None

    def test_wrong_plugin_skipped(self):
        from src.services.resource_service import ResourceService
        svc = ResourceService()
        task = make_task(plugin_id="other_plugin", params={"dashboard_id": "42"})
        assert svc._get_last_llm_task_for_dashboard(42, "env-1", [task]) is None

    def test_wrong_dashboard_skipped(self):
        from src.services.resource_service import ResourceService
        svc = ResourceService()
        task = make_task(params={"dashboard_id": "99"})
        assert svc._get_last_llm_task_for_dashboard(42, "env-1", [task]) is None

    def test_wrong_env_skipped(self):
        from src.services.resource_service import ResourceService
        svc = ResourceService()
        task = make_task(params={"dashboard_id": "42", "environment_id": "env-2"})
        assert svc._get_last_llm_task_for_dashboard(42, "env-1", [task]) is None

    def test_matching_task_returns_summary(self):
        from src.services.resource_service import ResourceService
        svc = ResourceService()
        task = make_task(params={"dashboard_id": "42", "environment_id": "env-1"})
        result = svc._get_last_llm_task_for_dashboard(42, "env-1", [task])
        assert result is not None
        assert result["task_id"] == "task-1"
        assert result["status"] == "COMPLETED"
        assert result["validation_status"] == "PASS"

    def test_prefers_latest(self):
        from src.services.resource_service import ResourceService
        svc = ResourceService()
        old = make_task(
            id="old", params={"dashboard_id": "42", "environment_id": "env-1"},
            started_at=datetime(2026, 1, 1, 10, 0, 0, tzinfo=UTC),
        )
        new = make_task(
            id="new", params={"dashboard_id": "42", "environment_id": "env-1"},
            started_at=datetime(2026, 1, 2, 10, 0, 0, tzinfo=UTC),
        )
        result = svc._get_last_llm_task_for_dashboard(42, "env-1", [old, new])
        assert result["task_id"] == "new"

    def test_null_result_handled(self):
        from src.services.resource_service import ResourceService
        svc = ResourceService()
        task = make_task(result=None, params={"dashboard_id": "42", "environment_id": "env-1"})
        result = svc._get_last_llm_task_for_dashboard(42, "env-1", [task])
        assert result is not None
        assert result["validation_status"] is None

    def test_env_id_none_matches_any(self):
        from src.services.resource_service import ResourceService
        svc = ResourceService()
        task = make_task(params={"dashboard_id": "42"})
        result = svc._get_last_llm_task_for_dashboard(42, None, [task])
        assert result is not None

    def test_env_match_with_env_key_fallback(self):
        from src.services.resource_service import ResourceService
        svc = ResourceService()
        task = make_task(params={"dashboard_id": "42", "env": "env-1"})
        result = svc._get_last_llm_task_for_dashboard(42, "env-1", [task])
        assert result is not None


class TestGetLastLlmTaskValidationStatusFallback:
    """_get_last_llm_task_for_dashboard — validation status fallback logic."""

    def test_decisive_task_used_when_latest_unknown(self):
        from src.services.resource_service import ResourceService
        svc = ResourceService()
        unknown = make_task(
            id="latest", params={"dashboard_id": "42", "environment_id": "env-1"},
            result={"status": "UNKNOWN"},
            started_at=datetime(2026, 1, 2, 10, 0, 0, tzinfo=UTC),
        )
        decisive = make_task(
            id="decisive", params={"dashboard_id": "42", "environment_id": "env-1"},
            result={"status": "FAIL"},
            started_at=datetime(2026, 1, 1, 10, 0, 0, tzinfo=UTC),
        )
        result = svc._get_last_llm_task_for_dashboard(42, "env-1", [unknown, decisive])
        assert result["task_id"] == "latest"
        assert result["validation_status"] == "FAIL"


class TestGetGitStatusForDashboard:
    """_get_git_status_for_dashboard — git repo status enrichment."""

    @pytest.mark.asyncio
    async def test_no_repo(self):
        from src.services.resource_service import ResourceService
        from fastapi import HTTPException
        svc = ResourceService()
        svc.git_service = MagicMock()
        svc.git_service.get_repo = AsyncMock(side_effect=HTTPException(status_code=404))
        result = await svc._get_git_status_for_dashboard(42)
        assert result["sync_status"] == "NO_REPO"
        assert result["has_repo"] is False

    @pytest.mark.asyncio
    async def test_repo_error(self):
        from src.services.resource_service import ResourceService
        svc = ResourceService()
        svc.git_service = MagicMock()
        svc.git_service.get_repo = AsyncMock(side_effect=Exception("Git error"))
        result = await svc._get_git_status_for_dashboard(42)
        assert result["sync_status"] == "NO_REPO"

    @pytest.mark.asyncio
    async def test_git_operations_error(self):
        from src.services.resource_service import ResourceService
        svc = ResourceService()
        repo = MagicMock()
        repo.active_branch.name = "main"
        repo.is_dirty.side_effect = Exception("Dirty check failed")
        svc.git_service = MagicMock()
        svc.git_service.get_repo = AsyncMock(return_value=repo)
        result = await svc._get_git_status_for_dashboard(42)
        assert result["sync_status"] == "ERROR"
        assert result["has_repo"] is True

    @pytest.mark.asyncio
    async def test_clean_repo(self):
        from src.services.resource_service import ResourceService
        svc = ResourceService()
        repo = MagicMock()
        repo.active_branch.name = "main"
        repo.is_dirty.return_value = False
        svc.git_service = MagicMock()
        svc.git_service.get_repo = AsyncMock(return_value=repo)
        repo.iter_commits.return_value = []

        result = await svc._get_git_status_for_dashboard(42)
        assert result["sync_status"] == "OK"
        assert result["has_repo"] is True
        assert result["branch"] == "main"

    @pytest.mark.asyncio
    async def test_dirty_repo(self):
        from src.services.resource_service import ResourceService
        svc = ResourceService()
        repo = MagicMock()
        repo.active_branch.name = "main"
        repo.is_dirty.return_value = True
        svc.git_service = MagicMock()
        svc.git_service.get_repo = AsyncMock(return_value=repo)
        repo.iter_commits.return_value = []

        result = await svc._get_git_status_for_dashboard(42)
        assert result["sync_status"] == "DIFF"
        assert result["has_changes_for_commit"] is True


@patch("src.services.resource_service.get_superset_client")
class TestGetDashboardsWithStatus:
    """get_dashboards_with_status — enrich dashboards with git/task status."""

    @pytest.fixture
    def service(self):
        from src.services.resource_service import ResourceService
        svc = ResourceService()
        svc._get_git_status_for_dashboard = AsyncMock(return_value={"sync_status": "OK"})
        svc._get_last_llm_task_for_dashboard = MagicMock(return_value={"status": "PASS"})
        return svc

    @pytest.mark.asyncio
    async def test_enriches_dashboards(self, mock_client_fn, service):
        mock_client = AsyncMock()
        mock_client.get_dashboards_summary.return_value = [
            {"id": 42, "dashboard_title": "Sales"},
            {"id": 99, "dashboard_title": "Marketing"},
        ]
        mock_client_fn.return_value = mock_client

        env = MagicMock()
        env.id = "env-1"
        result = await service.get_dashboards_with_status(env, tasks=[], include_git_status=True)
        assert len(result) == 2
        assert result[0]["git_status"] == {"sync_status": "OK"}
        assert result[0]["last_task"] == {"status": "PASS"}

    @pytest.mark.asyncio
    async def test_include_git_status_false(self, mock_client_fn, service):
        mock_client = AsyncMock()
        mock_client.get_dashboards_summary.return_value = [{"id": 42}]
        mock_client_fn.return_value = mock_client

        env = MagicMock()
        env.id = "env-1"
        result = await service.get_dashboards_with_status(env, tasks=[], include_git_status=False)
        assert result[0]["git_status"] is None


@patch("src.services.resource_service.get_superset_client")
class TestGetDashboardsPageWithStatus:
    """get_dashboards_page_with_status — paginated enrichment."""

    @pytest.fixture
    def service(self):
        from src.services.resource_service import ResourceService
        svc = ResourceService()
        svc._get_git_status_for_dashboard = AsyncMock(return_value={"sync_status": "OK"})
        svc._get_last_llm_task_for_dashboard = MagicMock(return_value={"status": "PASS"})
        return svc

    @pytest.mark.asyncio
    async def test_paginated_result(self, mock_client_fn, service):
        mock_client = AsyncMock()
        mock_client.get_dashboards_summary_page.return_value = (50, [
            {"id": 1, "dashboard_title": "A"},
            {"id": 2, "dashboard_title": "B"},
        ])
        mock_client_fn.return_value = mock_client

        env = MagicMock()
        env.id = "env-1"
        result = await service.get_dashboards_page_with_status(
            env, tasks=[], page=1, page_size=10
        )
        assert result["total"] == 50
        assert len(result["dashboards"]) == 2
        assert result["total_pages"] == 5

    @pytest.mark.asyncio
    async def test_zero_total_pages(self, mock_client_fn, service):
        mock_client = AsyncMock()
        mock_client.get_dashboards_summary_page.return_value = (0, [])
        mock_client_fn.return_value = mock_client

        env = MagicMock()
        env.id = "env-1"
        result = await service.get_dashboards_page_with_status(
            env, tasks=[], page=1, page_size=10
        )
        assert result["total"] == 0
        assert result["total_pages"] == 1
        assert result["dashboards"] == []

    @pytest.mark.asyncio
    async def test_with_search(self, mock_client_fn, service):
        mock_client = AsyncMock()
        mock_client.get_dashboards_summary_page.return_value = (1, [{"id": 42}])
        mock_client_fn.return_value = mock_client

        env = MagicMock()
        env.id = "env-1"
        result = await service.get_dashboards_page_with_status(
            env, tasks=[], page=1, page_size=20, search="Sales",
        )
        mock_client.get_dashboards_summary_page.assert_called_with(
            page=1, page_size=20, search="Sales", require_slug=False,
        )
        assert len(result["dashboards"]) == 1

    @pytest.mark.asyncio
    async def test_include_git_status_false(self, mock_client_fn, service):
        mock_client = AsyncMock()
        mock_client.get_dashboards_summary_page.return_value = (1, [{"id": 1}])
        mock_client_fn.return_value = mock_client

        env = MagicMock()
        env.id = "env-1"
        result = await service.get_dashboards_page_with_status(
            env, page=1, page_size=10, include_git_status=False,
        )
        assert result["dashboards"][0]["git_status"] is None


@patch("src.services.resource_service.get_superset_client")
class TestGetDatasetsWithStatus:
    """get_datasets_with_status — enrich datasets with task status and linked counts."""

    @pytest.fixture
    def service(self):
        from src.services.resource_service import ResourceService
        svc = ResourceService()
        svc._get_last_task_for_resource = MagicMock(return_value={"status": "COMPLETED"})
        return svc

    @pytest.mark.asyncio
    async def test_enriches_datasets(self, mock_client_fn, service):
        mock_client = AsyncMock()
        mock_client.get_datasets_summary.return_value = [
            {"id": 1, "table_name": "users"},
            {"id": 2, "table_name": "orders"},
        ]
        mock_client.get_dataset_linked_dashboard_count = AsyncMock(return_value=3)
        mock_client_fn.return_value = mock_client

        env = MagicMock()
        env.id = "env-1"
        result = await service.get_datasets_with_status(env, tasks=[])
        assert len(result) == 2
        assert result[0]["last_task"] == {"status": "COMPLETED"}
        assert result[0]["linked_dashboard_count"] == 3
        assert result[1]["linked_dashboard_count"] == 3

    @pytest.mark.asyncio
    async def test_linked_count_failure_returns_zero(self, mock_client_fn, service):
        mock_client = AsyncMock()
        mock_client.get_datasets_summary.return_value = [{"id": 1, "table_name": "users"}]
        mock_client.get_dataset_linked_dashboard_count = AsyncMock(
            side_effect=TimeoutError("Superset timeout")
        )
        mock_client_fn.return_value = mock_client

        env = MagicMock()
        env.id = "env-1"
        result = await service.get_datasets_with_status(env, tasks=[])
        assert result[0]["linked_dashboard_count"] == 0

    @pytest.mark.asyncio
    async def test_no_tasks_returns_none_last_task(self, mock_client_fn, service):
        mock_client = AsyncMock()
        mock_client.get_datasets_summary.return_value = [{"id": 1, "table_name": "users"}]
        mock_client.get_dataset_linked_dashboard_count = AsyncMock(return_value=0)
        mock_client_fn.return_value = mock_client

        service._get_last_task_for_resource.return_value = None

        env = MagicMock()
        env.id = "env-1"
        result = await service.get_datasets_with_status(env, tasks=None)
        assert result[0]["last_task"] is None
# #endregion Test.ResourceService
