"""Tests for _ensure_mr_pipeline_coverage, filter_events wiring, get_state_dict, and _find_repo."""

from __future__ import annotations

from unittest.mock import AsyncMock, MagicMock, patch

from conftest import make_event, make_issue, make_mr, make_pipeline, make_vuln

from project_hub.core.models import EventType, Pipeline, Repo
from project_hub.core.workspace import Workspace


# --- helpers ---


def _make_repo(name: str = "repo-alpha") -> Repo:
    return Repo(
        name=name,
        path=f"/fake/{name}",
        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",
    )


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


def _mr_with_branch(iid: int, branch: str, repo_name: str = "repo-alpha"):
    mr = make_mr(iid, repo_name=repo_name)
    mr.source_branch = branch
    return mr


# =============================================================================
# 1. _ensure_mr_pipeline_coverage
# =============================================================================


async def test_coverage_fetches_missing_branch():
    """MR branch not in pipelines -> client.list_pipelines called with ref."""
    repo = _make_repo()
    client = MagicMock()
    extra_pip = _pipeline(99, "feature-x")
    client.list_pipelines.return_value = [extra_pip]

    mrs = [_mr_with_branch(1, "feature-x")]
    pipelines = [_pipeline(10, "main")]

    ws = Workspace.__new__(Workspace)
    await ws._ensure_mr_pipeline_coverage(client, repo, mrs, pipelines)

    client.list_pipelines.assert_called_once_with(repo.project_path, "feature-x", 5)
    assert any(p.id == 99 for p in pipelines)
    assert any(p.id == 99 and p.repo_name == repo.name for p in pipelines)


async def test_coverage_skips_covered_branch():
    """MR branch already in pipelines -> no additional fetch."""
    repo = _make_repo()
    client = MagicMock()

    mrs = [_mr_with_branch(1, "main")]
    pipelines = [_pipeline(10, "main")]

    ws = Workspace.__new__(Workspace)
    await ws._ensure_mr_pipeline_coverage(client, repo, mrs, pipelines)

    client.list_pipelines.assert_not_called()
    assert len(pipelines) == 1


async def test_coverage_multiple_mrs_mixed():
    """Multiple MRs: only uncovered branches trigger fetches."""
    repo = _make_repo()
    client = MagicMock()

    pip_feat_a = _pipeline(50, "feat-a")
    pip_feat_c = _pipeline(51, "feat-c")
    client.list_pipelines.side_effect = lambda _pp, ref, _n: {
        "feat-a": [pip_feat_a],
        "feat-c": [pip_feat_c],
    }.get(ref, [])

    mrs = [
        _mr_with_branch(1, "main"),     # covered
        _mr_with_branch(2, "feat-a"),    # uncovered
        _mr_with_branch(3, "feat-b"),    # uncovered but client returns empty
        _mr_with_branch(4, "feat-c"),    # uncovered
    ]
    pipelines = [_pipeline(10, "main")]

    ws = Workspace.__new__(Workspace)
    await ws._ensure_mr_pipeline_coverage(client, repo, mrs, pipelines)

    called_refs = {call.args[1] for call in client.list_pipelines.call_args_list}
    assert "feat-a" in called_refs
    assert "feat-b" in called_refs
    assert "feat-c" in called_refs
    assert "main" not in called_refs

    assert any(p.id == 50 for p in pipelines)
    assert any(p.id == 51 for p in pipelines)


async def test_coverage_client_returns_empty():
    """Client returns no pipelines for uncovered branch -> no crash, list unchanged except original."""
    repo = _make_repo()
    client = MagicMock()
    client.list_pipelines.return_value = []

    mrs = [_mr_with_branch(1, "feature-x")]
    pipelines = [_pipeline(10, "main")]

    ws = Workspace.__new__(Workspace)
    await ws._ensure_mr_pipeline_coverage(client, repo, mrs, pipelines)

    client.list_pipelines.assert_called_once()
    assert len(pipelines) == 1
    assert pipelines[0].id == 10


async def test_coverage_client_raises_exception():
    """Client exception -> existing pipelines preserved."""
    repo = _make_repo()
    client = MagicMock()
    client.list_pipelines.side_effect = RuntimeError("API down")

    mrs = [_mr_with_branch(1, "feature-x")]
    original_pipelines = [_pipeline(10, "main")]
    pipelines = list(original_pipelines)

    ws = Workspace.__new__(Workspace)
    try:
        await ws._ensure_mr_pipeline_coverage(client, repo, mrs, pipelines)
    except RuntimeError:
        pass  # asyncio.to_thread propagates the exception

    # Original pipeline still present regardless
    assert pipelines[0].id == 10


