"""Tests for the Gitea webhook adapter."""

from unittest.mock import AsyncMock, patch

import pytest

from timmy.adapters.gitea_adapter import (
    BOT_USERNAMES,
    _extract_actor,
    _is_bot,
    _is_pr_merge,
    _normalize_issue_comment,
    _normalize_issue_opened,
    _normalize_pull_request,
    _normalize_push,
    handle_webhook,
)

# ── Fixtures: sample payloads ────────────────────────────────────────────────


def _sender(login: str) -> dict:
    return {"sender": {"login": login}}


def _push_payload(actor: str = "rockachopa", ref: str = "refs/heads/main") -> dict:
    return {
        **_sender(actor),
        "ref": ref,
        "repository": {"full_name": "rockachopa/Timmy-time-dashboard"},
        "commits": [
            {"message": "fix: something\n\nDetails here"},
            {"message": "chore: cleanup"},
        ],
    }


def _issue_payload(actor: str = "rockachopa", action: str = "opened") -> dict:
    return {
        **_sender(actor),
        "action": action,
        "repository": {"full_name": "rockachopa/Timmy-time-dashboard"},
        "issue": {"number": 42, "title": "Bug in dashboard"},
    }


def _issue_comment_payload(actor: str = "rockachopa") -> dict:
    return {
        **_sender(actor),
        "action": "created",
        "repository": {"full_name": "rockachopa/Timmy-time-dashboard"},
        "issue": {"number": 42, "title": "Bug in dashboard"},
        "comment": {"body": "I think this is related to the config change"},
    }


def _pr_payload(
    actor: str = "rockachopa",
    action: str = "opened",
    merged: bool = False,
) -> dict:
    return {
        **_sender(actor),
        "action": action,
        "repository": {"full_name": "rockachopa/Timmy-time-dashboard"},
        "pull_request": {
            "number": 99,
            "title": "feat: add new feature",
            "merged": merged,
        },
    }


# ── Unit tests: helpers ──────────────────────────────────────────────────────


class TestExtractActor:
    def test_normal_sender(self):
        assert _extract_actor({"sender": {"login": "rockachopa"}}) == "rockachopa"

    def test_missing_sender(self):
        assert _extract_actor({}) == "unknown"


class TestIsBot:
    @pytest.mark.parametrize("name", list(BOT_USERNAMES))
    def test_known_bots(self, name):
        assert _is_bot(name) is True

    def test_owner_not_bot(self):
        assert _is_bot("rockachopa") is False

    def test_case_insensitive(self):
        assert _is_bot("Kimi") is True


class TestIsPrMerge:
    def test_merged_pr(self):
        payload = _pr_payload(action="closed", merged=True)
        assert _is_pr_merge("pull_request", payload) is True

    def test_closed_not_merged(self):
        payload = _pr_payload(action="closed", merged=False)
        assert _is_pr_merge("pull_request", payload) is False

    def test_opened_pr(self):
        payload = _pr_payload(action="opened")
        assert _is_pr_merge("pull_request", payload) is False

    def test_non_pr_event(self):
        assert _is_pr_merge("push", {}) is False


# ── Unit tests: normalizers ──────────────────────────────────────────────────


class TestNormalizePush:
    def test_basic(self):
        data = _normalize_push(_push_payload(), "rockachopa")
        assert data["actor"] == "rockachopa"
        assert data["ref"] == "refs/heads/main"
        assert data["num_commits"] == 2
        assert data["head_message"] == "fix: something"
        assert data["repo"] == "rockachopa/Timmy-time-dashboard"

    def test_empty_commits(self):
        payload = {**_push_payload(), "commits": []}
        data = _normalize_push(payload, "rockachopa")
        assert data["num_commits"] == 0
        assert data["head_message"] == ""


class TestNormalizeIssueOpened:
    def test_basic(self):
        data = _normalize_issue_opened(_issue_payload(), "rockachopa")
        assert data["issue_number"] == 42
        assert data["title"] == "Bug in dashboard"
        assert data["action"] == "opened"


