"""
Tests for advanced cognitive modules:
  1. CognitiveMemoryManager (decay + consolidation + attention)
  2. ReflectionEngine (pattern detection + stagnation)
  3. SkillEvolutionManager (natural selection + mutation + niche)
  4. GitBrain (git-backed episodic memory)
  5. InnerLifeProcessor (System 1 unconscious processing)
  6. PatternLearningManager (pattern detection + institutional learning)
  7. SkillFitnessTracker windowed extensions
"""

import asyncio
import json
import math
import time
from pathlib import Path

import pytest


# ═══════════════════════════════════════════════════════════════════════════════
# 1. CognitiveMemoryManager
# ═══════════════════════════════════════════════════════════════════════════════


class TestCognitiveMemory:
    """Tests for multi-tier memory with decay and consolidation."""

    def test_add_memory(self, tmp_path):
        from opensable.core.cognitive_memory import CognitiveMemoryManager

        mgr = CognitiveMemoryManager(directory=tmp_path / "mem")
        item = mgr.add_memory("Hello world", category="test", importance=0.8)

        assert item.content == "Hello world"
        assert item.category == "test"
        assert item.importance_base == 0.8
        assert item.tier == "short_term"

    def test_decay_reduces_importance(self, tmp_path):
        from opensable.core.cognitive_memory import (
            CognitiveMemoryItem,
            MemoryDecay,
        )

        decay = MemoryDecay(half_life_short=10, half_life_long=100)
        # Create a memory at tick 0
        mem = CognitiveMemoryItem(
            content="test", category="test",
            importance_base=1.0, effective_importance=1.0,
            created_tick=0, last_accessed_tick=0,
        )
        # Apply decay at tick 10 (= half_life_short)
        result = decay.apply([mem], current_tick=10)
        # effective_importance should be significantly less than 1.0
        assert result[0].effective_importance < 0.8

        # Apply decay at tick 0,  should preserve full importance
        mem2 = CognitiveMemoryItem(
            content="fresh", category="test",
            importance_base=1.0, effective_importance=1.0,
            created_tick=0, last_accessed_tick=0,
        )
        result2 = decay.apply([mem2], current_tick=0)
        assert result2[0].effective_importance >= 0.9

    def test_consolidation_promotes_stm_to_ltm(self, tmp_path):
        from opensable.core.cognitive_memory import (
            CognitiveMemoryManager,
            MemoryConsolidation,
            CognitiveMemoryItem,
        )

        # Test consolidation directly
        consolidation = MemoryConsolidation(
            promote_threshold=0.5, demote_threshold=0.05,
        )
        mem = CognitiveMemoryItem(
            content="Important fact", category="test",
            tier="short_term",
            importance_base=0.9, effective_importance=0.9,
            created_tick=0, last_accessed_tick=0,
        )
        result = consolidation.apply([mem])
        assert len(result) == 1
        assert result[0].tier == "long_term"

    def test_full_pipeline_promotes_to_working(self, tmp_path):
        from opensable.core.cognitive_memory import CognitiveMemoryManager

        mgr = CognitiveMemoryManager(
            directory=tmp_path / "mem",
            promote_threshold=0.5,
            working_memory_size=3,
        )
        # High-importance item should be promoted through consolidation
        # then selected as working memory by attention filter
        item = mgr.add_memory("Important fact", importance=0.9)

        mgr.process_tick(current_tick=1)

        items = mgr.get_all_memories()
        promoted = [i for i in items if i.content == "Important fact"]
        assert len(promoted) == 1
        # After full pipeline: consolidation promotes STM→LTM,
        # then attention filter marks top-N as "working"
        assert promoted[0].tier == "working"

    def test_attention_filter_limits_working_memory(self, tmp_path):
        from opensable.core.cognitive_memory import CognitiveMemoryManager

        mgr = CognitiveMemoryManager(
            directory=tmp_path / "mem", working_memory_size=3,
        )
        # Add 10 memories with varying importance
        for i in range(10):
            mgr.add_memory(f"Memory {i}", importance=i * 0.1)

        working = mgr.get_working_memory()
        assert len(working) <= 3

    def test_persistence_roundtrip(self, tmp_path):
        from opensable.core.cognitive_memory import CognitiveMemoryManager

        mgr1 = CognitiveMemoryManager(directory=tmp_path / "mem")
        mgr1.add_memory("Persistent fact", importance=0.7)
        mgr1.add_memory("Another fact", importance=0.5)

        # Create new instance from same directory
        mgr2 = CognitiveMemoryManager(directory=tmp_path / "mem")
        items = mgr2.get_all_memories()
        assert len(items) == 2
        contents = {i.content for i in items}
        assert "Persistent fact" in contents

    def test_context_prompt(self, tmp_path):
        from opensable.core.cognitive_memory import CognitiveMemoryManager

        mgr = CognitiveMemoryManager(directory=tmp_path / "mem")
        mgr.add_memory("Sky is blue", importance=0.8)
        # Must process a tick so attention filter promotes to working memory
        mgr.process_tick(current_tick=1)

        prompt = mgr.get_context_prompt()
        assert "Sky is blue" in prompt

    def test_process_tick_with_decay(self, tmp_path):
        from opensable.core.cognitive_memory import CognitiveMemoryManager

        mgr = CognitiveMemoryManager(
            directory=tmp_path / "mem", half_life_short=2,
        )
        item = mgr.add_memory("Decaying memory", importance=0.3)
        original_effective = item.effective_importance

        # After many ticks, effective importance should decay
        for i in range(20):
            mgr.process_tick(current_tick=i)

        items = mgr.get_all_memories()
        if items:
            decayed = [i for i in items if i.content == "Decaying memory"]
            # Either removed (below demote threshold) or decayed
            if decayed:
                assert decayed[0].effective_importance <= original_effective


