"""test_llm_context.py — Unit tests for llm_context.py (no GPU / LLM needed)."""

import sys
import os
import tempfile

sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))

from saltybot_social.llm_context import (
    PersonContext, ContextStore,
    build_llama_prompt, MAX_TURNS, SUMMARY_KEEP,
    DEFAULT_SYSTEM_PROMPT,
)
import pytest


# ── PersonContext ─────────────────────────────────────────────────────────────

class TestPersonContext:
    def test_create(self):
        ctx = PersonContext("person_1", "Alice")
        assert ctx.person_id == "person_1"
        assert ctx.person_name == "Alice"
        assert ctx.turns == []
        assert ctx.interaction_count == 0

    def test_add_user_increments_count(self):
        ctx = PersonContext("person_1")
        ctx.add_user("Hello")
        assert ctx.interaction_count == 1
        assert ctx.turns[-1]["role"] == "user"
        assert ctx.turns[-1]["content"] == "Hello"

    def test_add_assistant(self):
        ctx = PersonContext("person_1")
        ctx.add_assistant("Hi there!")
        assert ctx.turns[-1]["role"] == "assistant"

    def test_needs_compression_false(self):
        ctx = PersonContext("p")
        for i in range(MAX_TURNS - 1):
            ctx.add_user(f"msg {i}")
        assert not ctx.needs_compression()

    def test_needs_compression_true(self):
        ctx = PersonContext("p")
        for i in range(MAX_TURNS + 1):
            ctx.add_user(f"msg {i}")
        assert ctx.needs_compression()

    def test_compress_keeps_recent_turns(self):
        ctx = PersonContext("p")
        for i in range(MAX_TURNS + 5):
            ctx.add_user(f"msg {i}")
        ctx.compress("Summary of conversation")
        assert len(ctx.turns) == SUMMARY_KEEP
        assert ctx.summary == "Summary of conversation"

    def test_compress_appends_summary(self):
        ctx = PersonContext("p")
        ctx.add_user("msg")
        ctx.compress("First summary")
        ctx.add_user("msg2")
        ctx.compress("Second summary")
        assert "First summary" in ctx.summary
        assert "Second summary" in ctx.summary

    def test_roundtrip_serialization(self):
        ctx = PersonContext("person_42", "Bob")
        ctx.add_user("test")
        ctx.add_assistant("response")
        ctx.summary = "Earlier they discussed robots"
        d = ctx.to_dict()
        ctx2 = PersonContext.from_dict(d)
        assert ctx2.person_id == "person_42"
        assert ctx2.person_name == "Bob"
        assert len(ctx2.turns) == 2
        assert ctx2.summary == "Earlier they discussed robots"


# ── ContextStore ──────────────────────────────────────────────────────────────

class TestContextStore:
    def test_get_creates_new(self):
        with tempfile.TemporaryDirectory() as tmpdir:
            db = os.path.join(tmpdir, "ctx.json")
            store = ContextStore(db)
            ctx = store.get("p1", "Alice")
            assert ctx.person_id == "p1"
            assert ctx.person_name == "Alice"

    def test_get_same_instance(self):
        with tempfile.TemporaryDirectory() as tmpdir:
            db = os.path.join(tmpdir, "ctx.json")
            store = ContextStore(db)
            ctx1 = store.get("p1")
            ctx2 = store.get("p1")
            assert ctx1 is ctx2

    def test_save_and_load(self):
        with tempfile.TemporaryDirectory() as tmpdir:
            db = os.path.join(tmpdir, "ctx.json")
            store = ContextStore(db)
            ctx = store.get("p1", "Alice")
            ctx.add_user("Hello")
            store.save()

            store2 = ContextStore(db)
            ctx2 = store2.get("p1")
            assert ctx2.person_name == "Alice"
            assert len(ctx2.turns) == 1
            assert ctx2.turns[0]["content"] == "Hello"

    def test_all_persons(self):
        with tempfile.TemporaryDirectory() as tmpdir:
            db = os.path.join(tmpdir, "ctx.json")
            store = ContextStore(db)
            store.get("p1")
            store.get("p2")
            store.get("p3")
            assert set(store.all_persons()) == {"p1", "p2", "p3"}


# ── Prompt builder ────────────────────────────────────────────────────────────

