"""Tests for Phase 5 dynamic skill library."""

import json
from unittest.mock import MagicMock

import pytest

from autodebug.agent.skill_library import (
    MAX_SKILLS,
    VERIFY_THRESHOLD,
    DynamicSkill,
    _auto_prune,
    _extract_tags,
    _slugify,
    auto_create_skill_from_session,
    get_skill,
    load_top_skills,
    record_skill_outcome,
    render_skills_for_prompt,
    save_skill,
    search_skills,
)


# ---------------------------------------------------------------------------
# DynamicSkill dataclass
# ---------------------------------------------------------------------------

class TestDynamicSkill:
    def test_round_trip(self):
        skill = DynamicSkill(
            skill_id="fix-race", description="Fix race conditions",
            procedure=["Step 1", "Step 2"], tags=["async", "race"],
            created_from="sess-1", created_at="2026-01-01T00:00:00",
            verified=True, times_used=5, times_succeeded=4,
        )
        d = skill.to_dict()
        restored = DynamicSkill.from_dict(d)
        assert restored.skill_id == "fix-race"
        assert restored.verified is True
        assert restored.times_used == 5

    def test_defaults(self):
        skill = DynamicSkill(skill_id="test", description="Test", procedure=[])
        assert skill.verified is False
        assert skill.times_used == 0
        assert skill.tags == []

    def test_success_rate(self):
        skill = DynamicSkill(skill_id="a", description="a", procedure=[], times_used=10, times_succeeded=7)
        assert skill.success_rate == 0.7

    def test_success_rate_zero_uses(self):
        skill = DynamicSkill(skill_id="a", description="a", procedure=[])
        assert skill.success_rate == 0.0


# ---------------------------------------------------------------------------
# save_skill
# ---------------------------------------------------------------------------

class TestSaveSkill:
    def test_creates_file(self, tmp_path):
        sid = save_skill(str(tmp_path), "Debug async issues", ["Step 1"], ["async"])
        assert sid == "debug-async-issues"
        path = tmp_path / ".hikaflow" / "skills.jsonl"
        assert path.is_file()
        data = json.loads(path.read_text().strip())
        assert data["skill_id"] == "debug-async-issues"

    def test_dedup_updates_existing(self, tmp_path):
        save_skill(str(tmp_path), "Fix race", ["Old step"], ["race"])
        save_skill(str(tmp_path), "Fix race", ["New step"], ["race", "async"])
        path = tmp_path / ".hikaflow" / "skills.jsonl"
        lines = path.read_text().strip().split("\n")
        assert len(lines) == 1
        data = json.loads(lines[0])
        assert data["procedure"] == ["New step"]
        assert data["tags"] == ["race", "async"]

    def test_creates_dir(self, tmp_path):
        save_skill(str(tmp_path / "subdir"), "Test", ["Step 1"])
        assert (tmp_path / "subdir" / ".hikaflow" / "skills.jsonl").is_file()

    def test_max_cap(self, tmp_path):
        for i in range(MAX_SKILLS + 5):
            save_skill(str(tmp_path), f"Skill {i}", [f"Step for {i}"])
        path = tmp_path / ".hikaflow" / "skills.jsonl"
        lines = [l for l in path.read_text().strip().split("\n") if l]
        assert len(lines) <= MAX_SKILLS


# ---------------------------------------------------------------------------
# search_skills
# ---------------------------------------------------------------------------

