"""Tests for the 9 MCP tools not covered in test_mcp_tools.py.

Covers: pull_safe, list_mrs, list_pipelines, get_vulnerabilities,
get_pipeline_jobs, auth_status, auth_login, configure_forge, rescan.

Same pattern as test_mcp_tools.py: MagicMock(spec=Workspace) + real Cache,
tools called directly via their handler functions.
"""

from __future__ import annotations

import json
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 MR, Job, Pipeline, Repo, RepoResult, Vulnerability
from project_hub.core.workspace import Workspace
from project_hub.mcp.server import (
    AppContext,
    NOT_INITIALIZED,
    auth_login,
    auth_status,
    configure_forge,
    get_pipeline_jobs,
    get_vulnerabilities,
    list_mrs,
    list_pipelines,
    pull_safe,
    rescan,
)

# ---------------------------------------------------------------------------
# Helpers (same convention as test_mcp_tools.py)
# ---------------------------------------------------------------------------


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


def _make_mr(iid: int, repo_name: str = "repo-alpha", state: str = "opened") -> MR:
    return MR(
        id=iid * 100,
        iid=iid,
        title=f"MR {iid}",
        author="alice",
        state=state,
        source_branch=f"feature/{iid}",
        target_branch="main",
        web_url=f"https://gitlab.com/mr/{iid}",
        user_notes_count=0,
        approved=False,
        created_at="2024-01-01T00:00:00Z",
        updated_at="2024-01-02T00:00:00Z",
        repo_name=repo_name,
        sha="abc123",
    )


def _make_pipeline(pid: int, repo_name: str = "repo-alpha", status: str = "success") -> Pipeline:
    return Pipeline(
        id=pid,
        status=status,
        ref="main",
        web_url=f"https://gitlab.com/pipelines/{pid}",
        created_at="2024-01-01T00:00:00Z",
        repo_name=repo_name,
    )


def _make_vuln(
    vid: int,
    repo_name: str = "repo-alpha",
    severity: str = "high",
    state: str = "detected",
) -> Vulnerability:
    return Vulnerability(
        id=vid,
        name=f"CVE-{vid}",
        severity=severity,
        state=state,
        source="sast",
        web_url=f"https://gitlab.com/vulns/{vid}",
        repo_name=repo_name,
    )


async def _setup_cache(tmp_path: Path) -> Cache:
    """Create and initialize a fresh cache."""
    cache = Cache(tmp_path / "cache.db")
    await cache.initialize()
    return cache


# ---------------------------------------------------------------------------
# 1. pull_safe
# ---------------------------------------------------------------------------


async def test_pull_safe_specific_repo(tmp_path):
    """pull_safe with a repo name calls pull_safe_one and returns the result."""
    cache = await _setup_cache(tmp_path)
    repo = _make_repo("repo-alpha", str(tmp_path / "repo-alpha"))

    ws = MagicMock(spec=Workspace)
    ws.cache = cache
    ws.repos = [repo]
    ws.config = Config()

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

    result_obj = RepoResult(repo="repo-alpha", status="updated", message="Fast-forward")

    with patch("project_hub.mcp.server.pull_safe_one", new_callable=AsyncMock, return_value=result_obj):
        raw = await pull_safe(ctx, repo="repo-alpha")

    data = json.loads(raw)
    assert len(data) == 1
    assert data[0]["repo"] == "repo-alpha"
    assert data[0]["status"] == "updated"
    assert data[0]["message"] == "Fast-forward"

    await cache.close()


async def test_pull_safe_all_repos(tmp_path):
    """pull_safe with no repo pulls all repos."""
    cache = await _setup_cache(tmp_path)
    repos = [
        _make_repo("repo-alpha", str(tmp_path / "repo-alpha")),
        _make_repo("repo-beta", str(tmp_path / "repo-beta")),
    ]

    ws = MagicMock(spec=Workspace)
    ws.cache = cache
    ws.repos = repos
    ws.config = Config()

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

    all_results = [
        RepoResult(repo="repo-alpha", status="updated", message="OK"),
        RepoResult(repo="repo-beta", status="already_up_to_date", message=None),
    ]

    with patch("project_hub.mcp.server.pull_safe_all", new_callable=AsyncMock, return_value=all_results):
        raw = await pull_safe(ctx, repo="")

    data = json.loads(raw)
    assert len(data) == 2
    assert data[0]["repo"] == "repo-alpha"
    assert data[1]["repo"] == "repo-beta"
    assert data[1]["status"] == "already_up_to_date"

    await cache.close()


