"""Tests for P2 medium-priority fixes in LogosADK."""
from __future__ import annotations

import os
import tempfile

import pytest


# ---------------------------------------------------------------------------
# P2.1 — Shared cosine_similarity
# ---------------------------------------------------------------------------


class TestSharedCosine:
    """Verify cosine_similarity lives in math_utils and is re-exported."""

    def test_math_utils_cosine_basic(self):
        from logos_adk.math_utils import cosine_similarity

        assert cosine_similarity([1, 0], [1, 0]) == pytest.approx(1.0)
        assert cosine_similarity([1, 0], [0, 1]) == pytest.approx(0.0)
        assert cosine_similarity([1, 0], [-1, 0]) == pytest.approx(-1.0)

    def test_math_utils_cosine_empty(self):
        from logos_adk.math_utils import cosine_similarity

        assert cosine_similarity([], []) == 0.0

    def test_math_utils_cosine_mismatch(self):
        from logos_adk.math_utils import cosine_similarity

        assert cosine_similarity([1, 2], [1, 2, 3]) == 0.0

    def test_math_utils_cosine_zero_vector(self):
        from logos_adk.math_utils import cosine_similarity

        assert cosine_similarity([0, 0, 0], [1, 2, 3]) == 0.0

    def test_cache_module_re_exports(self):
        from logos_adk.cache import _cosine_similarity
        from logos_adk.math_utils import cosine_similarity

        assert _cosine_similarity is cosine_similarity

    def test_rag_module_re_exports(self):
        from logos_adk.rag.vector_store import _cosine_similarity
        from logos_adk.math_utils import cosine_similarity

        assert _cosine_similarity is cosine_similarity


# ---------------------------------------------------------------------------
# P2.2 — Schema caching (_schema_ready flag)
# ---------------------------------------------------------------------------


class TestSchemaCaching:
    """Verify _schema_ready prevents repeated schema DDL."""

    def test_sqlite_task_queue_schema_ready_flag(self):
        from logos_adk.scaling import SQLiteTaskQueue

        with tempfile.TemporaryDirectory() as tmp:
            path = os.path.join(tmp, "tasks.db")
            queue = SQLiteTaskQueue(path=path)
            assert hasattr(queue, "_schema_ready")
            assert queue._schema_ready is False
            # Trigger schema creation
            conn = queue._connect()
            queue._ensure_schema(conn)
            conn.close()
            assert queue._schema_ready is True
            # Second call should be a no-op (fast path)
            conn = queue._connect()
            queue._ensure_schema(conn)
            conn.close()
            assert queue._schema_ready is True

    def test_sqlite_learning_store_schema_ready_flag(self):
        from logos_adk.learning.store import SQLiteLearningStore

        with tempfile.TemporaryDirectory() as tmp:
            path = os.path.join(tmp, "learning.db")
            store = SQLiteLearningStore(path=path)
            assert store._schema_ready is False
            conn = store._connect()
            store._ensure_schema(conn)
            conn.close()
            assert store._schema_ready is True

    def test_sqlite_state_store_schema_ready_flag(self):
        from logos_adk.state_store import SQLiteStateStore

        with tempfile.TemporaryDirectory() as tmp:
            path = os.path.join(tmp, "state.db")
            store = SQLiteStateStore(path=path)
            assert store._schema_ready is False
            conn = store._connect()
            store._ensure_schema(conn)
            conn.close()
            assert store._schema_ready is True

    def test_sqlite_event_bus_schema_ready_flag(self):
        """SQLiteEventBus already had _schema_ready — verify it still works."""
        from logos_adk.event_bus import SQLiteEventBus

        with tempfile.TemporaryDirectory() as tmp:
            path = os.path.join(tmp, "events.db")
            bus = SQLiteEventBus(path=path)
            assert bus._schema_ready is False
            bus.publish("test.topic", {"data": 1})
            assert bus._schema_ready is True


# ---------------------------------------------------------------------------
# P2.4 — Backpressure
# ---------------------------------------------------------------------------


