"""Tests for the `skills` tool (search / load / write / propose) against a fake catalog."""

from datetime import datetime, timezone
from types import SimpleNamespace

import pytest

from navi.skills.models import parse_document
from navi.skills.store import SkillEntry
from navi.tools._internal.base import ToolContext
from navi.tools.skills import SkillTool

SKILL_DOC = """---
kind: skill
name: Разбор логов
description: Быстрый поиск причины в логах.
area: workflows
tags: [logs]
triggers: [нужно найти ошибку]
---

Грепай ERROR, потом сужай по времени.
"""

PROTOCOL_DOC = """---
kind: protocol
name: Выключение
description: Порядок выключения.
area: infrastructure
steps: [Остановить сервисы, Отмонтировать диски]
post_checks: [Юнитов не осталось]
on_deviation: Остановиться и доложить.
on_blocked: Прервать и доложить.
on_already_done: Проверить и пропустить.
---
"""


def _entry(content: str, **kw) -> SkillEntry:
    doc = parse_document(content)
    base = dict(
        id=kw.pop("id", doc.kind + "-1"),
        kind=doc.kind,
        name=doc.name,
        description=doc.description,
        triggers=doc.triggers,
        tags=doc.tags,
        area=doc.area,
        visibility=kw.pop("visibility", "personal"),
        owner_id=kw.pop("owner_id", "u1"),
        content=content,
        version=kw.pop("version", 1),
        content_hash="hash",
        created_at=datetime.now(timezone.utc),
        updated_at=datetime.now(timezone.utc),
    )
    base.update(kw)
    return SkillEntry(**base)


class FakeStore:
    def __init__(self, entries):
        self.entries = entries
        self.calls = []

    async def list(self, *, user_id, kind=None, query=None, tag=None, area=None, limit=100):
        self.calls.append(("list", kind, query, tag, area))
        out = []
        for e in self.entries:
            if e.owner_id != user_id and e.visibility != "shared":
                continue
            if kind and e.kind != kind:
                continue
            if tag and tag not in e.tags:
                continue
            if area and e.area != area:
                continue
            if query and query.lower() not in (e.name + " " + e.description).lower():
                continue
            out.append(e)
        return out

    async def find(self, *, user_id, name=None, kind=None, skill_id=None):
        self.calls.append(("find", skill_id, name, kind))
        for e in self.entries:
            if e.owner_id != user_id and e.visibility != "shared":
                continue
            if skill_id and e.id != skill_id:
                continue
            if name and e.name.lower() != name.lower():
                continue
            if kind and e.kind != kind:
                continue
            return e
        return None

    async def get(self, skill_id):
        self.calls.append(("get", skill_id))
        for e in self.entries:
            if e.id == skill_id:
                return e
        return None

    async def create(self, *, owner_id, content, visibility="personal", edited_by="",
                     reason=None, kind=None):
        self.calls.append(("create", owner_id, content))
        entry = _entry(content, owner_id=owner_id)
        self.entries.append(entry)
        return entry

    async def update(self, skill_id, *, content, edited_by="", reason=None):
        self.calls.append(("update", skill_id, content))
        for e in self.entries:
            if e.id == skill_id:
                return e
        return None

    async def create_proposal(self, *, owner_id, content, session_id="", replaces_id=None):
        doc = parse_document(content, kind="protocol")   # as the real store: validate first
        self.calls.append(("propose", owner_id, session_id, replaces_id))
        self.proposal = SimpleNamespace(id="pr1", name=doc.name)
        return self.proposal


@pytest.fixture
def ctx():
    return ToolContext(session_id="s", user_id="u1")


async def test_search_lists_both_kinds(ctx):
    tool = SkillTool(FakeStore([_entry(SKILL_DOC), _entry(PROTOCOL_DOC)]))
    result = await tool.execute({"op": "search"}, ctx)
    assert result.success
    assert "2 matching entries" in result.output
    assert "[skill] Разбор логов" in result.output
    assert "[protocol] Выключение" in result.output
    assert "id=skill-1" in result.output


async def test_search_filters(ctx):
    store = FakeStore([_entry(SKILL_DOC), _entry(PROTOCOL_DOC)])
    tool = SkillTool(store)
    result = await tool.execute({"op": "search", "kind": "protocol", "area": "infrastructure"}, ctx)
    assert "[protocol]" in result.output
    assert "[skill]" not in result.output
    assert store.calls[-1] == ("list", "protocol", None, None, "infrastructure")


async def test_search_empty_explains_what_to_try(ctx):
    tool = SkillTool(FakeStore([]))
    result = await tool.execute({"op": "search", "query": "nothing"}, ctx)
    assert result.success
    assert "No matching skills or protocols" in result.output


