"""
tests/test_review_queue.py — Tests for the manual review queue feature (v14).

Coverage:
    - Flag a memory → verify status='needs_review'
    - Flagged memory excluded from retrieval
    - Approve a memory → verify status='active'
    - Approved memory appears in retrieval
    - Reject a memory → verify status='rejected'
    - Rejected memory excluded from retrieval
    - Bulk flag → verify counts
    - Review queue listing with pagination
    - Flag with reason → reason persisted
    - Approve non-flagged memory → 409
    - Reject non-flagged memory → 409
    - Flag/approve/reject nonexistent memory → 404
    - Bulk flag with mixed valid/invalid IDs
"""

from __future__ import annotations

import json
from dataclasses import dataclass, field
from typing import Any, Dict, List, Optional
from unittest.mock import AsyncMock, MagicMock, patch

import pytest

from cortex.types import SemanticClaim


# ---------------------------------------------------------------------------
# Helpers / stubs
# ---------------------------------------------------------------------------


def _make_claim(
    id: str = "claim-001",
    owner_id: str = "default",
    subject: str = "Andrew",
    predicate: str = "prefers",
    object_value: str = "dark mode",
    status: str = "active",
    category: str = "preferences",
    flag_reason: Optional[str] = None,
    **kwargs,
) -> SemanticClaim:
    return SemanticClaim(
        id=id,
        owner_id=owner_id,
        subject=subject,
        predicate=predicate,
        object_value=object_value,
        status=status,
        category=category,
        flag_reason=flag_reason,
        **kwargs,
    )


