"""Unit tests for timmy.kimi_delegation — Kimi research delegation pipeline."""

from unittest.mock import AsyncMock, MagicMock, patch

import pytest

# ---------------------------------------------------------------------------
# exceeds_local_capacity
# ---------------------------------------------------------------------------


class TestExceedsLocalCapacity:
    def test_heavy_keyword_triggers_delegation(self):
        from timmy.kimi_delegation import exceeds_local_capacity

        assert exceeds_local_capacity("Do a comprehensive review of the codebase") is True

    def test_all_heavy_keywords_detected(self):
        from timmy.kimi_delegation import _HEAVY_RESEARCH_KEYWORDS, exceeds_local_capacity

        for kw in _HEAVY_RESEARCH_KEYWORDS:
            assert exceeds_local_capacity(f"Please {kw} the topic") is True, f"Missed keyword: {kw}"

    def test_long_task_triggers_delegation(self):
        from timmy.kimi_delegation import _HEAVY_WORD_THRESHOLD, exceeds_local_capacity

        long_task = " ".join(["word"] * (_HEAVY_WORD_THRESHOLD + 1))
        assert exceeds_local_capacity(long_task) is True

    def test_short_simple_task_returns_false(self):
        from timmy.kimi_delegation import exceeds_local_capacity

        assert exceeds_local_capacity("Fix the typo in README") is False

    def test_exactly_at_word_threshold_triggers(self):
        from timmy.kimi_delegation import _HEAVY_WORD_THRESHOLD, exceeds_local_capacity

        task = " ".join(["word"] * _HEAVY_WORD_THRESHOLD)
        assert exceeds_local_capacity(task) is True

    def test_keyword_case_insensitive(self):
        from timmy.kimi_delegation import exceeds_local_capacity

        assert exceeds_local_capacity("Run a COMPREHENSIVE analysis") is True

    def test_empty_string_returns_false(self):
        from timmy.kimi_delegation import exceeds_local_capacity

        assert exceeds_local_capacity("") is False


# ---------------------------------------------------------------------------
# _slugify
# ---------------------------------------------------------------------------


class TestSlugify:
    def test_basic_text(self):
        from timmy.kimi_delegation import _slugify

        assert _slugify("Hello World") == "hello-world"

    def test_special_characters_removed(self):
        from timmy.kimi_delegation import _slugify

        assert _slugify("Research: AI & ML!") == "research-ai-ml"

    def test_underscores_become_dashes(self):
        from timmy.kimi_delegation import _slugify

        assert _slugify("some_snake_case") == "some-snake-case"

    def test_long_text_truncated_to_60(self):
        from timmy.kimi_delegation import _slugify

        long_text = "a" * 100
        result = _slugify(long_text)
        assert len(result) <= 60

    def test_leading_trailing_dashes_stripped(self):
        from timmy.kimi_delegation import _slugify

        result = _slugify("  hello  ")
        assert not result.startswith("-")
        assert not result.endswith("-")

    def test_multiple_spaces_become_single_dash(self):
        from timmy.kimi_delegation import _slugify

        assert _slugify("one   two") == "one-two"


# ---------------------------------------------------------------------------
# _build_research_template
# ---------------------------------------------------------------------------


class TestBuildResearchTemplate:
    def test_contains_task_title(self):
        from timmy.kimi_delegation import _build_research_template

        body = _build_research_template("My Task", "background", "the question?")
        assert "My Task" in body

    def test_contains_question(self):
        from timmy.kimi_delegation import _build_research_template

        body = _build_research_template("task", "context", "What is X?")
        assert "What is X?" in body

    def test_contains_context(self):
        from timmy.kimi_delegation import _build_research_template

        body = _build_research_template("task", "some context here", "q?")
        assert "some context here" in body

    def test_default_priority_normal(self):
        from timmy.kimi_delegation import _build_research_template

        body = _build_research_template("task", "ctx", "q?")
        assert "normal" in body

    def test_custom_priority_included(self):
        from timmy.kimi_delegation import _build_research_template

        body = _build_research_template("task", "ctx", "q?", priority="high")
        assert "high" in body

    def test_kimi_label_mentioned(self):
        from timmy.kimi_delegation import KIMI_READY_LABEL, _build_research_template

        body = _build_research_template("task", "ctx", "q?")
        assert KIMI_READY_LABEL in body

    def test_slugified_task_in_artifact_path(self):
        from timmy.kimi_delegation import _build_research_template

        body = _build_research_template("My Research Task", "ctx", "q?")
        assert "my-research-task" in body

    def test_sections_present(self):
        from timmy.kimi_delegation import _build_research_template

        body = _build_research_template("task", "ctx", "q?")
        assert "## Research Request" in body
        assert "### Research Question" in body
        assert "### Background / Context" in body
        assert "### Deliverables" in body


