"""
Tests for InsAIts Adaptive Context Manager (ACM)
==================================================
Covers: SettlementDetector, QualityGuard, AdaptiveContextManager

Author: Cristi Bogdan -- Steddy Nova SRL / YuyAI
"""

from __future__ import annotations

import pytest
from typing import Any, Dict, List

from insa_its.context.settlement_detector import SettlementDetector
from insa_its.context.quality_guard import (
    QualityGuard,
    ApprovalResult,
    CompressionApproval,
    VetoReason,
)
from insa_its.context.adaptive_context_manager import (
    AdaptiveContextManager,
    ContextState,
)

# ---------------------------------------------------------------------------
# Premium detection -- stub vs real implementation
# ---------------------------------------------------------------------------
try:
    import insa_its.premium._settlement
    _HAS_SETTLEMENT = True
except ImportError:
    _HAS_SETTLEMENT = False

try:
    import insa_its.premium._quality_guard
    _HAS_QUALITY_GUARD = True
except ImportError:
    _HAS_QUALITY_GUARD = False

try:
    import insa_its.premium._acm_core
    _HAS_ACM = True
except ImportError:
    _HAS_ACM = False



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

def _msg(
    text: str = "",
    tool: str = "",
    agent: str = "",
    resolved: bool = False,
    anomalies: list | None = None,
    tool_input: dict | None = None,
    ts: str = "2026-03-19T12:00:00Z",
) -> Dict[str, Any]:
    """Build a message dict for testing."""
    m: Dict[str, Any] = {"ts": ts}
    if text:
        m["text"] = text
    if tool:
        m["tool"] = tool
    if agent:
        m["agent"] = agent
    if resolved:
        m["resolved"] = True
    if anomalies is not None:
        m["anomalies"] = anomalies
    if tool_input is not None:
        m["tool_input"] = tool_input
    return m


def _make_stream(n: int, **kwargs) -> List[Dict[str, Any]]:
    """Create a stream of n messages with default text."""
    return [_msg(text=f"message {i}", **kwargs) for i in range(n)]


# ===========================================================================
# SettlementDetector Tests
# ===========================================================================

