# #region Test.Assistant.History [C:2] [TYPE Module] [SEMANTICS test,assistant,history,audit,confirmation]
# @BRIEF Tests for _history.py — conversation history, audit trail, confirmation persistence helpers.
# @RELATION BINDS_TO -> [AssistantHistory]
# @TEST_EDGE: append_history -> In-memory history appends correctly
# @TEST_EDGE: persist_message -> DB persistence with rollback on failure
# @TEST_EDGE: audit -> In-memory audit recording
# @TEST_EDGE: persist_audit -> DB audit persistence
# @TEST_EDGE: persist_confirmation -> Confirmation token persistence
# @TEST_EDGE: update_confirmation_state -> State transitions
# @TEST_EDGE: load_confirmation_from_db -> Confirmation loading
# @TEST_EDGE: ensure_conversation -> Conversation ID resolution
# @TEST_EDGE: resolve_or_create_conversation -> DB-backed conversation resolution
# @TEST_EDGE: cleanup_history_ttl -> TTL-based cleanup
# @TEST_EDGE: is_conversation_archived -> Archive detection
# @TEST_EDGE: coerce_query_bool -> Boolean coercion

from pathlib import Path
import sys

sys.path.insert(0, str(Path(__file__).parent.parent / "src"))

from datetime import UTC, datetime, timedelta
from unittest.mock import MagicMock, patch
import uuid

import pytest


class TestAppendHistory:
    """_append_history — in-memory conversation buffer."""

    def test_append_new_conversation(self):
        from datetime import UTC, datetime
        from src.api.routes.assistant._schemas import CONVERSATIONS
        from src.api.routes.assistant._history import _append_history

        CONVERSATIONS.clear()
        _append_history("user-1", "conv-1", "user", "Hello")
        key = ("user-1", "conv-1")
        assert key in CONVERSATIONS
        assert len(CONVERSATIONS[key]) == 1
        assert CONVERSATIONS[key][0]["role"] == "user"
        assert CONVERSATIONS[key][0]["text"] == "Hello"

    def test_append_existing_conversation(self):
        from src.api.routes.assistant._schemas import CONVERSATIONS
        from src.api.routes.assistant._history import _append_history

        CONVERSATIONS.clear()
        _append_history("user-1", "conv-1", "user", "First")
        _append_history("user-1", "conv-1", "assistant", "Response")
        key = ("user-1", "conv-1")
        assert len(CONVERSATIONS[key]) == 2
        assert CONVERSATIONS[key][0]["role"] == "user"
        assert CONVERSATIONS[key][1]["role"] == "assistant"

    def test_append_with_state_and_task(self):
        from src.api.routes.assistant._schemas import CONVERSATIONS
        from src.api.routes.assistant._history import _append_history

        CONVERSATIONS.clear()
        _append_history("user-1", "conv-1", "assistant", "Done", state="success", task_id="task-42")
        key = ("user-1", "conv-1")
        assert CONVERSATIONS[key][0]["state"] == "success"
        assert CONVERSATIONS[key][0]["task_id"] == "task-42"

    def test_message_id_is_uuid(self):
        from src.api.routes.assistant._schemas import CONVERSATIONS
        from src.api.routes.assistant._history import _append_history

        CONVERSATIONS.clear()
        _append_history("user-1", "conv-1", "user", "test")
        mid = CONVERSATIONS[("user-1", "conv-1")][0]["message_id"]
        assert uuid.UUID(mid)


class TestPersistMessage:
    """_persist_message — DB persistence."""

    def test_persist_success(self):
        from src.api.routes.assistant._history import _persist_message

        db = MagicMock()
        _persist_message(db, "user-1", "conv-1", "user", "Hello", state="active")
        db.add.assert_called_once()
        db.commit.assert_called_once()

    def test_persist_with_metadata(self):
        from src.api.routes.assistant._history import _persist_message

        db = MagicMock()
        _persist_message(db, "user-1", "conv-1", "user", "test", metadata={"key": "val"})
        # Verify the payload field was set
        added = db.add.call_args[0][0]
        assert added.payload == {"key": "val"}

    def test_persist_failure_rollback(self):
        from src.api.routes.assistant._history import _persist_message

        db = MagicMock()
        db.commit.side_effect = Exception("DB error")
        # Should not raise — just log warning
        _persist_message(db, "user-1", "conv-1", "user", "Hello")
        db.rollback.assert_called_once()