class TestNormalizeIssueComment:
    def test_basic(self):
        data = _normalize_issue_comment(_issue_comment_payload(), "rockachopa")
        assert data["issue_number"] == 42
        assert data["comment_body"].startswith("I think this is related")

    def test_long_comment_truncated(self):
        payload = _issue_comment_payload()
        payload["comment"]["body"] = "x" * 500
        data = _normalize_issue_comment(payload, "rockachopa")
        assert len(data["comment_body"]) == 200


class TestNormalizePullRequest:
    def test_opened(self):
        data = _normalize_pull_request(_pr_payload(), "rockachopa")
        assert data["pr_number"] == 99
        assert data["merged"] is False
        assert data["action"] == "opened"

    def test_merged(self):
        payload = _pr_payload(action="closed", merged=True)
        data = _normalize_pull_request(payload, "rockachopa")
        assert data["merged"] is True


# ── Integration tests: handle_webhook ────────────────────────────────────────


@pytest.mark.asyncio
class TestHandleWebhook:
    @patch("timmy.adapters.gitea_adapter.emit", new_callable=AsyncMock)
    async def test_push_emitted(self, mock_emit):
        result = await handle_webhook("push", _push_payload())
        assert result is True
        mock_emit.assert_called_once()
        args = mock_emit.call_args
        assert args[0][0] == "gitea.push"
        assert args[1]["data"]["num_commits"] == 2

    @patch("timmy.adapters.gitea_adapter.emit", new_callable=AsyncMock)
    async def test_issue_opened_emitted(self, mock_emit):
        result = await handle_webhook("issues", _issue_payload())
        assert result is True
        mock_emit.assert_called_once()
        assert mock_emit.call_args[0][0] == "gitea.issue.opened"

    @patch("timmy.adapters.gitea_adapter.emit", new_callable=AsyncMock)
    async def test_issue_comment_emitted(self, mock_emit):
        result = await handle_webhook("issue_comment", _issue_comment_payload())
        assert result is True
        assert mock_emit.call_args[0][0] == "gitea.issue.comment"

    @patch("timmy.adapters.gitea_adapter.emit", new_callable=AsyncMock)
    async def test_pull_request_emitted(self, mock_emit):
        result = await handle_webhook("pull_request", _pr_payload())
        assert result is True
        assert mock_emit.call_args[0][0] == "gitea.pull_request"

    @patch("timmy.adapters.gitea_adapter.emit", new_callable=AsyncMock)
    async def test_unsupported_event_filtered(self, mock_emit):
        result = await handle_webhook("fork", {"sender": {"login": "someone"}})
        assert result is False
        mock_emit.assert_not_called()

    @patch("timmy.adapters.gitea_adapter.emit", new_callable=AsyncMock)
    async def test_bot_push_filtered(self, mock_emit):
        result = await handle_webhook("push", _push_payload(actor="kimi"))
        assert result is False
        mock_emit.assert_not_called()

    @patch("timmy.adapters.gitea_adapter.emit", new_callable=AsyncMock)
    async def test_bot_issue_filtered(self, mock_emit):
        result = await handle_webhook("issues", _issue_payload(actor="hermes"))
        assert result is False
        mock_emit.assert_not_called()

    @patch("timmy.adapters.gitea_adapter.emit", new_callable=AsyncMock)
    async def test_bot_pr_merge_not_filtered(self, mock_emit):
        """Bot PR merges should still be emitted."""
        payload = _pr_payload(actor="kimi", action="closed", merged=True)
        result = await handle_webhook("pull_request", payload)
        assert result is True
        mock_emit.assert_called_once()
        data = mock_emit.call_args[1]["data"]
        assert data["merged"] is True

    @patch("timmy.adapters.gitea_adapter.emit", new_callable=AsyncMock)
    async def test_bot_pr_close_without_merge_filtered(self, mock_emit):
        """Bot PR close (not merge) should be filtered."""
        payload = _pr_payload(actor="manus", action="closed", merged=False)
        result = await handle_webhook("pull_request", payload)
        assert result is False
        mock_emit.assert_not_called()

    @patch("timmy.adapters.gitea_adapter.emit", new_callable=AsyncMock)
    async def test_owner_activity_always_emitted(self, mock_emit):
        result = await handle_webhook("push", _push_payload(actor="rockachopa"))
        assert result is True
        mock_emit.assert_called_once()
