"""Tests for MCP server tool logic.

Tests call tool handler functions directly with mocked contexts,
bypassing the MCP transport layer.
"""

from __future__ import annotations

import json
from datetime import datetime
from pathlib import Path
from unittest.mock import AsyncMock, MagicMock, patch

import pytest

from project_hub.core.cache import Cache
from project_hub.core.config import Config
from project_hub.core.models import Event, EventType, Repo
from project_hub.core.workspace import Workspace
from project_hub.mcp.server import (
    AppContext,
    acknowledge_alerts,
    app_lifespan,
    fetch_all,
    init,
    list_issues,
    list_repos,
    repo_status,
    NOT_INITIALIZED,
)


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


def _make_ctx(app: AppContext) -> MagicMock:
    """Build a fake MCP Context whose request_context.lifespan_context returns *app*."""
    ctx = MagicMock()
    ctx.request_context.lifespan_context = app
    ctx.session = AsyncMock()
    return ctx


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}",
    )


# ---------------------------------------------------------------------------
# Tests
# ---------------------------------------------------------------------------


async def test_init_creates_project_hub_dir(tmp_workspace: Path):
    """init tool creates .project-hub/ with config and workspace.md."""
    import project_hub.mcp.server as srv

    app = AppContext(workspace=None)
    ctx = _make_ctx(app)
    old_ctx = srv._app_context
    srv._app_context = None  # ensure init skips workspace activation

    try:
        with patch("project_hub.mcp.server.Path") as mock_path_cls:
            mock_path_cls.cwd.return_value = tmp_workspace
            mock_path_cls.side_effect = Path

            result = await init(ctx)
    finally:
        srv._app_context = old_ctx

    assert "Initialized project-hub" in result
    assert (tmp_workspace / ".project-hub").is_dir()
    assert (tmp_workspace / ".project-hub" / "config.yaml").exists()
    assert (tmp_workspace / ".project-hub" / "workspace.md").exists()
    assert "repo-alpha" in result
    assert "repo-beta" in result


async def test_init_rejects_git_repo(tmp_path: Path):
    """init refuses to run inside a git repo."""
    (tmp_path / ".git").mkdir()

    app = AppContext(workspace=None)
    ctx = _make_ctx(app)

    with patch("project_hub.mcp.server.Path") as mock_path_cls:
        mock_path_cls.cwd.return_value = tmp_path
        mock_path_cls.side_effect = Path

        result = await init(ctx)

    assert "itself a git repo" in result


async def test_init_rejects_too_few_repos(tmp_path: Path):
    """init refuses when there's only one git subdirectory."""
    (tmp_path / "solo" / ".git").mkdir(parents=True)

    app = AppContext(workspace=None)
    ctx = _make_ctx(app)

    with patch("project_hub.mcp.server.Path") as mock_path_cls:
        mock_path_cls.cwd.return_value = tmp_path
        mock_path_cls.side_effect = Path

        result = await init(ctx)

    assert "1 git repo" in result


@patch("project_hub.core.workspace.resolve_token", return_value="fake-token")
@patch("project_hub.core.workspace.GitLabProvider")
async def test_list_repos_returns_data(mock_gl, mock_token, tmp_hub: Path):
    """list_repos returns JSON with repo data when workspace is active."""
    ws = Workspace(tmp_hub)
    await ws.start()

    app = AppContext(workspace=ws)
    ctx = _make_ctx(app)

    raw = await list_repos(ctx)
    data = json.loads(raw)

    assert len(data) == 3
    names = {r["name"] for r in data}
    assert names == {"repo-alpha", "repo-beta", "repo-gamma"}

    await ws.cache.close()


@patch("project_hub.core.workspace.resolve_token", return_value="fake-token")
@patch("project_hub.core.workspace.GitLabProvider")
async def test_repo_status_returns_branch_info(mock_gl, mock_token, tmp_hub: Path):
    """repo_status returns git status with branch information."""
    from project_hub.git.ops import git_status as _git_status

    ws = Workspace(tmp_hub)
    await ws.start()

    # Populate the git status cache (normally done by refresh cycle)
    for r in ws.repos:
        ws._git_statuses[r.name] = _git_status(Path(r.path))

    app = AppContext(workspace=ws)
    ctx = _make_ctx(app)

    raw = await repo_status(ctx, repo="repo-alpha")
    data = json.loads(raw)

    assert len(data) == 1
    assert data[0]["repo"] == "repo-alpha"
    # Should have branch info (might be 'main' or 'master' depending on git defaults)
    assert "branch" in data[0]
    assert "dirty" in data[0]

    await ws.cache.close()