class TestAudit:
    """_audit — in-memory audit recording."""

    def test_audit_new_user(self):
        from src.api.routes.assistant._schemas import ASSISTANT_AUDIT
        from src.api.routes.assistant._history import _audit

        ASSISTANT_AUDIT.clear()
        _audit("user-1", {"decision": "executed", "task_id": "task-42"})
        assert "user-1" in ASSISTANT_AUDIT
        assert len(ASSISTANT_AUDIT["user-1"]) == 1
        entry = ASSISTANT_AUDIT["user-1"][0]
        assert entry["decision"] == "executed"
        assert "created_at" in entry

    def test_audit_append(self):
        from src.api.routes.assistant._schemas import ASSISTANT_AUDIT
        from src.api.routes.assistant._history import _audit

        ASSISTANT_AUDIT.clear()
        _audit("user-1", {"decision": "first"})
        _audit("user-1", {"decision": "second"})
        assert len(ASSISTANT_AUDIT["user-1"]) == 2


class TestPersistAudit:
    """_persist_audit — DB audit persistence."""

    def test_persist_audit_success(self):
        from src.api.routes.assistant._history import _persist_audit

        db = MagicMock()
        _persist_audit(db, "user-1", {"decision": "executed"}, "conv-1")
        db.add.assert_called_once()
        db.commit.assert_called_once()

    def test_persist_audit_failure(self):
        from src.api.routes.assistant._history import _persist_audit

        db = MagicMock()
        db.commit.side_effect = Exception("DB error")
        _persist_audit(db, "user-1", {"decision": "executed"}, "conv-1")
        db.rollback.assert_called_once()


class TestPersistConfirmation:
    """_persist_confirmation — Confirmation token DB persistence."""

    def test_persist_confirmation_success(self):
        from src.api.routes.assistant._history import _persist_confirmation
        from src.api.routes.assistant._schemas import ConfirmationRecord

        db = MagicMock()
        record = ConfirmationRecord(
            id="conf-1",
            user_id="user-1",
            conversation_id="conv-1",
            state="pending",
            intent={"op": "test"},
            dispatch={},
            expires_at=datetime.now(UTC) + timedelta(minutes=5),
            created_at=datetime.now(UTC),
        )
        _persist_confirmation(db, record)
        db.merge.assert_called_once()
        db.commit.assert_called_once()

    def test_persist_confirmation_failure(self):
        from src.api.routes.assistant._history import _persist_confirmation
        from src.api.routes.assistant._schemas import ConfirmationRecord

        db = MagicMock()
        db.commit.side_effect = Exception("DB error")
        record = ConfirmationRecord(
            id="conf-1",
            user_id="user-1",
            conversation_id="conv-1",
            state="pending",
            intent={"op": "test"},
            dispatch={},
            expires_at=datetime.now(UTC) + timedelta(minutes=5),
            created_at=datetime.now(UTC),
        )
        _persist_confirmation(db, record)
        db.rollback.assert_called_once()


class TestUpdateConfirmationState:
    """_update_confirmation_state — confirmation lifecycle."""

    def test_update_to_consumed(self):
        from src.api.routes.assistant._history import _update_confirmation_state
        from src.models.assistant import AssistantConfirmationRecord

        db = MagicMock()
        row = MagicMock(spec=AssistantConfirmationRecord)
        row.state = "pending"
        row.consumed_at = None
        db.query.return_value.filter.return_value.first.return_value = row

        _update_confirmation_state(db, "conf-1", "consumed")
        assert row.state == "consumed"
        assert row.consumed_at is not None
        db.commit.assert_called_once()

    def test_update_not_found(self):
        from src.api.routes.assistant._history import _update_confirmation_state

        db = MagicMock()
        db.query.return_value.filter.return_value.first.return_value = None
        # Should not raise
        _update_confirmation_state(db, "conf-nonexistent", "consumed")
        db.commit.assert_not_called()

    def test_update_failure_rollback(self):
        from src.api.routes.assistant._history import _update_confirmation_state
        from src.models.assistant import AssistantConfirmationRecord

        db = MagicMock()
        row = MagicMock(spec=AssistantConfirmationRecord)
        row.state = "pending"
        db.query.return_value.filter.return_value.first.return_value = row
        db.commit.side_effect = Exception("DB error")

        _update_confirmation_state(db, "conf-1", "consumed")
        db.rollback.assert_called_once()


