#!/usr/bin/env python3
"""
Audit: evidence_accumulator DDM scoring against the real agent_registry.json.

Verifies all five evidence streams produce correct values for agents that
appear in the production registry (state/agent_registry.json).

Real fixtures used (verified from current state/agent_registry.json):
  gemini_interactive_19156_daemon  — 7 tasks: 3 success / 4 failure, status idle
  claude_daemon                    — 4 tasks: 0 success / 4 failure, status idle
  gemini                           — 0 tasks (cold start), status idle,
                                     capabilities: ["code", "refactoring", "docs"]
  claude                           — 0 tasks, status booting
  (most others)                    — 0 tasks, status booting
"""
import json
import sys
import tempfile
import unittest
from contextlib import contextmanager
from pathlib import Path
from unittest.mock import patch

ROOT = Path(__file__).resolve().parent.parent
sys.path.insert(0, str(ROOT))

REAL_REG = ROOT / "state" / "agent_registry.json"

# Historical fixture data — agents that appeared in the registry when these
# tests were authored. Live registry changes; synthetic data stays stable.
SYNTHETIC_REGISTRY = {
    "gemini_interactive_19156_daemon": {
        "agent_id": "gemini_interactive_19156_daemon",
        "model": "gemini-pro",
        "status": "idle",
        "capabilities": [],
        "last_heartbeat": "2026-03-01T00:00:00+00:00",
        "current_task": None,
        "history": {
            "tasks_completed": 7,
            "domains": {
                "architecture": {"success": 3, "failure": 4},
            },
        },
    },
    "claude_daemon": {
        "agent_id": "claude_daemon",
        "model": "claude-opus-4-6",
        "status": "idle",
        "capabilities": [],
        "last_heartbeat": "2026-03-01T00:00:00+00:00",
        "current_task": None,
        "history": {
            "tasks_completed": 4,
            "domains": {
                "general": {"success": 0, "failure": 4},
            },
        },
    },
    "gemini": {
        "agent_id": "gemini",
        "model": "gemini-pro",
        "status": "idle",
        "capabilities": ["code", "refactoring", "docs"],
        "last_heartbeat": "2026-03-01T00:00:00+00:00",
        "current_task": None,
        "history": {"tasks_completed": 0, "domains": {}},
    },
    "claude": {
        "agent_id": "claude",
        "model": "claude-sonnet-4-6",
        "status": "booting",
        "capabilities": [],
        "last_heartbeat": "2026-03-01T00:00:00+00:00",
        "current_task": None,
        "history": {"tasks_completed": 0, "domains": {}},
    },
}


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

def _load_real_registry() -> dict:
    if REAL_REG.exists():
        return json.loads(REAL_REG.read_text(encoding="utf-8"))
    return {}


@contextmanager
def _patch_state(tmp_path: Path, registry: dict):
    """Point evidence_accumulator at tmp_path containing the given registry."""
    reg_file = tmp_path / "agent_registry.json"
    reg_file.write_text(json.dumps(registry), encoding="utf-8")
    with patch("pulse.tasks.evidence_accumulator.get_state_dir",
               return_value=tmp_path):
        yield


# ---------------------------------------------------------------------------
# Sanity: registry is loadable
# ---------------------------------------------------------------------------

class TestRealRegistryLoad(unittest.TestCase):
    """The real registry file must parse cleanly."""

    def test_registry_exists_and_is_valid_json(self):
        self.assertTrue(REAL_REG.exists(), f"Missing {REAL_REG}")
        data = _load_real_registry()
        self.assertIsInstance(data, dict)
        self.assertGreater(len(data), 0)

    def test_known_agents_present(self):
        data = _load_real_registry()
        for agent_id in ("gemini", "claude"):
            self.assertIn(agent_id, data, f"Expected agent missing: {agent_id}")

    def test_gemini_interactive_fixture_values(self):
        """Confirm synthetic fixture values used in later stream tests."""
        agent = SYNTHETIC_REGISTRY["gemini_interactive_19156_daemon"]
        domains = agent["history"]["domains"]
        arch = domains["architecture"]
        self.assertEqual(arch["success"], 3)
        self.assertEqual(arch["failure"], 4)
        self.assertEqual(agent["status"], "idle")

    def test_claude_daemon_fixture_values(self):
        agent = SYNTHETIC_REGISTRY["claude_daemon"]
        domains = agent["history"]["domains"]
        general = domains["general"]
        self.assertEqual(general["success"], 0)
        self.assertEqual(general["failure"], 4)
        self.assertEqual(agent["status"], "idle")

    def test_gemini_fixture_values(self):
        data = _load_real_registry()
        agent = data["gemini"]
        self.assertEqual(agent["history"]["tasks_completed"], 0)
        self.assertEqual(agent["status"], "idle")
        self.assertIn("code", agent["capabilities"])
        self.assertIn("refactoring", agent["capabilities"])
        self.assertIn("docs", agent["capabilities"])


