#!/usr/bin/env python3
"""Tests for Phase 2 AGI Refinements — Infrastructure Wiring.

Covers:
1. Cognitive load gating (tier-dependent boot briefing)
2. Reward signal unification (single pipeline)
3. Embedding search wired into brain_search
4. RPE-proportional reward propagation
5. Domain classification confidence gate
6. LLM extraction module
7. Synaptic homeostasis stage
8. Reactivation-gated reconsolidation
"""
import unittest
import sys
from pathlib import Path

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


class TestCognitiveLoadGating(unittest.TestCase):
    """Boot briefing should vary by tier."""

    def test_tier_limits_exist_in_boot(self):
        from pulse.brain.pulse_boot import boot
        # Just verify boot runs for each tier without error
        for tier in ["fast", "standard", "premium"]:
            ctx = boot(f"test_{tier}", tier)
            self.assertIsInstance(ctx, str)
            self.assertGreater(len(ctx), 100)

    def test_fast_smaller_than_premium(self):
        from pulse.brain.pulse_boot import boot
        fast_ctx = boot("test_fast", "fast")
        prem_ctx = boot("test_prem", "premium")
        # Premium should have more context (or equal if brain is small)
        self.assertGreaterEqual(len(prem_ctx), len(fast_ctx))

    def test_auto_detect_tier(self):
        from pulse.brain.pulse_boot import auto_detect_tier
        self.assertEqual(auto_detect_tier("opus"), "premium")
        self.assertEqual(auto_detect_tier("claude-sonnet-4-6"), "standard")
        self.assertEqual(auto_detect_tier("haiku"), "fast")
        self.assertEqual(auto_detect_tier("unknown-model"), "standard")


class TestRewardSignalUnification(unittest.TestCase):
    """All rewards should flow through one pipeline."""

    def test_single_reward_constants(self):
        from pulse.core.reward.reward_signal import REINFORCE_DELTA, CONTRADICT_DELTA
        # Loss aversion from Phase 1
        self.assertGreater(CONTRADICT_DELTA, REINFORCE_DELTA)

    def test_prediction_feedback_uses_reward_signal(self):
        """prediction_feedback should import from reward_signal (not its own _record_reward)."""
        import inspect
        from pulse.daemons.neural import prediction_feedback
        source = inspect.getsource(prediction_feedback.record_prediction_outcome)
        self.assertIn("record_task_outcome", source,
                       "prediction_feedback should call reward_signal.record_task_outcome")

    def test_rpe_computation(self):
        from pulse.core.reward.reward_signal import compute_rpe, assign_credit
        rpe = compute_rpe(success=True, expected_success_rate=0.3)
        self.assertGreater(rpe, 0.5)
        credits = assign_credit(["a", "b", "c"], rpe)
        self.assertGreater(credits["a"], credits["b"])
        self.assertGreater(credits["b"], credits["c"])

    def test_proportional_scaling(self):
        """High-credit items should get bigger deltas than low-credit items."""
        from pulse.core.reward.reward_signal import REINFORCE_DELTA
        import math
        # High credit
        high_mag = 0.8
        high_delta = round(REINFORCE_DELTA * min(high_mag * 2, 1.5), 3)
        # Low credit
        low_mag = 0.1
        low_delta = round(REINFORCE_DELTA * min(low_mag * 2, 1.5), 3)
        self.assertGreater(high_delta, low_delta)


class TestEmbeddingSearch(unittest.TestCase):
    """Embedding search should be wired into brain_search."""

    def test_embedding_source_in_search(self):
        """brain_search should attempt embedding search (even if index missing)."""
        import inspect
        from pulse.brain import brain_search
        source = inspect.getsource(brain_search.brain_search)
        self.assertIn("search_similar", source,
                       "brain_search should call rule_embeddings.search_similar")
        self.assertIn("embeddings", source,
                       "brain_search should have 'embeddings' source key")

    def test_embedding_infrastructure(self):
        from pulse.brain.index.rule_embeddings import (
            embed_texts, search_similar, build_rule_index,
            _cosine_similarity, EMBED_INDEX_FILE
        )
        self.assertTrue(callable(embed_texts))
        self.assertTrue(callable(search_similar))
        self.assertTrue(callable(build_rule_index))
        # Cosine similarity basic check
        sim = _cosine_similarity([1, 0, 0], [1, 0, 0])
        self.assertAlmostEqual(sim, 1.0)
        sim_ortho = _cosine_similarity([1, 0, 0], [0, 1, 0])
        self.assertAlmostEqual(sim_ortho, 0.0)


