"""Unit tests for timmy.vassal.agent_health."""

from __future__ import annotations

from datetime import UTC, datetime, timedelta
from unittest.mock import AsyncMock, MagicMock, patch

import pytest

from timmy.vassal.agent_health import AgentHealthReport, AgentStatus

pytestmark = pytest.mark.unit

# ---------------------------------------------------------------------------
# AgentStatus
# ---------------------------------------------------------------------------


def test_agent_status_idle_default():
    s = AgentStatus(agent="claude")
    assert s.is_idle is True
    assert s.is_stuck is False
    assert s.needs_reassignment is False


def test_agent_status_active():
    s = AgentStatus(agent="kimi", active_issue_numbers=[10, 11])
    s.is_idle = len(s.active_issue_numbers) == 0
    assert s.is_idle is False


def test_agent_status_stuck():
    s = AgentStatus(
        agent="claude",
        active_issue_numbers=[7],
        stuck_issue_numbers=[7],
        is_idle=False,
    )
    assert s.is_stuck is True
    assert s.needs_reassignment is True


def test_agent_status_checked_at_is_iso_string():
    s = AgentStatus(agent="claude")
    # Should be parseable as an ISO datetime
    dt = datetime.fromisoformat(s.checked_at)
    assert dt.tzinfo is not None


def test_agent_status_multiple_stuck_issues():
    s = AgentStatus(agent="kimi", stuck_issue_numbers=[1, 2, 3])
    assert s.is_stuck is True
    assert s.needs_reassignment is True


def test_agent_status_active_but_not_stuck():
    s = AgentStatus(agent="claude", active_issue_numbers=[5], is_idle=False)
    assert s.is_stuck is False
    assert s.needs_reassignment is False


# ---------------------------------------------------------------------------
# AgentHealthReport
# ---------------------------------------------------------------------------


def test_report_any_stuck():
    claude = AgentStatus(agent="claude", stuck_issue_numbers=[3])
    kimi = AgentStatus(agent="kimi")
    report = AgentHealthReport(agents=[claude, kimi])
    assert report.any_stuck is True


def test_report_not_any_stuck():
    report = AgentHealthReport(agents=[AgentStatus(agent="claude"), AgentStatus(agent="kimi")])
    assert report.any_stuck is False


def test_report_all_idle():
    report = AgentHealthReport(agents=[AgentStatus(agent="claude"), AgentStatus(agent="kimi")])
    assert report.all_idle is True


def test_report_not_all_idle():
    claude = AgentStatus(agent="claude", active_issue_numbers=[1], is_idle=False)
    report = AgentHealthReport(agents=[claude, AgentStatus(agent="kimi")])
    assert report.all_idle is False


def test_report_for_agent_found():
    kimi = AgentStatus(agent="kimi", active_issue_numbers=[42])
    report = AgentHealthReport(agents=[AgentStatus(agent="claude"), kimi])
    found = report.for_agent("kimi")
    assert found is kimi


def test_report_for_agent_not_found():
    report = AgentHealthReport(agents=[AgentStatus(agent="claude")])
    assert report.for_agent("timmy") is None


def test_report_generated_at_is_iso_string():
    report = AgentHealthReport()
    dt = datetime.fromisoformat(report.generated_at)
    assert dt.tzinfo is not None


def test_report_empty_agents():
    report = AgentHealthReport(agents=[])
    assert report.any_stuck is False
    assert report.all_idle is True


# ---------------------------------------------------------------------------
# _issue_created_time
# ---------------------------------------------------------------------------


@pytest.mark.asyncio
async def test_issue_created_time_valid():
    from timmy.vassal.agent_health import _issue_created_time

    issue = {"created_at": "2024-01-15T10:30:00Z"}
    result = await _issue_created_time(issue)
    assert result is not None
    assert result.year == 2024
    assert result.month == 1
    assert result.day == 15


@pytest.mark.asyncio
async def test_issue_created_time_missing_key():
    from timmy.vassal.agent_health import _issue_created_time

    result = await _issue_created_time({})
    assert result is None