# ---------------------------------------------------------------------------
# _extract_action_items
# ---------------------------------------------------------------------------


class TestExtractActionItems:
    def test_checkbox_items_extracted(self):
        from timmy.kimi_delegation import _extract_action_items

        text = "- [ ] Fix the bug\n- [ ] Write tests\n"
        items = _extract_action_items(text)
        assert "Fix the bug" in items
        assert "Write tests" in items

    def test_numbered_list_extracted(self):
        from timmy.kimi_delegation import _extract_action_items

        text = "1. Deploy to staging\n2. Run smoke tests\n"
        items = _extract_action_items(text)
        assert "Deploy to staging" in items
        assert "Run smoke tests" in items

    def test_action_prefix_extracted(self):
        from timmy.kimi_delegation import _extract_action_items

        text = "Action: Update the config file\n"
        items = _extract_action_items(text)
        assert "Update the config file" in items

    def test_todo_prefix_extracted(self):
        from timmy.kimi_delegation import _extract_action_items

        text = "TODO: Add error handling\n"
        items = _extract_action_items(text)
        assert "Add error handling" in items

    def test_next_step_prefix_extracted(self):
        from timmy.kimi_delegation import _extract_action_items

        text = "Next step: Validate results\n"
        items = _extract_action_items(text)
        assert "Validate results" in items

    def test_case_insensitive_prefixes(self):
        from timmy.kimi_delegation import _extract_action_items

        text = "todo: lowercase todo\nACTION: uppercase action\n"
        items = _extract_action_items(text)
        assert "lowercase todo" in items
        assert "uppercase action" in items

    def test_deduplication(self):
        from timmy.kimi_delegation import _extract_action_items

        text = "1. Do the thing\n2. Do the thing\n"
        items = _extract_action_items(text)
        assert items.count("Do the thing") == 1

    def test_empty_text_returns_empty_list(self):
        from timmy.kimi_delegation import _extract_action_items

        assert _extract_action_items("") == []

    def test_no_action_items_returns_empty_list(self):
        from timmy.kimi_delegation import _extract_action_items

        text = "This is just plain prose with no action items here."
        assert _extract_action_items(text) == []

    def test_mixed_sources_combined(self):
        from timmy.kimi_delegation import _extract_action_items

        text = "- [ ] checkbox item\n1. numbered item\nAction: action item\n"
        items = _extract_action_items(text)
        assert len(items) == 3


# ---------------------------------------------------------------------------
# _get_or_create_label (async)
# ---------------------------------------------------------------------------


class TestGetOrCreateLabel:
    @pytest.mark.asyncio
    async def test_returns_existing_label_id(self):
        from timmy.kimi_delegation import KIMI_READY_LABEL, _get_or_create_label

        mock_resp = MagicMock()
        mock_resp.status_code = 200
        mock_resp.json.return_value = [{"name": KIMI_READY_LABEL, "id": 42}]

        client = MagicMock()
        client.get = AsyncMock(return_value=mock_resp)

        result = await _get_or_create_label(
            client, "http://git", {"Authorization": "token x"}, "owner/repo"
        )
        assert result == 42

    @pytest.mark.asyncio
    async def test_creates_label_when_missing(self):
        from timmy.kimi_delegation import _get_or_create_label

        list_resp = MagicMock()
        list_resp.status_code = 200
        list_resp.json.return_value = []  # no existing labels

        create_resp = MagicMock()
        create_resp.status_code = 201
        create_resp.json.return_value = {"id": 99}

        client = MagicMock()
        client.get = AsyncMock(return_value=list_resp)
        client.post = AsyncMock(return_value=create_resp)

        result = await _get_or_create_label(
            client, "http://git", {"Authorization": "token x"}, "owner/repo"
        )
        assert result == 99

    @pytest.mark.asyncio
    async def test_returns_none_on_list_exception(self):
        from timmy.kimi_delegation import _get_or_create_label

        client = MagicMock()
        client.get = AsyncMock(side_effect=Exception("network error"))

        result = await _get_or_create_label(client, "http://git", {}, "owner/repo")
        assert result is None

    @pytest.mark.asyncio
    async def test_returns_none_on_create_exception(self):
        from timmy.kimi_delegation import _get_or_create_label

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

        client = MagicMock()
        client.get = AsyncMock(return_value=list_resp)
        client.post = AsyncMock(side_effect=Exception("create failed"))

        result = await _get_or_create_label(client, "http://git", {}, "owner/repo")
        assert result is None


