"""Tests for dependency suggestions (Stream D)."""

from __future__ import annotations

import pytest

from loom.graph.suggestions import _title_overlap, suggest_dependencies
from loom.validator import normalise


class TestTitleOverlap:
    """Unit tests for _title_overlap helper."""

    def test_identical_titles(self):
        tokens = normalise("Fix authentication bug")
        assert _title_overlap(tokens, tokens) == 1.0

    def test_partial_overlap(self):
        a = normalise("Fix authentication module")
        b = normalise("Update authentication tests")
        score = _title_overlap(a, b)
        assert 0.0 < score < 1.0
        # "authentication" overlaps, "fix"/"module" and "update"/"tests" don't

    def test_no_overlap(self):
        a = normalise("Fix database connection")
        b = normalise("Update frontend styles")
        assert _title_overlap(a, b) == 0.0

    def test_empty_tokens(self):
        assert _title_overlap([], normalise("something")) == 0.0
        assert _title_overlap(normalise("something"), []) == 0.0
        assert _title_overlap([], []) == 0.0

    def test_single_token_overlap(self):
        a = normalise("Create user model")
        b = normalise("Test user endpoint")
        score = _title_overlap(a, b)
        assert score > 0.0  # "user" overlaps


class TestSuggestDependencies:
    """Integration-style tests for suggest_dependencies using mocked pool."""

    @pytest.fixture
    def mock_pool(self):
        """Create a mock asyncpg pool that returns controlled task data."""
        import asyncio
        from unittest.mock import AsyncMock, MagicMock

        pool = MagicMock()
        conn = AsyncMock()

        # Make pool.acquire() work as async context manager
        ctx_mgr = MagicMock()
        ctx_mgr.__aenter__ = AsyncMock(return_value=conn)
        ctx_mgr.__aexit__ = AsyncMock(return_value=False)
        pool.acquire.return_value = ctx_mgr

        return pool, conn

    @pytest.mark.asyncio
    async def test_empty_project(self, mock_pool):
        pool, conn = mock_pool
        conn.fetch.return_value = []

        result = await suggest_dependencies(pool, "project-1", "Fix bug")
        assert result == []

    @pytest.mark.asyncio
    async def test_title_overlap_suggestion(self, mock_pool):
        pool, conn = mock_pool
        conn.fetch.return_value = [
            {"id": "loom-001", "title": "Fix auth module", "context": {}, "status": "pending"},
            {"id": "loom-002", "title": "Update README", "context": {}, "status": "pending"},
        ]

        result = await suggest_dependencies(
            pool, "project-1", "Test auth module", threshold=0.1,
        )
        # Should suggest loom-001 (auth + module overlap) but probably not loom-002
        task_ids = [s["task_id"] for s in result]
        assert "loom-001" in task_ids

    @pytest.mark.asyncio
    async def test_file_overlap_suggestion(self, mock_pool):
        pool, conn = mock_pool
        conn.fetch.return_value = [
            {"id": "loom-001", "title": "Something unrelated",
             "context": {"files": ["loom/auth.py", "loom/config.py"]}, "status": "pending"},
            {"id": "loom-002", "title": "Another task",
             "context": {"files": ["loom/dashboard/app.py"]}, "status": "pending"},
        ]

        result = await suggest_dependencies(
            pool, "project-1", "New feature",
            context={"files": ["loom/auth.py", "loom/models.py"]},
            threshold=0.1,
        )
        task_ids = [s["task_id"] for s in result]
        assert "loom-001" in task_ids  # file overlap on loom/auth.py

    @pytest.mark.asyncio
    async def test_respects_limit(self, mock_pool):
        pool, conn = mock_pool
        # Create many matching tasks
        conn.fetch.return_value = [
            {"id": f"loom-{i:03d}", "title": "Fix auth module", "context": {}, "status": "pending"}
            for i in range(20)
        ]

        result = await suggest_dependencies(
            pool, "project-1", "Test auth module", limit=3, threshold=0.1,
        )
        assert len(result) <= 3

    @pytest.mark.asyncio
    async def test_threshold_filtering(self, mock_pool):
        pool, conn = mock_pool
        conn.fetch.return_value = [
            {"id": "loom-001", "title": "completely different topic", "context": {}, "status": "pending"},
        ]

        result = await suggest_dependencies(
            pool, "project-1", "Fix database connection", threshold=0.2,
        )
        # Should return empty because "completely different topic" won't overlap with "Fix database connection"
        assert result == []

    @pytest.mark.asyncio
    async def test_score_and_reason_format(self, mock_pool):
        pool, conn = mock_pool
        conn.fetch.return_value = [
            {"id": "loom-001", "title": "Fix auth module",
             "context": {"files": ["loom/auth.py"]}, "status": "pending"},
        ]

        result = await suggest_dependencies(
            pool, "project-1", "Update auth module",
            context={"files": ["loom/auth.py"]},
            threshold=0.1,
        )
        assert len(result) > 0
        suggestion = result[0]
        assert "task_id" in suggestion
        assert "title" in suggestion
        assert "score" in suggestion
        assert "reason" in suggestion
        assert isinstance(suggestion["score"], float)

    @pytest.mark.asyncio
    async def test_context_as_json_string(self, mock_pool):
        """Context stored as JSON string (as can happen in some DB drivers) is parsed correctly."""
        import json
        pool, conn = mock_pool
        conn.fetch.return_value = [
            {"id": "loom-001", "title": "Fix auth module",
             "context": json.dumps({"files": ["loom/auth.py"]}), "status": "pending"},
        ]

        result = await suggest_dependencies(
            pool, "project-1", "Update auth module",
            context={"files": ["loom/auth.py"]},
            threshold=0.1,
        )
        assert len(result) > 0
        assert result[0]["task_id"] == "loom-001"

    @pytest.mark.asyncio
    async def test_sorted_by_score_descending(self, mock_pool):
        pool, conn = mock_pool
        conn.fetch.return_value = [
            {"id": "loom-001", "title": "Fix auth module",
             "context": {"files": ["loom/auth.py"]}, "status": "pending"},
            {"id": "loom-002", "title": "Update auth",
             "context": {}, "status": "pending"},
        ]

        result = await suggest_dependencies(
            pool, "project-1", "Fix auth module",
            context={"files": ["loom/auth.py"]},
            threshold=0.1,
        )
        # Results should be sorted by score descending
        if len(result) >= 2:
            assert result[0]["score"] >= result[1]["score"]

    @pytest.mark.asyncio
    async def test_no_file_overlap_when_no_files(self, mock_pool):
        """When neither new task nor existing tasks have files, file overlap is 0."""
        pool, conn = mock_pool
        conn.fetch.return_value = [
            {"id": "loom-001", "title": "Fix auth module", "context": {}, "status": "pending"},
        ]

        result = await suggest_dependencies(
            pool, "project-1", "Fix auth module",
            context=None,
            threshold=0.1,
        )
        assert len(result) > 0
        # Only title overlap contributes, no file overlap in reason
        assert "file overlap" not in result[0]["reason"]

    @pytest.mark.asyncio
    async def test_none_context_handled(self, mock_pool):
        """None context in DB row is handled gracefully."""
        pool, conn = mock_pool
        conn.fetch.return_value = [
            {"id": "loom-001", "title": "Fix auth module", "context": None, "status": "pending"},
        ]

        result = await suggest_dependencies(
            pool, "project-1", "Fix auth module", threshold=0.1,
        )
        # Should not crash, and may return a result based on title overlap
        assert isinstance(result, list)


def test_create_suggest_deps_flag():
    """CLI create command has --suggest-deps flag."""
    from click.testing import CliRunner
    from loom.cli import cli

    runner = CliRunner()
    result = runner.invoke(cli, ["create", "--help"])
    assert "--suggest-deps" in result.output
