"""Tests for autodebug.agent.reflections — episodic memory via Reflexion."""

import json
from pathlib import Path

import pytest

from autodebug.agent.reflections import (
    MAX_REFLECTIONS,
    Reflection,
    auto_reflect_from_session,
    load_recent_reflections,
    render_reflections_for_prompt,
    save_reflection,
    search_reflections,
)


@pytest.fixture
def project_dir(tmp_path):
    """Temp project directory with .hikaflow/ pre-created."""
    (tmp_path / ".hikaflow").mkdir()
    return str(tmp_path)


def _jsonl_path(project_dir: str) -> Path:
    return Path(project_dir) / ".hikaflow" / "reflections.jsonl"


# ---------------------------------------------------------------------------
# Reflection dataclass
# ---------------------------------------------------------------------------

class TestReflection:
    def test_round_trip(self):
        r = Reflection(
            session_id="abc",
            timestamp="2026-01-01T00:00:00Z",
            task="debug flaky test",
            outcome="success",
            reflection="The test was order-dependent",
            key_files=["tests/test_foo.py"],
            turns=5,
        )
        d = r.to_dict()
        r2 = Reflection.from_dict(d)
        assert r2.session_id == "abc"
        assert r2.task == "debug flaky test"
        assert r2.key_files == ["tests/test_foo.py"]
        assert r2.turns == 5

    def test_from_dict_defaults(self):
        r = Reflection.from_dict({})
        assert r.session_id == ""
        assert r.key_files == []
        assert r.turns == 0


# ---------------------------------------------------------------------------
# save_reflection
# ---------------------------------------------------------------------------

class TestSaveReflection:
    def test_creates_file(self, project_dir):
        save_reflection(project_dir, "s1", "task1", "success", "learned X")
        path = _jsonl_path(project_dir)
        assert path.is_file()
        lines = path.read_text().strip().splitlines()
        assert len(lines) == 1
        data = json.loads(lines[0])
        assert data["task"] == "task1"
        assert data["outcome"] == "success"

    def test_appends_multiple(self, project_dir):
        save_reflection(project_dir, "s1", "task1", "success", "r1")
        save_reflection(project_dir, "s2", "task2", "failed", "r2")
        lines = _jsonl_path(project_dir).read_text().strip().splitlines()
        assert len(lines) == 2

    def test_sliding_window(self, project_dir):
        # Fill beyond MAX_REFLECTIONS
        for i in range(MAX_REFLECTIONS + 10):
            save_reflection(project_dir, f"s{i}", f"task{i}", "success", f"r{i}")
        lines = _jsonl_path(project_dir).read_text().strip().splitlines()
        assert len(lines) == MAX_REFLECTIONS
        # Oldest entries trimmed — first entry should be task10
        first = json.loads(lines[0])
        assert first["task"] == "task10"

    def test_creates_hikaflow_dir(self, tmp_path):
        # No .hikaflow dir yet
        project = str(tmp_path / "newproject")
        save_reflection(project, "s1", "task1", "success", "r1")
        assert _jsonl_path(project).is_file()


# ---------------------------------------------------------------------------
# load_recent_reflections
# ---------------------------------------------------------------------------

class TestLoadRecentReflections:
    def test_returns_empty_for_missing_file(self, project_dir):
        result = load_recent_reflections(project_dir)
        assert result == []

    def test_returns_last_n(self, project_dir):
        for i in range(5):
            save_reflection(project_dir, f"s{i}", f"task{i}", "success", f"r{i}")
        result = load_recent_reflections(project_dir, limit=3)
        assert len(result) == 3
        assert result[0].task == "task2"
        assert result[2].task == "task4"

    def test_returns_all_when_fewer_than_limit(self, project_dir):
        save_reflection(project_dir, "s1", "task1", "success", "r1")
        result = load_recent_reflections(project_dir, limit=3)
        assert len(result) == 1

    def test_handles_corrupt_lines(self, project_dir):
        path = _jsonl_path(project_dir)
        path.write_text('{"task":"good","session_id":"s1","timestamp":"t","outcome":"ok","reflection":"r"}\nBAD JSON\n')
        result = load_recent_reflections(project_dir, limit=5)
        assert len(result) == 1
        assert result[0].task == "good"


# ---------------------------------------------------------------------------
# search_reflections
# ---------------------------------------------------------------------------