class TestSettlementDetectorBasic:
    """Core settlement detection logic."""

    def test_settled_when_subsequent_confirms(self):
        msgs = [
            _msg(text="Read file output: class Foo", tool="Read"),
            _msg(text="Confirmed, proceeding with the implementation"),
        ]
        det = SettlementDetector(lookback_window=5)
        assert det.is_settled(0, msgs) is True
    def test_settled_when_done_signal(self):
        msgs = [
            _msg(text="Bash output: tests passed", tool="Bash"),
            _msg(text="All done, moving on to next task"),
        ]
        det = SettlementDetector(lookback_window=5)
        assert det.is_settled(0, msgs) is True
    def test_not_settled_no_subsequent(self):
        msgs = [_msg(text="Read file output", tool="Read")]
        det = SettlementDetector(lookback_window=5)
        assert det.is_settled(0, msgs) is False

    def test_not_settled_anti_signal_retry(self):
        msgs = [
            _msg(text="Bash output: error", tool="Bash"),
            _msg(text="retrying the command"),
        ]
        det = SettlementDetector(lookback_window=5)
        assert det.is_settled(0, msgs) is False

    def test_anti_settlement_blocks_settlement(self):
        """Anti-settlement takes priority even if settlement signal present."""
        msgs = [
            _msg(text="Build output", tool="Bash"),
            _msg(text="Done but still failing, need to retry"),
        ]
        det = SettlementDetector(lookback_window=5)
        assert det.is_settled(0, msgs) is False

    def test_not_settled_when_unresolved(self):
        msgs = [
            _msg(text="Error in module", tool="Bash"),
            _msg(text="This is unresolved, need to investigate"),
        ]
        det = SettlementDetector(lookback_window=5)
        assert det.is_settled(0, msgs) is False

    def test_settled_by_superseding_read(self):
        """Newer read of same file by same agent settles the old read."""
        msgs = [
            _msg(text="file contents v1", tool="Read", agent="opus",
                 tool_input={"file_path": "/src/foo.py"}),
            _msg(text="file contents v2", tool="Read", agent="opus",
                 tool_input={"file_path": "/src/foo.py"}),
        ]
        det = SettlementDetector(lookback_window=5)
        assert det.is_settled(0, msgs) is True
    def test_not_settled_by_different_file(self):
        msgs = [
            _msg(text="file A contents", tool="Read", agent="opus",
                 tool_input={"file_path": "/src/a.py"}),
            _msg(text="file B contents", tool="Read", agent="opus",
                 tool_input={"file_path": "/src/b.py"}),
        ]
        det = SettlementDetector(lookback_window=5)
        assert det.is_settled(0, msgs) is False

    def test_not_settled_by_different_agent(self):
        msgs = [
            _msg(text="file contents", tool="Read", agent="opus",
                 tool_input={"file_path": "/src/foo.py"}),
            _msg(text="file contents", tool="Read", agent="sonnet",
                 tool_input={"file_path": "/src/foo.py"}),
        ]
        det = SettlementDetector(lookback_window=5)
        assert det.is_settled(0, msgs) is False

    def test_lookback_window_limits_scan(self):
        """Settlement signal beyond lookback window is ignored."""
        msgs = [_msg(text="original output", tool="Bash")]
        # Add 15 filler messages
        for i in range(15):
            msgs.append(_msg(text=f"filler {i}"))
        msgs.append(_msg(text="confirmed, all done"))
        det = SettlementDetector(lookback_window=5)
        assert det.is_settled(0, msgs) is False

    def test_out_of_bounds_index(self):
        det = SettlementDetector()
        assert det.is_settled(-1, []) is False
        assert det.is_settled(5, [_msg()]) is False

    def test_settled_by_implemented_signal(self):
        msgs = [
            _msg(text="plan output", tool="Read"),
            _msg(text="implemented the changes successfully"),
        ]
        det = SettlementDetector(lookback_window=5)
        assert det.is_settled(0, msgs) is True
    def test_settled_by_verified_signal(self):
        msgs = [
            _msg(text="test run output", tool="Bash"),
            _msg(text="all tests passing, validated the fix"),
        ]
        det = SettlementDetector(lookback_window=5)
        assert det.is_settled(0, msgs) is True
class TestSettlementDetectorDuplicate:
    """Duplicate detection via Jaccard similarity."""

    def test_exact_duplicate_detected(self):
        msgs = [
            _msg(text="the quick brown fox jumps over the lazy dog", agent="a"),
            _msg(text="the quick brown fox jumps over the lazy dog", agent="a"),
        ]
        det = SettlementDetector()
        assert det.is_duplicate(0, msgs, 0.85) == 1
    def test_near_duplicate_detected(self):
        msgs = [
            _msg(text="the quick brown fox jumps over the lazy dog today", agent="a"),
            _msg(text="the quick brown fox jumps over the lazy dog tomorrow", agent="a"),
        ]
        det = SettlementDetector()
        result = det.is_duplicate(0, msgs, 0.80)
        assert result == 1
    def test_different_messages_not_duplicate(self):
        msgs = [
            _msg(text="completely different message about python code", agent="a"),
            _msg(text="entirely unrelated text about database design", agent="a"),
        ]
        det = SettlementDetector()
        assert det.is_duplicate(0, msgs, 0.85) is None

    def test_different_agent_not_duplicate(self):
        msgs = [
            _msg(text="the quick brown fox jumps over the lazy dog", agent="a"),
            _msg(text="the quick brown fox jumps over the lazy dog", agent="b"),
        ]
        det = SettlementDetector()
        assert det.is_duplicate(0, msgs, 0.85) is None

    def test_short_text_skipped(self):
        msgs = [
            _msg(text="hi", agent="a"),
            _msg(text="hi", agent="a"),
        ]
        det = SettlementDetector()
        assert det.is_duplicate(0, msgs, 0.85) is None

    def test_empty_text_skipped(self):
        msgs = [
            _msg(agent="a"),
            _msg(text="something", agent="a"),
        ]
        det = SettlementDetector()
        assert det.is_duplicate(0, msgs, 0.85) is None


