"""Tests for Weft memory integration — config, injection, and claim guard."""

from __future__ import annotations

import sys
from types import ModuleType
from unittest.mock import AsyncMock, MagicMock

import pytest

from loom.config import WeftConfig, load_config


# ---------- env vars to clear so host config doesn't leak ----------
_WEFT_ENV_VARS = (
    "LOOM_WEFT_ENABLED",
    "LOOM_WEFT_DATABASE_URL",
    "LOOM_WEFT_BUDGET_TOKENS",
    "LOOM_WEFT_PROJECT_ID",
)


@pytest.fixture(autouse=True)
def _clean_env(monkeypatch, tmp_path):
    for var in _WEFT_ENV_VARS:
        monkeypatch.delenv(var, raising=False)
    for var in (
        "LOOM_DATABASE_URL", "LOOM_REDIS_URL", "LOOM_PROJECT_ID",
        "LOOM_SKILLS_API_KEY", "ANTHROPIC_API_KEY",
    ):
        monkeypatch.delenv(var, raising=False)
    monkeypatch.setenv("HOME", str(tmp_path))


def _make_mock_embedder():
    """Create a mock embedder with async embed(text) -> list[float]."""
    embedder = MagicMock()
    embedder.embed = AsyncMock(return_value=[0.1] * 384)
    return embedder


@pytest.fixture()
def mock_weft_context():
    """Install a fake weft.context module so lazy imports succeed.

    build_context signature: (pool, query_embedding, *, budget_tokens, project_id, ...)
    Returns a dict with {"memories": [...], ...}.
    """
    mock_build = AsyncMock(return_value={"memories": [], "total_tokens": 0, "count": 0})
    mod = ModuleType("weft.context")
    mod.build_context = mock_build
    # Also need a top-level weft package so `from weft.context import ...` works
    weft_pkg = ModuleType("weft")
    weft_pkg.context = mod
    old_weft = sys.modules.get("weft")
    old_ctx = sys.modules.get("weft.context")
    sys.modules["weft"] = weft_pkg
    sys.modules["weft.context"] = mod
    # Force re-import of the integration module so it picks up the mock
    sys.modules.pop("loom.integrations.weft", None)
    yield mock_build
    # Restore
    if old_weft is not None:
        sys.modules["weft"] = old_weft
    else:
        sys.modules.pop("weft", None)
    if old_ctx is not None:
        sys.modules["weft.context"] = old_ctx
    else:
        sys.modules.pop("weft.context", None)
    sys.modules.pop("loom.integrations.weft", None)


# ==================== Config tests ====================


class TestWeftConfig:
    def test_defaults(self):
        cfg = WeftConfig()
        assert cfg.enabled is False
        assert cfg.database_url == ""
        assert cfg.budget_tokens == 500
        assert cfg.project_id == ""

    def test_config_defaults_in_loom_config(self):
        config = load_config(project_dir=None)
        assert config.integrations.weft.enabled is False
        assert config.integrations.weft.budget_tokens == 500
        assert config.integrations.weft.project_id == ""

    def test_env_var_enabled(self, monkeypatch):
        monkeypatch.setenv("LOOM_WEFT_ENABLED", "true")
        config = load_config(project_dir=None)
        assert config.integrations.weft.enabled is True

    def test_env_var_enabled_variants(self, monkeypatch):
        for val in ("1", "yes", "True", "YES"):
            monkeypatch.setenv("LOOM_WEFT_ENABLED", val)
            config = load_config(project_dir=None)
            assert config.integrations.weft.enabled is True

    def test_env_var_disabled(self, monkeypatch):
        monkeypatch.setenv("LOOM_WEFT_ENABLED", "false")
        config = load_config(project_dir=None)
        assert config.integrations.weft.enabled is False

    def test_env_var_database_url(self, monkeypatch):
        monkeypatch.setenv("LOOM_WEFT_DATABASE_URL", "postgresql://weft:weft@db:5432/weft")
        config = load_config(project_dir=None)
        assert config.integrations.weft.database_url == "postgresql://weft:weft@db:5432/weft"

    def test_env_var_budget_tokens(self, monkeypatch):
        monkeypatch.setenv("LOOM_WEFT_BUDGET_TOKENS", "1000")
        config = load_config(project_dir=None)
        assert config.integrations.weft.budget_tokens == 1000

    def test_env_var_project_id(self, monkeypatch):
        monkeypatch.setenv("LOOM_WEFT_PROJECT_ID", "my-project")
        config = load_config(project_dir=None)
        assert config.integrations.weft.project_id == "my-project"

    def test_yaml_override(self, tmp_path):
        loom_dir = tmp_path / ".loom"
        loom_dir.mkdir()
        (loom_dir / "config.yaml").write_text(
            "integrations:\n"
            "  weft:\n"
            "    enabled: true\n"
            "    database_url: postgresql://custom:5432/weft\n"
            "    budget_tokens: 750\n"
        )
        config = load_config(project_dir=str(tmp_path))
        assert config.integrations.weft.enabled is True
        assert config.integrations.weft.database_url == "postgresql://custom:5432/weft"
        assert config.integrations.weft.budget_tokens == 750


