"""
tests/test_feedback.py — Unit tests for cortex/core/feedback.py (Module 12)

All tests use a lightweight MockStorageAdapter — no real database required.
"""

from __future__ import annotations

import uuid
import pytest
import pytest_asyncio
from datetime import datetime, timezone
from typing import Dict, List, Optional

from cortex.core.feedback import FeedbackSystem, FeedbackScore, MemoryFeedback


# ---------------------------------------------------------------------------
# Mock StorageAdapter
# ---------------------------------------------------------------------------

class MockStorageAdapter:
    """
    Minimal in-memory implementation of the StorageAdapter contract
    for FeedbackSystem.  Only the feedback-related methods are needed.
    """

    def __init__(self):
        self._records: Dict[str, MemoryFeedback] = {}

    # --- create_feedback ---------------------------------------------------

    async def create_feedback(
        self,
        target_type: str,
        target_id: str,
        feedback_type: str,
        feedback_value: Optional[str] = None,
        source: str = "operator",
        owner_id: str = "default",
        context: Optional[str] = None,
        status: str = "pending",
    ) -> MemoryFeedback:
        record = MemoryFeedback(
            id=str(uuid.uuid4()),
            owner_id=owner_id,
            target_type=target_type,
            target_id=target_id,
            feedback_type=feedback_type,
            feedback_value=feedback_value,
            source=source,
            status=status,
            context=context,
            created_at=datetime.now(timezone.utc).isoformat(),
            applied_at=None,
        )
        self._records[record.id] = record
        return record

    # --- get_pending_feedback ----------------------------------------------

    async def get_pending_feedback(
        self,
        owner_id: str = "default",
    ) -> List[MemoryFeedback]:
        return [
            r for r in self._records.values()
            if r.owner_id == owner_id and r.status == "pending"
        ]

    # --- apply_feedback ----------------------------------------------------

    async def apply_feedback(self, feedback_id: str) -> MemoryFeedback:
        record = self._records[feedback_id]
        record.status = "applied"
        record.applied_at = datetime.now(timezone.utc).isoformat()
        return record

    # --- get_feedback_by_target (optional, but implemented here) -----------

    async def get_feedback_by_target(
        self,
        target_id: str,
        owner_id: str = "default",
    ) -> List[MemoryFeedback]:
        return [
            r for r in self._records.values()
            if r.target_id == target_id and r.owner_id == owner_id
        ]


# ---------------------------------------------------------------------------
# Fixtures
# ---------------------------------------------------------------------------

@pytest.fixture
def storage() -> MockStorageAdapter:
    return MockStorageAdapter()


@pytest.fixture
def system(storage: MockStorageAdapter) -> FeedbackSystem:
    return FeedbackSystem(storage, owner_id="default")


# ---------------------------------------------------------------------------
# Tests — submit_feedback
# ---------------------------------------------------------------------------

@pytest.mark.asyncio
async def test_submit_helpful_returns_pending_record(system):
    """submit_feedback(helpful=True) creates a 'pending' helpful record."""
    fb = await system.submit_feedback("mem-001", helpful=True, context="test ctx")

    assert fb.target_id == "mem-001"
    assert fb.feedback_type == "helpful"
    assert fb.status == "pending"
    assert fb.context == "test ctx"
    assert fb.owner_id == "default"
    assert fb.id  # has a non-empty ID


@pytest.mark.asyncio
async def test_submit_harmful_returns_pending_record(system):
    """submit_feedback(helpful=False) creates a 'pending' harmful record."""
    fb = await system.submit_feedback("mem-002", helpful=False, context="bad recall")

    assert fb.feedback_type == "harmful"
    assert fb.status == "pending"


@pytest.mark.asyncio
async def test_submit_multiple_feedback_for_same_memory(system):
    """Multiple feedback records can exist for the same memory."""
    await system.submit_feedback("mem-dup", helpful=True)
    await system.submit_feedback("mem-dup", helpful=True)
    await system.submit_feedback("mem-dup", helpful=False)

    pending = await system.get_pending()
    mem_records = [r for r in pending if r.target_id == "mem-dup"]
    assert len(mem_records) == 3


