"""
tests/test_peer_modeling.py — Peer Modeling test suite (M15).

Tests:
  1-5:  Peer CRUD (create / get / list / update / delete)
  6-7:  get_peer_by_entity
  8:    Auto-create from entity list (threshold)
  9-10: Representation building (basic + no-op on empty claims)
  11:   Interaction counting (update_interaction)
  12:   Idempotent get_or_create_peer
  13-17: HTTP endpoints (list / get / create / patch / delete / rebuild)
"""
from __future__ import annotations

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


# ---------------------------------------------------------------------------
# Minimal in-memory storage stub
# ---------------------------------------------------------------------------

class MemoryPeerStorage:
    """Pure in-memory storage stub implementing only the peer CRUD methods."""

    def __init__(self):
        self._peers: Dict[str, Any] = {}
        self._id_counter = 0

    def _next_id(self) -> str:
        self._id_counter += 1
        return f"peer{self._id_counter:04d}"

    async def create_peer(
        self,
        owner_id: str,
        display_name: str,
        entity_id: Optional[str] = None,
        relationship: str = "unknown",
        first_interaction: Optional[str] = None,
        last_interaction: Optional[str] = None,
        interaction_count: int = 1,
        representation: str = "{}",
        created_at: Optional[str] = None,
        updated_at: Optional[str] = None,
    ):
        from cortex.types import Peer
        from datetime import datetime, timezone
        now = datetime.now(timezone.utc).isoformat(timespec="milliseconds")
        peer = Peer(
            id=self._next_id(),
            owner_id=owner_id,
            entity_id=entity_id,
            display_name=display_name,
            relationship=relationship,
            first_interaction=first_interaction or now,
            last_interaction=last_interaction or now,
            interaction_count=interaction_count,
            representation=representation,
            created_at=created_at or now,
            updated_at=updated_at or now,
        )
        self._peers[peer.id] = peer
        return peer

    async def get_peer(self, peer_id: str):
        return self._peers.get(peer_id)

    async def get_peer_by_entity(self, entity_id: str):
        for p in self._peers.values():
            if p.entity_id == entity_id:
                return p
        return None

    async def list_peers(self, owner_id: str = "default"):
        return [p for p in self._peers.values() if p.owner_id == owner_id]

    async def update_peer(self, peer_id: str, **kwargs):
        from cortex.types import Peer
        p = self._peers.get(peer_id)
        if p is None:
            raise ValueError(f"Peer not found: {peer_id}")
        d = {
            "id": p.id,
            "owner_id": p.owner_id,
            "entity_id": p.entity_id,
            "display_name": p.display_name,
            "relationship": p.relationship,
            "first_interaction": p.first_interaction,
            "last_interaction": p.last_interaction,
            "interaction_count": p.interaction_count,
            "representation": p.representation,
            "created_at": p.created_at,
            "updated_at": p.updated_at,
        }
        allowed = {
            "entity_id", "display_name", "relationship",
            "first_interaction", "last_interaction", "interaction_count",
            "representation", "owner_id", "updated_at",
        }
        for k, v in kwargs.items():
            if k in allowed:
                d[k] = v
        updated = Peer(**d)
        self._peers[peer_id] = updated
        return updated

    async def delete_peer(self, peer_id: str):
        self._peers.pop(peer_id, None)

    async def list_entities(self, owner_id: str = "default", entity_type: str = None, limit: int = 500):
        return []  # stub — tests that need entities mock this separately

    async def initialize(self) -> None:
        """No-op stub — satisfies lifespan startup."""
        pass

    async def close(self) -> None:
        """No-op stub — satisfies lifespan shutdown."""
        pass


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

@pytest.fixture
def storage():
    return MemoryPeerStorage()


@pytest.fixture
def peer_manager(storage):
    from cortex.peers import PeerManager
    return PeerManager(storage=storage)


# ---------------------------------------------------------------------------
# 1. CREATE peer
# ---------------------------------------------------------------------------

@pytest.mark.asyncio
async def test_create_peer_basic(storage):
    peer = await storage.create_peer(owner_id="alice", display_name="Bob")
    assert peer.id is not None
    assert peer.display_name == "Bob"
    assert peer.owner_id == "alice"
    assert peer.interaction_count == 1
    assert peer.relationship == "unknown"


# ---------------------------------------------------------------------------
# 2. GET peer
# ---------------------------------------------------------------------------

@pytest.mark.asyncio
async def test_get_peer_returns_correct(storage):
    created = await storage.create_peer(owner_id="alice", display_name="Carol")
    fetched = await storage.get_peer(created.id)
    assert fetched is not None
    assert fetched.id == created.id
    assert fetched.display_name == "Carol"