# ==================== inject_weft_memories tests ====================


class TestInjectWeftMemories:
    @pytest.mark.asyncio
    async def test_memories_injected(self, mock_weft_context):
        from loom.integrations.weft import inject_weft_memories

        task = {
            "id": "t-1",
            "title": "Fix login bug",
            "context": {"description": "Auth fails on redirect", "files": ["src/auth.py"]},
        }
        memories = [{"content": "auth.py uses OAuth2 flow", "topic": ["auth"]}]
        mock_weft_context.return_value = {"memories": memories, "total_tokens": 50, "count": 1}
        embedder = _make_mock_embedder()

        result = await inject_weft_memories(task, MagicMock(), embedder, budget_tokens=500)

        assert result["context"]["weft_memories"] == memories
        embedder.embed.assert_called_once()
        mock_weft_context.assert_called_once()

    @pytest.mark.asyncio
    async def test_empty_query_skips(self, mock_weft_context):
        from loom.integrations.weft import inject_weft_memories

        task = {"id": "t-2", "title": "", "context": {}}
        embedder = _make_mock_embedder()

        result = await inject_weft_memories(task, MagicMock(), embedder)

        mock_weft_context.assert_not_called()
        embedder.embed.assert_not_called()
        assert "weft_memories" not in result.get("context", {})

    @pytest.mark.asyncio
    async def test_exception_swallowed(self, mock_weft_context):
        from loom.integrations.weft import inject_weft_memories

        task = {"id": "t-3", "title": "Something", "context": {"description": "desc"}}
        mock_weft_context.side_effect = RuntimeError("db down")
        embedder = _make_mock_embedder()

        result = await inject_weft_memories(task, MagicMock(), embedder)

        assert result["id"] == "t-3"
        assert "weft_memories" not in result.get("context", {})

    @pytest.mark.asyncio
    async def test_no_memories_skips_injection(self, mock_weft_context):
        from loom.integrations.weft import inject_weft_memories

        task = {"id": "t-4", "title": "Refactor cache", "context": {}}
        mock_weft_context.return_value = {"memories": [], "total_tokens": 0, "count": 0}
        embedder = _make_mock_embedder()

        result = await inject_weft_memories(task, MagicMock(), embedder)

        assert "weft_memories" not in result.get("context", {})

    @pytest.mark.asyncio
    async def test_none_context_gets_dict(self, mock_weft_context):
        from loom.integrations.weft import inject_weft_memories

        task = {"id": "t-5", "title": "New task", "context": None}
        memories = [{"content": "some learning"}]
        mock_weft_context.return_value = {"memories": memories, "total_tokens": 20, "count": 1}
        embedder = _make_mock_embedder()

        result = await inject_weft_memories(task, MagicMock(), embedder)

        assert isinstance(result["context"], dict)
        assert result["context"]["weft_memories"] == memories

    @pytest.mark.asyncio
    async def test_project_id_passed_to_build_context(self, mock_weft_context):
        from loom.integrations.weft import inject_weft_memories

        task = {"id": "t-6", "title": "Deploy service", "context": {}}
        mock_weft_context.return_value = {"memories": [{"content": "tip"}], "total_tokens": 10, "count": 1}
        pool = MagicMock()
        embedder = _make_mock_embedder()

        await inject_weft_memories(task, pool, embedder, budget_tokens=300, project_id="proj-abc")

        mock_weft_context.assert_called_once()
        call_kwargs = mock_weft_context.call_args
        assert call_kwargs.kwargs["project_id"] == "proj-abc"
        assert call_kwargs.kwargs["budget_tokens"] == 300

    @pytest.mark.asyncio
    async def test_embedding_vector_passed_to_build_context(self, mock_weft_context):
        from loom.integrations.weft import inject_weft_memories

        task = {"id": "t-7", "title": "Test embeddings", "context": {}}
        mock_weft_context.return_value = {"memories": [{"content": "x"}], "total_tokens": 5, "count": 1}
        pool = MagicMock()
        embedder = _make_mock_embedder()
        embedder.embed.return_value = [0.5] * 384

        await inject_weft_memories(task, pool, embedder)

        # build_context called with (pool, embedding_vector, ...)
        args = mock_weft_context.call_args[0]
        assert args[0] is pool
        assert args[1] == [0.5] * 384