@pytest.mark.asyncio
async def test_submit_with_owner_override(system, storage):
    """owner_id kwarg overrides the instance default."""
    fb = await system.submit_feedback("mem-x", helpful=True, owner_id="user-99")
    assert fb.owner_id == "user-99"

    # should NOT appear in default owner's pending queue
    default_pending = await system.get_pending()
    assert not any(r.id == fb.id for r in default_pending)


# ---------------------------------------------------------------------------
# Tests — get_pending
# ---------------------------------------------------------------------------

@pytest.mark.asyncio
async def test_get_pending_empty_initially(system):
    """Fresh adapter has no pending feedback."""
    result = await system.get_pending()
    assert result == []


@pytest.mark.asyncio
async def test_get_pending_returns_submitted_records(system):
    """Items appear in the queue immediately after submission."""
    await system.submit_feedback("mem-a", helpful=True)
    await system.submit_feedback("mem-b", helpful=False)

    pending = await system.get_pending()
    assert len(pending) == 2
    ids = {r.target_id for r in pending}
    assert ids == {"mem-a", "mem-b"}


@pytest.mark.asyncio
async def test_get_pending_excludes_applied(system):
    """Applied records do not appear in the pending queue."""
    await system.submit_feedback("mem-c", helpful=True)
    await system.process_pending()  # marks all as applied

    pending = await system.get_pending()
    assert pending == []


# ---------------------------------------------------------------------------
# Tests — process_pending
# ---------------------------------------------------------------------------

@pytest.mark.asyncio
async def test_process_pending_returns_count(system):
    """process_pending returns the number of processed records."""
    await system.submit_feedback("mem-1", helpful=True)
    await system.submit_feedback("mem-2", helpful=False)

    count = await system.process_pending()
    assert count == 2


@pytest.mark.asyncio
async def test_process_pending_marks_records_applied(system, storage):
    """After process_pending, all records are marked 'applied'."""
    await system.submit_feedback("mem-3", helpful=True)
    await system.process_pending()

    all_records = list(storage._records.values())
    assert all(r.status == "applied" for r in all_records)
    assert all(r.applied_at is not None for r in all_records)


@pytest.mark.asyncio
async def test_process_pending_empty_queue(system):
    """process_pending on an empty queue returns 0 without error."""
    count = await system.process_pending()
    assert count == 0


@pytest.mark.asyncio
async def test_process_pending_is_idempotent(system):
    """Calling process_pending twice doesn't double-apply records."""
    await system.submit_feedback("mem-4", helpful=True)
    first = await system.process_pending()
    second = await system.process_pending()  # nothing left pending

    assert first == 1
    assert second == 0


@pytest.mark.asyncio
async def test_process_pending_continues_on_partial_failure(system, storage):
    """
    If apply_feedback raises for one record, processing continues
    and the count reflects only successfully applied records.
    """
    await system.submit_feedback("mem-ok", helpful=True)
    await system.submit_feedback("mem-fail", helpful=False)

    # Patch apply_feedback to fail for one specific record
    original = storage.apply_feedback
    fail_id = None

    async def patched_apply(feedback_id):
        nonlocal fail_id
        # Fail whichever record is for mem-fail
        record = storage._records[feedback_id]
        if record.target_id == "mem-fail":
            raise RuntimeError("simulated storage error")
        return await original(feedback_id)

    storage.apply_feedback = patched_apply

    count = await system.process_pending()
    assert count == 1  # only the successful one counted


# ---------------------------------------------------------------------------
# Tests — get_score
# ---------------------------------------------------------------------------

@pytest.mark.asyncio
async def test_get_score_neutral_when_no_feedback(system):
    """Neutral 0.5 score when no feedback exists for a memory."""
    score = await system.get_score("unknown-mem")

    assert score.memory_id == "unknown-mem"
    assert score.total == 0
    assert score.score == 0.5
    assert score.pending == 0