async def test_coverage_deduplicates_by_id():
    """Pipeline already in list by id is not added again."""
    repo = _make_repo()
    client = MagicMock()
    # Client returns a pipeline with same id as existing
    dup_pip = _pipeline(10, "feature-x")
    client.list_pipelines.return_value = [dup_pip]

    mrs = [_mr_with_branch(1, "feature-x")]
    pipelines = [_pipeline(10, "main")]

    ws = Workspace.__new__(Workspace)
    await ws._ensure_mr_pipeline_coverage(client, repo, mrs, pipelines)

    assert len(pipelines) == 1  # no duplicate added


# =============================================================================
# 2. Event filtering integration (vulns_min_severity wiring)
# =============================================================================


@patch("project_hub.core.workspace.check_pull_status_sync", return_value="up-to-date")
@patch("project_hub.core.workspace.fetch_all", new_callable=AsyncMock, return_value=[])
@patch("project_hub.core.workspace.resolve_token", return_value="fake-token")
@patch("project_hub.core.workspace.GitLabProvider")
async def test_filter_events_receives_vulns_min_severity(
    mock_client_cls, mock_token, mock_fetch, mock_pull_status, tmp_hub
):
    """Verify workspace passes watch_vulns_min_severity to filter_events."""
    fake_client = MagicMock()
    fake_client.get_username.return_value = "alice"
    # First refresh seeds cache with no vulns
    fake_client.list_merge_requests.return_value = []
    fake_client.list_pipelines.return_value = []
    fake_client.get_vulnerabilities.return_value = []
    fake_client.list_issues.return_value = []
    mock_client_cls.return_value = fake_client

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

    # Second refresh: new vuln appears -> VULN_NEW event
    # Set min_severity to "critical" so "high" vuln events get filtered out
    ws.config.watch_vulns_min_severity = "critical"
    fake_client.get_vulnerabilities.side_effect = lambda _pp: [make_vuln(200)]

    with patch("project_hub.core.workspace.filter_events", wraps=__import__(
        "project_hub.core.events", fromlist=["filter_events"]
    ).filter_events) as spy:
        await ws.refresh_once()

        # filter_events was called at least once (once per repo)
        assert spy.call_count >= 1
        for call in spy.call_args_list:
            assert call.kwargs.get("vulns_min_severity") == "critical"

    await ws.cache.close()


@patch("project_hub.core.workspace.check_pull_status_sync", return_value="up-to-date")
@patch("project_hub.core.workspace.fetch_all", new_callable=AsyncMock, return_value=[])
@patch("project_hub.core.workspace.resolve_token", return_value="fake-token")
@patch("project_hub.core.workspace.GitLabProvider")
async def test_high_vuln_filtered_when_min_severity_critical(
    mock_client_cls, mock_token, mock_fetch, mock_pull_status, tmp_hub
):
    """With min_severity=critical, a high-severity VULN_NEW event should be dropped."""
    fake_client = MagicMock()
    fake_client.get_username.return_value = "alice"
    fake_client.list_merge_requests.return_value = []
    fake_client.list_pipelines.return_value = []
    fake_client.get_vulnerabilities.return_value = []
    fake_client.list_issues.return_value = []
    mock_client_cls.return_value = fake_client

    ws = Workspace(tmp_hub)
    await ws.start()
    # First refresh: seed empty cache
    await ws.refresh_once()

    # Now configure critical-only filtering
    ws.config.watch_vulns_min_severity = "critical"
    # New vuln with severity=high appears
    fake_client.get_vulnerabilities.side_effect = lambda _pp: [make_vuln(300)]

    await ws.refresh_once()

    events = await ws.cache.get_unseen_events()
    vuln_events = [e for e in events if e.type == EventType.VULN_NEW]
    # High severity vuln events should be filtered out when min is critical
    assert len(vuln_events) == 0

    await ws.cache.close()


# =============================================================================
# 3. get_state_dict sync wrappers integration
# =============================================================================


@patch("project_hub.core.workspace.check_pull_status_sync", return_value="up-to-date")
@patch("project_hub.core.workspace.fetch_all", new_callable=AsyncMock, return_value=[])
@patch("project_hub.core.workspace.resolve_token", return_value="fake-token")
@patch("project_hub.core.workspace.GitLabProvider")
async def test_get_state_dict_contains_stored_data(
    mock_client_cls, mock_token, mock_fetch, mock_pull_status, tmp_hub
):
    """Populate cache via async methods, then verify get_state_dict returns it all."""
    fake_client = MagicMock()
    fake_client.get_username.return_value = "alice"
    fake_client.list_merge_requests.side_effect = lambda _pp: [make_mr(1)]
    fake_client.list_pipelines.side_effect = lambda *_a, **_kw: [make_pipeline(10)]
    fake_client.get_vulnerabilities.side_effect = lambda _pp: [make_vuln(200)]
    fake_client.list_issues.side_effect = lambda _pp: [make_issue(5)]
    mock_client_cls.return_value = fake_client

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

    state = ws.get_state_dict()

    # MRs: 3 repos * 1 MR each = 3
    assert len(state["mrs"]) == 3
    assert all(m["iid"] == 1 for m in state["mrs"])
    assert all(m["title"] == "MR 1" for m in state["mrs"])
    assert all(m["author"] == "alice" for m in state["mrs"])

    # Pipelines: 3 repos * 1 pipeline each = 3
    assert len(state["pipelines"]) == 3
    assert all(p["id"] == 10 for p in state["pipelines"])
    assert all(p["status"] == "success" for p in state["pipelines"])

    # Vulns: 3 repos * 1 vuln each = 3
    assert len(state["vulns"]) == 3
    assert all(v["id"] == 200 for v in state["vulns"])
    assert all(v["severity"] == "high" for v in state["vulns"])

    # Issues: 3 repos * 1 issue each = 3
    assert len(state["issues"]) == 3
    assert all(i["iid"] == 5 for i in state["issues"])
    assert all(i["title"] == "Issue 5" for i in state["issues"])

    # Repo entries present
    assert len(state["repos"]) == 3
    repo_names = {r["name"] for r in state["repos"]}
    assert repo_names == {"repo-alpha", "repo-beta", "repo-gamma"}

    # Metadata
    assert state["ready"] is True
    assert state["sync_status"] == "idle"
    assert state["last_refresh"] is not None

    await ws.cache.close()