async def test_fetch_all_noop_when_running(tmp_hub: Path):
    """fetch_all returns 'already running' when a refresh is in progress."""
    ws = MagicMock(spec=Workspace)
    ws._refresh_running = True

    app = AppContext(workspace=ws)
    ctx = _make_ctx(app)

    result = await fetch_all(ctx)

    assert "already in progress" in result
    ws.refresh_once.assert_not_called()


async def test_tools_return_error_when_dormant():
    """Active-mode tools return NOT_INITIALIZED when workspace is None."""
    app = AppContext(workspace=None)
    ctx = _make_ctx(app)

    result = await list_repos(ctx)
    assert result == NOT_INITIALIZED

    result = await repo_status(ctx)
    assert result == NOT_INITIALIZED

    result = await fetch_all(ctx)
    assert result == NOT_INITIALIZED


@patch("project_hub.core.workspace.resolve_token", return_value="fake-token")
@patch("project_hub.core.workspace.GitLabProvider")
async def test_acknowledge_alerts_marks_seen(mock_gl, mock_token, tmp_hub: Path):
    """acknowledge_alerts marks specific events as seen."""
    ws = Workspace(tmp_hub)
    await ws.start()

    # Insert some events directly into the cache.
    events = [
        Event(
            id=None,
            type=EventType.PIPELINE_FAILED,
            repo_name="repo-alpha",
            summary="Pipeline #1 failed",
            detail="",
            created_at=datetime.now(),
        ),
        Event(
            id=None,
            type=EventType.MR_NEW_COMMENT,
            repo_name="repo-beta",
            summary="New comment on MR !5",
            detail="",
            created_at=datetime.now(),
        ),
    ]
    await ws.cache.store_events(events)

    # Verify they're unseen.
    unseen = await ws.cache.get_unseen_events()
    assert len(unseen) == 2
    event_ids = [e.id for e in unseen]

    app = AppContext(workspace=ws)
    ctx = _make_ctx(app)

    # Acknowledge the first event.
    result = await acknowledge_alerts(ctx, event_ids=str(event_ids[0]))
    assert "1 event(s) as seen" in result

    unseen_after = await ws.cache.get_unseen_events()
    assert len(unseen_after) == 1
    assert unseen_after[0].id == event_ids[1]

    await ws.cache.close()


@patch("project_hub.core.workspace.resolve_token", return_value="fake-token")
@patch("project_hub.core.workspace.GitLabProvider")
async def test_acknowledge_alerts_marks_all(mock_gl, mock_token, tmp_hub: Path):
    """acknowledge_alerts with empty event_ids marks all events as seen."""
    ws = Workspace(tmp_hub)
    await ws.start()

    events = [
        Event(id=None, type=EventType.PIPELINE_FAILED, repo_name="repo-alpha",
              summary="fail1", detail="", created_at=datetime.now()),
        Event(id=None, type=EventType.PIPELINE_FAILED, repo_name="repo-beta",
              summary="fail2", detail="", created_at=datetime.now()),
    ]
    await ws.cache.store_events(events)

    app = AppContext(workspace=ws)
    ctx = _make_ctx(app)

    result = await acknowledge_alerts(ctx, event_ids="")
    assert "All events marked as seen" in result

    unseen = await ws.cache.get_unseen_events()
    assert len(unseen) == 0

    await ws.cache.close()


async def test_list_issues_returns_cached(tmp_path):
    """list_issues tool returns cached issues as JSON."""
    from project_hub.core.models import Issue

    cache = Cache(tmp_path / "cache.db")
    await cache.initialize()

    issue = Issue(
        id=100, iid=1, title="Fix bug", author="alice", state="opened",
        web_url="https://gitlab.com/issues/1", user_notes_count=3,
        created_at="2024-01-01T00:00:00Z", updated_at="2024-01-02T00:00:00Z",
        repo_name="repo-alpha", labels=["bug"], assignees=["alice"],
        participants=["alice", "bob"], confidential=False,
    )
    await cache.store_issues("repo-alpha", [issue])

    ws = MagicMock(spec=Workspace)
    ws.cache = cache
    ws.repos = [_make_repo("repo-alpha")]

    app = AppContext(workspace=ws)
    ctx = _make_ctx(app)

    result = await list_issues(ctx)
    data = json.loads(result)
    assert len(data) == 1
    assert data[0]["iid"] == 1
    assert data[0]["title"] == "Fix bug"
    assert data[0]["labels"] == ["bug"]

    await cache.close()


@pytest.mark.anyio
async def test_lifespan_git_version_too_old():
    """If git version check fails, lifespan yields an empty AppContext (dormant-like)."""
    from project_hub.git.ops import GitVersionError

    fake_server = MagicMock()

    with patch(
        "project_hub.mcp.server.check_git_version",
        side_effect=GitVersionError("git 2.10 is too old; minimum required is 2.38"),
    ):
        async with app_lifespan(fake_server) as ctx:
            assert isinstance(ctx, AppContext)
            assert ctx.workspace is None