def _make_mock_storage(claims: Optional[Dict[str, SemanticClaim]] = None):
    """Create a mock storage adapter with claims dict."""
    storage = AsyncMock()
    _claims = dict(claims) if claims else {}

    async def get_claim(claim_id, owner_id=None):
        return _claims.get(claim_id)

    async def update_claim(claim_id, owner_id=None, **kwargs):
        claim = _claims.get(claim_id)
        if claim is None:
            raise ValueError(f"Claim {claim_id} not found")
        for k, v in kwargs.items():
            if hasattr(claim, k):
                object.__setattr__(claim, k, v)
        return claim

    async def get_changelog(claim_id):
        return []

    async def log_changelog(**kwargs):
        pass

    async def browse_memories(owner_id="default", page=1, per_page=50,
                               status=None, category=None, **kwargs):
        items = []
        for c in _claims.values():
            if c.owner_id != owner_id:
                continue
            if status and c.status != status:
                continue
            if category and c.category != category:
                continue
            items.append({
                "id": c.id,
                "subject": c.subject,
                "predicate": c.predicate,
                "object_value": c.object_value,
                "status": c.status,
                "category": c.category,
                "flag_reason": c.flag_reason,
            })
        total = len(items)
        pages = max(1, (total + per_page - 1) // per_page)
        start = (page - 1) * per_page
        end = start + per_page
        return {
            "items": items[start:end],
            "total": total,
            "page": page,
            "per_page": per_page,
            "pages": pages,
            "category_counts": {},
        }

    storage.get_claim = AsyncMock(side_effect=get_claim)
    storage.update_claim = AsyncMock(side_effect=update_claim)
    storage.get_changelog = AsyncMock(side_effect=get_changelog)
    storage.log_changelog = AsyncMock(side_effect=log_changelog)
    storage.browse_memories = AsyncMock(side_effect=browse_memories)
    storage._claims = _claims
    return storage


def _make_mock_plugin(storage):
    """Create a mock plugin with storage attached."""
    plugin = MagicMock()
    plugin.storage = storage
    return plugin


# ---------------------------------------------------------------------------
# FastAPI test client setup
# ---------------------------------------------------------------------------


@pytest.fixture
def app_and_client(monkeypatch):
    """Create a FastAPI app with memory routes and TestClient."""
    from fastapi import FastAPI
    from fastapi.testclient import TestClient

    from cortex.api.routes.memory import _make_routes, router as memory_router
    from cortex.api import deps

    app = FastAPI()

    # Create claims for testing
    claims = {
        "c1": _make_claim(id="c1", subject="Andrew", predicate="prefers", object_value="dark mode"),
        "c2": _make_claim(id="c2", subject="Andrew", predicate="lives_in", object_value="Bangkok"),
        "c3": _make_claim(id="c3", subject="Andrew", predicate="uses", object_value="Python"),
        "c4": _make_claim(id="c4", subject="Andrew", predicate="likes", object_value="coffee",
                          status="needs_review", flag_reason="test_fixture"),
    }
    storage = _make_mock_storage(claims)
    plugin = _make_mock_plugin(storage)

    # Override deps
    async def mock_verify_api_key():
        return None

    async def mock_get_plugin():
        return plugin

    monkeypatch.setattr(deps, "plugin_holder", {"plugin": plugin})

    _make_routes(mock_verify_api_key)

    # Override get_plugin dependency
    from cortex.api.deps import get_plugin
    app.dependency_overrides[get_plugin] = mock_get_plugin
    app.dependency_overrides[mock_verify_api_key] = mock_verify_api_key
    app.include_router(memory_router)

    client = TestClient(app)
    return app, client, storage, claims


# ---------------------------------------------------------------------------
# Tests — Flag endpoint
# ---------------------------------------------------------------------------


class TestFlagMemory:
    def test_flag_memory_success(self, app_and_client):
        _, client, storage, claims = app_and_client
        resp = client.patch("/api/v1/memories/c1/flag", json={"reason": "test_fixture"})
        assert resp.status_code == 200
        data = resp.json()
        assert data["id"] == "c1"
        assert data["status"] == "needs_review"
        assert data["flag_reason"] == "test_fixture"
        # Verify storage was updated
        assert claims["c1"].status == "needs_review"
        assert claims["c1"].flag_reason == "test_fixture"

    def test_flag_memory_no_reason(self, app_and_client):
        _, client, storage, claims = app_and_client
        resp = client.patch("/api/v1/memories/c2/flag", json={})
        assert resp.status_code == 200
        data = resp.json()
        assert data["status"] == "needs_review"
        assert data["flag_reason"] is None

    def test_flag_memory_with_empty_body(self, app_and_client):
        """PATCH with no body should still work (reason is optional)."""
        _, client, storage, claims = app_and_client
        resp = client.patch("/api/v1/memories/c3/flag")
        assert resp.status_code == 200
        data = resp.json()
        assert data["status"] == "needs_review"

    def test_flag_nonexistent_memory(self, app_and_client):
        _, client, _, _ = app_and_client
        resp = client.patch("/api/v1/memories/nonexistent/flag", json={})
        assert resp.status_code == 404


# ---------------------------------------------------------------------------
# Tests — Approve endpoint
# ---------------------------------------------------------------------------


class TestApproveMemory:
    def test_approve_flagged_memory(self, app_and_client):
        _, client, storage, claims = app_and_client
        # c4 is already needs_review
        resp = client.patch("/api/v1/memories/c4/approve")
        assert resp.status_code == 200
        data = resp.json()
        assert data["id"] == "c4"
        assert data["status"] == "active"
        assert claims["c4"].status == "active"

    def test_approve_active_memory_409(self, app_and_client):
        _, client, _, _ = app_and_client
        resp = client.patch("/api/v1/memories/c1/approve")
        assert resp.status_code == 409

    def test_approve_nonexistent_404(self, app_and_client):
        _, client, _, _ = app_and_client
        resp = client.patch("/api/v1/memories/nonexistent/approve")
        assert resp.status_code == 404


# ---------------------------------------------------------------------------
# Tests — Reject endpoint
# ---------------------------------------------------------------------------


class TestRejectMemory:
    def test_reject_flagged_memory(self, app_and_client):
        _, client, storage, claims = app_and_client
        # c4 is needs_review
        resp = client.patch("/api/v1/memories/c4/reject")
        assert resp.status_code == 200
        data = resp.json()
        assert data["id"] == "c4"
        assert data["status"] == "rejected"
        assert claims["c4"].status == "rejected"

    def test_reject_active_memory_409(self, app_and_client):
        _, client, _, _ = app_and_client
        resp = client.patch("/api/v1/memories/c1/reject")
        assert resp.status_code == 409

    def test_reject_nonexistent_404(self, app_and_client):
        _, client, _, _ = app_and_client
        resp = client.patch("/api/v1/memories/nonexistent/reject")
        assert resp.status_code == 404


# ---------------------------------------------------------------------------
# Tests — Review queue listing
# ---------------------------------------------------------------------------


class TestReviewQueue:
    def test_review_queue_lists_flagged(self, app_and_client):
        _, client, storage, claims = app_and_client
        resp = client.get("/api/v1/memories/review-queue")
        assert resp.status_code == 200
        data = resp.json()
        assert data["total"] == 1  # only c4 is needs_review
        assert len(data["items"]) == 1
        assert data["items"][0]["id"] == "c4"
        assert data["items"][0]["status"] == "needs_review"

    def test_review_queue_empty(self, app_and_client):
        _, client, storage, claims = app_and_client
        # Approve c4 first
        claims["c4"].status = "active"
        resp = client.get("/api/v1/memories/review-queue")
        assert resp.status_code == 200
        data = resp.json()
        assert data["total"] == 0
        assert data["items"] == []

    def test_review_queue_pagination(self, app_and_client):
        _, client, storage, claims = app_and_client
        resp = client.get("/api/v1/memories/review-queue?page=1&per_page=10")
        assert resp.status_code == 200
        data = resp.json()
        assert data["page"] == 1
        assert data["per_page"] == 10


# ---------------------------------------------------------------------------
# Tests — Bulk flag endpoint
# ---------------------------------------------------------------------------


class TestBulkFlag:
    def test_bulk_flag_all_valid(self, app_and_client):
        _, client, storage, claims = app_and_client
        resp = client.post("/api/v1/memories/flag-batch", json={
            "ids": ["c1", "c2"],
            "reason": "batch_cleanup",
        })
        assert resp.status_code == 200
        data = resp.json()
        assert data["flagged"] == 2
        assert data["failed"] == []
        assert claims["c1"].status == "needs_review"
        assert claims["c2"].status == "needs_review"
        assert claims["c1"].flag_reason == "batch_cleanup"

    def test_bulk_flag_mixed_valid_invalid(self, app_and_client):
        _, client, storage, claims = app_and_client
        resp = client.post("/api/v1/memories/flag-batch", json={
            "ids": ["c1", "nonexistent"],
            "reason": "test",
        })
        assert resp.status_code == 200
        data = resp.json()
        assert data["flagged"] == 1
        assert len(data["failed"]) == 1
        assert data["failed"][0]["id"] == "nonexistent"

    def test_bulk_flag_no_reason(self, app_and_client):
        _, client, storage, claims = app_and_client
        resp = client.post("/api/v1/memories/flag-batch", json={
            "ids": ["c3"],
        })
        assert resp.status_code == 200
        data = resp.json()
        assert data["flagged"] == 1

    def test_bulk_flag_empty_ids_422(self, app_and_client):
        _, client, _, _ = app_and_client
        resp = client.post("/api/v1/memories/flag-batch", json={
            "ids": [],
        })
        assert resp.status_code == 422  # min_length=1 validation


# ---------------------------------------------------------------------------
# Tests — Status penalty in retrieval
# ---------------------------------------------------------------------------


class TestRetrievalExclusion:
    def test_needs_review_penalty_is_zero(self):
        """Verify needs_review gets penalty=0.0 in default status penalties."""
        from cortex.core.retrieval import RetrievalPipeline
        penalties = RetrievalPipeline._DEFAULT_STATUS_PENALTIES
        assert penalties["needs_review"] == 0.0

    def test_rejected_penalty_is_zero(self):
        """Verify rejected gets penalty=0.0 in default status penalties."""
        from cortex.core.retrieval import RetrievalPipeline
        penalties = RetrievalPipeline._DEFAULT_STATUS_PENALTIES
        assert penalties["rejected"] == 0.0

    def test_active_penalty_is_one(self):
        """Verify active claims are unpenalized."""
        from cortex.core.retrieval import RetrievalPipeline
        penalties = RetrievalPipeline._DEFAULT_STATUS_PENALTIES
        assert penalties["active"] == 1.0


# ---------------------------------------------------------------------------
# Tests — Schema / types
# ---------------------------------------------------------------------------


class TestSchemaUpdates:
    def test_semantic_claim_has_flag_reason(self):
        """Verify SemanticClaim dataclass includes flag_reason field."""
        claim = SemanticClaim(
            id="test",
            owner_id="default",
            subject="test",
            predicate="is",
            object_value="test",
            flag_reason="test_reason",
        )
        assert claim.flag_reason == "test_reason"

    def test_semantic_claim_flag_reason_default_none(self):
        """Verify flag_reason defaults to None."""
        claim = SemanticClaim(
            id="test",
            owner_id="default",
            subject="test",
            predicate="is",
            object_value="test",
        )
        assert claim.flag_reason is None

    def test_needs_review_is_valid_status(self):
        """Verify 'needs_review' can be set as claim status."""
        claim = SemanticClaim(
            id="test",
            owner_id="default",
            subject="test",
            predicate="is",
            object_value="test",
            status="needs_review",
        )
        assert claim.status == "needs_review"

    def test_rejected_is_valid_status(self):
        """Verify 'rejected' can be set as claim status."""
        claim = SemanticClaim(
            id="test",
            owner_id="default",
            subject="test",
            predicate="is",
            object_value="test",
            status="rejected",
        )
        assert claim.status == "rejected"


# ---------------------------------------------------------------------------
# Tests — Migration
# ---------------------------------------------------------------------------


class TestMigration:
    def test_migration_v14_exists(self):
        """Verify migration v14 module exists and has required attributes."""
        from cortex.storage.migrations import v14_review_queue
        assert v14_review_queue.VERSION == 14
        assert "flag_reason" in v14_review_queue.SQL_UP
        assert "review_queue" in v14_review_queue.SQL_UP