@patch("project_hub.core.workspace.check_pull_status_sync", return_value="up-to-date")
@patch("project_hub.core.workspace.fetch_all", new_callable=AsyncMock, return_value=[])
@patch("project_hub.core.workspace.resolve_token", return_value="fake-token")
@patch("project_hub.core.workspace.GitLabProvider")
async def test_get_state_dict_events_after_state_change(
    mock_client_cls, mock_token, mock_fetch, mock_pull_status, tmp_hub
):
    """Events generated from state changes appear in get_state_dict."""
    fake_client = MagicMock()
    fake_client.get_username.return_value = "alice"
    fake_client.list_merge_requests.side_effect = lambda _pp: [make_mr(1, notes=0)]
    fake_client.list_pipelines.side_effect = lambda *_a, **_kw: [make_pipeline(10)]
    fake_client.get_vulnerabilities.return_value = []
    fake_client.list_issues.return_value = []
    mock_client_cls.return_value = fake_client

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

    # Trigger comment events on second refresh
    fake_client.list_merge_requests.side_effect = lambda _pp: [make_mr(1, notes=3)]
    await ws.refresh_once()

    state = ws.get_state_dict()
    comment_events = [e for e in state["events"] if e["type"] == "mr_new_comment"]
    assert len(comment_events) == 3  # one per repo
    assert all("3 new comment(s)" in e["summary"] for e in comment_events)

    await ws.cache.close()


# =============================================================================
# 4. Workspace._find_repo
# =============================================================================


@patch("project_hub.core.workspace.resolve_token", return_value="fake-token")
@patch("project_hub.core.workspace.GitLabProvider")
async def test_find_repo_exact_match(mock_client_cls, mock_token, tmp_hub):
    """Find by exact name returns the repo."""
    mock_client_cls.return_value.get_username.return_value = None
    ws = Workspace(tmp_hub)
    await ws.start()

    repo = ws._find_repo("repo-alpha")
    assert repo is not None
    assert repo.name == "repo-alpha"
    assert "repo-alpha" in repo.path

    await ws.cache.close()


@patch("project_hub.core.workspace.resolve_token", return_value="fake-token")
@patch("project_hub.core.workspace.GitLabProvider")
async def test_find_repo_case_sensitive(mock_client_cls, mock_token, tmp_hub):
    """_find_repo is case-sensitive — wrong case returns None."""
    mock_client_cls.return_value.get_username.return_value = None
    ws = Workspace(tmp_hub)
    await ws.start()

    assert ws._find_repo("Repo-Alpha") is None
    assert ws._find_repo("REPO-ALPHA") is None
    assert ws._find_repo("repo-alpha") is not None

    await ws.cache.close()


@patch("project_hub.core.workspace.resolve_token", return_value="fake-token")
@patch("project_hub.core.workspace.GitLabProvider")
async def test_find_repo_partial_no_match(mock_client_cls, mock_token, tmp_hub):
    """_find_repo does exact match — partial name returns None."""
    mock_client_cls.return_value.get_username.return_value = None
    ws = Workspace(tmp_hub)
    await ws.start()

    assert ws._find_repo("repo") is None
    assert ws._find_repo("alpha") is None
    assert ws._find_repo("repo-alph") is None

    await ws.cache.close()


@patch("project_hub.core.workspace.resolve_token", return_value="fake-token")
@patch("project_hub.core.workspace.GitLabProvider")
async def test_find_repo_nonexistent(mock_client_cls, mock_token, tmp_hub):
    """Find non-existent name returns None."""
    mock_client_cls.return_value.get_username.return_value = None
    ws = Workspace(tmp_hub)
    await ws.start()

    assert ws._find_repo("does-not-exist") is None
    assert ws._find_repo("") is None

    await ws.cache.close()
