"""Tests for brynq.context_optimizer — Context Window Optimizer (Phase 64)."""

import json
import pytest
from pathlib import Path


@pytest.fixture(autouse=True)
def _isolate_context(tmp_path, monkeypatch):
    """Redirect context optimizer storage to temp directory."""
    cache_dir = tmp_path / "context_cache"
    cache_dir.mkdir()
    channels_dir = tmp_path / "channels"
    channels_dir.mkdir()
    profiles_dir = tmp_path / "profiles"
    profiles_dir.mkdir()
    memory_dir = tmp_path / "memory"
    memory_dir.mkdir()
    monkeypatch.setattr("brynq.context_optimizer.CONTEXT_CACHE_DIR", cache_dir)
    monkeypatch.setattr("brynq.context_optimizer.BROKER_DIR", tmp_path)


# ── estimate_tokens ───────────────────────────────────────────────────


class TestEstimateTokens:
    def test_empty_string(self):
        from brynq.context_optimizer import estimate_tokens
        assert estimate_tokens("") == 0

    def test_single_word(self):
        from brynq.context_optimizer import estimate_tokens
        result = estimate_tokens("hello")
        assert result == 2  # ceil(1 * 1.3)

    def test_multiple_words(self):
        from brynq.context_optimizer import estimate_tokens
        result = estimate_tokens("the quick brown fox jumps")
        assert result == 7  # ceil(5 * 1.3)

    def test_whitespace_only(self):
        from brynq.context_optimizer import estimate_tokens
        assert estimate_tokens("   ") == 0

    def test_long_text(self):
        from brynq.context_optimizer import estimate_tokens
        text = " ".join(["word"] * 100)
        result = estimate_tokens(text)
        assert result == 130  # ceil(100 * 1.3)


# ── chunk_text ────────────────────────────────────────────────────────


class TestChunkText:
    def test_empty_text(self):
        from brynq.context_optimizer import chunk_text
        assert chunk_text("") == []

    def test_short_text_single_chunk(self):
        from brynq.context_optimizer import chunk_text
        result = chunk_text("short text", chunk_size=500)
        assert result == ["short text"]

    def test_splits_long_text(self):
        from brynq.context_optimizer import chunk_text
        text = "a" * 1000
        chunks = chunk_text(text, chunk_size=300, overlap=50)
        assert len(chunks) >= 3
        # Each chunk should be <= chunk_size
        for chunk in chunks:
            assert len(chunk) <= 300

    def test_overlap(self):
        from brynq.context_optimizer import chunk_text
        text = "a" * 200
        chunks = chunk_text(text, chunk_size=100, overlap=20)
        assert len(chunks) >= 2
        # First chunk ends at 100, second starts at 80
        # So the overlap region should be chars 80-100

    def test_custom_sizes(self):
        from brynq.context_optimizer import chunk_text
        text = "x" * 500
        chunks = chunk_text(text, chunk_size=100, overlap=0)
        assert len(chunks) == 5


# ── extract_key_points ────────────────────────────────────────────────


class TestExtractKeyPoints:
    def test_empty_messages(self):
        from brynq.context_optimizer import extract_key_points
        result = extract_key_points([])
        assert result == {"decisions": [], "action_items": [], "questions": [], "status_updates": []}

    def test_detects_decisions(self):
        from brynq.context_optimizer import extract_key_points
        msgs = [{"message": "We decided to use Python for the backend."}]
        result = extract_key_points(msgs)
        assert len(result["decisions"]) == 1

    def test_detects_action_items(self):
        from brynq.context_optimizer import extract_key_points
        msgs = [{"message": "TODO: update the database schema"}]
        result = extract_key_points(msgs)
        assert len(result["action_items"]) == 1

    def test_detects_questions(self):
        from brynq.context_optimizer import extract_key_points
        msgs = [{"message": "What about the deployment timeline?"}]
        result = extract_key_points(msgs)
        assert len(result["questions"]) == 1

    def test_detects_status_updates(self):
        from brynq.context_optimizer import extract_key_points
        msgs = [{"message": "API migration completed successfully."}]
        result = extract_key_points(msgs)
        assert len(result["status_updates"]) == 1

    def test_mixed_messages(self):
        from brynq.context_optimizer import extract_key_points
        msgs = [
            {"message": "We decided to go with Redis."},
            {"message": "TODO: write migration scripts"},
            {"message": "When is the deadline?"},
            {"message": "Auth module is done."},
        ]
        result = extract_key_points(msgs)
        assert len(result["decisions"]) >= 1
        assert len(result["action_items"]) >= 1
        assert len(result["questions"]) >= 1
        assert len(result["status_updates"]) >= 1

    def test_uses_content_field_fallback(self):
        from brynq.context_optimizer import extract_key_points
        msgs = [{"content": "We agreed on the approach."}]
        result = extract_key_points(msgs)
        assert len(result["decisions"]) == 1


