"""
InsAIts SDK - 3-Tier Anchor Injection System Tests
====================================================
Tests for trigger_engine.py and message_builder.py (Phase 7F).

Run with: python -m pytest tests/test_anchor_system.py -v
"""

import os
import sys
import time

sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))

import pytest
from pathlib import Path

from insa_its.anchor.trigger_engine import AnchorTriggerEngine, TriggerType
from insa_its.anchor.message_builder import AnchorMessageBuilder


# ===========================================================================
# TRIGGER ENGINE TESTS
# ===========================================================================

class TestTriggerEngineSessionStart:
    """First tool call should trigger all 3 layers."""

    def test_first_call_triggers_session_start(self):
        engine = AnchorTriggerEngine()
        triggers = engine.evaluate("Read", {"file_path": "test.py"})
        assert len(triggers) == 1
        assert triggers[0]["trigger"] == TriggerType.SESSION_START
        assert set(triggers[0]["layers"]) == {1, 2, 3}

    def test_second_call_no_trigger(self):
        engine = AnchorTriggerEngine()
        engine.evaluate("Read", {"file_path": "test.py"})  # session start
        triggers = engine.evaluate("Read", {"file_path": "test2.py"})
        assert len(triggers) == 0

    def test_session_start_respects_disabled_layers(self):
        engine = AnchorTriggerEngine(layer2_enabled=False)
        triggers = engine.evaluate("Read", {})
        assert triggers[0]["layers"] == [1, 3]


class TestTriggerEngineAgentSpawn:
    """Agent tool call should trigger Layer 2."""

    def test_agent_spawn_triggers_layer2(self):
        engine = AnchorTriggerEngine()
        engine.evaluate("Read", {})  # session start
        triggers = engine.evaluate(
            "Agent",
            {"subagent_type": "Explore", "description": "find files"},
        )
        assert any(
            t["trigger"] == TriggerType.AGENT_SPAWN and t["layers"] == [2]
            for t in triggers
        )

    def test_duplicate_agent_still_triggers_task_start(self):
        """With agent_task_start_injection=True (default), every Agent call
        gets an anchor — even if the same agent type+desc was seen before.
        The unique-agent spawn (Layer 2 only) won't re-fire, but the
        task-start injection (Layer 1+2) still fires."""
        engine = AnchorTriggerEngine()
        engine.evaluate("Read", {})
        engine.evaluate("Agent", {"subagent_type": "Explore", "description": "find files"})
        triggers = engine.evaluate("Agent", {"subagent_type": "Explore", "description": "find files"})
        assert any(
            t["trigger"] == TriggerType.AGENT_SPAWN for t in triggers
        )

    def test_duplicate_agent_no_trigger_when_task_start_disabled(self):
        """When agent_task_start_injection is disabled, duplicate agents
        should NOT re-trigger (original behavior)."""
        engine = AnchorTriggerEngine(agent_task_start_injection=False)
        engine.evaluate("Read", {})
        engine.evaluate("Agent", {"subagent_type": "Explore", "description": "find files"})
        triggers = engine.evaluate("Agent", {"subagent_type": "Explore", "description": "find files"})
        assert not any(
            t["trigger"] == TriggerType.AGENT_SPAWN for t in triggers
        )

    def test_different_agent_triggers(self):
        engine = AnchorTriggerEngine()
        engine.evaluate("Read", {})
        engine.evaluate("Agent", {"subagent_type": "Explore", "description": "find files"})
        triggers = engine.evaluate("Agent", {"subagent_type": "Plan", "description": "plan feature"})
        assert any(
            t["trigger"] == TriggerType.AGENT_SPAWN for t in triggers
        )


class TestTriggerEngineSubagentSpawn:
    """Subagent context should trigger Layer 3."""

    def test_subagent_triggers_layer3(self):
        engine = AnchorTriggerEngine()
        engine.evaluate("Read", {})  # session start
        triggers = engine.evaluate(
            "Grep", {"pattern": "test"},
            is_subagent=True, agent_id="sub_001",
        )
        assert any(
            t["trigger"] == TriggerType.SUBAGENT_SPAWN and t["layers"] == [3]
            for t in triggers
        )

    def test_same_subagent_no_retrigger(self):
        engine = AnchorTriggerEngine()
        engine.evaluate("Read", {})
        engine.evaluate("Grep", {}, is_subagent=True, agent_id="sub_001")
        triggers = engine.evaluate("Read", {}, is_subagent=True, agent_id="sub_001")
        assert not any(
            t["trigger"] == TriggerType.SUBAGENT_SPAWN for t in triggers
        )