class TestSettlementDetectorFindSettled:
    """Batch settlement detection."""

    def test_find_settled_indices(self):
        # Separate settled and unsettled with enough distance
        # so anti-settlement of one doesn't bleed into the other
        msgs = [
            _msg(text="read output", tool="Read"),        # 0 - should settle
            _msg(text="confirmed, done with read"),        # 1 - settlement signal
            _msg(text="neutral filler A"),                 # 2
            _msg(text="neutral filler B"),                 # 3
            _msg(text="neutral filler C"),                 # 4
            _msg(text="neutral filler D"),                 # 5
            _msg(text="neutral filler E"),                 # 6
            _msg(text="bash output", tool="Bash"),         # 7 - should NOT settle
            _msg(text="retrying the command"),              # 8 - anti-settlement
            _msg(text="neutral filler F"),                 # 9
            _msg(text="neutral filler G"),                 # 10
        ]
        det = SettlementDetector(lookback_window=3)
        settled = det.find_settled_indices(msgs, start_idx=0, min_age=1)
        assert 0 in settled   # read output -> confirmed (within lookback=3)
        assert 7 not in settled  # bash output -> retrying (anti-settlement)

    def test_find_settled_respects_min_age(self):
        msgs = _make_stream(5)
        msgs[1]["text"] = "confirmed, done"
        det = SettlementDetector(lookback_window=5)
        # min_age=10 means nothing qualifies in a 5-msg stream
        settled = det.find_settled_indices(msgs, min_age=10)
        assert settled == []


# ===========================================================================
# QualityGuard Tests
# ===========================================================================

