"""
Tests for Peer-Circadian Integration (#169).

Verifies:
  - _dream_peer_refresh runs during dream cycle
  - Max 20 peer limit is respected
  - No crash when no peers exist
  - No crash when peer_manager is None
  - Trait change detection (_detect_trait_changes)
  - Peer refresh with claims triggers build_representation
  - Peer refresh skips peers with no active claims
  - DreamReport includes peers_refreshed and peer_changes fields
  - PeerManager.get_active_peers delegates to storage
  - get_active_peer_ids storage method (unit)
  - End-to-end dream cycle with peer integration
  - Trait detection edge cases (empty reps, partial overlap)

All storage operations use mocks — no real SQLite required.
"""

from __future__ import annotations

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

import pytest

from cortex.circadian.sleep import (
    DreamReport,
    SleepConsolidator,
)
from cortex.peers.manager import PeerManager
from cortex.types import Peer, SemanticClaim


# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------

def _now_iso() -> str:
    return datetime.now(timezone.utc).isoformat(timespec="milliseconds")


def _make_peer(
    peer_id: str = "peer-001",
    owner_id: str = "default",
    display_name: str = "Alice",
    entity_id: str = "ent-001",
    representation: Optional[dict] = None,
) -> Peer:
    rep = representation or {}
    return Peer(
        id=peer_id,
        owner_id=owner_id,
        entity_id=entity_id,
        display_name=display_name,
        relationship="friend",
        first_interaction=_now_iso(),
        last_interaction=_now_iso(),
        interaction_count=5,
        representation=json.dumps(rep),
        created_at=_now_iso(),
        updated_at=_now_iso(),
    )


def _make_claim(
    claim_id: str = "claim-001",
    subject: str = "Alice",
    predicate: str = "interested in",
    object_value: str = "machine learning",
    entity_id: Optional[str] = "ent-001",
) -> SemanticClaim:
    return SemanticClaim(
        id=claim_id,
        owner_id="default",
        subject=subject,
        predicate=predicate,
        object_value=object_value,
        confidence=0.85,
        memory_type="semantic",
        source_session="sess-001",
        created_at=_now_iso(),
        updated_at=_now_iso(),
        status="active",
        subject_entity_id=entity_id,
    )


# ---------------------------------------------------------------------------
# Mock adapters
# ---------------------------------------------------------------------------

class MockStorage:
    """Minimal mock storage for SleepConsolidator tests."""

    def __init__(self):
        self.peers: Dict[str, Peer] = {}
        self.claims: List[SemanticClaim] = []
        self._active_peer_ids: List[str] = []

    async def get_active_peer_ids(self, owner_id, since_dt, limit=20):
        return self._active_peer_ids[:limit]

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

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

    async def update_peer(self, peer_id, **kwargs):
        peer = self.peers.get(peer_id)
        if peer is None:
            raise ValueError(f"Peer not found: {peer_id}")
        for k, v in kwargs.items():
            if hasattr(peer, k):
                object.__setattr__(peer, k, v)
        return peer

    async def get_claims_by_entity(self, entity_id, owner_id=None):
        return [
            c for c in self.claims
            if getattr(c, "subject_entity_id", None) == entity_id
            and (owner_id is None or getattr(c, "owner_id", owner_id) == owner_id)
        ]

    # Stubs for SleepConsolidator that we don't test here
    async def get_orphaned_sessions(self, idle_seconds):
        return []

    async def get_claims_by_status(self, status, owner_id="default"):
        return []

    async def get_active_session(self, owner_id="default"):
        return None

    async def get_session_claims(self, session_id):
        return []

    async def get_cornerstones(self, owner_id="default"):
        return []

    async def update_session(self, session_id, **kwargs):
        pass

    async def get_pending_feedback(self, owner_id="default"):
        return []

    async def set_config(self, key, value):
        pass

    async def get_config(self, key):
        return None


class MockConfig:
    owner_id = "default"
    dream_cycle_idle_timeout_sec = 3600
    dream_decay_window_hours = 24
    promoted_confidence_default = 0.8


class MockLLM:
    async def complete(self, **kwargs):
        resp = MagicMock()
        resp.text = '{"contradictions": []}'
        return resp


def _make_consolidator(
    storage=None,
    peer_manager=None,
) -> SleepConsolidator:
    """Create a SleepConsolidator with minimal mocks."""
    storage = storage or MockStorage()
    return SleepConsolidator(
        storage=storage,
        config=MockConfig(),
        llm=MockLLM(),
        cornerstone_guardian=MagicMock(),
        drift_calculator=MagicMock(),
        deprecation_pipeline=MagicMock(),
        entity_resolver=None,
        feedback_system=None,
        peer_manager=peer_manager,
    )


# ---------------------------------------------------------------------------
# Tests
# ---------------------------------------------------------------------------