class TestTriggerEnginePeriodicRefresh:
    """Opus periodic refresh at randomized intervals."""

    def test_periodic_fires_within_range(self):
        engine = AnchorTriggerEngine(opus_refresh_min=3, opus_refresh_max=5)
        engine.evaluate("Read", {})  # session start (call 1)
        fired = False
        for i in range(10):
            triggers = engine.evaluate("Bash", {"command": f"test{i}"})
            if any(t["trigger"] == TriggerType.OPUS_PERIODIC for t in triggers):
                fired = True
                break
        assert fired, "Periodic refresh should fire within 10 calls (range 3-5)"

    def test_periodic_interval_varies(self):
        """Next refresh interval should differ each time."""
        engine = AnchorTriggerEngine(opus_refresh_min=3, opus_refresh_max=8)
        intervals = set()
        # Run through several refresh cycles
        for i in range(50):
            triggers = engine.evaluate("Bash", {"command": f"cmd{i}"})
            for t in triggers:
                if t["trigger"] == TriggerType.OPUS_PERIODIC:
                    intervals.add(engine.next_opus_refresh_at - engine.tool_call_count)
        # Should have multiple different intervals (statistical — may rarely fail)
        assert len(intervals) >= 1


class TestTriggerEngineAnomalyThreshold:
    """Anomaly delta threshold should trigger Layer 1+2 with delay."""

    def test_anomaly_threshold_triggers(self):
        engine = AnchorTriggerEngine(anomaly_delta_threshold=5)
        engine.evaluate("Read", {})  # session start
        triggers = engine.evaluate(
            "Bash", {"command": "test"},
            anomaly_count=10,  # delta = 10 > threshold 5
        )
        assert any(
            t["trigger"] == TriggerType.ANOMALY_THRESHOLD for t in triggers
        )

    def test_anomaly_below_threshold_no_trigger(self):
        engine = AnchorTriggerEngine(anomaly_delta_threshold=25)
        engine.evaluate("Read", {})
        triggers = engine.evaluate("Bash", {}, anomaly_count=10)
        assert not any(
            t["trigger"] == TriggerType.ANOMALY_THRESHOLD for t in triggers
        )

    def test_anomaly_trigger_has_delay(self):
        engine = AnchorTriggerEngine(
            anomaly_delta_threshold=5, anomaly_delay_max_seconds=10,
        )
        engine.evaluate("Read", {})
        triggers = engine.evaluate("Bash", {}, anomaly_count=10)
        anomaly_triggers = [
            t for t in triggers if t["trigger"] == TriggerType.ANOMALY_THRESHOLD
        ]
        assert len(anomaly_triggers) == 1
        assert anomaly_triggers[0]["delay"] >= 0
        assert anomaly_triggers[0]["delay"] <= 10


class TestTriggerEngineStealth:
    """Stealth mode suppresses all injections."""

    def test_stealth_suppresses_session_start(self):
        engine = AnchorTriggerEngine(stealth_disables_injection=True)
        triggers = engine.evaluate("Read", {}, stealth=True)
        assert len(triggers) == 0

    def test_stealth_disabled_allows_injection(self):
        engine = AnchorTriggerEngine(stealth_disables_injection=False)
        triggers = engine.evaluate("Read", {}, stealth=True)
        assert len(triggers) == 1  # session start


class TestTriggerEngineCritical:
    """Critical events trigger Layer 1."""

    def test_critical_triggers_layer1(self):
        engine = AnchorTriggerEngine()
        engine.evaluate("Read", {})  # session start
        triggers = engine.evaluate("Bash", {}, has_critical=True)
        assert any(
            t["trigger"] == TriggerType.CRITICAL_EVENT and t["layers"] == [1]
            for t in triggers
        )


class TestTriggerEngineState:
    """State tracking and serialization."""

    def test_get_state_returns_dict(self):
        engine = AnchorTriggerEngine()
        engine.evaluate("Read", {})
        state = engine.get_state()
        assert "tool_call_count" in state
        assert "next_opus_refresh_at" in state
        assert state["tool_call_count"] == 1

    def test_injection_history_bounded(self):
        engine = AnchorTriggerEngine(opus_refresh_min=1, opus_refresh_max=2)
        for i in range(30):
            engine.evaluate("Bash", {"command": f"cmd{i}"})
        assert len(engine.injection_history) <= 10


# ===========================================================================
# MESSAGE BUILDER TESTS
# ===========================================================================

TEMPLATES_DIR = str(
    Path(__file__).parent.parent / "insa_its" / "anchor" / "templates"
)