# ---------------------------------------------------------------------------
# Stream 3: success rate
# ---------------------------------------------------------------------------

class TestSuccessRateStream(unittest.TestCase):
    """_get_agent_success_rate reads 'success'/'failure' keys from real domains."""

    @classmethod
    def setUpClass(cls):
        cls.registry = _load_real_registry()

    def _rate(self, tmp, agent_id: str, registry=None) -> float:
        from pulse.tasks.evidence_accumulator import _get_agent_success_rate
        reg = registry if registry is not None else self.registry
        with _patch_state(Path(tmp), reg):
            return _get_agent_success_rate(agent_id)

    def test_gemini_interactive_19156_rate(self):
        """3 wins / (3+4) total = 3/7 ≈ 0.429."""
        with tempfile.TemporaryDirectory() as tmp:
            rate = self._rate(tmp, "gemini_interactive_19156_daemon", SYNTHETIC_REGISTRY)
        self.assertAlmostEqual(rate, round(3 / 7, 3), places=2)

    def test_claude_daemon_zero_rate(self):
        """0 wins / 4 total = 0.0 (not cold start — 4 tasks completed)."""
        with tempfile.TemporaryDirectory() as tmp:
            rate = self._rate(tmp, "claude_daemon", SYNTHETIC_REGISTRY)
        self.assertEqual(rate, 0.0)

    def test_gemini_cold_start_returns_half(self):
        """gemini has 0 tasks_completed → cold start → 0.5."""
        with tempfile.TemporaryDirectory() as tmp:
            rate = self._rate(tmp, "gemini")
        self.assertEqual(rate, 0.5)

    def test_all_cold_start_agents_return_half(self):
        """Every agent with tasks_completed < 3 must return 0.5."""
        cold = [
            aid for aid, ag in self.registry.items()
            if ag.get("history", {}).get("tasks_completed", 0) < 3
        ]
        self.assertGreater(len(cold), 0, "Need at least one cold-start agent")
        with tempfile.TemporaryDirectory() as tmp:
            for agent_id in cold:
                rate = self._rate(tmp, agent_id)
                self.assertEqual(
                    rate, 0.5,
                    f"{agent_id} (cold start) must return 0.5, got {rate}"
                )

    def test_experienced_agents_are_not_half(self):
        """Agents with ≥ 3 completed tasks must not always return 0.5."""
        experienced = [
            aid for aid, ag in SYNTHETIC_REGISTRY.items()
            if ag.get("history", {}).get("tasks_completed", 0) >= 3
        ]
        self.assertGreater(len(experienced), 0, "Need at least one experienced agent")
        with tempfile.TemporaryDirectory() as tmp:
            rates = [self._rate(tmp, aid, SYNTHETIC_REGISTRY) for aid in experienced]
        # At least one should differ from 0.5 (e.g. gemini_interactive=0.429, claude=0.0)
        self.assertTrue(
            any(r != 0.5 for r in rates),
            "Experienced agents must have non-default success rates"
        )


# ---------------------------------------------------------------------------
# Stream 4: availability
# ---------------------------------------------------------------------------