class TestQualityGuardApproval:
    """Core approval/veto logic."""

    def test_empty_plan_approved(self):
        guard = QualityGuard()
        result = guard.approve([], [], hot_count=50, total_count=100)
        assert result.result == ApprovalResult.APPROVED
        assert result.approved_indices == []

    def test_full_approval(self):
        guard = QualityGuard(min_hot_items=5, max_compression_pct=50.0)
        msgs = _make_stream(100)
        plan = [10, 20, 30]
        result = guard.approve(plan, msgs, hot_count=50, total_count=100)
        assert result.result == ApprovalResult.APPROVED
        assert result.approved_indices == [10, 20, 30]
    def test_veto_too_few_hot_items_partial(self):
        """When some items can still be approved, result is PARTIAL."""
        guard = QualityGuard(min_hot_items=5, max_compression_pct=80.0)
        msgs = _make_stream(50)
        # 8 hot, compressing 10 would leave -2, so guard keeps 5 hot -> vetoes some
        plan = list(range(10))
        result = guard.approve(plan, msgs, hot_count=8, total_count=50)
        assert result.result == ApprovalResult.PARTIAL
        assert len(result.vetoed_indices) > 0
        assert any(r.condition == "active_task_count_below_threshold" for r in result.reasons)
    def test_veto_too_few_hot_items_full_veto(self):
        """When ALL items must be vetoed, result is VETOED."""
        guard = QualityGuard(min_hot_items=20, max_compression_pct=80.0)
        msgs = _make_stream(50)
        # 15 hot, compressing 5 -> all 5 vetoed to keep min 20
        plan = list(range(5))
        result = guard.approve(plan, msgs, hot_count=15, total_count=50)
        assert result.result == ApprovalResult.VETOED
        assert any(r.condition == "active_task_count_below_threshold" for r in result.reasons)
    def test_veto_too_aggressive_compression(self):
        guard = QualityGuard(min_hot_items=1, max_compression_pct=10.0)
        msgs = _make_stream(100)
        # 50% compression in one cycle, max is 10%
        plan = list(range(50))
        result = guard.approve(plan, msgs, hot_count=100, total_count=100)
        assert any(r.condition == "compression_ratio_too_aggressive" for r in result.reasons)
        assert len(result.approved_indices) <= 10
    def test_veto_recent_critical_anomaly(self):
        guard = QualityGuard(recent_critical_window=5)
        msgs = _make_stream(50)
        # Add unresolved CRITICAL anomaly in recent window
        msgs[-2]["anomalies"] = [{"type": "credential_exposure", "severity": "CRITICAL"}]
        plan = [10, 20, 30]
        result = guard.approve(plan, msgs, hot_count=40, total_count=50)
        assert result.result == ApprovalResult.VETOED
        assert any(r.condition == "recent_critical_anomaly" for r in result.reasons)
    def test_resolved_critical_allows_compression(self):
        guard = QualityGuard(min_hot_items=1, recent_critical_window=5)
        msgs = _make_stream(50)
        msgs[-2]["anomalies"] = [{"type": "cred", "severity": "CRITICAL"}]
        msgs[-2]["resolved"] = True
        plan = [5, 10, 15]
        result = guard.approve(plan, msgs, hot_count=40, total_count=50)
        assert result.result != ApprovalResult.VETOED

    def test_veto_cross_agent_dependency(self):
        guard = QualityGuard(min_hot_items=1, max_compression_pct=80.0)
        msgs = _make_stream(50)
        msgs[10]["text"] = "waiting for agent-B to finish the analysis"
        plan = [10, 20, 30]
        result = guard.approve(plan, msgs, hot_count=40, total_count=50)
        assert 10 in result.vetoed_indices
        assert any(r.condition == "cross_agent_dependency_detected" for r in result.reasons)
    def test_veto_error_resolution(self):
        guard = QualityGuard(min_hot_items=1, max_compression_pct=80.0)
        msgs = _make_stream(50)
        msgs[20]["text"] = "debugging the traceback from the failed import"
        plan = [10, 20, 30]
        result = guard.approve(plan, msgs, hot_count=40, total_count=50)
        assert 20 in result.vetoed_indices
        assert any(r.condition == "error_resolution_in_progress" for r in result.reasons)
class TestQualityGuardRisk:
    """Quality risk assessment."""

    def test_risk_none_for_empty(self):
        guard = QualityGuard()
        assert guard.get_quality_risk(0, 0, 0) == "none"
    def test_risk_none_mostly_hot(self):
        guard = QualityGuard()
        assert guard.get_quality_risk(80, 20, 100) == "none"
    def test_risk_low_half_cold(self):
        guard = QualityGuard()
        assert guard.get_quality_risk(40, 60, 100) == "low"

    def test_risk_medium_mostly_cold(self):
        guard = QualityGuard()
        assert guard.get_quality_risk(20, 80, 100) == "medium"
class TestQualityGuardStats:
    """Stats tracking for dashboard."""

    def test_initial_stats(self):
        guard = QualityGuard()
        stats = guard.get_stats()
        assert stats["approvals"] == 0
        assert stats["vetoes"] == 0
        assert stats["veto_rate_pct"] == 0.0

    def test_veto_rate_tracks(self):
        guard = QualityGuard(min_hot_items=1, max_compression_pct=80.0,
                             recent_critical_window=3)
        msgs = _make_stream(50)
        # Approved cycle
        guard.approve([5], msgs, hot_count=40, total_count=50)
        # Vetoed cycle (critical anomaly)
        msgs[-1]["anomalies"] = [{"type": "x", "severity": "CRITICAL"}]
        guard.approve([10], msgs, hot_count=40, total_count=50)
        stats = guard.get_stats()
        assert stats["vetoes"] == 1
        assert stats["approvals"] == 1
        assert stats["veto_rate_pct"] == 50.0
    def test_veto_history_bounded(self):
        guard = QualityGuard(min_hot_items=1, recent_critical_window=3)
        msgs = _make_stream(50)
        msgs[-1]["anomalies"] = [{"type": "x", "severity": "CRITICAL"}]
        for _ in range(10):
            guard.approve([5], msgs, hot_count=40, total_count=50)
        stats = guard.get_stats()
        # recent_veto_reasons capped at 5
        assert len(stats["recent_veto_reasons"]) <= 5