class TestDreamPeerRefresh:
    """Tests for _dream_peer_refresh stage."""

    @pytest.mark.asyncio
    async def test_peer_refresh_runs_during_dream(self):
        """Peer refresh stage executes and refreshes active peers."""
        storage = MockStorage()
        peer = _make_peer(
            peer_id="p1", entity_id="e1", display_name="Bob",
            representation={"communication_style": ["formal"]},
        )
        storage.peers["p1"] = peer
        storage._active_peer_ids = ["p1"]
        storage.claims = [
            _make_claim("c1", "Bob", "interested in", "AI", "e1"),
        ]

        pm = PeerManager(storage=storage)
        consolidator = _make_consolidator(storage=storage, peer_manager=pm)

        refreshed, changes = await consolidator._dream_peer_refresh("default")

        assert refreshed == 1

    @pytest.mark.asyncio
    async def test_max_20_peer_limit(self):
        """At most 20 peers are refreshed per dream cycle."""
        storage = MockStorage()
        # Create 25 peers
        for i in range(25):
            pid = f"p{i}"
            eid = f"e{i}"
            peer = _make_peer(peer_id=pid, entity_id=eid, display_name=f"Peer{i}")
            storage.peers[pid] = peer
            storage._active_peer_ids.append(pid)
            storage.claims.append(
                _make_claim(f"c{i}", f"Peer{i}", "likes", "coding", eid)
            )

        pm = PeerManager(storage=storage)
        consolidator = _make_consolidator(storage=storage, peer_manager=pm)

        refreshed, _ = await consolidator._dream_peer_refresh("default")

        # Should cap at 20
        assert refreshed <= 20

    @pytest.mark.asyncio
    async def test_no_crash_when_no_peers(self):
        """Dream peer refresh handles empty peer list gracefully."""
        storage = MockStorage()
        pm = PeerManager(storage=storage)
        consolidator = _make_consolidator(storage=storage, peer_manager=pm)

        refreshed, changes = await consolidator._dream_peer_refresh("default")

        assert refreshed == 0
        assert changes == []

    @pytest.mark.asyncio
    async def test_no_crash_when_peer_manager_is_none(self):
        """Dream peer refresh is skipped when peer_manager is None."""
        consolidator = _make_consolidator(peer_manager=None)

        refreshed, changes = await consolidator._dream_peer_refresh("default")

        assert refreshed == 0
        assert changes == []

    @pytest.mark.asyncio
    async def test_trait_change_detection(self):
        """Detects new traits added during refresh."""
        storage = MockStorage()
        peer = _make_peer(
            peer_id="p1", entity_id="e1", display_name="Carol",
            representation={
                "communication_style": ["formal"],
                "topics_of_interest": [],
                "preferences": [],
                "relationship_dynamics": [],
            },
        )
        storage.peers["p1"] = peer
        storage._active_peer_ids = ["p1"]
        # Claim that introduces new traits
        storage.claims = [
            _make_claim("c1", "Carol", "interested in", "robotics", "e1"),
            _make_claim("c2", "Carol", "prefers", "dark mode", "e1"),
        ]

        pm = PeerManager(storage=storage)
        consolidator = _make_consolidator(storage=storage, peer_manager=pm)

        refreshed, changes = await consolidator._dream_peer_refresh("default")

        assert refreshed == 1
        assert len(changes) == 1
        change = changes[0]
        assert change["peer_id"] == "p1"
        assert change["name"] == "Carol"
        assert len(change["changes"]) > 0

    @pytest.mark.asyncio
    async def test_skips_peers_without_claims(self):
        """Peers with no active claims are skipped."""
        storage = MockStorage()
        peer = _make_peer(peer_id="p1", entity_id="e1")
        storage.peers["p1"] = peer
        storage._active_peer_ids = ["p1"]
        # No claims for entity e1
        storage.claims = []

        pm = PeerManager(storage=storage)
        consolidator = _make_consolidator(storage=storage, peer_manager=pm)

        refreshed, changes = await consolidator._dream_peer_refresh("default")

        assert refreshed == 0
        assert changes == []

    @pytest.mark.asyncio
    async def test_dream_report_includes_peer_fields(self):
        """DreamReport dataclass has peers_refreshed and peer_changes."""
        report = DreamReport(owner_id="default")
        assert report.peers_refreshed == 0
        assert report.peer_changes == []

        report.peers_refreshed = 5
        report.peer_changes = [
            {"peer_id": "p1", "name": "Alice", "changes": ["+topics_of_interest: AI"]},
        ]
        assert report.peers_refreshed == 5
        assert len(report.peer_changes) == 1