class TestAvailabilityStream(unittest.TestCase):
    """_get_availability_score reads 'status' from real registry."""

    @classmethod
    def setUpClass(cls):
        cls.registry = _load_real_registry()

    def _avail(self, tmp, agent_id: str) -> float:
        from pulse.tasks.evidence_accumulator import _get_availability_score
        with _patch_state(Path(tmp), self.registry):
            return _get_availability_score(agent_id)

    def test_idle_agents_score_1_0(self):
        idle = [aid for aid, ag in self.registry.items()
                if ag.get("status") == "idle"]
        self.assertGreater(len(idle), 0, "Need at least one idle agent")
        with tempfile.TemporaryDirectory() as tmp:
            for agent_id in idle:
                score = self._avail(tmp, agent_id)
                self.assertEqual(
                    score, 1.0,
                    f"Idle agent '{agent_id}' must score 1.0, got {score}"
                )

    def test_booting_agents_score_half(self):
        booting = [aid for aid, ag in self.registry.items()
                   if ag.get("status") == "booting"]
        self.assertGreater(len(booting), 0, "Need at least one booting agent")
        with tempfile.TemporaryDirectory() as tmp:
            for agent_id in booting:
                score = self._avail(tmp, agent_id)
                self.assertEqual(
                    score, 0.5,
                    f"Booting agent '{agent_id}' must score 0.5, got {score}"
                )

    def test_gemini_interactive_19156_idle(self):
        with tempfile.TemporaryDirectory() as tmp:
            score = self._avail(tmp, "gemini_interactive_19156_daemon")
        self.assertEqual(score, 1.0)

    def test_claude_booting_gives_half(self):
        self.assertEqual(self.registry["claude"]["status"], "booting")
        with tempfile.TemporaryDirectory() as tmp:
            score = self._avail(tmp, "claude")
        self.assertEqual(score, 0.5)


# ---------------------------------------------------------------------------
# Stream 5: capability overlap
# ---------------------------------------------------------------------------

class TestCapabilityStream(unittest.TestCase):
    """_get_capability_overlap reads 'capabilities' list from real registry."""

    @classmethod
    def setUpClass(cls):
        cls.registry = _load_real_registry()

    def _cap(self, tmp, agent_id: str, required: set) -> float:
        from pulse.tasks.evidence_accumulator import _get_capability_overlap
        with _patch_state(Path(tmp), self.registry):
            return _get_capability_overlap(agent_id, required)

    def test_empty_required_returns_one(self):
        """No requirements → any agent scores 1.0."""
        with tempfile.TemporaryDirectory() as tmp:
            for agent_id in list(self.registry)[:5]:
                score = self._cap(tmp, agent_id, set())
                self.assertEqual(score, 1.0,
                                 f"{agent_id}: empty required must return 1.0")

    def test_gemini_full_code_overlap(self):
        """gemini has 'code' → full overlap with {'code'}."""
        with tempfile.TemporaryDirectory() as tmp:
            score = self._cap(tmp, "gemini", {"code"})
        self.assertEqual(score, 1.0)

    def test_gemini_full_triple_overlap(self):
        """gemini has all three → full overlap with {'code','refactoring','docs'}."""
        with tempfile.TemporaryDirectory() as tmp:
            score = self._cap(tmp, "gemini", {"code", "refactoring", "docs"})
        self.assertEqual(score, 1.0)

    def test_gemini_partial_overlap(self):
        """gemini lacks 'security' → 0.5 overlap with {'code','security'}."""
        with tempfile.TemporaryDirectory() as tmp:
            score = self._cap(tmp, "gemini", {"code", "security"})
        self.assertAlmostEqual(score, 0.5, places=2)

    def test_gemini_zero_overlap(self):
        """gemini has no 'java'/'kubernetes' → 0.0 overlap."""
        with tempfile.TemporaryDirectory() as tmp:
            score = self._cap(tmp, "gemini", {"java", "kubernetes"})
        self.assertEqual(score, 0.0)

    def test_empty_capabilities_agents_return_half(self):
        """Agents with empty capabilities list return 0.5 (unknown match)."""
        no_caps = [aid for aid, ag in self.registry.items()
                   if not ag.get("capabilities")]
        self.assertGreater(len(no_caps), 0)
        with tempfile.TemporaryDirectory() as tmp:
            for agent_id in no_caps:
                score = self._cap(tmp, agent_id, {"python"})
                self.assertEqual(
                    score, 0.5,
                    f"{agent_id} empty caps must return 0.5, got {score}"
                )