# ═══════════════════════════════════════════════════════════════════════════════
# 2. ReflectionEngine
# ═══════════════════════════════════════════════════════════════════════════════


class TestReflectionEngine:
    """Tests for self-reflection pattern detection."""

    def test_record_outcome(self, tmp_path):
        from opensable.core.self_reflection import ReflectionEngine, TickOutcome

        engine = ReflectionEngine(directory=tmp_path / "ref")
        outcome = TickOutcome(
            tick=1, success=True, summary="Test tick",
            tools_used=["search"], errors=[], goals_progressed=["g1"],
        )
        engine.record_outcome(outcome)
        assert len(engine._outcomes) == 1

    def test_detect_stagnation_pattern(self, tmp_path):
        from opensable.core.self_reflection import ReflectionEngine, TickOutcome

        engine = ReflectionEngine(
            directory=tmp_path / "ref", min_outcomes=3,
        )
        # Record identical outcomes (stagnation)
        for i in range(5):
            engine.record_outcome(TickOutcome(
                tick=i, success=True, summary="Same thing",
                tools_used=["search", "read"], errors=[],
                goals_progressed=[],
            ))

        patterns = engine.detect_patterns(tick=5)
        # Should detect stagnation (same tools repeated)
        assert patterns["stagnation"] is True

    def test_detect_failure_rate(self, tmp_path):
        from opensable.core.self_reflection import ReflectionEngine, TickOutcome

        engine = ReflectionEngine(
            directory=tmp_path / "ref", min_outcomes=3,
        )
        # Record mostly failures
        for i in range(5):
            engine.record_outcome(TickOutcome(
                tick=i, success=False, summary=f"Failed {i}",
                tools_used=["exec"], errors=["timeout"],
                goals_progressed=[],
            ))

        patterns = engine.detect_patterns(tick=5)
        assert patterns["failure_rate"] >= 0.8

    def test_should_reflect(self, tmp_path):
        from opensable.core.self_reflection import ReflectionEngine, TickOutcome

        engine = ReflectionEngine(
            directory=tmp_path / "ref",
            min_outcomes=3,
            reflection_interval=5,
        )
        # Add enough outcomes
        for i in range(6):
            engine.record_outcome(TickOutcome(
                tick=i, success=True, summary=f"Tick {i}",
                tools_used=[], errors=[], goals_progressed=[],
            ))

        # Should reflect at tick 5 (interval = 5)
        assert engine.should_reflect(tick=5) is True
        # Should not at tick 3
        assert engine.should_reflect(tick=3) is False

    def test_build_reflection_prompt(self, tmp_path):
        from opensable.core.self_reflection import ReflectionEngine, TickOutcome

        engine = ReflectionEngine(directory=tmp_path / "ref", min_outcomes=1)
        engine.record_outcome(TickOutcome(
            tick=0, success=True, summary="Did stuff",
            tools_used=["search"], errors=[], goals_progressed=["g1"],
        ))

        prompt = engine.build_reflection_prompt(tick=1)
        assert "SELF-REFLECTION" in prompt or "reflection" in prompt.lower()

    def test_persistence(self, tmp_path):
        from opensable.core.self_reflection import ReflectionEngine, TickOutcome

        engine1 = ReflectionEngine(directory=tmp_path / "ref")
        engine1.record_outcome(TickOutcome(
            tick=0, success=True, summary="Persistent",
            tools_used=[], errors=[], goals_progressed=[],
        ))

        engine2 = ReflectionEngine(directory=tmp_path / "ref")
        assert len(engine2._outcomes) == 1


