"""Tests for fuzzy dependency resolution and epic coherence in _build_task_graph."""

from __future__ import annotations

from loom.graph.task import TaskStatus
from loom.skills.decomposer import (
    GraphBuildResult,
    TaskDefinition,
    _build_task_graph,
    _fuzzy_resolve_title,
    _warn_disconnected_epics,
)

PROJECT = "test-project-id"


def _td(
    title: str,
    type: str = "task",
    parent_epic: str | None = None,
    depends_on_titles: list[str] | None = None,
) -> TaskDefinition:
    return TaskDefinition(
        title=title,
        type=type,
        parent_epic=parent_epic,
        depends_on_titles=depends_on_titles or [],
    )


# ── Exact match regression ───────────────────────────────────────────


def test_exact_match_still_works():
    """Exact title-based deps resolve without fuzzy matching."""
    defs = [
        _td("Build API server"),
        _td("Write API tests", depends_on_titles=["Build API server"]),
    ]
    result = _build_task_graph(defs, PROJECT)
    by_title = {t.title: t for t in result.tasks}

    assert by_title["Write API tests"].status == TaskStatus.BLOCKED
    assert len(by_title["Write API tests"].depends_on) == 1
    assert by_title["Write API tests"].depends_on[0] == by_title["Build API server"].id
    assert result.fuzzy_resolved == []
    assert result.dropped_edges == []


# ── Fuzzy match scenarios ────────────────────────────────────────────


def test_fuzzy_prefix_substring():
    """Dep title is a prefix/substring of the real title — fuzzy resolves."""
    defs = [
        _td("Implement user authentication service"),
        _td("Write auth tests", depends_on_titles=["Implement user authentication"]),
    ]
    result = _build_task_graph(defs, PROJECT)
    by_title = {t.title: t for t in result.tasks}

    # Should fuzzy-resolve to the full title
    assert by_title["Write auth tests"].status == TaskStatus.BLOCKED
    assert len(result.fuzzy_resolved) == 1
    assert result.fuzzy_resolved[0]["resolved_to"] == "Implement user authentication service"


def test_fuzzy_reordered_words():
    """Dep title has reordered words — Jaccard similarity handles it."""
    defs = [
        _td("Setup database connection pool"),
        _td("Run migrations", depends_on_titles=["Database connection pool setup"]),
    ]
    result = _build_task_graph(defs, PROJECT)
    by_title = {t.title: t for t in result.tasks}

    assert by_title["Run migrations"].status == TaskStatus.BLOCKED
    assert len(result.fuzzy_resolved) == 1
    assert result.fuzzy_resolved[0]["resolved_to"] == "Setup database connection pool"


def test_fuzzy_minor_typo():
    """Dep title has a minor typo — SequenceMatcher handles it."""
    defs = [
        _td("Configure Redis caching layer"),
        _td("Test cache", depends_on_titles=["Configure Redis cachng layer"]),
    ]
    result = _build_task_graph(defs, PROJECT)
    by_title = {t.title: t for t in result.tasks}

    assert by_title["Test cache"].status == TaskStatus.BLOCKED
    assert len(result.fuzzy_resolved) == 1
    assert result.fuzzy_resolved[0]["resolved_to"] == "Configure Redis caching layer"


# ── Drop scenarios ───────────────────────────────────────────────────


def test_hallucinated_title_dropped():
    """Totally made-up dep title scores well below threshold — dropped."""
    defs = [
        _td("Build API server"),
        _td("Deploy to cloud", depends_on_titles=["Quantum flux capacitor integration"]),
    ]
    result = _build_task_graph(defs, PROJECT)
    by_title = {t.title: t for t in result.tasks}

    # Should be dropped, not resolved
    assert by_title["Deploy to cloud"].status == TaskStatus.PENDING
    assert len(by_title["Deploy to cloud"].depends_on) == 0
    assert len(result.dropped_edges) == 1
    assert result.dropped_edges[0]["dep_title"] == "Quantum flux capacitor integration"


def test_ambiguous_match_dropped():
    """Two titles score within the ambiguity margin — both dropped."""
    defs = [
        _td("Build user API endpoint"),
        _td("Build admin API endpoint"),
        _td("Write tests", depends_on_titles=["Build API endpoint"]),
    ]
    result = _build_task_graph(defs, PROJECT)
    by_title = {t.title: t for t in result.tasks}

    # Both candidates are similarly close — ambiguity guard drops
    assert by_title["Write tests"].status == TaskStatus.PENDING
    assert len(result.dropped_edges) == 1
    assert result.dropped_edges[0]["reason"] == "no match above threshold"


# ── Result metadata ──────────────────────────────────────────────────


def test_fuzzy_resolved_deps_in_result():
    """GraphBuildResult.fuzzy_resolved is populated with match metadata."""
    defs = [
        _td("Implement authentication middleware"),
        _td("Add login route", depends_on_titles=["Implement authentication"]),
    ]
    result = _build_task_graph(defs, PROJECT)

    assert isinstance(result, GraphBuildResult)
    assert len(result.fuzzy_resolved) == 1
    entry = result.fuzzy_resolved[0]
    assert entry["task"] == "Add login route"
    assert entry["original_dep"] == "Implement authentication"
    assert entry["resolved_to"] == "Implement authentication middleware"
    assert 0.60 <= entry["score"] <= 1.0


def test_dropped_deps_in_result():
    """GraphBuildResult.dropped_edges is populated for unresolvable deps."""
    defs = [
        _td("Build frontend"),
        _td("Polish UI", depends_on_titles=["Nonexistent backend service"]),
    ]
    result = _build_task_graph(defs, PROJECT)

    assert len(result.dropped_edges) == 1
    entry = result.dropped_edges[0]
    assert entry["task"] == "Polish UI"
    assert entry["dep_title"] == "Nonexistent backend service"


# ── Epic coherence ───────────────────────────────────────────────────


def test_epic_coherence_warning_fires():
    """Epic with 2+ children and zero intra-epic deps triggers warning."""
    defs = [
        _td("Backend Epic", type="epic"),
        _td("Build API", parent_epic="Backend Epic"),
        _td("Build CLI", parent_epic="Backend Epic"),
        _td("Build Worker", parent_epic="Backend Epic"),
    ]
    result = _build_task_graph(defs, PROJECT)
    warnings = _warn_disconnected_epics(result.tasks)

    assert len(warnings) == 1
    assert warnings[0]["child_count"] == 3
    assert "Backend Epic" in warnings[0]["message"]


def test_epic_coherence_no_warning_with_deps():
    """Epic whose children have intra-epic deps — no warning."""
    defs = [
        _td("Backend Epic", type="epic"),
        _td("Build API", parent_epic="Backend Epic"),
        _td("Build CLI", parent_epic="Backend Epic", depends_on_titles=["Build API"]),
    ]
    result = _build_task_graph(defs, PROJECT)
    warnings = _warn_disconnected_epics(result.tasks)

    assert len(warnings) == 0


def test_epic_coherence_no_warning_single_child():
    """Epic with a single child — no warning (nothing to connect)."""
    defs = [
        _td("Solo Epic", type="epic"),
        _td("Only Task", parent_epic="Solo Epic"),
    ]
    result = _build_task_graph(defs, PROJECT)
    warnings = _warn_disconnected_epics(result.tasks)

    assert len(warnings) == 0
