"""Tests for MCP server resource functions.

Resources are sync functions that read from the module-level _app_context.
Tests set _app_context directly and call the resource functions.
"""

from __future__ import annotations

import json
from datetime import datetime
from unittest.mock import MagicMock, patch

import pytest

import project_hub.mcp.server as server_mod
from project_hub.core.cache import Cache
from project_hub.core.models import Event, EventType, GitStatus, Repo
from project_hub.core.workspace import Workspace
from project_hub.mcp.server import (
    AppContext,
    NOT_INITIALIZED,
    alerts,
    dashboard_url,
    notes,
    status,
    topology,
)


# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------


def _make_repo(name: str = "repo-alpha", path: str = "/tmp/fake") -> Repo:
    return Repo(
        name=name,
        path=path,
        remote_url=f"git@gitlab.com:group/{name}.git",
        forge_host="gitlab.com",
        forge_type="gitlab",
        has_forge=True,
        project_path=f"group/{name}",
        default_branch="main",
    )


@pytest.fixture
def _clear_app_context():
    """Reset the module-level _app_context before and after each test."""
    original = server_mod._app_context
    server_mod._app_context = None
    yield
    server_mod._app_context = original


# ---------------------------------------------------------------------------
# workspace://topology
# ---------------------------------------------------------------------------


class TestTopologyResource:

    def test_returns_repo_list_with_forge_info(self, tmp_hub, _clear_app_context):
        """topology returns JSON array with repo data and branch info."""
        ws = MagicMock(spec=Workspace)
        repos = [_make_repo("repo-alpha", str(tmp_hub / "repo-alpha")),
                 _make_repo("repo-beta", str(tmp_hub / "repo-beta"))]
        ws.repos = repos
        ws._git_statuses = {
            "repo-alpha": GitStatus(branch="main", ahead=0, behind=0, dirty=False, stale=False),
            "repo-beta": GitStatus(branch="main", ahead=0, behind=0, dirty=False, stale=False),
        }

        server_mod._app_context = AppContext(workspace=ws)

        result = topology()

        data = json.loads(result)
        assert len(data) == 2
        names = {r["name"] for r in data}
        assert names == {"repo-alpha", "repo-beta"}
        # Each entry has forge info
        for entry in data:
            assert entry["forge_host"] == "gitlab.com"
            assert entry["forge_type"] == "gitlab"
            assert entry["has_forge"] is True
            assert entry["default_branch"] == "main"
            assert entry["branch"] == "main"

    def test_with_no_repos_returns_empty_list(self, _clear_app_context):
        """topology with no repos returns an empty JSON array."""
        ws = MagicMock(spec=Workspace)
        ws.repos = []
        server_mod._app_context = AppContext(workspace=ws)

        result = topology()
        data = json.loads(result)
        assert data == []

    def test_not_initialized_returns_error(self, _clear_app_context):
        """topology returns error JSON when workspace is None."""
        server_mod._app_context = AppContext(workspace=None)

        result = topology()
        data = json.loads(result)
        assert "error" in data
        assert data["error"] == NOT_INITIALIZED

    def test_no_app_context_returns_error(self, _clear_app_context):
        """topology returns error JSON when _app_context is None."""
        server_mod._app_context = None

        result = topology()
        data = json.loads(result)
        assert "error" in data

    def test_no_cached_status_sets_branch_none(self, _clear_app_context):
        """When no cached git status exists, branch is set to None."""
        ws = MagicMock(spec=Workspace)
        ws.repos = [_make_repo("repo-alpha")]
        ws._git_statuses = {}
        server_mod._app_context = AppContext(workspace=ws)

        result = topology()

        data = json.loads(result)
        assert len(data) == 1
        assert data[0]["branch"] is None


# ---------------------------------------------------------------------------
# workspace://status
# ---------------------------------------------------------------------------