# ═══════════════════════════════════════════════════════════════════════════════
# 3. SkillEvolutionManager
# ═══════════════════════════════════════════════════════════════════════════════


class TestSkillEvolution:
    """Tests for evolutionary skill management."""

    def test_record_event(self, tmp_path):
        from opensable.core.skill_evolution import SkillEvolutionManager

        mgr = SkillEvolutionManager(directory=tmp_path / "evo")
        mgr.record_event("cap_created", "weather_skill", tick=1)
        assert len(mgr._events) == 1

    def test_compute_fitness(self, tmp_path):
        from opensable.core.skill_evolution import SkillEvolutionManager, compute_fitness

        mgr = SkillEvolutionManager(directory=tmp_path / "evo")
        mgr.record_event("cap_created", "skill_a", tick=0)
        mgr.record_event("cap_used", "skill_a", tick=1)
        mgr.record_event("cap_used", "skill_a", tick=2)

        rankings = mgr.get_fitness_rankings(tick=3)
        assert len(rankings) >= 1
        assert rankings[0].name == "skill_a"
        assert rankings[0].fitness_score > 0

    def test_natural_selection_condemns_low_fitness(self, tmp_path):
        from opensable.core.skill_evolution import SkillEvolutionManager

        mgr = SkillEvolutionManager(
            directory=tmp_path / "evo",
            fitness_threshold=0.5,
            min_age_ticks=2,
        )
        # Create a skill that's never used
        mgr.record_event("cap_created", "useless_skill", tick=0)
        # Create a good skill
        mgr.record_event("cap_created", "good_skill", tick=0)
        mgr.record_event("cap_used", "good_skill", tick=1)
        mgr.record_event("cap_used", "good_skill", tick=2)

        result = mgr.evaluate_tick(tick=10)
        # useless_skill should be condemned (never used, old enough)
        condemned = result.get("condemned", [])
        # It won't be condemned if fitness is above threshold, 
        # just verify the mechanism runs without error
        assert isinstance(condemned, list)

    def test_mutation_pressure(self, tmp_path):
        from opensable.core.skill_evolution import SkillEvolutionManager

        mgr = SkillEvolutionManager(
            directory=tmp_path / "evo",
            stagnation_ticks=3,
            error_threshold=0.3,
        )
        # Create and immediately have errors
        mgr.record_event("cap_created", "buggy_skill", tick=0)
        mgr.record_event("cap_error", "buggy_skill", tick=1)
        mgr.record_event("cap_error", "buggy_skill", tick=2)
        mgr.record_event("cap_used", "buggy_skill", tick=3)

        result = mgr.evaluate_tick(tick=5)
        mutants = result.get("mutations", [])
        assert isinstance(mutants, list)

    def test_recombination(self, tmp_path):
        from opensable.core.skill_evolution import SkillEvolutionManager

        mgr = SkillEvolutionManager(directory=tmp_path / "evo")
        mgr.record_event("cap_created", "skill_a", tick=0)
        mgr.record_event("cap_created", "skill_b", tick=0)
        mgr.record_event("cap_used", "skill_a", tick=1)
        mgr.record_event("cap_used", "skill_b", tick=1)

        # Recombination should identify co-occurring skills
        result = mgr.evaluate_tick(tick=2)
        assert isinstance(result, dict)

    def test_build_evolution_prompt(self, tmp_path):
        from opensable.core.skill_evolution import SkillEvolutionManager

        mgr = SkillEvolutionManager(directory=tmp_path / "evo")
        mgr.record_event("cap_created", "skill_x", tick=0)
        prompt = mgr.build_evolution_prompt(tick=1)
        assert isinstance(prompt, str)
        assert len(prompt) > 0

    def test_persistence(self, tmp_path):
        from opensable.core.skill_evolution import SkillEvolutionManager

        mgr1 = SkillEvolutionManager(directory=tmp_path / "evo")
        mgr1.record_event("cap_created", "persisted_skill", tick=0)
        mgr1.record_event("cap_used", "persisted_skill", tick=1)

        mgr2 = SkillEvolutionManager(directory=tmp_path / "evo")
        assert len(mgr2._events) == 2