# ---------------------------------------------------------------------------
# create_kimi_research_issue (async)
# ---------------------------------------------------------------------------


class TestCreateKimiResearchIssue:
    @pytest.mark.asyncio
    async def test_returns_error_when_gitea_disabled(self):
        from timmy.kimi_delegation import create_kimi_research_issue

        with patch("timmy.kimi_delegation.settings") as mock_settings:
            mock_settings.gitea_enabled = False
            mock_settings.gitea_token = ""
            result = await create_kimi_research_issue("task", "ctx", "q?")

        assert result["success"] is False
        assert "not configured" in result["error"]

    @pytest.mark.asyncio
    async def test_returns_error_when_no_token(self):
        from timmy.kimi_delegation import create_kimi_research_issue

        with patch("timmy.kimi_delegation.settings") as mock_settings:
            mock_settings.gitea_enabled = True
            mock_settings.gitea_token = ""
            result = await create_kimi_research_issue("task", "ctx", "q?")

        assert result["success"] is False

    @pytest.mark.asyncio
    async def test_successful_issue_creation(self):
        from timmy.kimi_delegation import create_kimi_research_issue

        mock_settings = MagicMock()
        mock_settings.gitea_enabled = True
        mock_settings.gitea_token = "tok"
        mock_settings.gitea_url = "http://git"
        mock_settings.gitea_repo = "owner/repo"

        label_resp = MagicMock()
        label_resp.status_code = 200
        label_resp.json.return_value = [{"name": "kimi-ready", "id": 5}]

        issue_resp = MagicMock()
        issue_resp.status_code = 201
        issue_resp.json.return_value = {"number": 42, "html_url": "http://git/issues/42"}

        async_client = AsyncMock()
        async_client.get = AsyncMock(return_value=label_resp)
        async_client.post = AsyncMock(return_value=issue_resp)
        async_client.__aenter__ = AsyncMock(return_value=async_client)
        async_client.__aexit__ = AsyncMock(return_value=False)

        with (
            patch("timmy.kimi_delegation.settings", mock_settings),
            patch("timmy.kimi_delegation.httpx") as mock_httpx,
        ):
            mock_httpx.AsyncClient.return_value = async_client
            result = await create_kimi_research_issue("task", "ctx", "q?")

        assert result["success"] is True
        assert result["issue_number"] == 42
        assert "http://git/issues/42" in result["issue_url"]

    @pytest.mark.asyncio
    async def test_api_error_returns_failure(self):
        from timmy.kimi_delegation import create_kimi_research_issue

        mock_settings = MagicMock()
        mock_settings.gitea_enabled = True
        mock_settings.gitea_token = "tok"
        mock_settings.gitea_url = "http://git"
        mock_settings.gitea_repo = "owner/repo"

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

        create_label_resp = MagicMock()
        create_label_resp.status_code = 201
        create_label_resp.json.return_value = {"id": 1}

        issue_resp = MagicMock()
        issue_resp.status_code = 500
        issue_resp.text = "Internal Server Error"

        async_client = AsyncMock()
        async_client.get = AsyncMock(return_value=label_resp)
        async_client.post = AsyncMock(side_effect=[create_label_resp, issue_resp])
        async_client.__aenter__ = AsyncMock(return_value=async_client)
        async_client.__aexit__ = AsyncMock(return_value=False)

        with (
            patch("timmy.kimi_delegation.settings", mock_settings),
            patch("timmy.kimi_delegation.httpx") as mock_httpx,
        ):
            mock_httpx.AsyncClient.return_value = async_client
            result = await create_kimi_research_issue("task", "ctx", "q?")

        assert result["success"] is False
        assert "500" in result["error"]

    @pytest.mark.asyncio
    async def test_exception_returns_failure(self):
        from timmy.kimi_delegation import create_kimi_research_issue

        mock_settings = MagicMock()
        mock_settings.gitea_enabled = True
        mock_settings.gitea_token = "tok"
        mock_settings.gitea_url = "http://git"
        mock_settings.gitea_repo = "owner/repo"

        async_client = AsyncMock()
        async_client.__aenter__ = AsyncMock(side_effect=Exception("connection refused"))
        async_client.__aexit__ = AsyncMock(return_value=False)

        with (
            patch("timmy.kimi_delegation.settings", mock_settings),
            patch("timmy.kimi_delegation.httpx") as mock_httpx,
        ):
            mock_httpx.AsyncClient.return_value = async_client
            result = await create_kimi_research_issue("task", "ctx", "q?")

        assert result["success"] is False
        assert result["error"] != ""


