"""Unit tests for the tool-call telemetry normalization logic.

The DB-touching path of `log_tool_outcomes` requires a running PostgreSQL,
so we test the pure normalization by stubbing the session. The shape
contract that `generation_task.py` actually emits is:

    {"function": <name>, "arguments": ..., "result": <dict>, "status": ...}

with `result["success"]` being False for failures and `result["error"]`
the message. We verify the helper splits these correctly into the
attempted / succeeded / failed buckets.
"""
from __future__ import annotations

from unittest.mock import AsyncMock, MagicMock, patch

import pytest


@pytest.mark.asyncio
async def test_log_tool_outcomes_splits_success_and_failure():
    """A mix of success and error entries should split into the right buckets."""
    captured: dict = {}

    class _StubSession:
        def __init__(self):
            self.added = []

        async def __aenter__(self):
            return self

        async def __aexit__(self, *exc):
            return False

        def add(self, row):
            self.added.append(row)
            captured["row"] = row

        async def commit(self):
            return None

    stub = _StubSession()

    from fabledassistant.services import generation_log

    with patch.object(generation_log, "async_session", lambda: stub):
        await generation_log.log_tool_outcomes(
            user_id=1,
            conv_id=42,
            assistant_message_id=99,
            model="qwen3:8b",
            think_enabled=False,
            tools_available=["record_moment", "search_notes", "create_task"],
            tool_calls=[
                {
                    "function": "record_moment",
                    "arguments": {},
                    "result": {"success": True, "moment_id": 17},
                    "status": "success",
                },
                {
                    "function": "create_task",
                    "arguments": {},
                    "result": {"success": False, "error": "missing required field 'title'"},
                    "status": "error",
                },
            ],
        )

    row = captured["row"]
    assert row.user_id == 1
    assert row.conv_id == 42
    assert row.assistant_message_id == 99
    assert row.model == "qwen3:8b"
    assert row.think_enabled is False
    assert row.tools_available == ["create_task", "record_moment", "search_notes"]
    assert row.tools_attempted == ["record_moment", "create_task"]
    assert row.tools_succeeded == ["record_moment"]
    assert row.tools_failed == [{"name": "create_task", "error": "missing required field 'title'"}]


@pytest.mark.asyncio
async def test_log_tool_outcomes_handles_empty_tool_calls():
    """A turn with no tool calls produces an empty-attempted row, not an exception."""
    captured: dict = {}

    class _StubSession:
        async def __aenter__(self):
            return self

        async def __aexit__(self, *exc):
            return False

        def add(self, row):
            captured["row"] = row

        async def commit(self):
            return None

    from fabledassistant.services import generation_log

    with patch.object(generation_log, "async_session", lambda: _StubSession()):
        await generation_log.log_tool_outcomes(
            user_id=1,
            conv_id=42,
            assistant_message_id=99,
            model="mistral-small:22b",
            think_enabled=False,
            tools_available=["record_moment"],
            tool_calls=[],
        )

    row = captured["row"]
    assert row.tools_attempted == []
    assert row.tools_succeeded == []
    assert row.tools_failed == []


@pytest.mark.asyncio
async def test_log_tool_outcomes_swallows_exceptions():
    """Telemetry failure must NOT propagate — it would break user-facing flow."""
    from fabledassistant.services import generation_log

    bad_session = MagicMock()
    bad_session.return_value.__aenter__ = AsyncMock(side_effect=RuntimeError("db down"))

    with patch.object(generation_log, "async_session", bad_session):
        # Should NOT raise.
        await generation_log.log_tool_outcomes(
            user_id=1,
            conv_id=42,
            assistant_message_id=99,
            model="qwen3:8b",
            think_enabled=False,
            tools_available=[],
            tool_calls=[],
        )


@pytest.mark.asyncio
async def test_log_tool_outcomes_detects_success_false_without_error_field():
    """A tool result with success=False but no error string should still go to failed."""
    captured: dict = {}

    class _StubSession:
        async def __aenter__(self):
            return self

        async def __aexit__(self, *exc):
            return False

        def add(self, row):
            captured["row"] = row

        async def commit(self):
            return None

    from fabledassistant.services import generation_log

    with patch.object(generation_log, "async_session", lambda: _StubSession()):
        await generation_log.log_tool_outcomes(
            user_id=1,
            conv_id=42,
            assistant_message_id=99,
            model="qwen3:8b",
            think_enabled=False,
            tools_available=["search_notes"],
            tool_calls=[
                {
                    "function": "search_notes",
                    "arguments": {},
                    "result": {"success": False},  # no explicit error field
                    "status": "success",  # but status incorrectly says success
                },
            ],
        )

    row = captured["row"]
    assert row.tools_succeeded == []
    assert row.tools_failed == [{"name": "search_notes", "error": "unspecified"}]