async def test_load_by_id_renders_protocol(ctx):
    tool = SkillTool(FakeStore([_entry(PROTOCOL_DOC)]))
    result = await tool.execute({"op": "load", "id": "protocol-1"}, ctx)
    assert result.success
    assert result.output.startswith("PROTOCOL: Выключение")
    assert "1. Остановить сервисы" in result.output
    assert "If the run goes off plan" in result.output
    assert result.metadata["kind"] == "protocol"


async def test_load_by_name(ctx):
    tool = SkillTool(FakeStore([_entry(SKILL_DOC)]))
    result = await tool.execute({"op": "load", "name": "разбор логов"}, ctx)
    assert result.success
    assert "Грепай ERROR" in result.output


async def test_load_missing_points_at_search(ctx):
    tool = SkillTool(FakeStore([]))
    result = await tool.execute({"op": "load", "id": "nope"}, ctx)
    assert not result.success
    assert "Search first" in result.error


async def test_load_requires_id_or_name(ctx):
    tool = SkillTool(FakeStore([]))
    result = await tool.execute({"op": "load"}, ctx)
    assert not result.success
    assert "'id' or 'name'" in result.error


async def test_unknown_op(ctx):
    tool = SkillTool(FakeStore([]))
    result = await tool.execute({"op": "delete"}, ctx)
    assert not result.success
    assert "Unknown op" in result.error


class TestWrite:
    async def test_create_saves_a_skill_for_the_caller(self, ctx):
        store = FakeStore([])
        result = await SkillTool(store).execute(
            {"op": "create", "content": SKILL_DOC}, ctx,
        )
        assert result.success
        assert result.metadata["skill_id"] == "skill-1"
        assert store.calls[-1][0] == "create"
        assert store.calls[-1][1] == "u1"

    async def test_create_refuses_a_protocol(self, ctx):
        store = FakeStore([])
        result = await SkillTool(store).execute(
            {"op": "create", "content": PROTOCOL_DOC}, ctx,
        )
        assert not result.success
        assert "op='propose'" in result.error
        assert store.calls == []

    async def test_create_reports_a_malformed_document(self, ctx):
        store = FakeStore([])
        result = await SkillTool(store).execute(
            {"op": "create", "content": "no front matter"}, ctx,
        )
        assert not result.success
        assert "front matter" in result.error
        assert store.calls == []

    async def test_update_writes_a_new_version(self, ctx):
        store = FakeStore([_entry(SKILL_DOC)])
        result = await SkillTool(store).execute(
            {"op": "update", "id": "skill-1", "content": SKILL_DOC}, ctx,
        )
        assert result.success
        assert "v1" in result.output
        assert store.calls[-1][0] == "update"

    async def test_update_sends_a_protocol_to_propose(self, ctx):
        store = FakeStore([_entry(PROTOCOL_DOC)])
        result = await SkillTool(store).execute(
            {"op": "update", "id": "protocol-1", "content": PROTOCOL_DOC}, ctx,
        )
        assert not result.success
        assert "replaces_id='protocol-1'" in result.error
        assert store.calls[-1][0] == "get"

    async def test_update_refuses_someone_elses_entry(self, ctx):
        store = FakeStore([_entry(SKILL_DOC, owner_id="u2")])
        result = await SkillTool(store).execute(
            {"op": "update", "id": "skill-1", "content": SKILL_DOC}, ctx,
        )
        assert not result.success
        assert "belongs to someone else" in result.error
        assert not any(c[0] == "update" for c in store.calls)


class TestPropose:
    async def test_propose_stores_it_and_renders_the_card(self, ctx):
        store = FakeStore([])
        result = await SkillTool(store).execute(
            {"op": "propose", "content": PROTOCOL_DOC}, ctx,
        )
        assert result.success
        assert "NOT in the catalog yet" in result.output
        assert result.metadata["proposal_id"] == "pr1"
        card = result.metadata["ui_component"]
        assert card["component"] == "skill_proposal"
        assert card["payload"] == {"proposal_id": "pr1"}
        assert store.calls[-1] == ("propose", "u1", "s", None)

    async def test_propose_passes_the_protocol_being_replaced(self, ctx):
        store = FakeStore([])
        await SkillTool(store).execute(
            {"op": "propose", "content": PROTOCOL_DOC, "replaces_id": "protocol-1"}, ctx,
        )
        assert store.calls[-1][3] == "protocol-1"

    async def test_propose_refuses_a_skill(self, ctx):
        store = FakeStore([])
        result = await SkillTool(store).execute(
            {"op": "propose", "content": SKILL_DOC}, ctx,
        )
        assert not result.success
        assert store.calls == []