# ---------------------------------------------------------------------------
# poll_kimi_issue (async)
# ---------------------------------------------------------------------------


class TestPollKimiIssue:
    @pytest.mark.asyncio
    async def test_returns_error_when_gitea_not_configured(self):
        from timmy.kimi_delegation import poll_kimi_issue

        with patch("timmy.kimi_delegation.settings") as mock_settings:
            mock_settings.gitea_enabled = False
            mock_settings.gitea_token = ""
            result = await poll_kimi_issue(123)

        assert result["completed"] is False
        assert "not configured" in result["error"]

    @pytest.mark.asyncio
    async def test_returns_completed_when_issue_closed(self):
        from timmy.kimi_delegation import poll_kimi_issue

        mock_settings = MagicMock()
        mock_settings.gitea_enabled = True
        mock_settings.gitea_token = "tok"
        mock_settings.gitea_url = "http://git"
        mock_settings.gitea_repo = "owner/repo"

        resp = MagicMock()
        resp.status_code = 200
        resp.json.return_value = {"state": "closed", "body": "Done!"}

        async_client = AsyncMock()
        async_client.get = AsyncMock(return_value=resp)
        async_client.__aenter__ = AsyncMock(return_value=async_client)
        async_client.__aexit__ = AsyncMock(return_value=False)

        with (
            patch("timmy.kimi_delegation.settings", mock_settings),
            patch("timmy.kimi_delegation.httpx") as mock_httpx,
        ):
            mock_httpx.AsyncClient.return_value = async_client
            result = await poll_kimi_issue(42, poll_interval=0, max_wait=1)

        assert result["completed"] is True
        assert result["state"] == "closed"
        assert result["body"] == "Done!"

    @pytest.mark.asyncio
    async def test_times_out_when_issue_stays_open(self):
        from timmy.kimi_delegation import poll_kimi_issue

        mock_settings = MagicMock()
        mock_settings.gitea_enabled = True
        mock_settings.gitea_token = "tok"
        mock_settings.gitea_url = "http://git"
        mock_settings.gitea_repo = "owner/repo"

        resp = MagicMock()
        resp.status_code = 200
        resp.json.return_value = {"state": "open", "body": ""}

        async_client = AsyncMock()
        async_client.get = AsyncMock(return_value=resp)
        async_client.__aenter__ = AsyncMock(return_value=async_client)
        async_client.__aexit__ = AsyncMock(return_value=False)

        with (
            patch("timmy.kimi_delegation.settings", mock_settings),
            patch("timmy.kimi_delegation.httpx") as mock_httpx,
            patch("timmy.kimi_delegation.asyncio.sleep", new_callable=AsyncMock),
        ):
            mock_httpx.AsyncClient.return_value = async_client
            # poll_interval > max_wait so it exits immediately after first sleep
            result = await poll_kimi_issue(42, poll_interval=10, max_wait=5)

        assert result["completed"] is False
        assert result["state"] == "timeout"


# ---------------------------------------------------------------------------
# index_kimi_artifact (async)
# ---------------------------------------------------------------------------