@pytest.mark.asyncio
async def test_get_score_all_helpful(system):
    """100% helpful → score 1.0."""
    for _ in range(3):
        await system.submit_feedback("mem-good", helpful=True)
    await system.process_pending()

    score = await system.get_score("mem-good")
    assert score.helpful == 3
    assert score.harmful == 0
    assert score.score == 1.0


@pytest.mark.asyncio
async def test_get_score_all_harmful(system):
    """100% harmful → score 0.0."""
    for _ in range(4):
        await system.submit_feedback("mem-bad", helpful=False)
    await system.process_pending()

    score = await system.get_score("mem-bad")
    assert score.score == 0.0
    assert score.harmful == 4


@pytest.mark.asyncio
async def test_get_score_mixed_feedback(system):
    """Mixed feedback: 3 helpful, 1 harmful → score 0.75."""
    await system.submit_feedback("mem-mix", helpful=True)
    await system.submit_feedback("mem-mix", helpful=True)
    await system.submit_feedback("mem-mix", helpful=True)
    await system.submit_feedback("mem-mix", helpful=False)
    await system.process_pending()

    score = await system.get_score("mem-mix")
    assert score.helpful == 3
    assert score.harmful == 1
    assert score.total == 4
    assert score.score == pytest.approx(0.75)


@pytest.mark.asyncio
async def test_get_score_excludes_pending_from_ratio(system):
    """
    Pending records don't count toward the score ratio; they are
    counted separately in FeedbackScore.pending.
    """
    # One applied helpful record
    await system.submit_feedback("mem-q", helpful=True)
    await system.process_pending()

    # One still-pending harmful record (not processed yet)
    await system.submit_feedback("mem-q", helpful=False)

    score = await system.get_score("mem-q")
    assert score.total == 1       # only applied records
    assert score.helpful == 1
    assert score.harmful == 0
    assert score.score == 1.0
    assert score.pending == 1     # the un-processed harmful record


@pytest.mark.asyncio
async def test_get_score_uses_owner_scope(system):
    """Scores are isolated by owner_id."""
    await system.submit_feedback("mem-scoped", helpful=True, owner_id="user-A")
    await system.submit_feedback("mem-scoped", helpful=False, owner_id="user-B")

    # Process each owner's queue
    await system.process_pending(owner_id="user-A")
    await system.process_pending(owner_id="user-B")

    score_a = await system.get_score("mem-scoped", owner_id="user-A")
    score_b = await system.get_score("mem-scoped", owner_id="user-B")

    assert score_a.score == 1.0
    assert score_b.score == 0.0


# ---------------------------------------------------------------------------
# Tests — FeedbackScore fields
# ---------------------------------------------------------------------------

@pytest.mark.asyncio
async def test_feedback_score_fields_complete(system):
    """FeedbackScore has all expected fields with correct types."""
    await system.submit_feedback("mem-fs", helpful=True)
    await system.process_pending()

    score = await system.get_score("mem-fs")
    assert isinstance(score, FeedbackScore)
    assert isinstance(score.memory_id, str)
    assert isinstance(score.total, int)
    assert isinstance(score.helpful, int)
    assert isinstance(score.harmful, int)
    assert isinstance(score.score, float)
    assert isinstance(score.pending, int)
    assert 0.0 <= score.score <= 1.0


# ---------------------------------------------------------------------------
# Tests — MemoryFeedback fields
# ---------------------------------------------------------------------------

@pytest.mark.asyncio
async def test_memory_feedback_has_required_fields(system):
    """MemoryFeedback record contains all expected fields."""
    fb = await system.submit_feedback("mem-rf", helpful=True, context="ctx")

    assert fb.id
    assert fb.owner_id == "default"
    assert fb.target_type == "claim"
    assert fb.target_id == "mem-rf"
    assert fb.feedback_type == "helpful"
    assert fb.source == "operator"
    assert fb.status == "pending"
    assert fb.context == "ctx"
    assert fb.created_at
    assert fb.applied_at is None