class TestLoadConfirmationFromDb:
    """_load_confirmation_from_db — confirmation loading."""

    def test_load_found(self):
        from src.api.routes.assistant._history import _load_confirmation_from_db
        from src.models.assistant import AssistantConfirmationRecord

        db = MagicMock()
        mock_row = MagicMock(spec=AssistantConfirmationRecord)
        mock_row.id = "conf-1"
        mock_row.user_id = "user-1"
        mock_row.conversation_id = "conv-1"
        mock_row.intent = {"op": "test"}
        mock_row.dispatch = {}
        mock_row.state = "pending"
        mock_row.expires_at = datetime.now(UTC) + timedelta(hours=1)
        mock_row.created_at = datetime.now(UTC)
        db.query.return_value.filter.return_value.first.return_value = mock_row

        record = _load_confirmation_from_db(db, "conf-1")
        assert record is not None
        assert record.id == "conf-1"
        assert record.user_id == "user-1"
        assert record.intent == {"op": "test"}

    def test_load_not_found(self):
        from src.api.routes.assistant._history import _load_confirmation_from_db

        db = MagicMock()
        db.query.return_value.filter.return_value.first.return_value = None
        record = _load_confirmation_from_db(db, "conf-nonexistent")
        assert record is None


class TestEnsureConversation:
    """_ensure_conversation — conversation ID resolution."""

    def test_with_explicit_id(self):
        from src.api.routes.assistant._schemas import USER_ACTIVE_CONVERSATION
        from src.api.routes.assistant._history import _ensure_conversation

        USER_ACTIVE_CONVERSATION.clear()
        result = _ensure_conversation("user-1", "conv-1")
        assert result == "conv-1"
        assert USER_ACTIVE_CONVERSATION.get("user-1") == "conv-1"

    def test_with_active_conversation(self):
        from src.api.routes.assistant._schemas import USER_ACTIVE_CONVERSATION
        from src.api.routes.assistant._history import _ensure_conversation

        USER_ACTIVE_CONVERSATION.clear()
        USER_ACTIVE_CONVERSATION["user-1"] = "existing-conv"
        result = _ensure_conversation("user-1", None)
        assert result == "existing-conv"

    def test_creates_new(self):
        from src.api.routes.assistant._schemas import USER_ACTIVE_CONVERSATION
        from src.api.routes.assistant._history import _ensure_conversation

        USER_ACTIVE_CONVERSATION.clear()
        result = _ensure_conversation("user-1", None)
        assert result is not None
        assert uuid.UUID(result)
        assert USER_ACTIVE_CONVERSATION.get("user-1") == result


class TestResolveOrCreateConversation:
    """_resolve_or_create_conversation — DB-backed conversation resolution."""

    def test_with_explicit_id(self):
        from src.api.routes.assistant._schemas import USER_ACTIVE_CONVERSATION
        from src.api.routes.assistant._history import _resolve_or_create_conversation

        USER_ACTIVE_CONVERSATION.clear()
        db = MagicMock()
        result = _resolve_or_create_conversation("user-1", "conv-1", db)
        assert result == "conv-1"

    def test_with_active_memory(self):
        from src.api.routes.assistant._schemas import USER_ACTIVE_CONVERSATION
        from src.api.routes.assistant._history import _resolve_or_create_conversation

        USER_ACTIVE_CONVERSATION.clear()
        USER_ACTIVE_CONVERSATION["user-1"] = "mem-conv"
        db = MagicMock()
        result = _resolve_or_create_conversation("user-1", None, db)
        assert result == "mem-conv"

    def test_creates_new_without_db_hit(self):
        from src.api.routes.assistant._schemas import USER_ACTIVE_CONVERSATION
        from src.api.routes.assistant._history import _resolve_or_create_conversation

        USER_ACTIVE_CONVERSATION.clear()
        db = MagicMock()
        db.query.return_value.filter.return_value.order_by.return_value.first.return_value = None
        result = _resolve_or_create_conversation("user-1", None, db)
        assert result is not None
        assert uuid.UUID(result)