class TestIndexKimiArtifact:
    @pytest.mark.asyncio
    async def test_empty_artifact_returns_error(self):
        from timmy.kimi_delegation import index_kimi_artifact

        result = await index_kimi_artifact(1, "title", "   ")
        assert result["success"] is False
        assert "Empty artifact" in result["error"]

    @pytest.mark.asyncio
    async def test_successful_indexing(self):
        from timmy.kimi_delegation import index_kimi_artifact

        mock_entry = MagicMock()
        mock_entry.id = "mem-123"

        with patch(
            "timmy.kimi_delegation.asyncio.to_thread", new_callable=AsyncMock
        ) as mock_thread:
            mock_thread.return_value = mock_entry
            result = await index_kimi_artifact(42, "My Research", "Some research content here")

        assert result["success"] is True
        assert result["memory_id"] == "mem-123"

    @pytest.mark.asyncio
    async def test_exception_returns_failure(self):
        from timmy.kimi_delegation import index_kimi_artifact

        with patch(
            "timmy.kimi_delegation.asyncio.to_thread", new_callable=AsyncMock
        ) as mock_thread:
            mock_thread.side_effect = Exception("DB error")
            result = await index_kimi_artifact(42, "title", "some content")

        assert result["success"] is False
        assert result["error"] != ""


# ---------------------------------------------------------------------------
# extract_and_create_followups (async)
# ---------------------------------------------------------------------------


class TestExtractAndCreateFollowups:
    @pytest.mark.asyncio
    async def test_no_action_items_returns_empty_created(self):
        from timmy.kimi_delegation import extract_and_create_followups

        result = await extract_and_create_followups("Plain prose, nothing to do.", 1)
        assert result["success"] is True
        assert result["created"] == []

    @pytest.mark.asyncio
    async def test_gitea_not_configured_returns_error(self):
        from timmy.kimi_delegation import extract_and_create_followups

        text = "1. Do something important\n"

        with patch("timmy.kimi_delegation.settings") as mock_settings:
            mock_settings.gitea_enabled = False
            mock_settings.gitea_token = ""
            result = await extract_and_create_followups(text, 5)

        assert result["success"] is False

    @pytest.mark.asyncio
    async def test_creates_followup_issues(self):
        from timmy.kimi_delegation import extract_and_create_followups

        text = "1. Deploy the service\n2. Run integration tests\n"

        mock_settings = MagicMock()
        mock_settings.gitea_enabled = True
        mock_settings.gitea_token = "tok"
        mock_settings.gitea_url = "http://git"
        mock_settings.gitea_repo = "owner/repo"

        issue_resp = MagicMock()
        issue_resp.status_code = 201
        issue_resp.json.return_value = {"number": 10}

        async_client = AsyncMock()
        async_client.post = AsyncMock(return_value=issue_resp)
        async_client.__aenter__ = AsyncMock(return_value=async_client)
        async_client.__aexit__ = AsyncMock(return_value=False)

        with (
            patch("timmy.kimi_delegation.settings", mock_settings),
            patch("timmy.kimi_delegation.httpx") as mock_httpx,
        ):
            mock_httpx.AsyncClient.return_value = async_client
            result = await extract_and_create_followups(text, 5)

        assert result["success"] is True
        assert len(result["created"]) == 2


# ---------------------------------------------------------------------------
# delegate_research_to_kimi (async)
# ---------------------------------------------------------------------------


class TestDelegateResearchToKimi:
    @pytest.mark.asyncio
    async def test_empty_task_returns_error(self):
        from timmy.kimi_delegation import delegate_research_to_kimi

        result = await delegate_research_to_kimi("", "ctx", "q?")
        assert result["success"] is False
        assert "required" in result["error"]

    @pytest.mark.asyncio
    async def test_whitespace_task_returns_error(self):
        from timmy.kimi_delegation import delegate_research_to_kimi

        result = await delegate_research_to_kimi("   ", "ctx", "q?")
        assert result["success"] is False
        assert "required" in result["error"]

    @pytest.mark.asyncio
    async def test_empty_question_returns_error(self):
        from timmy.kimi_delegation import delegate_research_to_kimi

        result = await delegate_research_to_kimi("valid task", "ctx", "")
        assert result["success"] is False
        assert "required" in result["error"]

    @pytest.mark.asyncio
    async def test_delegates_to_create_issue(self):
        from timmy.kimi_delegation import delegate_research_to_kimi

        with patch(
            "timmy.kimi_delegation.create_kimi_research_issue",
            new_callable=AsyncMock,
        ) as mock_create:
            mock_create.return_value = {
                "success": True,
                "issue_number": 7,
                "issue_url": "http://x",
                "error": None,
            }
            result = await delegate_research_to_kimi(
                "Research X", "ctx", "What is X?", priority="high"
            )

        assert result["success"] is True
        assert result["issue_number"] == 7
        mock_create.assert_awaited_once_with("Research X", "ctx", "What is X?", "high")