# ===========================================================================
# AdaptiveContextManager Tests
# ===========================================================================

class TestACMClassification:
    """Message classification rules."""

    def test_recency_window_always_hot(self):
        acm = AdaptiveContextManager(recency_window=5)
        msgs = _make_stream(20)
        # Last 5 messages should be HOT
        for i in range(15, 20):
            assert acm.classify(i, msgs) == ContextState.HOT

    def test_always_hot_patterns_todo(self):
        acm = AdaptiveContextManager(recency_window=3)
        msgs = _make_stream(20)
        msgs[0]["text"] = "TODO: fix the broken import"
        assert acm.classify(0, msgs) == ContextState.HOT

    def test_always_hot_patterns_fixme(self):
        acm = AdaptiveContextManager(recency_window=3)
        msgs = _make_stream(20)
        msgs[0]["text"] = "FIXME: handle edge case"
        assert acm.classify(0, msgs) == ContextState.HOT

    def test_always_hot_patterns_blocked(self):
        acm = AdaptiveContextManager(recency_window=3)
        msgs = _make_stream(20)
        msgs[0]["text"] = "BLOCKED waiting for API response"
        assert acm.classify(0, msgs) == ContextState.HOT

    def test_always_hot_claude_md(self):
        acm = AdaptiveContextManager(recency_window=3)
        msgs = _make_stream(20)
        msgs[0]["text"] = "Reading CLAUDE.md for rules"
        assert acm.classify(0, msgs) == ContextState.HOT

    def test_always_hot_session_rule(self):
        acm = AdaptiveContextManager(recency_window=3)
        msgs = _make_stream(20)
        msgs[0]["text"] = "Apply session_rule for this context"
        assert acm.classify(0, msgs) == ContextState.HOT

    def test_always_hot_task_anchor(self):
        acm = AdaptiveContextManager(recency_window=3)
        msgs = _make_stream(20)
        msgs[0]["text"] = "task_anchor injection for agent scope"
        assert acm.classify(0, msgs) == ContextState.HOT

    def test_critical_anomaly_stays_hot(self):
        acm = AdaptiveContextManager(recency_window=3)
        msgs = _make_stream(20)
        msgs[0]["anomalies"] = [{"type": "cred", "severity": "CRITICAL"}]
        assert acm.classify(0, msgs) == ContextState.HOT

    def test_high_anomaly_stays_hot(self):
        acm = AdaptiveContextManager(recency_window=3)
        msgs = _make_stream(20)
        msgs[0]["anomalies"] = [{"type": "injection", "severity": "HIGH"}]
        assert acm.classify(0, msgs) == ContextState.HOT

    def test_resolved_info_anomaly_goes_cold(self):
        acm = AdaptiveContextManager(recency_window=3)
        msgs = _make_stream(20)
        msgs[0]["anomalies"] = [{"type": "subagent_tool_call", "severity": "INFO"}]
        msgs[0]["resolved"] = True
        assert acm.classify(0, msgs) == ContextState.COLD
    def test_settled_tool_output_goes_cold(self):
        acm = AdaptiveContextManager(recency_window=3)
        msgs = _make_stream(20)
        msgs[0] = _msg(text="file contents here", tool="Read")
        msgs[1] = _msg(text="confirmed, proceeding with implementation")
        state = acm.classify(0, msgs)
        assert state == ContextState.COLD
    def test_default_is_hot(self):
        acm = AdaptiveContextManager(recency_window=3)
        msgs = _make_stream(20)
        msgs[0]["text"] = "some neutral message without any signals"
        # No settlement signals in subsequent msgs either
        for i in range(1, 5):
            msgs[i]["text"] = "another neutral message"
        assert acm.classify(0, msgs) == ContextState.HOT

    def test_classification_cached(self):
        acm = AdaptiveContextManager(recency_window=3)
        msgs = _make_stream(20)
        result1 = acm.classify(0, msgs)
        result2 = acm.classify(0, msgs)
        assert result1 == result2

    def test_error_pattern_stays_hot(self):
        acm = AdaptiveContextManager(recency_window=3)
        msgs = _make_stream(20)
        msgs[0]["text"] = "unresolved import error in module"
        assert acm.classify(0, msgs) == ContextState.HOT