class TestMessageBuilderLayer1:
    """Layer 1 (Opus) message building."""

    def test_layer1_has_context_header(self):
        builder = AnchorMessageBuilder(templates_dir=TEMPLATES_DIR)
        msg = builder.build_layer1()
        assert "[CONTEXT]" in msg

    def test_layer1_includes_digest(self):
        builder = AnchorMessageBuilder(templates_dir=TEMPLATES_DIR)
        msg = builder.build_layer1()
        assert "[ACTIVE_RULES]" in msg

    def test_layer1_no_forbidden_words(self):
        builder = AnchorMessageBuilder(templates_dir=TEMPLATES_DIR)
        msg = builder.build_layer1()
        forbidden = ["monitor", "detect", "security", "insaits", "warning", "flag"]
        for word in forbidden:
            assert word not in msg.lower(), f"Forbidden word '{word}' in Layer 1"


class TestMessageBuilderLayer2:
    """Layer 2 (Agent) message building."""

    def test_layer2_has_session_params_header(self):
        builder = AnchorMessageBuilder(templates_dir=TEMPLATES_DIR)
        msg = builder.build_layer2()
        assert "[SESSION_PARAMS]" in msg

    def test_layer2_contains_scope(self):
        builder = AnchorMessageBuilder(templates_dir=TEMPLATES_DIR)
        msg = builder.build_layer2()
        assert "scope" in msg.lower()


class TestMessageBuilderLayer3:
    """Layer 3 (Subagent) message building."""

    def test_layer3_has_constraint_header(self):
        builder = AnchorMessageBuilder(templates_dir=TEMPLATES_DIR)
        msg = builder.build_layer3()
        assert "[CONSTRAINT]" in msg

    def test_layer3_contains_report_to(self):
        builder = AnchorMessageBuilder(templates_dir=TEMPLATES_DIR)
        msg = builder.build_layer3()
        assert "report_to" in msg


class TestMessageBuilderVariation:
    """Anti-pattern: messages must vary between emissions."""

    def test_two_emissions_differ(self):
        builder = AnchorMessageBuilder(templates_dir=TEMPLATES_DIR)
        # Run multiple times — at least one pair should differ
        msgs = [builder.build_layer2() for _ in range(5)]
        unique = set(msgs)
        # With shuffling + random whitespace, almost always varies
        # Use >= 2 to account for rare collisions
        assert len(unique) >= 2, "Messages should vary between emissions"


class TestMessageBuilderCombined:
    """Combined multi-layer building."""

    def test_build_for_all_layers(self):
        builder = AnchorMessageBuilder(templates_dir=TEMPLATES_DIR)
        msg = builder.build_for_layers([1, 2, 3])
        assert "[CONTEXT]" in msg
        assert "[SESSION_PARAMS]" in msg
        assert "[CONSTRAINT]" in msg

    def test_build_single_layer(self):
        builder = AnchorMessageBuilder(templates_dir=TEMPLATES_DIR)
        msg = builder.build_for_layers([3])
        assert "[CONSTRAINT]" in msg
        assert "[CONTEXT]" not in msg

    def test_build_empty_layers(self):
        builder = AnchorMessageBuilder(templates_dir=TEMPLATES_DIR)
        msg = builder.build_for_layers([])
        assert msg == ""


class TestMessageBuilderForbiddenWords:
    """No injected content should contain forbidden words."""

    def test_all_layers_clean(self):
        builder = AnchorMessageBuilder(templates_dir=TEMPLATES_DIR)
        forbidden = ["monitor", "detect", "security", "insaits", "warning", "flag"]
        for layer in [1, 2, 3]:
            msg = builder.build_for_layers([layer])
            for word in forbidden:
                assert word not in msg.lower(), (
                    f"Forbidden '{word}' in Layer {layer}"
                )


class TestMessageBuilderDigest:
    """CLAUDE.md digest extraction."""

    def test_digest_fallback_template(self):
        builder = AnchorMessageBuilder(
            claude_md_path="/nonexistent/path",
            templates_dir=TEMPLATES_DIR,
        )
        digest = builder._build_digest()
        assert "[ACTIVE_RULES]" in digest

    def test_digest_from_real_claude_md(self):
        claude_md = str(
            Path(__file__).parent.parent.parent / "CLAUDE.md"
        )
        if not Path(claude_md).exists():
            pytest.skip("CLAUDE.md not found")
        builder = AnchorMessageBuilder(
            claude_md_path=claude_md,
            templates_dir=TEMPLATES_DIR,
        )
        digest = builder._build_digest()
        assert "[ACTIVE_RULES]" in digest
        assert len(digest) > 20

    def test_content_hash(self):
        builder = AnchorMessageBuilder(templates_dir=TEMPLATES_DIR)
        msg = builder.build_layer1()
        h = builder.content_hash(msg)
        assert len(h) == 8
        assert isinstance(h, str)