# ── summarize_conversation ────────────────────────────────────────────


class TestSummarizeConversation:
    def test_empty_messages(self):
        from brynq.context_optimizer import summarize_conversation
        assert summarize_conversation([]) == ""

    def test_produces_summary(self):
        from brynq.context_optimizer import summarize_conversation
        msgs = [
            {"message": "We decided to use PostgreSQL.", "sender": "alice"},
            {"message": "TODO: set up the database", "sender": "bob"},
            {"message": "Migration completed.", "sender": "alice"},
        ]
        summary = summarize_conversation(msgs)
        assert len(summary) > 0

    def test_respects_max_tokens(self):
        from brynq.context_optimizer import summarize_conversation, estimate_tokens
        msgs = [{"message": f"Long message number {i} with lots of text " * 10, "sender": "bot"} for i in range(50)]
        summary = summarize_conversation(msgs, max_tokens=100)
        assert estimate_tokens(summary) <= 150  # Allow some slack

    def test_fallback_to_recent_messages(self):
        from brynq.context_optimizer import summarize_conversation
        # Messages with no recognizable key points
        msgs = [
            {"message": "Hello everyone.", "sender": "alice"},
            {"message": "Good morning.", "sender": "bob"},
        ]
        summary = summarize_conversation(msgs)
        assert len(summary) > 0


# ── summarize_channel ─────────────────────────────────────────────────


class TestSummarizeChannel:
    def test_empty_channel(self, tmp_path):
        from brynq.context_optimizer import summarize_channel
        result = summarize_channel("empty-channel")
        assert "no messages" in result.lower()

    def test_channel_with_messages(self, tmp_path):
        from brynq.context_optimizer import summarize_channel, BROKER_DIR
        from brynq.server import _append_jsonl_sync
        channels_dir = BROKER_DIR / "channels"
        channels_dir.mkdir(parents=True, exist_ok=True)
        ch_file = channels_dir / "dev.jsonl"
        _append_jsonl_sync(ch_file, {"message": "We decided to use FastAPI.", "sender": "alice"})
        _append_jsonl_sync(ch_file, {"message": "Deployment done.", "sender": "bob"})
        summary = summarize_channel("dev")
        assert len(summary) > 0

    def test_caches_summary(self, tmp_path):
        from brynq.context_optimizer import summarize_channel, CONTEXT_CACHE_DIR, BROKER_DIR
        from brynq.server import _append_jsonl_sync
        channels_dir = BROKER_DIR / "channels"
        channels_dir.mkdir(parents=True, exist_ok=True)
        ch_file = channels_dir / "cached.jsonl"
        _append_jsonl_sync(ch_file, {"message": "hello", "sender": "x"})
        summarize_channel("cached")
        assert (CONTEXT_CACHE_DIR / "channel_cached.json").exists()


# ── compress_context ──────────────────────────────────────────────────