class TestBackpressure:
    """Verify EventBus backpressure via max_queue_depth."""

    def test_default_no_backpressure(self):
        from logos_adk.event_bus import InMemoryEventBus

        bus = InMemoryEventBus()
        assert bus.max_queue_depth == 0
        # Should be able to publish unlimited events
        for i in range(100):
            bus.publish("test.topic", {"i": i})

    def test_backpressure_blocks_publish(self):
        from logos_adk.event_bus import InMemoryEventBus
        from logos_adk.errors import QueueFullError

        bus = InMemoryEventBus(max_queue_depth=3)
        bus.publish("test.topic", {"i": 1})
        bus.publish("test.topic", {"i": 2})
        bus.publish("test.topic", {"i": 3})
        with pytest.raises(QueueFullError):
            bus.publish("test.topic", {"i": 4})

    def test_backpressure_allows_after_ack(self):
        from logos_adk.event_bus import InMemoryEventBus
        from logos_adk.errors import QueueFullError

        bus = InMemoryEventBus(max_queue_depth=2)
        bus.publish("test.topic", {"i": 1})
        bus.publish("test.topic", {"i": 2})

        # Queue is full
        with pytest.raises(QueueFullError):
            bus.publish("test.topic", {"i": 3})

        # Claim and ack one
        claimed = bus.claim("worker", ["test.topic"])
        bus.ack(claimed.id, "worker")

        # Now publish should work
        bus.publish("test.topic", {"i": 3})

    def test_queued_count(self):
        from logos_adk.event_bus import InMemoryEventBus

        bus = InMemoryEventBus()
        assert bus.queued_count() == 0
        bus.publish("test.topic", {"i": 1})
        bus.publish("test.topic", {"i": 2})
        assert bus.queued_count() == 2

        claimed = bus.claim("worker", ["test.topic"])
        # One claimed, one queued — both count
        assert bus.queued_count() == 2

        bus.ack(claimed.id, "worker")
        assert bus.queued_count() == 1

    def test_sqlite_backpressure(self):
        from logos_adk.event_bus import SQLiteEventBus
        from logos_adk.errors import QueueFullError

        with tempfile.TemporaryDirectory() as tmp:
            path = os.path.join(tmp, "bp.db")
            bus = SQLiteEventBus(path=path, max_queue_depth=2)
            bus.publish("test.topic", {"i": 1})
            bus.publish("test.topic", {"i": 2})
            with pytest.raises(QueueFullError):
                bus.publish("test.topic", {"i": 3})


# ---------------------------------------------------------------------------
# P2.5 — Retention purge
# ---------------------------------------------------------------------------


class TestRetentionPurge:
    """Verify purge_completed cleans up old events."""

    def test_purge_in_memory_removes_old_completed(self):
        from logos_adk.event_bus import InMemoryEventBus

        bus = InMemoryEventBus()
        r1 = bus.publish("test.topic", {"i": 1})
        r2 = bus.publish("test.topic", {"i": 2})

        # Complete both
        c1 = bus.claim("worker", ["test.topic"])
        bus.ack(c1.id, "worker")
        c2 = bus.claim("worker", ["test.topic"])
        bus.ack(c2.id, "worker")

        # Purge with max_age=0 → everything older than now
        deleted = bus.purge_completed(max_age_seconds=0)
        assert deleted == 2
        assert bus.get_event(r1.id) is None
        assert bus.get_event(r2.id) is None

    def test_purge_respects_age(self):
        from logos_adk.event_bus import InMemoryEventBus

        bus = InMemoryEventBus()
        r1 = bus.publish("test.topic", {"i": 1})
        c1 = bus.claim("worker", ["test.topic"])
        bus.ack(c1.id, "worker")

        # Purge with large max_age — nothing should be deleted
        deleted = bus.purge_completed(max_age_seconds=86400)
        assert deleted == 0
        assert bus.get_event(r1.id) is not None

    def test_purge_does_not_remove_queued(self):
        from logos_adk.event_bus import InMemoryEventBus

        bus = InMemoryEventBus()
        r1 = bus.publish("test.topic", {"i": 1})
        # Don't complete it — purge should not touch queued events
        deleted = bus.purge_completed(max_age_seconds=0)
        assert deleted == 0
        assert bus.get_event(r1.id) is not None

    def test_purge_sqlite(self):
        from logos_adk.event_bus import SQLiteEventBus

        with tempfile.TemporaryDirectory() as tmp:
            path = os.path.join(tmp, "purge.db")
            bus = SQLiteEventBus(path=path)
            r1 = bus.publish("test.topic", {"i": 1})
            c1 = bus.claim("worker", ["test.topic"])
            bus.ack(c1.id, "worker")

            deleted = bus.purge_completed(max_age_seconds=0)
            assert deleted == 1
            assert bus.get_event(r1.id) is None

    def test_purge_removes_dead_letter(self):
        from logos_adk.event_bus import InMemoryEventBus

        bus = InMemoryEventBus(max_attempts=1)
        r1 = bus.publish("test.topic", {"i": 1})
        c1 = bus.claim("worker", ["test.topic"])
        bus.requeue(c1.id, "worker", error="fail")

        event = bus.get_event(r1.id)
        assert event.status == "dead_letter"

        deleted = bus.purge_completed(max_age_seconds=0)
        assert deleted == 1
        assert bus.get_event(r1.id) is None