class TestACMCompression:
    """Compression (summary generation)."""

    def test_compress_produces_settled_format(self):
        acm = AdaptiveContextManager(recency_window=3)
        msgs = _make_stream(20)
        msgs[5] = _msg(text="file contents: class MyService", tool="Read",
                       agent="opus", ts="2026-03-19T12:00:00Z")
        msgs[6] = _msg(text="proceeding with edit", tool="Edit")
        summary = acm.compress(5, msgs)
        assert "[SETTLED:" in summary
        assert "tool=Read" in summary
        assert "agent=opus" in summary
    def test_compress_empty_text_returns_empty(self):
        acm = AdaptiveContextManager()
        msgs = [_msg()]
        assert acm.compress(0, msgs) == ""

    def test_compress_out_of_bounds_returns_empty(self):
        acm = AdaptiveContextManager()
        assert acm.compress(5, [_msg()]) == ""

    def test_compress_truncates_long_results(self):
        acm = AdaptiveContextManager()
        long_text = "x" * 200
        msgs = [_msg(text=long_text, tool="Bash")]
        summary = acm.compress(0, msgs)
        # Result should be truncated to ~80 chars
        assert len(summary) < 300
    def test_compress_determines_action(self):
        acm = AdaptiveContextManager()
        msgs = [
            _msg(text="output here", tool="Read"),
            _msg(text="something", tool="Edit"),
        ]
        summary = acm.compress(0, msgs)
        assert "proceeded_to_edit" in summary
class TestACMCompressionPlan:
    """Build and execute compression plans."""

    def test_no_compression_below_trigger(self):
        acm = AdaptiveContextManager(compression_trigger=80)
        msgs = _make_stream(30)
        approval = acm.build_compression_plan(msgs)
        assert approval.result == ApprovalResult.APPROVED
        assert approval.approved_indices == []

    def test_compression_triggered_above_threshold(self):
        acm = AdaptiveContextManager(
            recency_window=5,
            compression_trigger=20,
            min_hot_items=1,
        )
        msgs = _make_stream(30)
        # Make early messages settled
        for i in range(10):
            msgs[i] = _msg(text="read output", tool="Read")
            if i + 1 < 30:
                msgs[i + 1]["text"] = "confirmed, done"
        approval = acm.build_compression_plan(msgs)
        # Should have some approved or at least attempted
        assert acm._compression_cycles == 1

    def test_quality_guard_vetoes_critical(self):
        acm = AdaptiveContextManager(
            recency_window=3,
            compression_trigger=10,
            min_hot_items=1,
            recent_critical_window=5,
        )
        msgs = _make_stream(20)
        # Make some messages settled
        msgs[0] = _msg(text="read output", tool="Read")
        msgs[1] = _msg(text="confirmed, done")
        # Add unresolved CRITICAL near end
        msgs[-2]["anomalies"] = [{"type": "x", "severity": "CRITICAL"}]
        approval = acm.build_compression_plan(msgs)
        # Should be vetoed due to critical anomaly
        if approval.approved_indices == [] and len(approval.reasons) > 0:
            assert any(r.condition == "recent_critical_anomaly" for r in approval.reasons)
    def test_compression_cycles_increment(self):
        acm = AdaptiveContextManager(compression_trigger=5)
        msgs = _make_stream(10)
        acm.build_compression_plan(msgs)
        acm.build_compression_plan(msgs)
        assert acm._compression_cycles == 2


