from __future__ import annotations

from unittest.mock import AsyncMock, MagicMock, patch

from conftest import make_mr, make_pipeline, make_vuln

from project_hub.core.models import EventType
from project_hub.core.workspace import Workspace
from project_hub.forge.provider import AuthenticationError


# --- tests ---


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

    assert len(ws.repos) == 3
    names = {r.name for r in ws.repos}
    assert names == {"repo-alpha", "repo-beta", "repo-gamma"}

    # GitLab client created for the forge host from the repos' remotes
    assert "gitlab.com" in ws._forge_clients
    assert len(ws._forge_clients) == 1
    mock_client_cls.assert_called_once_with("gitlab.com", "fake-token")

    await ws.cache.close()


@patch("project_hub.core.workspace.resolve_token", return_value=None)
async def test_workspace_no_token_downgrades_forge(mock_token, tmp_hub):
    ws = Workspace(tmp_hub)
    await ws.start()

    # All repos point to gitlab.com, but no token -> all downgraded
    assert all(not r.has_forge for r in ws.repos)
    assert len(ws._forge_clients) == 0

    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_refresh_once_stores_data(mock_client_cls, mock_token, mock_fetch, mock_pull_status, tmp_hub):
    # side_effect returns fresh objects each call (avoids shared-object repo_name mutation)
    fake_client = MagicMock()
    fake_client.get_username.return_value = None
    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)]
    mock_client_cls.return_value = fake_client

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

    # Verify data is in cache for each repo
    for repo in ws.repos:
        mrs = await ws.cache.get_mrs(repo.name)
        assert len(mrs) == 1
        mr = mrs[0]
        assert mr.iid == 1
        assert mr.title == "MR 1"
        assert mr.author == "alice"
        assert mr.state == "opened"
        assert mr.source_branch == "feature/1"
        assert mr.target_branch == "main"
        assert mr.sha == "abc123"
        assert mr.repo_name == repo.name
        pipelines = await ws.cache.get_pipelines(repo.name)
        assert len(pipelines) == 1
        assert pipelines[0].id == 10
        assert pipelines[0].status == "success"
        vulns = await ws.cache.get_vulns(repo.name)
        assert len(vulns) == 1
        assert vulns[0].id == 200
        assert vulns[0].severity == "high"

    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_refresh_once_generates_events(mock_client_cls, mock_token, mock_fetch, mock_pull_status, tmp_hub):
    fake_client = MagicMock()
    fake_client.get_username.return_value = "alice"
    # First refresh: seed MR with 0 notes
    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.side_effect = lambda _pp: []
    mock_client_cls.return_value = fake_client

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

    # Second refresh: MR now has 2 notes -> should generate MR_NEW_COMMENT events
    fake_client.list_merge_requests.side_effect = lambda _pp: [make_mr(1, notes=2)]
    await ws.refresh_once()

    events = await ws.cache.get_unseen_events()
    comment_events = [e for e in events if e.type == EventType.MR_NEW_COMMENT]
    # One MR_NEW_COMMENT per repo (3 repos, each with 1 MR whose notes went 0->2)
    assert len(comment_events) == 3
    assert all("2 new comment(s)" in e.summary for e in comment_events)

    await ws.cache.close()


@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_refresh_once_noop_when_running(mock_client_cls, mock_token, mock_fetch, tmp_hub):
    fake_client = MagicMock()
    fake_client.get_username.return_value = None
    fake_client.list_merge_requests.return_value = []
    fake_client.list_pipelines.return_value = []
    fake_client.get_vulnerabilities.return_value = []
    mock_client_cls.return_value = fake_client

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

    # Simulate already running
    ws._refresh_running = True
    await ws.refresh_once()

    # fetch_all should not have been called since refresh was skipped
    mock_fetch.assert_not_called()

    await ws.cache.close()