async def test_pull_safe_unknown_repo(tmp_path):
    """pull_safe with unknown repo returns error."""
    cache = await _setup_cache(tmp_path)

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

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

    raw = await pull_safe(ctx, repo="nonexistent")
    assert "not found" in raw

    await cache.close()


async def test_pull_safe_dormant():
    """pull_safe returns NOT_INITIALIZED when workspace is None."""
    app = AppContext(workspace=None)
    ctx = _make_ctx(app)
    result = await pull_safe(ctx)
    assert result == NOT_INITIALIZED


# ---------------------------------------------------------------------------
# 2. list_mrs
# ---------------------------------------------------------------------------


async def test_list_mrs_no_filter(tmp_path):
    """list_mrs with no filter returns all cached opened MRs."""
    cache = await _setup_cache(tmp_path)

    mrs = [_make_mr(1, "repo-alpha"), _make_mr(2, "repo-beta")]
    await cache.store_mrs("repo-alpha", [mrs[0]])
    await cache.store_mrs("repo-beta", [mrs[1]])

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

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

    raw = await list_mrs(ctx)
    data = json.loads(raw)
    assert len(data) == 2
    iids = {d["iid"] for d in data}
    assert iids == {1, 2}

    # Verify JSON structure has expected fields
    for entry in data:
        assert "repo" in entry
        assert "iid" in entry
        assert "title" in entry
        assert "author" in entry
        assert "state" in entry
        assert "source_branch" in entry
        assert "web_url" in entry
        assert "notes" in entry
        assert "approved" in entry

    await cache.close()


async def test_list_mrs_repo_filter(tmp_path):
    """list_mrs with repo filter returns only that repo's MRs."""
    cache = await _setup_cache(tmp_path)

    await cache.store_mrs("repo-alpha", [_make_mr(1, "repo-alpha")])
    await cache.store_mrs("repo-beta", [_make_mr(2, "repo-beta")])

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

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

    raw = await list_mrs(ctx, repo="repo-alpha")
    data = json.loads(raw)
    assert len(data) == 1
    assert data[0]["repo"] == "repo-alpha"
    assert data[0]["iid"] == 1

    await cache.close()


async def test_list_mrs_state_filter(tmp_path):
    """list_mrs with state filter returns only MRs with that state."""
    cache = await _setup_cache(tmp_path)

    opened_mr = _make_mr(1, "repo-alpha", state="opened")
    merged_mr = _make_mr(2, "repo-alpha", state="merged")
    await cache.store_mrs("repo-alpha", [opened_mr, merged_mr])

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

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

    raw = await list_mrs(ctx, state="merged")
    data = json.loads(raw)
    assert len(data) == 1
    assert data[0]["iid"] == 2
    assert data[0]["state"] == "merged"

    await cache.close()


async def test_list_mrs_empty_cache(tmp_path):
    """list_mrs with empty cache returns empty list."""
    cache = await _setup_cache(tmp_path)

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

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

    # Default state filter is "opened", no MRs stored
    raw = await list_mrs(ctx)
    data = json.loads(raw)
    assert data == []

    await cache.close()


async def test_list_mrs_dormant():
    """list_mrs returns NOT_INITIALIZED when workspace is None."""
    app = AppContext(workspace=None)
    ctx = _make_ctx(app)
    result = await list_mrs(ctx)
    assert result == NOT_INITIALIZED


# ---------------------------------------------------------------------------
# 3. list_pipelines
# ---------------------------------------------------------------------------


async def test_list_pipelines_no_filter(tmp_path):
    """list_pipelines with no filter returns all cached pipelines."""
    cache = await _setup_cache(tmp_path)

    p1 = _make_pipeline(101, "repo-alpha", "success")
    p2 = _make_pipeline(102, "repo-beta", "failed")
    await cache.store_pipelines("repo-alpha", [p1])
    await cache.store_pipelines("repo-beta", [p2])

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

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

    raw = await list_pipelines(ctx)
    data = json.loads(raw)
    assert len(data) == 2
    ids = {d["id"] for d in data}
    assert ids == {101, 102}

    # Verify JSON structure
    for entry in data:
        assert "repo" in entry
        assert "id" in entry
        assert "status" in entry
        assert "ref" in entry
        assert "web_url" in entry
        assert "created_at" in entry

    await cache.close()