# ---------------------------------------------------------------------------
# Full score_agent_for_task against all real agents
# ---------------------------------------------------------------------------

class TestFullScoreAllRealAgents(unittest.TestCase):
    """score_agent_for_task must not raise for any agent in the real registry."""

    @classmethod
    def setUpClass(cls):
        cls.registry = _load_real_registry()

    def test_all_agents_general_domain(self):
        from pulse.tasks.evidence_accumulator import score_agent_for_task
        task = {"domain": "general", "required_capabilities": []}
        with tempfile.TemporaryDirectory() as tmp:
            with _patch_state(Path(tmp), self.registry):
                for agent_id in self.registry:
                    scores = score_agent_for_task(agent_id, task)
                    self.assertIn("total", scores, f"{agent_id} missing total")
                    self.assertGreaterEqual(scores["total"], 0.0,
                                           f"{agent_id} negative total")
                    self.assertLessEqual(scores["total"], 1.0,
                                        f"{agent_id} total > 1.0")

    def test_all_streams_present(self):
        from pulse.tasks.evidence_accumulator import score_agent_for_task
        expected = {"hebbian", "competence", "success_rate", "availability",
                    "capability", "total"}
        task = {"domain": "testing", "required_capabilities": ["python"]}
        with tempfile.TemporaryDirectory() as tmp:
            with _patch_state(Path(tmp), self.registry):
                scores = score_agent_for_task("gemini_interactive_19156_daemon", task)
        self.assertEqual(set(scores.keys()), expected)

    def test_idle_beats_booting_cold_start(self):
        """Idle cold-start agent must outscore booting cold-start agent (availability diff)."""
        from pulse.tasks.evidence_accumulator import score_agent_for_task
        idle_cold = next(
            (aid for aid, ag in self.registry.items()
             if ag.get("status") == "idle"
             and ag.get("history", {}).get("tasks_completed", 0) < 3),
            None
        )
        boot_cold = next(
            (aid for aid, ag in self.registry.items()
             if ag.get("status") == "booting"
             and ag.get("history", {}).get("tasks_completed", 0) < 3),
            None
        )
        if idle_cold is None or boot_cold is None:
            self.skipTest("Need both idle and booting cold-start agents")

        task = {"domain": "general", "required_capabilities": []}
        with tempfile.TemporaryDirectory() as tmp:
            with _patch_state(Path(tmp), self.registry):
                idle_sc = score_agent_for_task(idle_cold, task)
                boot_sc = score_agent_for_task(boot_cold, task)

        self.assertGreater(idle_sc["availability"], boot_sc["availability"],
                           "Idle must have higher availability than booting")
        self.assertGreater(idle_sc["total"], boot_sc["total"],
                           f"Idle {idle_cold} total must beat booting {boot_cold}")

    def test_weights_sum_to_one(self):
        """DDM stream weights 0.30+0.20+0.20+0.15+0.15 must equal 1.0."""
        self.assertAlmostEqual(0.30 + 0.20 + 0.20 + 0.15 + 0.15, 1.0, places=10)


# ---------------------------------------------------------------------------
# select_best_agent with real candidates
# ---------------------------------------------------------------------------