class TestACMApply:
    """Apply compression to message stream."""

    def test_apply_preserves_uncompressed(self):
        acm = AdaptiveContextManager()
        msgs = _make_stream(5)
        result = acm.apply(msgs)
        assert len(result) == 5
        for m in result:
            assert "_acm_compressed" not in m

    def test_apply_replaces_compressed(self):
        acm = AdaptiveContextManager()
        msgs = _make_stream(5)
        # Premium has _compressed_summaries dict
        acm._compressed_summaries[2] = "[SETTLED: test summary]"
        result = acm.apply(msgs)
        assert result[2]["_acm_compressed"] is True
        assert result[2]["_acm_summary"] == "[SETTLED: test summary]"
        assert "_acm_original_size" in result[2]
    def test_apply_does_not_mutate_original(self):
        acm = AdaptiveContextManager()
        msgs = _make_stream(5)
        acm._compressed_summaries[1] = "[SETTLED: x]"
        result = acm.apply(msgs)
        assert "_acm_compressed" not in msgs[1]
        assert "_acm_compressed" in result[1]
class TestACMReport:
    """Compression report for dashboard."""

    def test_initial_report(self):
        acm = AdaptiveContextManager()
        report = acm.get_compression_report()
        assert report["hot_items"] == 0
        assert report["cold_items"] == 0
        assert report["compressed_items"] == 0
        assert report["compression_ratio"] == 0.0
        assert report["quality_risk"] == "none"
        assert "guard_stats" in report

    def test_report_after_classification(self):
        acm = AdaptiveContextManager(recency_window=3)
        msgs = _make_stream(20)
        # Classify a few
        for i in range(10):
            acm.classify(i, msgs)
        report = acm.get_compression_report()
        assert report["hot_items"] + report["cold_items"] == 10
    def test_report_tokens_saved(self):
        acm = AdaptiveContextManager()
        # Premium tracks compressed summaries and tokens
        acm._compressed_summaries[0] = "[SETTLED: x]"
        acm._total_tokens_saved = 770
        report = acm.get_compression_report()
        assert report["estimated_tokens_saved"] == 770
    def test_report_duplicates_tracked(self):
        acm = AdaptiveContextManager()
        acm._duplicates_compressed = 3
        report = acm.get_compression_report()
        assert report["duplicates_compressed"] == 3
class TestACMGetText:
    """Text extraction from various message formats."""

    def test_text_field(self):
        assert AdaptiveContextManager._get_text({"text": "hello"}) == "hello"
    def test_content_field(self):
        assert AdaptiveContextManager._get_text({"content": "world"}) == "world"
    def test_description_field(self):
        assert AdaptiveContextManager._get_text({"description": "desc"}) == "desc"
    def test_anomaly_descriptions(self):
        msg = {
            "anomalies": [
                {"description": "found issue"},
                {"description": "another issue"},
            ]
        }
        text = AdaptiveContextManager._get_text(msg)
        assert "found issue" in text
        assert "another issue" in text
    def test_empty_message(self):
        assert AdaptiveContextManager._get_text({}) == ""
    def test_priority_order(self):
        msg = {"text": "primary", "content": "secondary"}
        assert AdaptiveContextManager._get_text(msg) == "primary"
class TestACMSummarizeResult:
    """Result summarization."""

    def test_basic_summary(self):
        result = AdaptiveContextManager._summarize_result(
            "class Foo:\n    def bar(self):\n        pass", "Read"
        )
        assert result == "class Foo:"
    def test_skips_headers(self):
        result = AdaptiveContextManager._summarize_result(
            "# Header\n---\nActual content here", "Read"
        )
        assert result == "Actual content here"
    def test_truncates_long_lines(self):
        long = "x" * 100
        result = AdaptiveContextManager._summarize_result(long, "Bash")
        assert len(result) <= 80
    def test_empty_text(self):
        assert AdaptiveContextManager._summarize_result("", "Read") == ""
    def test_whitespace_only(self):
        assert AdaptiveContextManager._summarize_result("   \n  \n  ", "Read") == ""