# ═══════════════════════════════════════════════════════════════════════════════
# 4. GitBrain
# ═══════════════════════════════════════════════════════════════════════════════


class TestGitBrain:
    """Tests for git-backed episodic memory."""

    def test_init_creates_repo(self, tmp_path):
        from opensable.core.git_brain import GitBrain

        brain = GitBrain(repo_dir=tmp_path / "repo")
        loop = asyncio.new_event_loop()
        loop.run_until_complete(brain.initialize())
        loop.close()

        assert (tmp_path / "repo" / ".git").exists()

    def test_write_episode(self, tmp_path):
        from opensable.core.git_brain import GitBrain

        brain = GitBrain(repo_dir=tmp_path / "repo")
        loop = asyncio.new_event_loop()
        loop.run_until_complete(brain.initialize())
        loop.run_until_complete(
            brain.write_episode(1, summary="Test episode content")
        )
        loop.close()

        episodes = brain.load_recent_episodes(max_episodes=5)
        assert len(episodes) >= 1
        assert "Test episode" in episodes[0]["content"]

    def test_context_prompt(self, tmp_path):
        from opensable.core.git_brain import GitBrain

        brain = GitBrain(repo_dir=tmp_path / "repo")
        loop = asyncio.new_event_loop()
        loop.run_until_complete(brain.initialize())
        loop.close()

        prompt = brain.get_context_prompt()
        assert isinstance(prompt, str)

    def test_get_stats(self, tmp_path):
        from opensable.core.git_brain import GitBrain

        brain = GitBrain(repo_dir=tmp_path / "repo")
        loop = asyncio.new_event_loop()
        loop.run_until_complete(brain.initialize())
        loop.close()

        stats = brain.get_stats()
        assert "initialized" in stats
        assert "repo_dir" in stats

    def test_evolution_pressure(self, tmp_path):
        from opensable.core.git_brain import GitBrain

        brain = GitBrain(repo_dir=tmp_path / "repo")
        loop = asyncio.new_event_loop()
        loop.run_until_complete(brain.initialize())
        loop.close()

        pressure = brain.get_evolution_pressure()
        assert isinstance(pressure, list)


# ═══════════════════════════════════════════════════════════════════════════════
# 5. InnerLifeProcessor
# ═══════════════════════════════════════════════════════════════════════════════


class TestInnerLife:
    """Tests for System 1 unconscious processing."""

    def test_create_processor(self, tmp_path):
        from opensable.core.inner_life import InnerLifeProcessor

        proc = InnerLifeProcessor(data_dir=tmp_path / "inner")
        assert proc.state is not None
        assert proc.emotion is not None

    def test_system1_prompt(self, tmp_path):
        from opensable.core.inner_life import InnerLifeProcessor

        proc = InnerLifeProcessor(data_dir=tmp_path / "inner")
        prompt = proc.get_system1_prompt(
            active_goal="Fix bugs",
            context="Recent events: Found a bug in auth module",
        )
        assert isinstance(prompt, str)
        assert len(prompt) > 50

    def test_process_response(self, tmp_path):
        from opensable.core.inner_life import InnerLifeProcessor

        proc = InnerLifeProcessor(data_dir=tmp_path / "inner")
        # Simulate LLM response as JSON
        response = json.dumps({
            "emotion": {"primary": "curious", "valence": 0.6, "arousal": 0.4, "trigger": "new code"},
            "impulse": "explore the new codebase",
            "fantasy": "a world of perfectly organized code",
            "wandering": "wondering about system boundaries",
            "temporal": "the project is still young",
        })
        proc.process_response(response, tick=1)
        assert proc.state.emotion.primary == "curious"

    def test_emotion_modulation(self, tmp_path):
        from opensable.core.inner_life import InnerLifeProcessor

        proc = InnerLifeProcessor(data_dir=tmp_path / "inner")
        mod = proc.get_emotion_modulation()
        # Default should be neutral (1.0)
        assert 0.8 <= mod <= 1.5

    def test_context_for_system2(self, tmp_path):
        from opensable.core.inner_life import InnerLifeProcessor

        proc = InnerLifeProcessor(data_dir=tmp_path / "inner")
        ctx = proc.get_context_for_system2()
        assert isinstance(ctx, str)

    def test_evolution_pressure(self, tmp_path):
        from opensable.core.inner_life import InnerLifeProcessor

        proc = InnerLifeProcessor(data_dir=tmp_path / "inner")
        pressure = proc.get_evolution_pressure()
        assert isinstance(pressure, list)

    def test_persistence(self, tmp_path):
        from opensable.core.inner_life import InnerLifeProcessor

        proc1 = InnerLifeProcessor(data_dir=tmp_path / "inner")
        # Manually update state and save
        response = json.dumps({
            "emotion": {"primary": "excited", "valence": 0.9, "arousal": 0.7, "trigger": "test"},
            "impulse": "celebrate",
        })
        proc1.process_response(response, tick=1)

        proc2 = InnerLifeProcessor(data_dir=tmp_path / "inner")
        assert proc2.state.emotion.primary == "excited"
        assert proc2.state.emotion.valence == 0.9