class TestSearchReflections:
    def test_keyword_match_in_task(self, project_dir):
        save_reflection(project_dir, "s1", "debug flaky test", "success", "was order dep")
        save_reflection(project_dir, "s2", "fix auth bug", "success", "token expired")
        results = search_reflections(project_dir, "flaky")
        assert len(results) == 1
        assert results[0].task == "debug flaky test"

    def test_keyword_match_in_reflection(self, project_dir):
        save_reflection(project_dir, "s1", "investigate error", "success", "found a race condition")
        results = search_reflections(project_dir, "race condition")
        assert len(results) == 1

    def test_case_insensitive(self, project_dir):
        save_reflection(project_dir, "s1", "Debug FLAKY Test", "success", "r")
        results = search_reflections(project_dir, "flaky")
        assert len(results) == 1

    def test_empty_on_no_match(self, project_dir):
        save_reflection(project_dir, "s1", "fix bug", "success", "was a typo")
        results = search_reflections(project_dir, "nonexistent")
        assert results == []

    def test_limit_applied(self, project_dir):
        for i in range(10):
            save_reflection(project_dir, f"s{i}", f"flaky test {i}", "success", f"r{i}")
        results = search_reflections(project_dir, "flaky", limit=3)
        assert len(results) == 3


# ---------------------------------------------------------------------------
# render_reflections_for_prompt
# ---------------------------------------------------------------------------

class TestRenderForPrompt:
    def test_empty_list(self):
        assert render_reflections_for_prompt([]) == ""

    def test_format_includes_header(self):
        r = Reflection("s1", "2026-01-15T12:00:00Z", "debug error", "success", "lesson", ["f.py"], 3)
        output = render_reflections_for_prompt([r])
        assert "## Past Session Reflections" in output
        assert "2026-01-15" in output
        assert "debug error" in output
        assert "success" in output
        assert "f.py" in output
        assert "lesson" in output

    def test_multiple_reflections(self):
        refs = [
            Reflection("s1", "2026-01-01T00:00:00Z", "task1", "success", "r1"),
            Reflection("s2", "2026-01-02T00:00:00Z", "task2", "failed", "r2"),
        ]
        output = render_reflections_for_prompt(refs)
        assert "task1" in output
        assert "task2" in output

    def test_no_files_line_when_empty(self):
        r = Reflection("s1", "2026-01-01T00:00:00Z", "task", "success", "r", [], 0)
        output = render_reflections_for_prompt([r])
        assert "Files:" not in output


# ---------------------------------------------------------------------------
# auto_reflect_from_session
# ---------------------------------------------------------------------------

class TestAutoReflect:
    def test_skips_empty_session_state(self, project_dir):
        from autodebug.agent.memory import MemoryBlocks
        blocks = MemoryBlocks()
        auto_reflect_from_session(project_dir, blocks)
        assert not _jsonl_path(project_dir).is_file()

    def test_skips_short_session_state(self, project_dir):
        from autodebug.agent.memory import MemoryBlocks
        blocks = MemoryBlocks()
        blocks._blocks["session_state"] = "too short"
        auto_reflect_from_session(project_dir, blocks)
        assert not _jsonl_path(project_dir).is_file()

    def test_saves_from_session_state(self, project_dir):
        from autodebug.agent.memory import MemoryBlocks
        blocks = MemoryBlocks()
        blocks._blocks["session_state"] = "Investigated auth bug in api/auth.py. Found expired token handling was wrong."
        auto_reflect_from_session(project_dir, blocks)
        path = _jsonl_path(project_dir)
        assert path.is_file()
        data = json.loads(path.read_text().strip())
        assert "auth bug" in data["task"].lower() or "investigated" in data["task"].lower()
        assert data["session_id"] == blocks.session_id

    def test_infers_success_outcome(self, project_dir):
        from autodebug.agent.memory import MemoryBlocks
        blocks = MemoryBlocks()
        blocks._blocks["session_state"] = "Fixed the auth bug. Token refresh now works correctly."
        auto_reflect_from_session(project_dir, blocks)
        data = json.loads(_jsonl_path(project_dir).read_text().strip())
        assert data["outcome"] == "success"

    def test_infers_failed_outcome(self, project_dir):
        from autodebug.agent.memory import MemoryBlocks
        blocks = MemoryBlocks()
        blocks._blocks["session_state"] = "Unable to reproduce the error. Failed to find root cause."
        auto_reflect_from_session(project_dir, blocks)
        data = json.loads(_jsonl_path(project_dir).read_text().strip())
        assert data["outcome"] == "failed"

    def test_infers_partial_outcome(self, project_dir):
        from autodebug.agent.memory import MemoryBlocks
        blocks = MemoryBlocks()
        blocks._blocks["session_state"] = "Analyzed the codebase structure. Found several potential issues."
        auto_reflect_from_session(project_dir, blocks)
        data = json.loads(_jsonl_path(project_dir).read_text().strip())
        assert data["outcome"] == "partial"
