"""Unit tests for navi.core.task_notes — pending background-task completion notes."""

import pytest

from navi.core import task_notes


class FakeKv:
    """In-memory stand-in for KvStore.get/set."""

    def __init__(self):
        self.data = {}

    async def get(self, user_id, session_id, scope, key):
        return self.data.get((user_id, session_id, scope, key))

    async def set(self, user_id, session_id, scope, key, value):
        self.data[(user_id, session_id, scope, key)] = value


def patch_settings(monkeypatch, **overrides):
    """Replace the frozen settings singleton with an overridden copy."""
    import navi.config as config_mod
    from navi.config import Settings

    new_settings = Settings(**overrides)
    monkeypatch.setattr(config_mod, "settings", new_settings)
    return new_settings


@pytest.fixture(autouse=True)
def kv(monkeypatch):
    store = FakeKv()
    task_notes.set_kv_store(store)
    yield store
    task_notes.set_kv_store(None)


def make_job(task_id="bt-ab12", session_id="s1", status="completed",
             preview_text="done 42", tokens=None):
    class Job:
        pass

    job = Job()
    job.task_id = task_id
    job.session_id = session_id
    job.tool = "terminal"
    job.status = status
    job.subagent_tokens = tokens
    job.preview = lambda limit=800: preview_text
    return job


class TestAddNote:
    async def test_note_recorded(self, kv):
        await task_notes.add_note(make_job())
        assert await task_notes.pending_count("s1") == 1

    async def test_cap_drops_oldest(self, kv, monkeypatch):
        patch_settings(monkeypatch, task_notes_max_pending=2)
        for i in range(4):
            await task_notes.add_note(make_job(task_id=f"bt-{i}"))
        assert await task_notes.pending_count("s1") == 2
        notes = await task_notes._load("s1")
        assert [n["task_id"] for n in notes] == ["bt-2", "bt-3"]

    async def test_no_store_is_noop(self, monkeypatch):
        task_notes.set_kv_store(None)
        await task_notes.add_note(make_job())  # must not raise
        assert await task_notes.drain("s1") is None


class TestDrain:
    async def test_drain_coalesces_and_empties(self, kv):
        await task_notes.add_note(make_job(task_id="bt-a", preview_text="42 files"))
        await task_notes.add_note(make_job(task_id="bt-b", status="failed",
                                           preview_text="boom"))
        text = await task_notes.drain("s1")
        assert text is not None
        assert "[Background task results]" in text
        assert "bt-a (terminal) completed: 42 files" in text
        assert "bt-b (terminal) failed: boom" in text
        assert "Do not start new background tasks" in text
        # queue is now empty
        assert await task_notes.drain("s1") is None
        assert await task_notes.pending_count("s1") == 0

    async def test_drain_limits_per_turn(self, kv, monkeypatch):
        patch_settings(monkeypatch, task_notes_per_turn=2)
        for i in range(3):
            await task_notes.add_note(make_job(task_id=f"bt-{i}"))
        text = await task_notes.drain("s1")
        assert "bt-0" in text and "bt-1" in text
        assert "bt-2" not in text.split("older result")[0]
        assert "1 older result(s)" in text
        # remaining note survives for the next turn
        assert await task_notes.pending_count("s1") == 1
        text2 = await task_notes.drain("s1")
        assert "bt-2" in text2

    async def test_drain_scoped_by_session(self, kv):
        await task_notes.add_note(make_job(session_id="s1"))
        assert await task_notes.drain("s2") is None
        assert await task_notes.drain("s1") is not None

    async def test_empty_preview_placeholder(self, kv):
        await task_notes.add_note(make_job(preview_text=""))
        text = await task_notes.drain("s1")
        assert "(no output)" in text

    async def test_subagent_tokens_recorded(self, kv):
        await task_notes.add_note(make_job(tokens=1234))
        notes = await task_notes._load("s1")
        assert notes[0]["subagent_tokens"] == 1234