class TestSearchSkills:
    def test_matches_description(self, tmp_path):
        save_skill(str(tmp_path), "Debug async race condition", ["Step 1"], ["async"])
        save_skill(str(tmp_path), "Fix memory leak", ["Step 1"], ["memory"])
        results = search_skills(str(tmp_path), "async race")
        assert len(results) == 1
        assert results[0].skill_id == "debug-async-race-condition"

    def test_matches_tags(self, tmp_path):
        save_skill(str(tmp_path), "Some skill", ["Step 1"], ["redis", "cache"])
        results = search_skills(str(tmp_path), "redis")
        assert len(results) == 1

    def test_matches_procedure(self, tmp_path):
        save_skill(str(tmp_path), "Generic fix", ["Check the database connection"], ["db"])
        results = search_skills(str(tmp_path), "database")
        assert len(results) == 1

    def test_multi_token_scoring(self, tmp_path):
        save_skill(str(tmp_path), "Fix async timeout", ["Step 1"], ["async", "timeout"])
        save_skill(str(tmp_path), "Fix timeout only", ["Step 1"], ["timeout"])
        results = search_skills(str(tmp_path), "async timeout")
        assert results[0].skill_id == "fix-async-timeout"

    def test_verified_ranked_higher(self, tmp_path):
        save_skill(str(tmp_path), "Unverified skill", ["Step"], ["debug"])
        save_skill(str(tmp_path), "Verified skill", ["Step"], ["debug"])
        # Manually verify the second one
        path = tmp_path / ".hikaflow" / "skills.jsonl"
        lines = path.read_text().strip().split("\n")
        d = json.loads(lines[1])
        d["verified"] = True
        d["times_used"] = 5
        d["times_succeeded"] = 5
        lines[1] = json.dumps(d)
        path.write_text("\n".join(lines) + "\n")
        results = search_skills(str(tmp_path), "debug")
        assert results[0].skill_id == "verified-skill"

    def test_empty_query(self, tmp_path):
        save_skill(str(tmp_path), "Some skill", ["Step 1"])
        assert search_skills(str(tmp_path), "") == []

    def test_limit(self, tmp_path):
        for i in range(5):
            save_skill(str(tmp_path), f"Debug item {i}", ["Step"], ["debug"])
        results = search_skills(str(tmp_path), "debug", limit=2)
        assert len(results) == 2


# ---------------------------------------------------------------------------
# get_skill
# ---------------------------------------------------------------------------

class TestGetSkill:
    def test_found(self, tmp_path):
        save_skill(str(tmp_path), "Fix timeout", ["Step 1"])
        s = get_skill(str(tmp_path), "fix-timeout")
        assert s is not None
        assert s.description == "Fix timeout"

    def test_not_found(self, tmp_path):
        assert get_skill(str(tmp_path), "nonexistent") is None


# ---------------------------------------------------------------------------
# record_skill_outcome
# ---------------------------------------------------------------------------

class TestRecordOutcome:
    def test_success_increment(self, tmp_path):
        save_skill(str(tmp_path), "Fix bug", ["Step 1"])
        record_skill_outcome(str(tmp_path), "fix-bug", success=True)
        s = get_skill(str(tmp_path), "fix-bug")
        assert s.times_used == 1
        assert s.times_succeeded == 1

    def test_failure_increment(self, tmp_path):
        save_skill(str(tmp_path), "Fix bug", ["Step 1"])
        record_skill_outcome(str(tmp_path), "fix-bug", success=False)
        s = get_skill(str(tmp_path), "fix-bug")
        assert s.times_used == 1
        assert s.times_succeeded == 0

    def test_auto_verify_at_threshold(self, tmp_path):
        save_skill(str(tmp_path), "Fix bug", ["Step 1"])
        for _ in range(VERIFY_THRESHOLD):
            record_skill_outcome(str(tmp_path), "fix-bug", success=True)
        s = get_skill(str(tmp_path), "fix-bug")
        assert s.verified is True

    def test_not_found(self, tmp_path):
        assert record_skill_outcome(str(tmp_path), "missing", success=True) is False


# ---------------------------------------------------------------------------
# _auto_prune
# ---------------------------------------------------------------------------

class TestAutoPrune:
    def test_prunes_low_rate(self):
        skills = [
            DynamicSkill(skill_id="bad", description="", procedure=[],
                         times_used=10, times_succeeded=1),
        ]
        result = _auto_prune(skills)
        assert len(result) == 0

    def test_keeps_below_min_uses(self):
        skills = [
            DynamicSkill(skill_id="new", description="", procedure=[],
                         times_used=2, times_succeeded=0),
        ]
        result = _auto_prune(skills)
        assert len(result) == 1

    def test_keeps_high_rate(self):
        skills = [
            DynamicSkill(skill_id="good", description="", procedure=[],
                         times_used=10, times_succeeded=8),
        ]
        result = _auto_prune(skills)
        assert len(result) == 1