@pytest.mark.asyncio
async def test_issue_created_time_invalid_format():
    from timmy.vassal.agent_health import _issue_created_time

    result = await _issue_created_time({"created_at": "not-a-date"})
    assert result is None


@pytest.mark.asyncio
async def test_issue_created_time_with_timezone():
    from timmy.vassal.agent_health import _issue_created_time

    issue = {"created_at": "2024-06-01T12:00:00+00:00"}
    result = await _issue_created_time(issue)
    assert result is not None
    assert result.tzinfo is not None


# ---------------------------------------------------------------------------
# _fetch_labeled_issues — mocked HTTP client
# ---------------------------------------------------------------------------


@pytest.mark.asyncio
async def test_fetch_labeled_issues_success():
    from timmy.vassal.agent_health import _fetch_labeled_issues

    mock_resp = MagicMock()
    mock_resp.status_code = 200
    mock_resp.json.return_value = [
        {"number": 1, "title": "Fix bug"},
        {"number": 2, "title": "Add feature", "pull_request": {"url": "..."}},
    ]

    mock_client = AsyncMock()
    mock_client.get = AsyncMock(return_value=mock_resp)

    result = await _fetch_labeled_issues(
        mock_client, "http://gitea/api/v1", {}, "owner/repo", "claude-ready"
    )

    # Only non-PR issues returned
    assert len(result) == 1
    assert result[0]["number"] == 1


@pytest.mark.asyncio
async def test_fetch_labeled_issues_http_error():
    from timmy.vassal.agent_health import _fetch_labeled_issues

    mock_resp = MagicMock()
    mock_resp.status_code = 401
    mock_resp.json.return_value = []

    mock_client = AsyncMock()
    mock_client.get = AsyncMock(return_value=mock_resp)

    result = await _fetch_labeled_issues(
        mock_client, "http://gitea/api/v1", {}, "owner/repo", "claude-ready"
    )
    assert result == []


@pytest.mark.asyncio
async def test_fetch_labeled_issues_exception():
    from timmy.vassal.agent_health import _fetch_labeled_issues

    mock_client = AsyncMock()
    mock_client.get = AsyncMock(side_effect=ConnectionError("network down"))

    result = await _fetch_labeled_issues(
        mock_client, "http://gitea/api/v1", {}, "owner/repo", "claude-ready"
    )
    assert result == []


@pytest.mark.asyncio
async def test_fetch_labeled_issues_filters_pull_requests():
    from timmy.vassal.agent_health import _fetch_labeled_issues

    mock_resp = MagicMock()
    mock_resp.status_code = 200
    mock_resp.json.return_value = [
        {"number": 10, "title": "Issue"},
        {"number": 11, "title": "PR", "pull_request": {"url": "http://gitea/pulls/11"}},
        {"number": 12, "title": "Another Issue"},
    ]

    mock_client = AsyncMock()
    mock_client.get = AsyncMock(return_value=mock_resp)

    result = await _fetch_labeled_issues(
        mock_client, "http://gitea/api/v1", {}, "owner/repo", "claude-ready"
    )
    # Issues with truthy pull_request field are excluded
    assert len(result) == 2
    assert all(i["number"] in (10, 12) for i in result)


# ---------------------------------------------------------------------------
# _last_comment_time — mocked HTTP client
# ---------------------------------------------------------------------------


@pytest.mark.asyncio
async def test_last_comment_time_with_comments():
    from timmy.vassal.agent_health import _last_comment_time

    mock_resp = MagicMock()
    mock_resp.status_code = 200
    mock_resp.json.return_value = [
        {"updated_at": "2024-03-10T14:00:00Z", "created_at": "2024-03-10T13:00:00Z"}
    ]

    mock_client = AsyncMock()
    mock_client.get = AsyncMock(return_value=mock_resp)

    result = await _last_comment_time(mock_client, "http://gitea/api/v1", {}, "owner/repo", 42)
    assert result is not None
    assert result.year == 2024
    assert result.month == 3