class TestCompressContext:
    def test_empty_dict(self):
        from brynq.context_optimizer import compress_context
        assert compress_context({}) == {}

    def test_small_context_unchanged(self):
        from brynq.context_optimizer import compress_context
        ctx = {"task": "build API", "status": "in_progress"}
        result = compress_context(ctx, target_tokens=4000)
        assert result == ctx

    def test_large_context_compressed(self):
        from brynq.context_optimizer import compress_context, estimate_tokens
        ctx = {
            "task": "build API",
            "metadata": "x " * 2000,
            "history": "y " * 2000,
            "context": "z " * 2000,
        }
        result = compress_context(ctx, target_tokens=100)
        result_tokens = estimate_tokens(json.dumps(result, default=str))
        # Should be significantly smaller
        assert result_tokens < estimate_tokens(json.dumps(ctx, default=str))

    def test_preserves_high_priority_fields(self):
        from brynq.context_optimizer import compress_context
        ctx = {
            "task": "important task",
            "metadata": "x " * 500,
            "history": "y " * 500,
        }
        result = compress_context(ctx, target_tokens=50)
        assert "task" in result

    def test_truncates_long_strings(self):
        from brynq.context_optimizer import compress_context
        ctx = {"metadata": "word " * 2000}  # ~2000 words => ~2600 tokens
        result = compress_context(ctx, target_tokens=50)
        if "metadata" in result:
            assert len(result["metadata"]) < len("word " * 2000)

    def test_truncates_long_lists(self):
        from brynq.context_optimizer import compress_context
        ctx = {"history": list(range(100))}
        result = compress_context(ctx, target_tokens=50)
        if "history" in result:
            assert len(result["history"]) <= 3


# ── build_agent_context ───────────────────────────────────────────────


class TestBuildAgentContext:
    def test_basic_context(self):
        from brynq.context_optimizer import build_agent_context
        ctx = build_agent_context("agent-1", "Build the API")
        assert ctx["agent_id"] == "agent-1"
        assert ctx["task_description"] == "Build the API"
        assert "built_at" in ctx

    def test_loads_agent_profile(self, tmp_path):
        from brynq.context_optimizer import build_agent_context, BROKER_DIR
        profiles_dir = BROKER_DIR / "profiles"
        profiles_dir.mkdir(parents=True, exist_ok=True)
        profile = {"name": "Agent Alpha", "capabilities": ["python"], "specialization": "backend"}
        (profiles_dir / "agent-p.json").write_text(json.dumps(profile))
        ctx = build_agent_context("agent-p", "Build API")
        assert "agent_profile" in ctx
        assert ctx["agent_profile"]["name"] == "Agent Alpha"

    def test_loads_agent_memory(self, tmp_path):
        from brynq.context_optimizer import build_agent_context, BROKER_DIR
        from brynq.server import _append_jsonl_sync
        memory_dir = BROKER_DIR / "memory"
        memory_dir.mkdir(parents=True, exist_ok=True)
        mem_file = memory_dir / "agent-m.jsonl"
        _append_jsonl_sync(mem_file, {"key": "fact1", "value": "Python is great"})
        ctx = build_agent_context("agent-m", "Write tests")
        assert "recent_knowledge" in ctx

    def test_respects_max_tokens(self):
        from brynq.context_optimizer import build_agent_context, estimate_tokens
        ctx = build_agent_context("agent-x", "Simple task", max_tokens=200)
        tokens = estimate_tokens(json.dumps(ctx, default=str))
        # Should fit within a reasonable range of target
        assert tokens <= 400  # Some slack allowed

    def test_includes_channel_summary(self, tmp_path):
        from brynq.context_optimizer import build_agent_context, BROKER_DIR
        from brynq.server import _append_jsonl_sync
        channels_dir = BROKER_DIR / "channels"
        channels_dir.mkdir(parents=True, exist_ok=True)
        ch_file = channels_dir / "general.jsonl"
        _append_jsonl_sync(ch_file, {"message": "We decided on FastAPI.", "sender": "lead"})
        ctx = build_agent_context("agent-c", "Implement endpoint")
        assert "channel_summary" in ctx