class TestDetectTraitChanges:
    """Tests for SleepConsolidator._detect_trait_changes."""

    def test_detects_new_traits(self):
        old = {"communication_style": ["formal"], "topics_of_interest": [], "preferences": [], "relationship_dynamics": []}
        new = {"communication_style": ["formal", "direct"], "topics_of_interest": ["AI"], "preferences": [], "relationship_dynamics": []}

        changes = SleepConsolidator._detect_trait_changes(old, new)

        assert "+communication_style: direct" in changes
        assert "+topics_of_interest: ai" in changes
        assert len(changes) == 2

    def test_no_changes_when_identical(self):
        rep = {"communication_style": ["formal"], "topics_of_interest": ["AI"], "preferences": [], "relationship_dynamics": []}

        changes = SleepConsolidator._detect_trait_changes(rep, rep)

        assert changes == []

    def test_empty_old_rep(self):
        old = {}
        new = {"communication_style": ["casual"], "topics_of_interest": [], "preferences": [], "relationship_dynamics": ["friend"]}

        changes = SleepConsolidator._detect_trait_changes(old, new)

        assert "+communication_style: casual" in changes
        assert "+relationship_dynamics: friend" in changes


class TestPeerManagerGetActivePeers:
    """Tests for PeerManager.get_active_peers."""

    @pytest.mark.asyncio
    async def test_returns_active_peers(self):
        storage = MockStorage()
        peer = _make_peer(peer_id="p1", entity_id="e1", display_name="Dave")
        storage.peers["p1"] = peer
        storage._active_peer_ids = ["p1"]

        pm = PeerManager(storage=storage)
        result = await pm.get_active_peers("default", days=7, limit=20)

        assert len(result) == 1
        assert result[0]["peer_id"] == "p1"
        assert result[0]["display_name"] == "Dave"

    @pytest.mark.asyncio
    async def test_empty_when_no_active_peers(self):
        storage = MockStorage()
        pm = PeerManager(storage=storage)

        result = await pm.get_active_peers("default")

        assert result == []

    @pytest.mark.asyncio
    async def test_respects_limit(self):
        storage = MockStorage()
        for i in range(10):
            pid = f"p{i}"
            peer = _make_peer(peer_id=pid, entity_id=f"e{i}", display_name=f"P{i}")
            storage.peers[pid] = peer
            storage._active_peer_ids.append(pid)

        pm = PeerManager(storage=storage)
        result = await pm.get_active_peers("default", limit=3)

        assert len(result) == 3

    @pytest.mark.asyncio
    async def test_fallback_when_no_get_active_peer_ids(self):
        """Falls back to list_peers when storage lacks get_active_peer_ids."""

        class MinimalStorage:
            async def list_peers(self, owner_id="default"):
                return [_make_peer(peer_id="px", display_name="Fallback")]
            async def get_peer(self, peer_id):
                return _make_peer(peer_id=peer_id, display_name="Fallback")

        pm = PeerManager(storage=MinimalStorage())
        result = await pm.get_active_peers("default")

        assert len(result) == 1
        assert result[0]["display_name"] == "Fallback"


class TestCircadianEnginePeerWiring:
    """Test that CircadianEngine passes peer_manager to SleepConsolidator."""

    def test_engine_accepts_peer_manager(self):
        from cortex.circadian.engine import CircadianEngine

        mock_storage = MagicMock()
        mock_config = MockConfig()
        mock_config.session_fatigue_nap_threshold = 0.7
        mock_config.session_fatigue_sleep_threshold = 0.9
        mock_config.dream_cycle_idle_timeout_sec = 3600
        mock_config.dream_cycle_hour_utc = 8
        mock_config.dream_cycle_enabled = False
        mock_config.fatigue_interval_sec = 300

        mock_pm = MagicMock()

        engine = CircadianEngine(
            storage=mock_storage,
            config=mock_config,
            llm=MagicMock(),
            cornerstone_guardian=MagicMock(),
            peer_manager=mock_pm,
        )

        assert engine.peer_manager is mock_pm

    def test_engine_passes_peer_manager_to_consolidator(self):
        from cortex.circadian.engine import CircadianEngine

        mock_config = MockConfig()
        mock_config.session_fatigue_nap_threshold = 0.7
        mock_config.session_fatigue_sleep_threshold = 0.9
        mock_config.dream_cycle_idle_timeout_sec = 3600
        mock_config.dream_cycle_hour_utc = 8
        mock_config.dream_cycle_enabled = False
        mock_config.fatigue_interval_sec = 300

        mock_pm = MagicMock()

        engine = CircadianEngine(
            storage=MagicMock(),
            config=mock_config,
            llm=MagicMock(),
            cornerstone_guardian=MagicMock(),
            deprecation_pipeline=MagicMock(),
            drift_calculator=MagicMock(),
            peer_manager=mock_pm,
        )

        assert engine._consolidator is not None
        assert engine._consolidator.peer_manager is mock_pm