class TestACMDetermineAction:
    """Action determination from subsequent messages."""

    def test_finds_next_tool(self):
        msgs = [
            _msg(text="output", tool="Read"),
            _msg(text="editing", tool="Edit"),
        ]
        assert AdaptiveContextManager._determine_action(0, msgs) == "proceeded_to_edit"
    def test_no_subsequent_tool(self):
        msgs = [_msg(text="output", tool="Read")]
        assert AdaptiveContextManager._determine_action(0, msgs) == "continued"
    def test_looks_ahead_max_5(self):
        msgs = [_msg(text="output", tool="Read")]
        for i in range(10):
            msgs.append(_msg(text=f"filler {i}"))
        msgs.append(_msg(text="late tool", tool="Bash"))
        # Should not find the tool 11 messages ahead
        assert AdaptiveContextManager._determine_action(0, msgs) == "continued"
class TestACMEndToEnd:
    """Full pipeline: classify -> plan -> compress -> apply -> report."""

    def test_full_pipeline(self):
        acm = AdaptiveContextManager(
            recency_window=5,
            compression_trigger=15,
            min_hot_items=3,
            max_compression_pct=60.0,
        )

        # Build a realistic 25-message stream
        msgs: List[Dict[str, Any]] = []
        for i in range(25):
            if i < 5:
                # Early settled tool outputs
                msgs.append(_msg(
                    text=f"Read file output chunk {i}", tool="Read",
                    agent="opus", ts=f"2026-03-19T12:0{i}:00Z",
                ))
            elif i == 5:
                msgs.append(_msg(text="confirmed all reads, proceeding with implementation"))
            elif i == 10:
                msgs.append(_msg(text="TODO: remember to update tests"))
            elif i == 15:
                msgs.append(_msg(
                    text="subagent report",
                    anomalies=[{"type": "subagent_tool_call", "severity": "INFO"}],
                    resolved=True,
                ))
            else:
                msgs.append(_msg(text=f"working on step {i}", agent="opus"))

        # Run compression plan
        approval = acm.build_compression_plan(msgs)

        # Apply compression
        result = acm.apply(msgs)
        assert len(result) == 25

        # Check report
        report = acm.get_compression_report()
        assert report["compression_cycles"] == 1
        assert report["quality_risk"] in ("none", "low", "medium")
        # quality_risk should NEVER be "high"
        assert report["quality_risk"] != "high"

    def test_quality_risk_never_high(self):
        """The spec says quality_risk must NEVER show 'high'."""
        acm = AdaptiveContextManager(
            recency_window=2,
            compression_trigger=5,
            min_hot_items=1,
        )
        # Create heavily compressed stream
        msgs = _make_stream(50)
        for i in range(40):
            msgs[i] = _msg(text="read output settled", tool="Read")
            if i + 1 < 50:
                msgs[i + 1]["text"] = "confirmed, done"

        acm.build_compression_plan(msgs)
        report = acm.get_compression_report()
        assert report["quality_risk"] != "high"


class TestCompressionApprovalProperty:
    """CompressionApproval.approved property."""

    def test_approved_is_true_for_approved(self):
        ca = CompressionApproval(ApprovalResult.APPROVED, [1, 2], [], [])
        assert ca.approved is True

    def test_approved_is_true_for_partial(self):
        ca = CompressionApproval(ApprovalResult.PARTIAL, [1], [2], [])
        assert ca.approved is True

    def test_approved_is_false_for_vetoed(self):
        ca = CompressionApproval(ApprovalResult.VETOED, [], [1, 2], [])
        assert ca.approved is False


class TestVetoReason:
    """VetoReason representation."""

    def test_repr(self):
        vr = VetoReason("test_condition", "test detail")
        assert "test_condition" in repr(vr)
        assert "test detail" in repr(vr)