async def test_list_pipelines_repo_filter(tmp_path):
    """list_pipelines with repo filter returns only that repo's pipelines."""
    cache = await _setup_cache(tmp_path)

    await cache.store_pipelines("repo-alpha", [_make_pipeline(101, "repo-alpha")])
    await cache.store_pipelines("repo-beta", [_make_pipeline(102, "repo-beta")])

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

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

    raw = await list_pipelines(ctx, repo="repo-beta")
    data = json.loads(raw)
    assert len(data) == 1
    assert data[0]["repo"] == "repo-beta"
    assert data[0]["id"] == 102

    await cache.close()


async def test_list_pipelines_status_filter(tmp_path):
    """list_pipelines with status filter returns only matching pipelines."""
    cache = await _setup_cache(tmp_path)

    p1 = _make_pipeline(101, "repo-alpha", "success")
    p2 = _make_pipeline(102, "repo-alpha", "failed")
    await cache.store_pipelines("repo-alpha", [p1, p2])

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

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

    raw = await list_pipelines(ctx, status="failed")
    data = json.loads(raw)
    assert len(data) == 1
    assert data[0]["status"] == "failed"
    assert data[0]["id"] == 102

    await cache.close()


async def test_list_pipelines_empty_cache(tmp_path):
    """list_pipelines with empty cache returns empty list."""
    cache = await _setup_cache(tmp_path)

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

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

    raw = await list_pipelines(ctx)
    data = json.loads(raw)
    assert data == []

    await cache.close()


# ---------------------------------------------------------------------------
# 4. get_vulnerabilities
# ---------------------------------------------------------------------------


async def test_get_vulnerabilities_no_filter(tmp_path):
    """get_vulnerabilities with no filter returns high+critical detected vulns (defaults)."""
    cache = await _setup_cache(tmp_path)

    vulns = [
        _make_vuln(1, "repo-alpha", severity="critical", state="detected"),
        _make_vuln(2, "repo-alpha", severity="high", state="detected"),
        _make_vuln(3, "repo-alpha", severity="medium", state="detected"),  # filtered out
        _make_vuln(4, "repo-alpha", severity="high", state="dismissed"),  # filtered out
    ]
    await cache.store_vulns("repo-alpha", vulns)

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

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

    raw = await get_vulnerabilities(ctx)
    data = json.loads(raw)
    assert len(data) == 2
    ids = {d["id"] for d in data}
    assert ids == {1, 2}

    await cache.close()


async def test_get_vulnerabilities_repo_filter(tmp_path):
    """get_vulnerabilities with repo filter returns only that repo's vulns."""
    cache = await _setup_cache(tmp_path)

    await cache.store_vulns("repo-alpha", [_make_vuln(1, "repo-alpha")])
    await cache.store_vulns("repo-beta", [_make_vuln(2, "repo-beta")])

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

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

    raw = await get_vulnerabilities(ctx, repo="repo-alpha")
    data = json.loads(raw)
    assert len(data) == 1
    assert data[0]["repo_name"] == "repo-alpha"

    await cache.close()


async def test_get_vulnerabilities_severity_filter(tmp_path):
    """get_vulnerabilities with severity filter overrides default severity filtering."""
    cache = await _setup_cache(tmp_path)

    vulns = [
        _make_vuln(1, severity="critical", state="detected"),
        _make_vuln(2, severity="medium", state="detected"),
        _make_vuln(3, severity="low", state="detected"),
    ]
    await cache.store_vulns("repo-alpha", vulns)

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

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

    raw = await get_vulnerabilities(ctx, severity="medium")
    data = json.loads(raw)
    assert len(data) == 1
    assert data[0]["severity"] == "medium"
    assert data[0]["id"] == 2

    await cache.close()


async def test_get_vulnerabilities_show_all(tmp_path):
    """get_vulnerabilities with show_all=True returns everything."""
    cache = await _setup_cache(tmp_path)

    vulns = [
        _make_vuln(1, severity="critical", state="detected"),
        _make_vuln(2, severity="low", state="dismissed"),
        _make_vuln(3, severity="medium", state="resolved"),
    ]
    await cache.store_vulns("repo-alpha", vulns)

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

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

    raw = await get_vulnerabilities(ctx, show_all=True)
    data = json.loads(raw)
    assert len(data) == 3

    await cache.close()


async def test_get_vulnerabilities_empty_cache(tmp_path):
    """get_vulnerabilities with empty cache returns empty list."""
    cache = await _setup_cache(tmp_path)

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

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

    raw = await get_vulnerabilities(ctx)
    data = json.loads(raw)
    assert data == []

    await cache.close()