# ---------------------------------------------------------------------------
# load_top_skills
# ---------------------------------------------------------------------------

class TestLoadTopSkills:
    def test_verified_first(self, tmp_path):
        save_skill(str(tmp_path), "Unverified one", ["Step"])
        save_skill(str(tmp_path), "Verified one", ["Step"])
        # Verify the second
        path = tmp_path / ".hikaflow" / "skills.jsonl"
        lines = path.read_text().strip().split("\n")
        d = json.loads(lines[1])
        d["verified"] = True
        d["times_used"] = 3
        d["times_succeeded"] = 3
        lines[1] = json.dumps(d)
        path.write_text("\n".join(lines) + "\n")
        top = load_top_skills(str(tmp_path), limit=2)
        assert top[0].verified is True

    def test_empty(self, tmp_path):
        assert load_top_skills(str(tmp_path)) == []

    def test_limit(self, tmp_path):
        for i in range(10):
            save_skill(str(tmp_path), f"Skill number {i}", [f"Step {i}"])
        top = load_top_skills(str(tmp_path), limit=3)
        assert len(top) == 3


# ---------------------------------------------------------------------------
# render_skills_for_prompt
# ---------------------------------------------------------------------------

class TestRenderForPrompt:
    def test_empty(self):
        assert render_skills_for_prompt([]) == ""

    def test_header(self):
        skills = [DynamicSkill(skill_id="fix-race", description="Fix race conditions", procedure=[])]
        result = render_skills_for_prompt(skills)
        assert "Learned Skills" in result
        assert "fix-race" in result

    def test_verified_badge(self):
        skills = [DynamicSkill(skill_id="fix-race", description="Fix race", procedure=[], verified=True)]
        result = render_skills_for_prompt(skills)
        assert "[verified]" in result


# ---------------------------------------------------------------------------
# _slugify
# ---------------------------------------------------------------------------

class TestSlugify:
    def test_basic(self):
        assert _slugify("Debug async issues") == "debug-async-issues"

    def test_special_chars(self):
        assert _slugify("Fix: race condition!") == "fix-race-condition"

    def test_length_cap(self):
        long_text = "a " * 100
        result = _slugify(long_text)
        assert len(result) <= 60


# ---------------------------------------------------------------------------
# auto_create_skill_from_session
# ---------------------------------------------------------------------------

class TestAutoCreate:
    def _mock_blocks(self, state: str, session_id: str = "sess-1") -> MagicMock:
        blocks = MagicMock()
        blocks.get.return_value = state
        blocks.session_id = session_id
        return blocks

    def test_skips_none_blocks(self):
        assert auto_create_skill_from_session("/tmp", None) is None

    def test_skips_empty_state(self):
        blocks = self._mock_blocks("")
        assert auto_create_skill_from_session("/tmp", blocks) is None

    def test_skips_failed_session(self):
        blocks = self._mock_blocks("Failed to fix the async timeout error\n1. Tried X\n2. Tried Y")
        assert auto_create_skill_from_session("/tmp", blocks) is None

    def test_skips_short_steps(self):
        blocks = self._mock_blocks("Fixed the async timeout issue successfully\nNo steps here.")
        assert auto_create_skill_from_session("/tmp", blocks) is None

    def test_creates_from_successful_session(self, tmp_path):
        state = (
            "Fixed async race condition in worker pool\n"
            "1. Identified shared mutable state\n"
            "2. Added asyncio.Lock around critical section\n"
            "3. Verified with concurrent test runs\n"
            "Successfully resolved the race condition."
        )
        blocks = self._mock_blocks(state)
        sid = auto_create_skill_from_session(str(tmp_path), blocks)
        assert sid is not None
        s = get_skill(str(tmp_path), sid)
        assert s is not None
        assert len(s.procedure) == 3

    def test_extracts_tags(self):
        tags = _extract_tags("fixed async race condition in redis cache")
        assert "async" in tags
        assert "race" in tags
        assert "redis" in tags
        assert "cache" in tags