@pytest.mark.asyncio
async def test_last_comment_time_uses_created_at_fallback():
    from timmy.vassal.agent_health import _last_comment_time

    mock_resp = MagicMock()
    mock_resp.status_code = 200
    mock_resp.json.return_value = [
        {"created_at": "2024-03-10T13:00:00Z"}  # no updated_at
    ]

    mock_client = AsyncMock()
    mock_client.get = AsyncMock(return_value=mock_resp)

    result = await _last_comment_time(mock_client, "http://gitea/api/v1", {}, "owner/repo", 42)
    assert result is not None


@pytest.mark.asyncio
async def test_last_comment_time_no_comments():
    from timmy.vassal.agent_health import _last_comment_time

    mock_resp = MagicMock()
    mock_resp.status_code = 200
    mock_resp.json.return_value = []

    mock_client = AsyncMock()
    mock_client.get = AsyncMock(return_value=mock_resp)

    result = await _last_comment_time(mock_client, "http://gitea/api/v1", {}, "owner/repo", 99)
    assert result is None


@pytest.mark.asyncio
async def test_last_comment_time_http_error():
    from timmy.vassal.agent_health import _last_comment_time

    mock_resp = MagicMock()
    mock_resp.status_code = 404

    mock_client = AsyncMock()
    mock_client.get = AsyncMock(return_value=mock_resp)

    result = await _last_comment_time(mock_client, "http://gitea/api/v1", {}, "owner/repo", 99)
    assert result is None


@pytest.mark.asyncio
async def test_last_comment_time_exception():
    from timmy.vassal.agent_health import _last_comment_time

    mock_client = AsyncMock()
    mock_client.get = AsyncMock(side_effect=TimeoutError("timed out"))

    result = await _last_comment_time(mock_client, "http://gitea/api/v1", {}, "owner/repo", 7)
    assert result is None


# ---------------------------------------------------------------------------
# check_agent_health — no Gitea in unit tests
# ---------------------------------------------------------------------------


@pytest.mark.asyncio
async def test_check_agent_health_unknown_agent():
    """Unknown agent name returns idle status without error."""
    from timmy.vassal.agent_health import check_agent_health

    status = await check_agent_health("unknown-bot")
    assert status.agent == "unknown-bot"
    assert status.is_idle is True


@pytest.mark.asyncio
async def test_check_agent_health_no_token():
    """Returns idle status gracefully when Gitea token is absent."""
    from timmy.vassal.agent_health import check_agent_health

    mock_settings = MagicMock()
    mock_settings.gitea_enabled = True
    mock_settings.gitea_token = ""  # explicitly no token → early return

    with patch("config.settings", mock_settings):
        status = await check_agent_health("claude")
    # Should not raise; returns idle (no active issues discovered)
    assert isinstance(status, AgentStatus)
    assert status.agent == "claude"


@pytest.mark.asyncio
async def test_check_agent_health_detects_stuck_issue(monkeypatch):
    """Issues with last activity before the cutoff are flagged as stuck."""
    import timmy.vassal.agent_health as ah

    old_time = (datetime.now(UTC) - timedelta(minutes=200)).isoformat()

    async def _fake_fetch(client, base_url, headers, repo, label):
        return [{"number": 55, "created_at": old_time}]

    async def _fake_last_comment(client, base_url, headers, repo, issue_number):
        return datetime.now(UTC) - timedelta(minutes=200)

    monkeypatch.setattr(ah, "_fetch_labeled_issues", _fake_fetch)
    monkeypatch.setattr(ah, "_last_comment_time", _fake_last_comment)

    mock_settings = MagicMock()
    mock_settings.gitea_enabled = True
    mock_settings.gitea_token = "fake-token"
    mock_settings.gitea_url = "http://gitea"
    mock_settings.gitea_repo = "owner/repo"

    with patch("config.settings", mock_settings):
        status = await ah.check_agent_health("claude", stuck_threshold_minutes=120)

    assert 55 in status.active_issue_numbers
    assert 55 in status.stuck_issue_numbers
    assert status.is_stuck is True