# ---------------------------------------------------------------------------
# 5. get_pipeline_jobs
# ---------------------------------------------------------------------------


async def test_get_pipeline_jobs_valid(tmp_path):
    """get_pipeline_jobs returns jobs from the forge client."""
    cache = await _setup_cache(tmp_path)

    repo = _make_repo("repo-alpha")

    mock_client = MagicMock()
    mock_client.get_pipeline_jobs.return_value = [
        Job(id=1, name="test-unit", status="success", stage="test", web_url="https://gitlab.com/jobs/1"),
        Job(id=2, name="build", status="failed", stage="build", web_url="https://gitlab.com/jobs/2"),
    ]

    ws = MagicMock(spec=Workspace)
    ws.cache = cache
    ws.repos = [repo]
    ws._forge_clients = {"gitlab.com": mock_client}

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

    raw = await get_pipeline_jobs(ctx, repo="repo-alpha", pipeline_id=101)
    data = json.loads(raw)

    assert len(data) == 2
    assert data[0]["name"] == "test-unit"
    assert data[0]["status"] == "success"
    assert data[1]["name"] == "build"
    assert data[1]["stage"] == "build"

    # Verify the client was called with correct args
    mock_client.get_pipeline_jobs.assert_called_once_with("group/repo-alpha", 101)

    await cache.close()


async def test_get_pipeline_jobs_unknown_repo(tmp_path):
    """get_pipeline_jobs for unknown repo returns error."""
    cache = await _setup_cache(tmp_path)

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

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

    raw = await get_pipeline_jobs(ctx, repo="nonexistent", pipeline_id=101)
    assert "not found" in raw

    await cache.close()


async def test_get_pipeline_jobs_no_forge_client(tmp_path):
    """get_pipeline_jobs for repo with no forge client returns error."""
    cache = await _setup_cache(tmp_path)

    repo = _make_repo("repo-alpha")

    ws = MagicMock(spec=Workspace)
    ws.cache = cache
    ws.repos = [repo]
    ws._forge_clients = {}  # no clients

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

    raw = await get_pipeline_jobs(ctx, repo="repo-alpha", pipeline_id=101)
    assert "No forge client" in raw

    await cache.close()


async def test_get_pipeline_jobs_dormant():
    """get_pipeline_jobs returns NOT_INITIALIZED when workspace is None."""
    app = AppContext(workspace=None)
    ctx = _make_ctx(app)
    result = await get_pipeline_jobs(ctx, repo="x", pipeline_id=1)
    assert result == NOT_INITIALIZED


# ---------------------------------------------------------------------------
# 6. auth_status
# ---------------------------------------------------------------------------


async def test_auth_status_with_instances():
    """auth_status returns list of configured instances with auth state."""
    from project_hub.forge.auth import AuthInfo

    mock_infos = [
        AuthInfo(instance="gitlab.com", has_token=True, authenticated=True),
        AuthInfo(instance="gitlab.example.com", has_token=False, authenticated=False),
    ]

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

    with patch("project_hub.mcp.server._auth_status", return_value=mock_infos):
        raw = await auth_status(ctx)

    data = json.loads(raw)
    assert len(data) == 2
    assert data[0]["instance"] == "gitlab.com"
    assert data[0]["authenticated"] is True
    assert data[1]["instance"] == "gitlab.example.com"
    assert data[1]["authenticated"] is False


async def test_auth_status_no_instances():
    """auth_status with no configured instances returns message."""
    app = AppContext(workspace=None)
    ctx = _make_ctx(app)

    with patch("project_hub.mcp.server._auth_status", return_value=[]):
        raw = await auth_status(ctx)

    data = json.loads(raw)
    assert "message" in data
    assert "No forge credentials" in data["message"]


# ---------------------------------------------------------------------------
# 7. auth_login
# ---------------------------------------------------------------------------


async def test_auth_login_existing_token():
    """auth_login with existing token returns 'already configured' message."""
    app = AppContext(workspace=None)
    ctx = _make_ctx(app)

    with patch("project_hub.mcp.server.resolve_token", return_value="existing-tok"):
        raw = await auth_login(ctx, instance="gitlab.com")

    assert "already configured" in raw
    assert "gitlab.com" in raw


async def test_auth_login_no_token():
    """auth_login with no existing token returns instructions."""
    app = AppContext(workspace=None)
    ctx = _make_ctx(app)

    with patch("project_hub.mcp.server.resolve_token", return_value=None):
        raw = await auth_login(ctx, instance="gitlab.example.com")

    assert "gitlab.example.com" in raw
    assert "personal_access_tokens" in raw
    assert "read_api" in raw
    assert "project-hub auth login" in raw


