#!/usr/bin/env python3
# Copyright (c) 2026 PULSE Contributors. Proprietary. All rights reserved.
"""Integration tests for P44 HIGH-priority bugs H23-H30."""
import json
import threading
import time
import unittest
from unittest.mock import patch


# ---- H23: Domain label inconsistency ----
class TestH23MetamemoryLevels(unittest.TestCase):
    LEVELS = ["expert", "competent", "novice", "blind_spot"]
    BAD_LEVELS = ["proficient", "intermediate"]

    def test_decay_uses_correct_levels(self):
        import inspect
        from pulse.brain import metamemory_tracking
        src = inspect.getsource(metamemory_tracking._decay_domain_expertise)
        for lvl in self.LEVELS:
            self.assertIn(lvl, src)
        for bad in self.BAD_LEVELS:
            self.assertNotIn(bad, src)

    def test_compute_metamemory_levels(self):
        import inspect
        from pulse.brain import metamemory
        src = inspect.getsource(metamemory.compute_metamemory)
        for lvl in self.LEVELS:
            self.assertIn(lvl, src)


# ---- H24: init_db failure permanent ----
class TestH24BrainDbRetry(unittest.TestCase):
    def test_flag_stays_false_on_failure(self):
        import sqlite3
        import pulse.core.brain_db as bd
        orig_flag, orig_init = bd._db_initialized, bd.init_db
        calls = [0]
        def fail_init():
            calls[0] += 1
            raise RuntimeError("simulated init failure")
        try:
            bd._db_initialized = False
            bd.init_db = fail_init
            if not hasattr(bd._local, "conn"):
                c = sqlite3.connect(":memory:")
                c.row_factory = sqlite3.Row
                bd._local.conn = c
            with self.assertRaises(RuntimeError):
                bd.get_conn()
            self.assertFalse(bd._db_initialized)
            self.assertEqual(calls[0], 1)
            with self.assertRaises(RuntimeError):
                bd.get_conn()
            self.assertEqual(calls[0], 2)
        finally:
            bd.init_db = orig_init
            bd._db_initialized = orig_flag

    def test_flag_true_on_success(self):
        import sqlite3
        import pulse.core.brain_db as bd
        orig_flag, orig_init = bd._db_initialized, bd.init_db
        try:
            bd._db_initialized = False
            bd.init_db = lambda: None
            if not hasattr(bd._local, "conn"):
                c = sqlite3.connect(":memory:")
                c.row_factory = sqlite3.Row
                bd._local.conn = c
            bd.get_conn()
            self.assertTrue(bd._db_initialized)
        finally:
            bd.init_db = orig_init
            bd._db_initialized = orig_flag


# ---- H25: Thread-unsafe STDP annealing ----
class TestH25StdpThreadSafety(unittest.TestCase):
    def test_lock_exists(self):
        from pulse.brain import stdp_annealing
        self.assertTrue(hasattr(stdp_annealing, "_anneal_lock"))

    def test_concurrent_no_corruption(self):
        import pulse.brain.plasticity.stdp as _stdp
        from pulse.brain.plasticity.stdp_annealing import apply_stdp_with_annealing
        orig_ltp, orig_ltd = _stdp.LTP_DELTA, _stdp.LTD_DELTA
        errors = []
        def worker(success, domain):
            try:
                apply_stdp_with_annealing("sess-" + domain, success, domain=domain)
            except Exception as exc:
                errors.append(str(exc))
        t1 = threading.Thread(target=worker, args=(True, "dom_a"))
        t2 = threading.Thread(target=worker, args=(False, "dom_b"))
        t1.start(); t2.start()
        t1.join(5); t2.join(5)
        self.assertEqual(errors, [], f"Thread errors: {errors}")
        self.assertAlmostEqual(_stdp.LTP_DELTA, orig_ltp, places=6)
        self.assertAlmostEqual(_stdp.LTD_DELTA, orig_ltd, places=6)


# ---- H26: prune_file missing ----
class TestH26PruneFileAlias(unittest.TestCase):
    def test_prune_file_importable(self):
        from pulse.admin.brain_ops.pulse_prune import prune_file
        self.assertTrue(callable(prune_file))

    def test_brain_files_importable(self):
        from pulse.admin.brain_ops.pulse_prune import BRAIN_FILES
        self.assertIsInstance(BRAIN_FILES, list)
        self.assertGreater(len(BRAIN_FILES), 0)

    def test_prune_file_is_alias(self):
        from pulse.admin.brain_ops.pulse_prune import prune_file, prune_jsonl
        self.assertIs(prune_file, prune_jsonl)