# ---------------------------------------------------------------------------
# 3. GET peer — not found
# ---------------------------------------------------------------------------

@pytest.mark.asyncio
async def test_get_peer_not_found(storage):
    result = await storage.get_peer("nonexistent-id")
    assert result is None


# ---------------------------------------------------------------------------
# 4. LIST peers
# ---------------------------------------------------------------------------

@pytest.mark.asyncio
async def test_list_peers(storage):
    await storage.create_peer(owner_id="alice", display_name="Dave")
    await storage.create_peer(owner_id="alice", display_name="Eve")
    await storage.create_peer(owner_id="bob", display_name="Frank")  # different owner
    peers = await storage.list_peers(owner_id="alice")
    assert len(peers) == 2
    names = {p.display_name for p in peers}
    assert names == {"Dave", "Eve"}


# ---------------------------------------------------------------------------
# 5. UPDATE + DELETE peer
# ---------------------------------------------------------------------------

@pytest.mark.asyncio
async def test_update_peer(storage):
    peer = await storage.create_peer(owner_id="alice", display_name="Grace")
    updated = await storage.update_peer(peer.id, relationship="colleague", display_name="Grace H.")
    assert updated.relationship == "colleague"
    assert updated.display_name == "Grace H."


@pytest.mark.asyncio
async def test_delete_peer(storage):
    peer = await storage.create_peer(owner_id="alice", display_name="Henry")
    await storage.delete_peer(peer.id)
    assert await storage.get_peer(peer.id) is None


# ---------------------------------------------------------------------------
# 6. GET peer by entity_id
# ---------------------------------------------------------------------------

@pytest.mark.asyncio
async def test_get_peer_by_entity(storage):
    peer = await storage.create_peer(
        owner_id="alice", display_name="Iris", entity_id="ent-001"
    )
    found = await storage.get_peer_by_entity("ent-001")
    assert found is not None
    assert found.id == peer.id


@pytest.mark.asyncio
async def test_get_peer_by_entity_not_found(storage):
    result = await storage.get_peer_by_entity("ent-999")
    assert result is None


# ---------------------------------------------------------------------------
# 8. Auto-create from entity list (threshold)
# ---------------------------------------------------------------------------

@pytest.mark.asyncio
async def test_auto_create_from_entities_threshold(storage):
    from cortex.peers import PeerManager
    mgr = PeerManager(storage=storage, auto_create_threshold=3)

    @dataclass
    class FakeEntity:
        entity_type: str
        canonical_name: str
        mention_count: int
        id: str

    entities = [
        FakeEntity("person", "Alice", 5, "e1"),   # should auto-create (≥3)
        FakeEntity("person", "Bob", 2, "e2"),     # should NOT (< 3)
        FakeEntity("place", "London", 10, "e3"),  # wrong type
    ]
    created = await mgr.auto_create_from_entities(owner_id="owner1", entities=entities)
    assert len(created) == 1
    assert created[0].display_name == "Alice"


# ---------------------------------------------------------------------------
# 9. Representation building — basic extraction
# ---------------------------------------------------------------------------

@pytest.mark.asyncio
async def test_build_representation_extracts_traits(storage, peer_manager):
    peer = await storage.create_peer(owner_id="alice", display_name="Judy")

    class FakeClaim:
        def __init__(self, predicate, object_value):
            self.predicate = predicate
            self.object_value = object_value

    claims = [
        FakeClaim("communication style", "very direct and concise"),
        FakeClaim("interest in", "machine learning and AI"),
        FakeClaim("prefers", "working in the morning"),
        FakeClaim("works on", "distributed systems"),
    ]
    rep = await peer_manager.build_representation(peer.id, claims)

    assert "communication_style" in rep
    assert "topics_of_interest" in rep
    assert "preferences" in rep
    assert "last_rebuilt" in rep
    assert rep["source_claim_count"] == 4


# ---------------------------------------------------------------------------
# 10. Representation building — empty claims
# ---------------------------------------------------------------------------

@pytest.mark.asyncio
async def test_build_representation_empty_claims(storage, peer_manager):
    peer = await storage.create_peer(owner_id="alice", display_name="Karl")
    rep = await peer_manager.build_representation(peer.id, claims=[])
    assert rep["source_claim_count"] == 0
    assert isinstance(rep["communication_style"], list)
    assert isinstance(rep["topics_of_interest"], list)


# ---------------------------------------------------------------------------
# 11. Interaction counting
# ---------------------------------------------------------------------------

@pytest.mark.asyncio
async def test_update_interaction_increments_count(storage, peer_manager):
    peer = await storage.create_peer(owner_id="alice", display_name="Lena")
    assert peer.interaction_count == 1
    updated = await peer_manager.update_interaction(peer.id, session_id="sess-001")
    assert updated.interaction_count == 2
    updated2 = await peer_manager.update_interaction(peer.id)
    assert updated2.interaction_count == 3