# ---------------------------------------------------------------------------
# 8. configure_forge
# ---------------------------------------------------------------------------


async def test_configure_forge_success(tmp_path):
    """configure_forge updates config and triggers rescan."""
    cache = await _setup_cache(tmp_path)

    # Set up a workspace with real config on disk
    hub_dir = tmp_path / ".project-hub"
    hub_dir.mkdir()
    config_path = hub_dir / "config.yaml"
    config_path.write_text("forges:\n  gitlab.com:\n    type: gitlab\n")

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

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

    raw = await configure_forge(ctx, host="gitlab.example.com", forge_type="gitlab")

    assert "gitlab.example.com" in raw
    assert "added to config" in raw
    ws.rescan.assert_awaited_once()

    # Verify config was actually written to disk
    import yaml
    updated = yaml.safe_load(config_path.read_text())
    assert "gitlab.example.com" in updated["forges"]
    assert updated["forges"]["gitlab.example.com"]["type"] == "gitlab"
    # Original forge should also still be there
    assert "gitlab.com" in updated["forges"]

    await cache.close()


async def test_configure_forge_invalid_type(tmp_path):
    """configure_forge with unsupported forge type returns error."""
    cache = await _setup_cache(tmp_path)

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

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

    raw = await configure_forge(ctx, host="bitbucket.org", forge_type="bitbucket")

    assert "Unsupported forge type" in raw
    assert "bitbucket" in raw

    await cache.close()


async def test_configure_forge_dormant():
    """configure_forge returns NOT_INITIALIZED when workspace is None."""
    app = AppContext(workspace=None)
    ctx = _make_ctx(app)
    result = await configure_forge(ctx, host="x", forge_type="gitlab")
    assert result == NOT_INITIALIZED


# ---------------------------------------------------------------------------
# 9. rescan
# ---------------------------------------------------------------------------


async def test_rescan_no_changes(tmp_path):
    """rescan with no new/removed repos returns 'no changes'."""
    cache = await _setup_cache(tmp_path)

    repos = [_make_repo("repo-alpha"), _make_repo("repo-beta")]

    ws = MagicMock(spec=Workspace)
    ws.cache = cache
    ws.repos = repos

    # rescan doesn't change ws.repos
    async def fake_rescan():
        pass

    ws.rescan = AsyncMock(side_effect=fake_rescan)

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

    raw = await rescan(ctx)
    assert "No changes" in raw
    assert "2 repos" in raw

    await cache.close()


async def test_rescan_new_repos(tmp_path):
    """rescan discovers new repos and reports them."""
    cache = await _setup_cache(tmp_path)

    original_repos = [_make_repo("repo-alpha"), _make_repo("repo-beta")]

    ws = MagicMock(spec=Workspace)
    ws.cache = cache
    ws.repos = original_repos

    # After rescan, ws.repos includes a new repo
    async def fake_rescan():
        ws.repos = [
            _make_repo("repo-alpha"),
            _make_repo("repo-beta"),
            _make_repo("repo-gamma"),
        ]

    ws.rescan = AsyncMock(side_effect=fake_rescan)

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

    raw = await rescan(ctx)
    assert "repo-gamma" in raw
    assert "New repos" in raw
    assert "3 repos" in raw
    assert "workspace.md" in raw

    await cache.close()


async def test_rescan_removed_repos(tmp_path):
    """rescan detects removed repos and reports them."""
    cache = await _setup_cache(tmp_path)

    original_repos = [
        _make_repo("repo-alpha"),
        _make_repo("repo-beta"),
        _make_repo("repo-gamma"),
    ]

    ws = MagicMock(spec=Workspace)
    ws.cache = cache
    ws.repos = original_repos

    async def fake_rescan():
        ws.repos = [_make_repo("repo-alpha")]

    ws.rescan = AsyncMock(side_effect=fake_rescan)

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

    raw = await rescan(ctx)
    assert "Removed repos" in raw
    assert "repo-beta" in raw
    assert "repo-gamma" in raw
    assert "1 repos" in raw

    await cache.close()


async def test_rescan_dormant():
    """rescan returns NOT_INITIALIZED when workspace is None."""
    app = AppContext(workspace=None)
    ctx = _make_ctx(app)
    result = await rescan(ctx)
    assert result == NOT_INITIALIZED