class TestCleanupHistoryTtl:
    """_cleanup_history_ttl — TTL-based history cleanup."""

    def test_cleanup_deletes_expired_db_rows(self):
        from src.api.routes.assistant._history import _cleanup_history_ttl

        db = MagicMock()
        db.query.return_value.filter.return_value = MagicMock()
        db.query.return_value.filter.return_value.filter.return_value.delete.return_value = 5

        _cleanup_history_ttl(db, "user-1")
        db.commit.assert_called_once()

    def test_cleanup_failure_rollback(self):
        from src.api.routes.assistant._history import _cleanup_history_ttl

        db = MagicMock()
        # The second filter (for TTL) raises
        q = MagicMock()
        q.filter.return_value = q  # chain .filter().filter()
        q.delete.side_effect = Exception("DB error")
        db.query.return_value = q

        _cleanup_history_ttl(db, "user-1")
        db.rollback.assert_called_once()

    def test_cleanup_clears_stale_in_memory(self):
        from datetime import UTC, datetime, timedelta
        from src.api.routes.assistant._schemas import CONVERSATIONS
        from src.api.routes.assistant._history import _cleanup_history_ttl

        CONVERSATIONS.clear()
        old_time = datetime.now() - timedelta(days=400)
        # Add a stale entry
        CONVERSATIONS[("user-1", "old-conv")] = [
            {"created_at": old_time, "role": "user", "text": "old"}
        ]
        # Add a recent entry
        CONVERSATIONS[("user-1", "new-conv")] = [
            {"created_at": datetime.now(UTC), "role": "user", "text": "new"}
        ]

        db = MagicMock()
        db.query.return_value.filter.return_value = MagicMock()
        db.query.return_value.filter.return_value.filter.return_value.delete.return_value = 0
        db.query.return_value.filter.return_value.filter.return_value = db.query.return_value.filter.return_value

        _cleanup_history_ttl(db, "user-1")

        # Old conversation should be removed
        assert ("user-1", "old-conv") not in CONVERSATIONS
        # New conversation should be kept
        assert ("user-1", "new-conv") in CONVERSATIONS


class TestIsConversationArchived:
    """_is_conversation_archived — archive detection."""

    def test_updated_at_none(self):
        from src.api.routes.assistant._history import _is_conversation_archived
        assert _is_conversation_archived(None) is False

    def test_recent_conversation(self):
        from datetime import datetime
        from src.api.routes.assistant._history import _is_conversation_archived
        recent = datetime.now()
        assert _is_conversation_archived(recent) is False

    def test_old_conversation(self):
        from datetime import datetime, timedelta
        from src.api.routes.assistant._history import _is_conversation_archived
        old = datetime.now() - timedelta(days=100)
        assert _is_conversation_archived(old) is True

    def test_aware_datetime(self):
        from datetime import UTC, datetime, timedelta
        from src.api.routes.assistant._history import _is_conversation_archived
        old = datetime.now(UTC) - timedelta(days=100)
        assert _is_conversation_archived(old) is True


class TestCoerceQueryBool:
    """_coerce_query_bool — boolean normalization."""

    def test_bool_values(self):
        from src.api.routes.assistant._history import _coerce_query_bool
        assert _coerce_query_bool(True) is True
        assert _coerce_query_bool(False) is False

    @pytest.mark.parametrize("val,expected", [
        ("1", True),
        ("true", True),
        ("yes", True),
        ("on", True),
        ("0", False),
        ("false", False),
        ("no", False),
        ("off", False),
        ("", False),
    ])
    def test_string_values(self, val, expected):
        from src.api.routes.assistant._history import _coerce_query_bool
        assert _coerce_query_bool(val) is expected

    def test_non_bool_non_string(self):
        from src.api.routes.assistant._history import _coerce_query_bool
        assert _coerce_query_bool(None) is False
        assert _coerce_query_bool(42) is False
        assert _coerce_query_bool([]) is False
# #endregion Test.Assistant.History