class TestTriggerEngineTimeBased:
    """Time-based periodic injection for subagents (every 4 minutes)."""

    def test_subagent_time_periodic_fires(self):
        """After subagent_time_interval_seconds, time-based periodic fires."""
        engine = AnchorTriggerEngine(
            subagent_refresh_min=100,  # high to avoid call-count trigger
            subagent_refresh_max=200,
            subagent_time_interval_seconds=1,  # 1 second for test
            subagent_periodic_enabled=True,  # opt-in (default is False)
        )
        engine.evaluate("Read", {})  # session start

        # First subagent call sets _last_subagent_injection_ts
        engine.evaluate("Grep", {}, is_subagent=True, agent_id="sub_time_1")

        # Simulate time passing
        import time as _time
        _time.sleep(1.1)

        # Next subagent call should fire time-based periodic
        triggers = engine.evaluate("Read", {}, is_subagent=True, agent_id="sub_time_1")
        periodic = [t for t in triggers if t["trigger"] == TriggerType.SUBAGENT_PERIODIC]
        assert len(periodic) == 1, "Time-based subagent periodic should fire after interval"

    def test_subagent_time_periodic_does_not_fire_early(self):
        """Before the interval, time-based periodic should NOT fire."""
        engine = AnchorTriggerEngine(
            subagent_refresh_min=100,
            subagent_refresh_max=200,
            subagent_time_interval_seconds=300,  # 5 minutes
            subagent_periodic_enabled=True,  # opt-in to test the timing
        )
        engine.evaluate("Read", {})
        engine.evaluate("Grep", {}, is_subagent=True, agent_id="sub_early_1")
        # Immediate next call — well within 300s
        triggers = engine.evaluate("Read", {}, is_subagent=True, agent_id="sub_early_1")
        periodic = [t for t in triggers if t["trigger"] == TriggerType.SUBAGENT_PERIODIC]
        assert len(periodic) == 0


class TestTriggerEngineAgentTaskStart:
    """Every Agent tool call fires an anchor (not just unique agents)."""

    def test_every_agent_call_gets_anchor(self):
        engine = AnchorTriggerEngine()
        engine.evaluate("Read", {})  # session start

        # First Agent call
        t1 = engine.evaluate("Agent", {"subagent_type": "Explore", "description": "a"})
        assert any(t["trigger"] == TriggerType.AGENT_SPAWN for t in t1)

        # Second call, same type — still fires
        t2 = engine.evaluate("Agent", {"subagent_type": "Explore", "description": "a"})
        assert any(t["trigger"] == TriggerType.AGENT_SPAWN for t in t2)

        # Third call, different type — still fires
        t3 = engine.evaluate("Agent", {"subagent_type": "Plan", "description": "b"})
        assert any(t["trigger"] == TriggerType.AGENT_SPAWN for t in t3)

    def test_agent_task_start_disabled(self):
        engine = AnchorTriggerEngine(agent_task_start_injection=False)
        engine.evaluate("Read", {})
        engine.evaluate("Agent", {"subagent_type": "Explore", "description": "a"})
        # Same agent, with task_start disabled — should NOT fire
        t = engine.evaluate("Agent", {"subagent_type": "Explore", "description": "a"})
        assert not any(t2["trigger"] == TriggerType.AGENT_SPAWN for t2 in t)


class TestTriggerEngineStatePersistence:
    """State save/load round-trip with new timestamp fields."""

    def test_save_load_round_trip(self, tmp_path):
        engine = AnchorTriggerEngine()
        engine.evaluate("Read", {})  # session start
        engine.evaluate("Agent", {"subagent_type": "Explore", "description": "test"})
        engine.evaluate("Grep", {}, is_subagent=True, agent_id="sub_rt_1")

        state_file = str(tmp_path / "state.json")
        engine.save_state_to_file(state_file)

        engine2 = AnchorTriggerEngine()
        loaded = engine2.load_state_from_file(state_file)
        assert loaded is True
        assert engine2._tool_call_count == engine._tool_call_count
        assert engine2._is_first_call == engine._is_first_call
        assert engine2._last_subagent_injection_ts == engine._last_subagent_injection_ts
        assert engine2._last_agent_injection_ts == engine._last_agent_injection_ts
        assert engine2._seen_agents == engine._seen_agents

    def test_load_missing_file_returns_false(self, tmp_path):
        engine = AnchorTriggerEngine()
        loaded = engine.load_state_from_file(str(tmp_path / "nonexistent.json"))
        assert loaded is False


if __name__ == "__main__":
    pytest.main([__file__, "-v"])