@pytest.mark.asyncio
async def test_check_agent_health_active_not_stuck(monkeypatch):
    """Recent activity means issue is active but not stuck."""
    import timmy.vassal.agent_health as ah

    recent_time = (datetime.now(UTC) - timedelta(minutes=5)).isoformat()

    async def _fake_fetch(client, base_url, headers, repo, label):
        return [{"number": 77, "created_at": recent_time}]

    async def _fake_last_comment(client, base_url, headers, repo, issue_number):
        return datetime.now(UTC) - timedelta(minutes=5)

    monkeypatch.setattr(ah, "_fetch_labeled_issues", _fake_fetch)
    monkeypatch.setattr(ah, "_last_comment_time", _fake_last_comment)

    mock_settings = MagicMock()
    mock_settings.gitea_enabled = True
    mock_settings.gitea_token = "fake-token"
    mock_settings.gitea_url = "http://gitea"
    mock_settings.gitea_repo = "owner/repo"

    with patch("config.settings", mock_settings):
        status = await ah.check_agent_health("claude", stuck_threshold_minutes=120)

    assert 77 in status.active_issue_numbers
    assert 77 not in status.stuck_issue_numbers
    assert status.is_idle is False


@pytest.mark.asyncio
async def test_check_agent_health_uses_issue_created_when_no_comments(monkeypatch):
    """Falls back to issue created_at when no comment time is available."""
    import timmy.vassal.agent_health as ah

    old_time = (datetime.now(UTC) - timedelta(minutes=300)).isoformat()

    async def _fake_fetch(client, base_url, headers, repo, label):
        return [{"number": 99, "created_at": old_time}]

    async def _fake_last_comment(client, base_url, headers, repo, issue_number):
        return None  # No comments

    monkeypatch.setattr(ah, "_fetch_labeled_issues", _fake_fetch)
    monkeypatch.setattr(ah, "_last_comment_time", _fake_last_comment)

    mock_settings = MagicMock()
    mock_settings.gitea_enabled = True
    mock_settings.gitea_token = "fake-token"
    mock_settings.gitea_url = "http://gitea"
    mock_settings.gitea_repo = "owner/repo"

    with patch("config.settings", mock_settings):
        status = await ah.check_agent_health("kimi", stuck_threshold_minutes=120)

    assert 99 in status.stuck_issue_numbers


@pytest.mark.asyncio
async def test_check_agent_health_gitea_disabled(monkeypatch):
    """When gitea_enabled=False, returns idle status without querying."""
    import timmy.vassal.agent_health as ah

    mock_settings = MagicMock()
    mock_settings.gitea_enabled = False
    mock_settings.gitea_token = "fake-token"

    with patch("config.settings", mock_settings):
        status = await ah.check_agent_health("claude")

    assert status.is_idle is True
    assert status.active_issue_numbers == []


@pytest.mark.asyncio
async def test_check_agent_health_fetch_exception(monkeypatch):
    """HTTP exception during check is handled gracefully."""
    import timmy.vassal.agent_health as ah

    async def _bad_fetch(client, base_url, headers, repo, label):
        raise RuntimeError("connection refused")

    monkeypatch.setattr(ah, "_fetch_labeled_issues", _bad_fetch)

    mock_settings = MagicMock()
    mock_settings.gitea_enabled = True
    mock_settings.gitea_token = "fake-token"
    mock_settings.gitea_url = "http://gitea"
    mock_settings.gitea_repo = "owner/repo"

    with patch("config.settings", mock_settings):
        status = await ah.check_agent_health("claude")

    assert isinstance(status, AgentStatus)
    assert status.is_idle is True


# ---------------------------------------------------------------------------
# get_full_health_report
# ---------------------------------------------------------------------------


@pytest.mark.asyncio
async def test_get_full_health_report_returns_both_agents():
    from timmy.vassal.agent_health import get_full_health_report

    mock_settings = MagicMock()
    mock_settings.gitea_enabled = False  # disabled → no network calls
    mock_settings.gitea_token = ""

    with patch("config.settings", mock_settings):
        report = await get_full_health_report()
    agent_names = {a.agent for a in report.agents}
    assert "claude" in agent_names
    assert "kimi" in agent_names


@pytest.mark.asyncio
async def test_get_full_health_report_structure():
    from timmy.vassal.agent_health import get_full_health_report

    mock_settings = MagicMock()
    mock_settings.gitea_enabled = False  # disabled → no network calls
    mock_settings.gitea_token = ""

    with patch("config.settings", mock_settings):
        report = await get_full_health_report()
    assert isinstance(report, AgentHealthReport)
    assert len(report.agents) == 2