class TestBuildLlamaPrompt:
    def test_contains_system_prompt(self):
        ctx = PersonContext("p1")
        prompt = build_llama_prompt(ctx, "hello", DEFAULT_SYSTEM_PROMPT)
        assert DEFAULT_SYSTEM_PROMPT in prompt

    def test_contains_user_text(self):
        ctx = PersonContext("p1")
        prompt = build_llama_prompt(ctx, "What time is it?", DEFAULT_SYSTEM_PROMPT)
        assert "What time is it?" in prompt

    def test_ends_with_assistant_tag(self):
        ctx = PersonContext("p1")
        prompt = build_llama_prompt(ctx, "hello", DEFAULT_SYSTEM_PROMPT)
        assert prompt.strip().endswith("<|assistant|>")

    def test_history_included(self):
        ctx = PersonContext("p1")
        ctx.add_user("Previous question")
        ctx.add_assistant("Previous answer")
        prompt = build_llama_prompt(ctx, "New question", DEFAULT_SYSTEM_PROMPT)
        assert "Previous question" in prompt
        assert "Previous answer" in prompt

    def test_summary_included(self):
        ctx = PersonContext("p1", "Alice")
        ctx.summary = "Alice visited last week and likes coffee"
        prompt = build_llama_prompt(ctx, "hello", DEFAULT_SYSTEM_PROMPT)
        assert "likes coffee" in prompt

    def test_group_persons_included(self):
        ctx = PersonContext("p1", "Alice")
        prompt = build_llama_prompt(ctx, "hello", DEFAULT_SYSTEM_PROMPT,
                                    group_persons=["p1", "p2_Bob", "p3_Charlie"])
        assert "p2_Bob" in prompt or "p3_Charlie" in prompt

    def test_no_group_persons_not_in_prompt(self):
        ctx = PersonContext("p1")
        prompt = build_llama_prompt(ctx, "hello", DEFAULT_SYSTEM_PROMPT)
        assert "Also present" not in prompt


# ── TTS utils (imported here for coverage) ────────────────────────────────────

class TestTtsUtils:
    def setup_method(self):
        sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
        from saltybot_social.tts_utils import (
            strip_ssml, split_sentences, split_first_sentence,
            pcm16_to_wav_bytes, chunk_pcm, estimate_duration_ms, apply_volume
        )
        self.strip_ssml = strip_ssml
        self.split_sentences = split_sentences
        self.split_first_sentence = split_first_sentence
        self.pcm16_to_wav_bytes = pcm16_to_wav_bytes
        self.chunk_pcm = chunk_pcm
        self.estimate_duration_ms = estimate_duration_ms
        self.apply_volume = apply_volume

    def test_strip_ssml(self):
        assert self.strip_ssml("<speak>Hello <break time='1s'/> world</speak>") == "Hello  world"
        assert self.strip_ssml("plain text") == "plain text"

    def test_split_sentences_basic(self):
        text = "Hello world. How are you? I am fine!"
        sentences = self.split_sentences(text)
        assert len(sentences) == 3

    def test_split_sentences_single(self):
        sentences = self.split_sentences("Hello world.")
        assert len(sentences) == 1
        assert sentences[0] == "Hello world."

    def test_split_sentences_empty(self):
        assert self.split_sentences("") == []
        assert self.split_sentences("   ") == []

    def test_split_first_sentence(self):
        text = "Hello world. How are you? I am fine."
        first, rest = self.split_first_sentence(text)
        assert first == "Hello world."
        assert "How are you?" in rest

    def test_wav_header(self):
        pcm = b"\x00\x00" * 1000
        wav = self.pcm16_to_wav_bytes(pcm, 22050)
        assert wav[:4] == b"RIFF"
        assert wav[8:12] == b"WAVE"
        assert len(wav) == 44 + len(pcm)

    def test_chunk_pcm_sizes(self):
        pcm = b"\x00\x00" * 2205  # ~100ms at 22050Hz
        chunks = list(self.chunk_pcm(pcm, 50, 22050))  # 50ms chunks
        for chunk in chunks:
            assert len(chunk) % 2 == 0  # int16 aligned

    def test_estimate_duration(self):
        pcm = b"\x00\x00" * 22050  # 1s at 22050Hz
        ms = self.estimate_duration_ms(pcm, 22050)
        assert abs(ms - 1000.0) < 1.0

    def test_apply_volume_unity(self):
        pcm = b"\x00\x40" * 100  # some non-zero samples
        result = self.apply_volume(pcm, 1.0)
        assert result == pcm

    def test_apply_volume_half(self):
        import struct
        pcm = struct.pack("<h", 1000)
        result = self.apply_volume(pcm, 0.5)
        val = struct.unpack("<h", result)[0]
        assert abs(val - 500) <= 1

    def test_apply_volume_clips(self):
        import struct
        pcm = struct.pack("<h", 30000)
        result = self.apply_volume(pcm, 2.0)
        val = struct.unpack("<h", result)[0]
        assert val == 32767