# ═══════════════════════════════════════════════════════════════════════════════
# 6. PatternLearningManager
# ═══════════════════════════════════════════════════════════════════════════════


class TestPatternLearner:
    """Tests for pattern detection + institutional learning."""

    def test_report_pattern(self, tmp_path):
        from opensable.core.pattern_learner import PatternLearningManager

        mgr = PatternLearningManager(directory=tmp_path / "pat")
        pattern = mgr.report_pattern(
            "browser_fails",
            "Browser skill fails on JS-heavy sites",
            confidence=0.8,
            pattern_type="failure_mode",
        )
        assert pattern.name == "browser_fails"
        assert pattern.confidence == 0.8

    def test_dedup_patterns(self, tmp_path):
        from opensable.core.pattern_learner import PatternLearningManager

        mgr = PatternLearningManager(directory=tmp_path / "pat")
        p1 = mgr.report_pattern("dup", "First description")
        p2 = mgr.report_pattern("dup", "Second description")
        assert p1 is p2  # Same object (dedup)

    def test_history_window(self, tmp_path):
        from opensable.core.pattern_learner import HistoryWindow
        from dataclasses import dataclass

        @dataclass
        class FakeEvent:
            tick: int
            event_type: str = "test"
            subject: str = "s"

        window = HistoryWindow(window_ticks=5)
        events = [FakeEvent(tick=i) for i in range(20)]
        windowed = window.apply(events, tick=19)

        # Only events with tick >= 14 should remain
        assert all(e.tick >= 14 for e in windowed)
        assert len(windowed) == 6  # 14,15,16,17,18,19

        # Archive summary should exist
        assert "Archived" in window.archive_summary

    def test_fitness_snapshotter(self, tmp_path):
        from opensable.core.pattern_learner import FitnessSnapshotter

        snapper = FitnessSnapshotter(
            directory=tmp_path / "snap", snapshot_interval=5,
        )

        # Should not snapshot at tick 1
        result = snapper.maybe_snapshot(1, [{"name": "a", "fitness_score": 0.5}])
        assert result is None

        # Should snapshot at tick 5
        result = snapper.maybe_snapshot(5, [{"name": "a", "fitness_score": 0.5}])
        assert result is not None
        assert result.tick == 5

    def test_fitness_trend(self, tmp_path):
        from opensable.core.pattern_learner import FitnessSnapshotter

        snapper = FitnessSnapshotter(
            directory=tmp_path / "snap", snapshot_interval=5,
        )
        snapper.maybe_snapshot(5, [{"name": "skill_a", "fitness_score": 0.3}])
        snapper.maybe_snapshot(10, [{"name": "skill_a", "fitness_score": 0.7}])
        snapper.maybe_snapshot(15, [{"name": "skill_a", "fitness_score": 0.9}])

        trend = snapper.get_trend("skill_a")
        assert len(trend) == 3
        assert trend[0]["fitness"] == 0.3
        assert trend[2]["fitness"] == 0.9

    def test_institutional_learning(self, tmp_path):
        from opensable.core.pattern_learner import (
            PatternLearningManager,
            EvolutionPattern,
        )

        mgr = PatternLearningManager(
            directory=tmp_path / "pat", min_confidence=0.5,
        )
        # Report a high-confidence failure pattern
        mgr.report_pattern(
            "failure:timeout",
            "Skills using HTTP timeout frequently",
            confidence=0.9,
            pattern_type="failure_mode",
        )

        # Process tick to trigger institutional learning
        result = mgr.process_tick(tick=1, events=[], fitness_records=[])
        assert result["new_rules"] >= 1

        # Check rules prompt
        prompt = mgr.get_rules_prompt()
        assert "timeout" in prompt.lower() or "INSTITUTIONAL" in prompt

    def test_process_tick_full_pipeline(self, tmp_path):
        from opensable.core.pattern_learner import PatternLearningManager
        from dataclasses import dataclass

        @dataclass
        class FakeEvent:
            tick: int
            event_type: str = "cap_created"
            subject: str = "skill_x"
            parent: str = ""
            outcome: str = "success"
            details: str = ""

        mgr = PatternLearningManager(
            directory=tmp_path / "pat",
            window_ticks=10,
            snapshot_interval=5,
        )

        events = [FakeEvent(tick=i, subject=f"skill_{i % 3}") for i in range(20)]
        fitness = [{"name": f"skill_{i}", "fitness_score": 0.5 + i * 0.1} for i in range(3)]

        result = mgr.process_tick(tick=10, events=events, fitness_records=fitness)
        assert "windowed_events" in result
        assert result["windowed_events"] <= len(events)
        assert result["snapshot_taken"] is True

    def test_persistence(self, tmp_path):
        from opensable.core.pattern_learner import PatternLearningManager

        mgr1 = PatternLearningManager(directory=tmp_path / "pat")
        mgr1.report_pattern("persisted_pattern", "Should survive reload", confidence=0.7)

        mgr2 = PatternLearningManager(directory=tmp_path / "pat")
        patterns = mgr2.detector.get_patterns()
        assert len(patterns) == 1
        assert patterns[0].name == "persisted_pattern"

    def test_get_stats(self, tmp_path):
        from opensable.core.pattern_learner import PatternLearningManager

        mgr = PatternLearningManager(directory=tmp_path / "pat")
        stats = mgr.get_stats()
        assert "patterns" in stats
        assert "rules" in stats
        assert "snapshots" in stats