# ---------------------------------------------------------------------------
# 12. Idempotent get_or_create_peer
# ---------------------------------------------------------------------------

@pytest.mark.asyncio
async def test_get_or_create_peer_idempotent(storage, peer_manager):
    p1 = await peer_manager.get_or_create_peer(owner_id="alice", entity_name="Mia")
    p2 = await peer_manager.get_or_create_peer(owner_id="alice", entity_name="Mia")
    assert p1.id == p2.id  # same peer returned both times


# ---------------------------------------------------------------------------
# HTTP endpoint tests (using FastAPI TestClient)
# ---------------------------------------------------------------------------

try:
    from fastapi.testclient import TestClient
    _FASTAPI_AVAILABLE = True
except ImportError:
    _FASTAPI_AVAILABLE = False


from contextlib import contextmanager
from unittest.mock import patch, MagicMock


@contextmanager
def _make_client(storage_instance):
    """Build a TestClient with our memory storage wired into the plugin mock."""
    from cortex.api.http_server import create_app
    from fastapi.testclient import TestClient

    app = create_app()

    mock_plugin = MagicMock()
    mock_plugin.storage = storage_instance
    mock_plugin.config = MagicMock()
    mock_plugin.config.owner_id = "default"

    with patch("cortex.api.plugin.CortexPlugin") as MockClass:
        MockClass.return_value = mock_plugin
        with TestClient(app, raise_server_exceptions=True) as tc:
            yield tc


@pytest.mark.skipif(not _FASTAPI_AVAILABLE, reason="fastapi not installed")
def test_http_list_peers_empty(storage):
    with _make_client(storage) as client:
        resp = client.get("/api/v1/peers")
        assert resp.status_code == 200
        data = resp.json()
        assert data["total"] == 0
        assert data["peers"] == []


@pytest.mark.skipif(not _FASTAPI_AVAILABLE, reason="fastapi not installed")
def test_http_create_peer(storage):
    with _make_client(storage) as client:
        resp = client.post("/api/v1/peers", json={"display_name": "Nora"})
        assert resp.status_code == 201
        data = resp.json()
        assert data["display_name"] == "Nora"
        assert data["id"] is not None


@pytest.mark.skipif(not _FASTAPI_AVAILABLE, reason="fastapi not installed")
@pytest.mark.asyncio
async def test_http_get_peer_detail(storage):
    peer = await storage.create_peer(owner_id="default", display_name="Oscar")
    with _make_client(storage) as client:
        resp = client.get(f"/api/v1/peers/{peer.id}")
        assert resp.status_code == 200
        assert resp.json()["display_name"] == "Oscar"


@pytest.mark.skipif(not _FASTAPI_AVAILABLE, reason="fastapi not installed")
def test_http_get_peer_not_found(storage):
    with _make_client(storage) as client:
        resp = client.get("/api/v1/peers/no-such-peer")
        assert resp.status_code == 404


@pytest.mark.skipif(not _FASTAPI_AVAILABLE, reason="fastapi not installed")
@pytest.mark.asyncio
async def test_http_patch_peer(storage):
    peer = await storage.create_peer(owner_id="default", display_name="Paula")
    with _make_client(storage) as client:
        resp = client.patch(f"/api/v1/peers/{peer.id}", json={"relationship": "friend"})
        assert resp.status_code == 200
        assert resp.json()["relationship"] == "friend"


@pytest.mark.skipif(not _FASTAPI_AVAILABLE, reason="fastapi not installed")
@pytest.mark.asyncio
async def test_http_delete_peer(storage):
    peer = await storage.create_peer(owner_id="default", display_name="Quinn")
    with _make_client(storage) as client:
        resp = client.delete(f"/api/v1/peers/{peer.id}")
        assert resp.status_code == 200
        assert resp.json()["deleted"] == peer.id
    # Confirm gone
    assert await storage.get_peer(peer.id) is None


@pytest.mark.skipif(not _FASTAPI_AVAILABLE, reason="fastapi not installed")
@pytest.mark.asyncio
async def test_http_rebuild_representation(storage):
    peer = await storage.create_peer(owner_id="default", display_name="Rita")
    with _make_client(storage) as client:
        resp = client.post(
            f"/api/v1/peers/{peer.id}/rebuild",
            json={
                "claims": [
                    {"predicate": "prefers", "object_value": "async communication"},
                    {"predicate": "interest in", "object_value": "space exploration"},
                ]
            },
        )
        assert resp.status_code == 200
        data = resp.json()
        assert data["peer_id"] == peer.id
        assert "representation" in data