class TestStatusResource:

    def test_returns_git_status_per_repo(self, tmp_hub, _clear_app_context):
        """status returns JSON with branch, ahead/behind, dirty, stale per repo."""
        ws = MagicMock(spec=Workspace)
        repos = [_make_repo("repo-alpha", str(tmp_hub / "repo-alpha"))]
        ws.repos = repos
        ws._git_statuses = {
            "repo-alpha": GitStatus(branch="main", ahead=1, behind=2, dirty=True, stale=False),
        }
        ws._default_divergence = {}
        server_mod._app_context = AppContext(workspace=ws)

        result = status()

        data = json.loads(result)
        assert len(data) == 1
        entry = data[0]
        assert entry["repo"] == "repo-alpha"
        assert entry["branch"] == "main"
        assert entry["ahead"] == 1
        assert entry["behind"] == 2
        assert entry["dirty"] is True
        assert entry["stale"] is False

    def test_not_initialized_returns_error(self, _clear_app_context):
        """status returns error when workspace is None."""
        server_mod._app_context = AppContext(workspace=None)

        result = status()
        data = json.loads(result)
        assert "error" in data

    def test_no_cached_status_returns_error_entry(self, _clear_app_context):
        """When no cached git status exists, the entry contains an error field."""
        ws = MagicMock(spec=Workspace)
        ws.repos = [_make_repo("repo-broken")]
        ws._git_statuses = {}
        ws._default_divergence = {}
        server_mod._app_context = AppContext(workspace=ws)

        result = status()

        data = json.loads(result)
        assert len(data) == 1
        assert data[0]["repo"] == "repo-broken"
        assert "error" in data[0]

    def test_feature_branch_includes_default_divergence(self, _clear_app_context):
        """When on a feature branch, status includes default_ahead/default_behind."""
        ws = MagicMock(spec=Workspace)
        repo = _make_repo("repo-alpha")
        repo.default_branch = "main"
        ws.repos = [repo]
        ws._git_statuses = {
            "repo-alpha": GitStatus(branch="feature/xyz", ahead=0, behind=0, dirty=False, stale=False),
        }
        ws._default_divergence = {"repo-alpha": (3, 5)}
        server_mod._app_context = AppContext(workspace=ws)

        result = status()

        data = json.loads(result)
        entry = data[0]
        assert entry["default_branch"] == "main"
        assert entry["default_ahead"] == 3
        assert entry["default_behind"] == 5


# ---------------------------------------------------------------------------
# workspace://alerts
# ---------------------------------------------------------------------------


class TestAlertsResource:

    async def test_returns_unseen_events(self, tmp_path, _clear_app_context):
        """alerts returns unseen events as JSON from the SQLite cache."""
        cache = Cache(tmp_path / ".project-hub" / "cache.db")
        (tmp_path / ".project-hub").mkdir(parents=True)
        await cache.initialize()

        events = [
            Event(
                id=None, type=EventType.PIPELINE_FAILED, repo_name="repo-alpha",
                summary="Pipeline #42 failed", detail="job build exited 1",
                created_at=datetime(2024, 6, 15, 10, 0, 0),
            ),
            Event(
                id=None, type=EventType.MR_NEW_COMMENT, repo_name="repo-beta",
                summary="New comment on MR !5", detail="",
                created_at=datetime(2024, 6, 15, 11, 0, 0),
            ),
        ]
        await cache.store_events(events)

        ws = MagicMock(spec=Workspace)
        ws.cache = cache
        server_mod._app_context = AppContext(workspace=ws)

        result = alerts()
        data = json.loads(result)
        assert len(data) == 2
        # Events ordered by created_at ASC (from get_unseen_events_sync)
        assert data[0]["summary"] == "Pipeline #42 failed"
        assert data[0]["repo_name"] == "repo-alpha"
        assert data[1]["summary"] == "New comment on MR !5"
        assert data[1]["repo_name"] == "repo-beta"
        for entry in data:
            assert "id" in entry
            assert "type" in entry
            assert "created_at" in entry

        await cache.close()

    async def test_no_events_returns_empty_list(self, tmp_path, _clear_app_context):
        """alerts with no events returns an empty JSON array."""
        cache = Cache(tmp_path / ".project-hub" / "cache.db")
        (tmp_path / ".project-hub").mkdir(parents=True)
        await cache.initialize()

        ws = MagicMock(spec=Workspace)
        ws.cache = cache
        server_mod._app_context = AppContext(workspace=ws)

        result = alerts()
        data = json.loads(result)
        assert data == []

        await cache.close()

    async def test_after_marking_seen_returns_empty(self, tmp_path, _clear_app_context):
        """alerts returns empty after all events are marked as seen."""
        cache = Cache(tmp_path / ".project-hub" / "cache.db")
        (tmp_path / ".project-hub").mkdir(parents=True)
        await cache.initialize()

        events = [
            Event(
                id=None, type=EventType.PIPELINE_FAILED, repo_name="repo-alpha",
                summary="Pipeline #42 failed", detail="",
                created_at=datetime(2024, 6, 15, 10, 0, 0),
            ),
        ]
        await cache.store_events(events)
        await cache.mark_all_events_seen()

        ws = MagicMock(spec=Workspace)
        ws.cache = cache
        server_mod._app_context = AppContext(workspace=ws)

        result = alerts()
        data = json.loads(result)
        assert data == []

        await cache.close()

    async def test_no_cache_db_returns_empty_list(self, tmp_path, _clear_app_context):
        """alerts returns empty list when cache has no events."""
        cache = Cache(tmp_path / ".project-hub" / "cache.db")
        (tmp_path / ".project-hub").mkdir(parents=True)
        await cache.initialize()

        ws = MagicMock(spec=Workspace)
        ws.cache = cache
        server_mod._app_context = AppContext(workspace=ws)

        result = alerts()
        data = json.loads(result)
        assert data == []

        await cache.close()

    def test_not_initialized_returns_error(self, _clear_app_context):
        """alerts returns error JSON when workspace is None."""
        server_mod._app_context = AppContext(workspace=None)

        result = alerts()
        data = json.loads(result)
        assert "error" in data