# ---- H27: Emotional enhancement dead ----
class TestH27EmotionalIntensity(unittest.TestCase):
    def test_append_knowledge_md_accepts_provenance(self):
        import inspect
        from pulse.brain.save_writers.writers import append_knowledge_md
        self.assertIn("provenance", inspect.signature(append_knowledge_md).parameters)

    def test_emotional_intensity_in_provenance(self):
        captured = {}
        def mock_append(fp, text, agent, ts, domain, confidence,
                        salience=1.0, dry_run=False, provenance=None):
            if provenance:
                captured.update(provenance)
        with patch("pulse.brain.save_writers.writers.append_knowledge_md",
                   side_effect=mock_append):
            with patch("pulse.core.brain_analysis.compute_item_salience",
                       return_value=2.0):
                with patch("pulse.core.brain_analysis.compute_item_confidence",
                           return_value=0.7):
                    from pulse.brain.save_writers.writers import _write_entries_to_md
                    from pulse.core.brain_utils import LESSONS_FILE
                    _write_entries_to_md(
                        LESSONS_FILE, "Lessons", "desc",
                        ["CRITICAL FAILURE: everything crashed!!! Totally broken!"],
                        "test_agent",
                    )
        self.assertIn("emotional_intensity", captured)


# ---- H28: Reward momentum biology ----
class TestH28RewardMomentumBiology(unittest.TestCase):
    def setUp(self):
        from pulse.core.reward import reward_momentum
        reward_momentum._CONSECUTIVE_SUCCESSES.clear()

    def test_five_successes_reduce_rpe(self):
        from pulse.core.reward.reward_momentum import record_success_momentum
        total = sum(record_success_momentum("dom", True) for _ in range(5))
        self.assertLessEqual(total, 0.0,
                             f"5 consecutive successes must yield <=0 RPE delta, got {total}")

    def test_failure_after_streak_negative(self):
        from pulse.core.reward.reward_momentum import (
            record_success_momentum, _CONSECUTIVE_SUCCESSES)
        _CONSECUTIVE_SUCCESSES.clear()
        for _ in range(5):
            record_success_momentum("streak_dom", True)
        delta = record_success_momentum("streak_dom", False)
        self.assertLess(delta, 0.0, f"Failure after streak must be <0, got {delta}")

    def test_isolated_failure_zero(self):
        from pulse.core.reward.reward_momentum import (
            record_success_momentum, _CONSECUTIVE_SUCCESSES)
        _CONSECUTIVE_SUCCESSES.clear()
        self.assertEqual(record_success_momentum("fresh", False), 0.0)

    def test_streak_resets_after_failure(self):
        from pulse.core.reward.reward_momentum import (
            record_success_momentum, get_domain_streak, _CONSECUTIVE_SUCCESSES)
        _CONSECUTIVE_SUCCESSES.clear()
        for _ in range(3):
            record_success_momentum("x", True)
        record_success_momentum("x", False)
        self.assertEqual(get_domain_streak("x"), 0)


# ---- H29: prediction_feedback path ----
class TestH29PredictionFeedbackPath(unittest.TestCase):
    def test_same_path(self):
        from pulse.core.reward.reward_signal import REWARD_SIGNALS_FILE as rs
        from pulse.daemons.neural.prediction_feedback import REWARD_SIGNALS_FILE as pf
        self.assertEqual(str(rs), str(pf))

    def test_same_object(self):
        import pulse.core.reward.reward_signal as rs_mod
        import pulse.daemons.neural.prediction_feedback as pf_mod
        self.assertIs(pf_mod.REWARD_SIGNALS_FILE, rs_mod.REWARD_SIGNALS_FILE)

    def test_in_reward_subdir(self):
        from pulse.core.reward.reward_signal import REWARD_SIGNALS_FILE
        self.assertIn("reward", REWARD_SIGNALS_FILE.parts)


# ---- H30: Event channels wired ----
class TestH30EventSubscribers(unittest.TestCase):
    def setUp(self):
        import pulse.core.event_subscribers as es
        from pulse.core import event_bus
        es._wired = False
        event_bus._subscribers.clear()
        es.wire_all_subscribers()

    def test_reward_signal_has_subscriber(self):
        from pulse.core import event_bus
        self.assertGreater(
            len(event_bus._subscribers.get("reward.signal", [])), 0)

    def test_workspace_broadcast_has_subscriber(self):
        from pulse.core import event_bus
        from pulse.core.enums import EventChannel
        self.assertGreater(
            len(event_bus._subscribers.get(EventChannel.WORKSPACE_BROADCAST, [])), 0)

    def test_conscience_brain_write_has_subscriber(self):
        from pulse.core import event_bus
        from pulse.core.enums import EventChannel
        self.assertGreater(
            len(event_bus._subscribers.get(EventChannel.CONSCIENCE_WRITE, [])), 0)

    def test_reward_signal_writes_log(self):
        from pulse.core import event_bus
        from pulse.core.paths import get_state_dir
        log_path = get_state_dir() / "health" / "reward_log.jsonl"
        if log_path.exists():
            log_path.unlink()
        event_bus.publish("reward.signal", "test",
                          {"task_id": "t1", "success": True, "rpe_delta": 0.5})
        time.sleep(0.1)
        self.assertTrue(log_path.exists(), "reward_log.jsonl must be created")
        with open(log_path, "r", encoding="utf-8") as fh:
            lines = [ln.strip() for ln in fh if ln.strip()]
        self.assertGreater(len(lines), 0)
        ev = json.loads(lines[-1])
        self.assertEqual(ev.get("type"), "reward.signal")


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