class TestDomainClassification(unittest.TestCase):
    """Domain classification should require 2+ keyword matches."""

    def test_single_keyword_returns_general(self):
        from pulse.brain.extraction.wants_classify import _classify_domain
        # Single keyword match should be "general" (not a false domain)
        self.assertEqual(_classify_domain("bridge construction"), "general")
        self.assertEqual(_classify_domain("test something"), "general")

    def test_multiple_keywords_classify_correctly(self):
        from pulse.brain.extraction.wants_classify import _classify_domain
        self.assertEqual(_classify_domain("brain memory pattern"), "neural_infra")
        self.assertEqual(_classify_domain("deploy commit push"), "deployment")
        self.assertEqual(_classify_domain("test verify check"), "testing")

    def test_no_keywords_returns_general(self):
        from pulse.brain.extraction.wants_classify import _classify_domain
        self.assertEqual(_classify_domain("random text here"), "general")
        self.assertEqual(_classify_domain(""), "general")


class TestLLMExtraction(unittest.TestCase):
    """LLM extraction module should exist and be importable."""

    def test_module_importable(self):
        from pulse.daemons.bridge.llm_extraction import extract_with_llm
        self.assertTrue(callable(extract_with_llm))

    def test_bridge_uses_llm_extraction(self):
        import inspect
        from pulse.daemons.bridge import pulse_bridge
        source = inspect.getsource(pulse_bridge.PulseBridge._flush_interaction)
        self.assertIn("extract_with_llm", source,
                       "Bridge should call extract_with_llm for low-yield interactions")
        self.assertIn("regex_yield", source,
                       "Bridge should check regex yield before LLM fallback")


class TestSynapticHomeostasis(unittest.TestCase):
    """Homeostasis stage should be wired into consolidation."""

    def test_stage_importable(self):
        from pulse.daemons.consolidation.consolidation_stages import stage_synaptic_homeostasis
        self.assertTrue(callable(stage_synaptic_homeostasis))

    def test_wired_into_daemon(self):
        import inspect
        import pulse.daemons.consolidation.consolidation_daemon as daemon
        source = inspect.getsource(daemon)
        self.assertIn("stage_synaptic_homeostasis", source,
                       "Homeostasis must be wired into consolidation daemon")
        self.assertIn("homeostasis_result", source)


class TestReactivationGate(unittest.TestCase):
    """Reconsolidation should only process retrieved items."""

    def test_stage_references_retrieval_metrics(self):
        import inspect
        from pulse.daemons.consolidation.consolidation_stages import stage_reconsolidation
        source = inspect.getsource(stage_reconsolidation)
        self.assertIn("RETRIEVAL_METRICS", source,
                       "Reconsolidation must check retrieval metrics for reactivation gate")
        self.assertIn("retrieved_keys", source,
                       "Reconsolidation must filter by retrieved keys")

    def test_deep_consolidation_unified(self):
        from pulse.daemons.consolidation.consolidation_stages import stage_deep_consolidation
        from pulse.daemons.consolidation.deep_consolidation import deep_consolidation_cycle
        self.assertIs(deep_consolidation_cycle, stage_deep_consolidation,
                      "deep_consolidation should re-export from stages")


class TestGraphUnification(unittest.TestCase):
    """Knowledge graph should use SQLite as primary store."""

    def test_sqlite_first(self):
        from pulse.graph.knowledge_graph_query import get_graph_stats
        stats = get_graph_stats()
        self.assertIn("storage", stats)
        # If SQLite has data, it should be the primary
        if stats["total_nodes"] > 0:
            self.assertEqual(stats["storage"], "sqlite")

    def test_all_query_functions(self):
        from pulse.graph.knowledge_graph_query import (
            query_causes, query_prevents, query_related,
            query_chain, get_hub_nodes, get_graph_stats
        )
        for fn in [query_causes, query_prevents, query_related,
                   query_chain, get_hub_nodes, get_graph_stats]:
            self.assertTrue(callable(fn))


class TestSharedJSONLLoader(unittest.TestCase):
    """Shared JSONL title extractor should work for both neural and boot."""

    def test_shared_helper_exists(self):
        from pulse.core.brain_utils import _extract_jsonl_titles, LESSONS_JSONL
        items = _extract_jsonl_titles(LESSONS_JSONL, max_chars=80)
        self.assertIsInstance(items, list)
        if items:
            self.assertIn("title", items[0])
            self.assertIn("confidence", items[0])

    def test_neural_render_uses_shared(self):
        from pulse.daemons.neural.neural_render import _compress_learnings
        result = _compress_learnings()
        self.assertIsInstance(result, list)

    def test_boot_loader_uses_shared(self):
        from pulse.brain.boot.loaders import _load_jsonl_titles
        from pulse.core.brain_utils import LESSONS_JSONL
        result = _load_jsonl_titles(LESSONS_JSONL, limit=3)
        self.assertIsInstance(result, list)


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