@patch("project_hub.core.workspace.check_pull_status_sync", return_value=None)
@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_refresh_auth_error_generates_event(mock_client_cls, mock_token, mock_fetch, mock_pull_status, tmp_hub):
    fake_client = MagicMock()
    fake_client.get_username.return_value = None
    fake_client.list_merge_requests.side_effect = AuthenticationError("expired")
    mock_client_cls.return_value = fake_client

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

    events = await ws.cache.get_unseen_events()
    auth_events = [e for e in events if e.type == EventType.AUTH_EXPIRED]
    # One AUTH_EXPIRED event per repo (3 repos, each hits AuthenticationError independently)
    assert len(auth_events) == 3
    assert all("Authentication failed" in e.summary for e in auth_events)
    assert all("gitlab.com" in e.summary for e in auth_events)

    await ws.cache.close()


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

    assert ws.cache._db is not None
    await ws.stop()
    assert ws.cache._db is None


# --- sync_status tests ---


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

    assert ws.sync_status == "idle"

    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_sync_status_in_progress_during_refresh(mock_client_cls, mock_token, mock_fetch, mock_pull_status, tmp_hub):
    fake_client = MagicMock()
    fake_client.get_username.return_value = None
    fake_client.list_merge_requests.return_value = []
    fake_client.list_pipelines.return_value = []
    fake_client.get_vulnerabilities.return_value = []
    mock_client_cls.return_value = fake_client

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

    captured_statuses = []

    original_inner = ws._do_refresh_inner

    async def spy_inner(logger, notify_callback=None):
        captured_statuses.append(ws.sync_status)
        await original_inner(logger, notify_callback)

    ws._do_refresh_inner = spy_inner
    await ws.refresh_once()

    assert "in_progress" in captured_statuses
    assert ws.sync_status == "idle"

    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_sync_status_idle_after_refresh(mock_client_cls, mock_token, mock_fetch, mock_pull_status, tmp_hub):
    fake_client = MagicMock()
    fake_client.get_username.return_value = None
    fake_client.list_merge_requests.return_value = []
    fake_client.list_pipelines.return_value = []
    fake_client.get_vulnerabilities.return_value = []
    mock_client_cls.return_value = fake_client

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

    assert ws.sync_status == "idle"

    await ws.cache.close()


# --- pull_status in refresh tests ---


@patch("project_hub.core.workspace.check_pull_status_sync", return_value="clean")
@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_refresh_stores_pull_status(mock_client_cls, mock_token, mock_fetch, mock_pull_status, tmp_hub):
    fake_client = MagicMock()
    fake_client.get_username.return_value = None
    fake_client.list_merge_requests.return_value = []
    fake_client.list_pipelines.return_value = []
    fake_client.get_vulnerabilities.return_value = []
    mock_client_cls.return_value = fake_client

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

    # All repos should have pull_status "clean" in cache
    for repo in ws.repos:
        status = await ws.cache.get_pull_status(repo.name)
        assert status == "clean"

    await ws.cache.close()


# --- get_state_dict tests ---


@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_has_new_fields(mock_client_cls, mock_token, mock_fetch, mock_pull_status, tmp_hub):
    fake_client = MagicMock()
    fake_client.get_username.return_value = None
    fake_client.list_merge_requests.return_value = []
    fake_client.list_pipelines.return_value = []
    fake_client.get_vulnerabilities.return_value = []
    mock_client_cls.return_value = fake_client

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

    state = ws.get_state_dict()

    # New top-level fields
    assert "sync_status" in state
    assert state["sync_status"] == "idle"
    assert "usernames" in state
    assert isinstance(state["usernames"], dict)
    assert "pinned" in state
    assert "repo_order" in state
    assert "excluded" in state
    assert "forges" in state

    # Repos have pull_status, not dirty
    for repo in state["repos"]:
        assert "pull_status" in repo
        assert "dirty" not in repo

    await ws.cache.close()


# --- _find_repo tests ---


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

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

    await ws.cache.close()


# --- exclude/include tests ---


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

    assert len(ws.repos) == 3
    await ws.exclude_repo("repo-alpha")

    assert "repo-alpha" in ws.config.exclude
    assert all(r.name != "repo-alpha" for r in ws.repos)

    await ws.cache.close()


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

    await ws.exclude_repo("repo-alpha")
    assert all(r.name != "repo-alpha" for r in ws.repos)

    await ws.include_repo("repo-alpha")
    assert "repo-alpha" not in ws.config.exclude
    assert any(r.name == "repo-alpha" for r in ws.repos)

    await ws.cache.close()