# ═══════════════════════════════════════════════════════════════════════════════
# 7. SkillFitnessTracker,  Windowed extensions
# ═══════════════════════════════════════════════════════════════════════════════


class TestSkillFitnessWindowed:
    """Tests for windowed fitness computation extensions."""

    def test_compute_fitness_windowed(self, tmp_path):
        from opensable.core.skill_fitness import SkillFitnessTracker

        tracker = SkillFitnessTracker(directory=tmp_path / "fit")
        tracker.record_created("old_skill", tick=0)
        tracker.record_used("old_skill", tick=1)
        tracker.record_created("new_skill", tick=50)
        tracker.record_used("new_skill", tick=51)

        # Full fitness should show both
        full = tracker.compute_fitness()
        names_full = {r.name for r in full}
        assert "old_skill" in names_full
        assert "new_skill" in names_full

        # Windowed fitness (window=10, tick=55) should only show new_skill
        windowed = tracker.compute_fitness_windowed(current_tick=55, window_ticks=10)
        names_windowed = {r.name for r in windowed}
        assert "new_skill" in names_windowed
        assert "old_skill" not in names_windowed

    def test_get_fitness_dicts(self, tmp_path):
        from opensable.core.skill_fitness import SkillFitnessTracker

        tracker = SkillFitnessTracker(directory=tmp_path / "fit")
        tracker.record_created("skill_a", tick=0)
        tracker.record_used("skill_a", tick=1)

        dicts = tracker.get_fitness_dicts()
        assert isinstance(dicts, list)
        assert len(dicts) >= 1
        assert "name" in dicts[0]
        assert "fitness_score" in dicts[0]

    def test_get_events_since(self, tmp_path):
        from opensable.core.skill_fitness import SkillFitnessTracker

        tracker = SkillFitnessTracker(directory=tmp_path / "fit")
        tracker.record_created("s", tick=0)
        tracker.record_used("s", tick=5)
        tracker.record_used("s", tick=10)

        since_5 = tracker.get_events_since(5)
        assert len(since_5) == 2  # tick 5 and tick 10
        assert all(e.tick >= 5 for e in since_5)

    def test_windowed_dicts(self, tmp_path):
        from opensable.core.skill_fitness import SkillFitnessTracker

        tracker = SkillFitnessTracker(directory=tmp_path / "fit")
        tracker.record_created("old", tick=0)
        tracker.record_created("new", tick=90)
        tracker.record_used("new", tick=95)

        dicts = tracker.get_fitness_dicts(current_tick=100, window_ticks=20)
        names = {d["name"] for d in dicts}
        assert "new" in names
        assert "old" not in names