class TestDDMSelectionRealCandidates(unittest.TestCase):
    """select_best_agent correctness with real registry agents."""

    @classmethod
    def setUpClass(cls):
        cls.registry = _load_real_registry()

    def test_gemini_interactive_beats_claude_daemon(self):
        """Agent with 3/7 success rate must beat agent with 0/4 success rate."""
        from pulse.tasks.evidence_accumulator import select_best_agent
        candidates = ["gemini_interactive_19156_daemon", "claude_daemon"]
        task = {"domain": "architecture", "required_capabilities": []}

        with tempfile.TemporaryDirectory() as tmp:
            with _patch_state(Path(tmp), self.registry):
                best, scores = select_best_agent(candidates, task)

        self.assertEqual(
            best, "gemini_interactive_19156_daemon",
            "Agent with 3/7 success rate must beat agent with 0/4 success rate"
        )
        self.assertGreaterEqual(scores["total"], 0)

    def test_select_returns_valid_agent_from_idle_pool(self):
        """select_best_agent always returns an agent from the candidate list."""
        from pulse.tasks.evidence_accumulator import select_best_agent
        idle = [aid for aid, ag in self.registry.items()
                if ag.get("status") == "idle"]
        task = {"domain": "general", "required_capabilities": []}

        with tempfile.TemporaryDirectory() as tmp:
            with _patch_state(Path(tmp), self.registry):
                best, scores = select_best_agent(idle, task)

        self.assertIn(best, idle, "Result must be one of the input candidates")
        self.assertIn("total", scores)

    def test_urgent_task_instant_assignment(self):
        """Urgent tasks bypass threshold and return a valid agent instantly."""
        from pulse.tasks.evidence_accumulator import select_best_agent
        candidates = list(self.registry.keys())[:4]
        task = {"domain": "general", "required_capabilities": [],
                "priority": "urgent"}

        with tempfile.TemporaryDirectory() as tmp:
            with _patch_state(Path(tmp), self.registry):
                best, scores = select_best_agent(candidates, task)

        self.assertIn(best, candidates)
        self.assertIn("total", scores)

    def test_single_candidate_returns_immediately(self):
        """Single candidate: select_best_agent returns it without comparison."""
        from pulse.tasks.evidence_accumulator import select_best_agent
        task = {"domain": "testing", "required_capabilities": []}

        with tempfile.TemporaryDirectory() as tmp:
            with _patch_state(Path(tmp), self.registry):
                best, scores = select_best_agent(
                    ["gemini_interactive_19156_daemon"], task
                )

        self.assertEqual(best, "gemini_interactive_19156_daemon")
        self.assertIn("total", scores)

    def test_empty_candidates_returns_empty(self):
        from pulse.tasks.evidence_accumulator import select_best_agent
        best, scores = select_best_agent([], {"domain": "x"})
        self.assertEqual(best, "")
        self.assertEqual(scores, {})

    def test_numeric_total_for_gemini_interactive(self):
        """Spot-check numeric total for gemini_interactive_19156_daemon.

        With no hebbian/metamemory data:
          hebbian=0.5, competence=0.5, success_rate=3/7≈0.429,
          availability=1.0 (idle), capability=1.0 (no required)
          total = 0.30*0.5 + 0.20*0.5 + 0.20*(3/7) + 0.15*1.0 + 0.15*1.0
                = 0.15 + 0.10 + 0.0857 + 0.15 + 0.15 ≈ 0.636
        """
        from pulse.tasks.evidence_accumulator import score_agent_for_task
        task = {"domain": "architecture", "required_capabilities": []}
        with tempfile.TemporaryDirectory() as tmp:
            with _patch_state(Path(tmp), SYNTHETIC_REGISTRY):
                scores = score_agent_for_task("gemini_interactive_19156_daemon", task)

        # success_rate stream must reflect 3/7
        self.assertAlmostEqual(scores["success_rate"], round(3 / 7, 3), places=2)
        # availability: idle → 1.0
        self.assertEqual(scores["availability"], 1.0)
        # capability: no required → 1.0
        self.assertEqual(scores["capability"], 1.0)
        # total lower bound (even if hebbian/competence vary)
        self.assertGreater(scores["total"], 0.4)

    def test_numeric_total_for_claude_daemon(self):
        """Spot-check numeric total for claude_daemon.

        success_rate = 0.0, availability = 1.0 (idle), capability = 1.0
        total ≥ 0.30*0 + 0.20*0 + 0.20*0.0 + 0.15*1.0 + 0.15*1.0 = 0.30
        """
        from pulse.tasks.evidence_accumulator import score_agent_for_task
        task = {"domain": "general", "required_capabilities": []}
        with tempfile.TemporaryDirectory() as tmp:
            with _patch_state(Path(tmp), SYNTHETIC_REGISTRY):
                scores = score_agent_for_task("claude_daemon", task)

        self.assertEqual(scores["success_rate"], 0.0)
        self.assertEqual(scores["availability"], 1.0)
        self.assertGreaterEqual(scores["total"], 0.30)


if __name__ == "__main__":
    unittest.main(verbosity=2)