# ---------------------------------------------------------------------------
# workspace://notes
# ---------------------------------------------------------------------------


class TestNotesResource:

    def test_returns_workspace_md_content(self, tmp_hub, _clear_app_context):
        """notes returns content of workspace.md with preamble."""
        notes_path = tmp_hub / ".project-hub" / "workspace.md"
        notes_path.write_text("repo-alpha is the main service\nrepo-beta handles auth")

        ws = MagicMock(spec=Workspace)
        ws.root = tmp_hub
        server_mod._app_context = AppContext(workspace=ws)

        result = notes()
        assert "repo-alpha is the main service" in result
        assert "repo-beta handles auth" in result
        # Should have the preamble about workspace-scoped notes
        assert "Workspace-scoped notes" in result

    def test_missing_workspace_md_returns_empty(self, tmp_path, _clear_app_context):
        """notes returns empty string when workspace.md doesn't exist."""
        (tmp_path / ".project-hub").mkdir(parents=True)

        ws = MagicMock(spec=Workspace)
        ws.root = tmp_path
        server_mod._app_context = AppContext(workspace=ws)

        result = notes()
        assert result == ""

    def test_empty_workspace_md_returns_setup_prompt(self, tmp_hub, _clear_app_context):
        """notes returns a setup prompt when workspace.md is empty."""
        notes_path = tmp_hub / ".project-hub" / "workspace.md"
        notes_path.write_text("")

        ws = MagicMock(spec=Workspace)
        ws.root = tmp_hub
        server_mod._app_context = AppContext(workspace=ws)

        result = notes()
        assert "workspace.md is empty" in result
        assert "Ask the user" in result

    def test_whitespace_only_workspace_md_returns_setup_prompt(self, tmp_hub, _clear_app_context):
        """notes treats whitespace-only workspace.md as empty."""
        notes_path = tmp_hub / ".project-hub" / "workspace.md"
        notes_path.write_text("   \n  \n  ")

        ws = MagicMock(spec=Workspace)
        ws.root = tmp_hub
        server_mod._app_context = AppContext(workspace=ws)

        result = notes()
        assert "workspace.md is empty" in result

    def test_not_initialized_returns_empty(self, _clear_app_context):
        """notes returns empty string when workspace is None."""
        server_mod._app_context = AppContext(workspace=None)

        result = notes()
        assert result == ""


# ---------------------------------------------------------------------------
# workspace://dashboard
# ---------------------------------------------------------------------------


class TestDashboardResource:

    def test_returns_url_with_port(self, _clear_app_context):
        """dashboard returns the localhost URL when webui is running."""
        server_mod._app_context = AppContext(workspace=MagicMock(), webui_port=8400)

        result = dashboard_url()
        assert result == "http://localhost:8400"

    def test_returns_url_with_custom_port(self, _clear_app_context):
        """dashboard returns the URL with whatever port was assigned."""
        server_mod._app_context = AppContext(workspace=MagicMock(), webui_port=9999)

        result = dashboard_url()
        assert result == "http://localhost:9999"

    def test_no_webui_returns_not_running(self, _clear_app_context):
        """dashboard returns 'not running' when webui_port is None."""
        server_mod._app_context = AppContext(workspace=MagicMock(), webui_port=None)

        result = dashboard_url()
        assert result == "Web UI is not running."

    def test_no_app_context_returns_not_running(self, _clear_app_context):
        """dashboard returns 'not running' when _app_context is None."""
        server_mod._app_context = None

        result = dashboard_url()
        assert result == "Web UI is not running."