# ==================== _maybe_inject_weft helper ====================


class TestMaybeInjectWeft:
    @pytest.mark.asyncio
    async def test_skips_when_weft_not_configured(self):
        from loom.mcp.tools import _maybe_inject_weft
        from loom.mcp.server import AppContext

        app = AppContext(
            pool=MagicMock(), redis=MagicMock(), project_id="proj-1",
            config=load_config(project_dir=None), project_dir="/tmp",
        )
        resp = {"id": "t-1", "title": "test"}

        result = await _maybe_inject_weft(app, resp)

        assert result == resp

    @pytest.mark.asyncio
    async def test_injects_when_weft_configured(self, mock_weft_context):
        from loom.mcp.tools import _maybe_inject_weft
        from loom.mcp.server import AppContext

        memories = [{"content": "relevant memory"}]
        mock_weft_context.return_value = {"memories": memories, "total_tokens": 15, "count": 1}
        embedder = _make_mock_embedder()

        config = load_config(project_dir=None)
        config.integrations.weft.project_id = "test-proj"
        app = AppContext(
            pool=MagicMock(), redis=MagicMock(), project_id="proj-1",
            config=config, project_dir="/tmp",
            weft_pool=MagicMock(), weft_embedder=embedder,
        )
        resp = {"id": "t-1", "title": "Fix bug", "context": {}}

        result = await _maybe_inject_weft(app, resp)

        assert result["context"]["weft_memories"] == memories

    @pytest.mark.asyncio
    async def test_passes_empty_project_id_as_none(self, mock_weft_context):
        from loom.mcp.tools import _maybe_inject_weft
        from loom.mcp.server import AppContext

        mock_weft_context.return_value = {"memories": [{"content": "m"}], "total_tokens": 5, "count": 1}
        embedder = _make_mock_embedder()

        config = load_config(project_dir=None)
        # project_id is "" (default) — should be passed as None
        app = AppContext(
            pool=MagicMock(), redis=MagicMock(), project_id="proj-1",
            config=config, project_dir="/tmp",
            weft_pool=MagicMock(), weft_embedder=embedder,
        )
        resp = {"id": "t-2", "title": "Some task", "context": {}}

        await _maybe_inject_weft(app, resp)

        call_kwargs = mock_weft_context.call_args.kwargs
        assert call_kwargs["project_id"] is None


# ==================== project_id derivation ====================


class TestWeftProjectIdDerivation:
    def test_project_id_derived_from_dir_basename(self):
        """The lifespan derives project_id from os.path.basename when empty."""
        import os
        config = load_config(project_dir=None)
        config.integrations.weft.enabled = True
        assert config.integrations.weft.project_id == ""

        # Simulate the lifespan derivation logic
        project_dir = "/home/user/projects/MyProject"
        if not config.integrations.weft.project_id:
            config.integrations.weft.project_id = os.path.basename(project_dir).lower()

        assert config.integrations.weft.project_id == "myproject"

    def test_explicit_project_id_not_overridden(self, monkeypatch):
        monkeypatch.setenv("LOOM_WEFT_PROJECT_ID", "explicit-id")
        config = load_config(project_dir=None)
        assert config.integrations.weft.project_id == "explicit-id"

        # Lifespan would NOT override because it's already set
        import os
        project_dir = "/home/user/projects/OtherProject"
        if not config.integrations.weft.project_id:
            config.integrations.weft.project_id = os.path.basename(project_dir).lower()

        assert config.integrations.weft.project_id == "explicit-id"


# ==================== loom_claim guard ====================


class TestClaimWeftGuard:
    @pytest.mark.asyncio
    async def test_claim_skips_when_weft_not_configured(self):
        """When weft_pool is None (default), claim should not call inject."""
        from loom.mcp.server import AppContext

        app = AppContext(
            pool=MagicMock(),
            redis=MagicMock(),
            project_id="proj-1",
            config=load_config(project_dir=None),
            project_dir="/tmp",
        )
        assert app.weft_pool is None
        assert app.weft_embedder is None