# ---------------------------------------------------------------------------
# nudge_stuck_agent
# ---------------------------------------------------------------------------


@pytest.mark.asyncio
async def test_nudge_stuck_agent_no_token():
    """Returns False gracefully when Gitea is not configured."""
    from timmy.vassal.agent_health import nudge_stuck_agent

    mock_settings = MagicMock()
    mock_settings.gitea_enabled = False
    mock_settings.gitea_token = ""

    with patch("config.settings", mock_settings):
        result = await nudge_stuck_agent("claude", 123)
        assert result is False


@pytest.mark.asyncio
async def test_nudge_stuck_agent_success(monkeypatch):
    """Returns True when comment is posted successfully."""
    import timmy.vassal.agent_health as ah

    mock_resp = MagicMock()
    mock_resp.status_code = 201

    mock_client_instance = AsyncMock()
    mock_client_instance.post = AsyncMock(return_value=mock_resp)
    mock_client_instance.__aenter__ = AsyncMock(return_value=mock_client_instance)
    mock_client_instance.__aexit__ = AsyncMock(return_value=False)

    mock_settings = MagicMock()
    mock_settings.gitea_enabled = True
    mock_settings.gitea_token = "fake-token"
    mock_settings.gitea_url = "http://gitea"
    mock_settings.gitea_repo = "owner/repo"

    with (
        patch("config.settings", mock_settings),
        patch("httpx.AsyncClient", return_value=mock_client_instance),
    ):
        result = await ah.nudge_stuck_agent("claude", 55)

    assert result is True


@pytest.mark.asyncio
async def test_nudge_stuck_agent_http_failure(monkeypatch):
    """Returns False when API returns non-2xx status."""
    import timmy.vassal.agent_health as ah

    mock_resp = MagicMock()
    mock_resp.status_code = 500

    mock_client_instance = AsyncMock()
    mock_client_instance.post = AsyncMock(return_value=mock_resp)
    mock_client_instance.__aenter__ = AsyncMock(return_value=mock_client_instance)
    mock_client_instance.__aexit__ = AsyncMock(return_value=False)

    mock_settings = MagicMock()
    mock_settings.gitea_enabled = True
    mock_settings.gitea_token = "fake-token"
    mock_settings.gitea_url = "http://gitea"
    mock_settings.gitea_repo = "owner/repo"

    with (
        patch("config.settings", mock_settings),
        patch("httpx.AsyncClient", return_value=mock_client_instance),
    ):
        result = await ah.nudge_stuck_agent("kimi", 77)

    assert result is False


@pytest.mark.asyncio
async def test_nudge_stuck_agent_gitea_disabled(monkeypatch):
    """Returns False when gitea_enabled=False."""
    import timmy.vassal.agent_health as ah

    mock_settings = MagicMock()
    mock_settings.gitea_enabled = False
    mock_settings.gitea_token = "fake-token"

    with patch("config.settings", mock_settings):
        result = await ah.nudge_stuck_agent("claude", 42)

    assert result is False


@pytest.mark.asyncio
async def test_nudge_stuck_agent_exception(monkeypatch):
    """Returns False on network exception."""
    import timmy.vassal.agent_health as ah

    mock_client_instance = AsyncMock()
    mock_client_instance.post = AsyncMock(side_effect=ConnectionError("refused"))
    mock_client_instance.__aenter__ = AsyncMock(return_value=mock_client_instance)
    mock_client_instance.__aexit__ = AsyncMock(return_value=False)

    mock_settings = MagicMock()
    mock_settings.gitea_enabled = True
    mock_settings.gitea_token = "fake-token"
    mock_settings.gitea_url = "http://gitea"
    mock_settings.gitea_repo = "owner/repo"

    with (
        patch("config.settings", mock_settings),
        patch("httpx.AsyncClient", return_value=mock_client_instance),
    ):
        result = await ah.nudge_stuck_agent("claude", 10)

    assert result is False
