# Familiar - Self-Hosted AI Companion Platform
# Copyright (c) 2026 George Scott Foley
# Licensed under the MIT License

"""Tests for the Memory Tree skill (agent file-op interface)."""

from unittest.mock import patch

import pytest

from familiar.core.memory_tree import MemoryTree
from familiar.skills.memory_tree.skill import (
    _parse_path,
    memory_delete,
    memory_link,
    memory_list,
    memory_read,
    memory_search,
    memory_write,
)


@pytest.fixture
def tree(tmp_path):
    return MemoryTree(db_path=tmp_path / "skill_test.db")


@pytest.fixture(autouse=True)
def _patch_tree(tree):
    with patch("familiar.skills.memory_tree.skill._get_tree", return_value=tree):
        yield


# ── Path Parsing ──


class TestParsePath:
    def test_full_path(self):
        assert _parse_path("/working/user-name") == ("working", "user-name")

    def test_no_leading_slash(self):
        assert _parse_path("long/my-recipe") == ("long", "my-recipe")

    def test_default_tier(self):
        assert _parse_path("some-slug") == ("long", "some-slug")

    def test_invalid_tier_defaults(self):
        assert _parse_path("bogus/slug") == ("long", "bogus/slug")

    def test_all_tiers(self):
        for tier in ["working", "short", "long", "archival"]:
            assert _parse_path(f"/{tier}/x")[0] == tier

    def test_whitespace_stripped(self):
        assert _parse_path("  /working/x  ") == ("working", "x")


# ── memory_write ──


class TestMemoryWrite:
    def test_write_with_path(self, tree):
        result = memory_write({"path": "/long/test-note", "content": "hello"})
        assert "Saved" in result
        assert "Test Note" in result

    def test_write_with_title(self, tree):
        result = memory_write({"title": "My Title", "content": "body text"})
        assert "Saved" in result
        assert "My Title" in result

    def test_write_no_path_or_title(self):
        result = memory_write({"content": "orphan"})
        assert "provide" in result.lower()

    def test_write_no_content(self):
        result = memory_write({"path": "/long/x"})
        assert "provide" in result.lower()

    def test_write_with_tags(self, tree):
        result = memory_write(
            {
                "path": "/long/tagged",
                "content": "tagged content",
                "tags": ["alpha", "beta"],
            }
        )
        assert "alpha, beta" in result

    def test_write_dedup_updates(self, tree):
        memory_write({"path": "/long/dup", "content": "v1"})
        result = memory_write({"path": "/long/dup", "content": "v2"})
        # Second write should update (find_duplicate matches title)
        # Actually find_duplicate uses content hash, so different content = new entry
        assert "Saved" in result or "Updated" in result

    def test_write_importance(self, tree):
        memory_write(
            {
                "path": "/long/imp",
                "content": "important",
                "importance": 0.9,
            }
        )
        results = tree.search("important")
        assert results[0].importance == 0.9

    def test_write_source(self, tree):
        memory_write(
            {
                "path": "/long/src",
                "content": "sourced",
                "source": "conv:123",
            }
        )
        results = tree.search("sourced")
        assert results[0].source == "conv:123"


# ── memory_read ──


class TestMemoryRead:
    def test_read_by_path(self, tree):
        memory_write({"path": "/long/readable", "content": "read me"})
        result = memory_read({"path": "/long/readable"})
        assert "Readable" in result
        assert "read me" in result

    def test_read_by_id(self, tree):
        memory_write({"path": "/long/byid", "content": "id content"})
        entries = tree.search("id content")
        result = memory_read({"memory_id": entries[0].memory_id})
        assert "id content" in result

    def test_read_not_found(self):
        result = memory_read({"path": "/long/nonexistent"})
        assert "not found" in result.lower()

    def test_read_no_args(self):
        result = memory_read({})
        assert "provide" in result.lower()

    def test_read_shows_metadata(self, tree):
        memory_write(
            {
                "path": "/working/meta-test",
                "content": "metadata test",
                "tags": ["info"],
                "importance": 0.8,
            }
        )
        result = memory_read({"path": "/working/meta-test"})
        assert "Tier: working" in result
        assert "Importance: 0.8" in result


# ── memory_search ──


class TestMemorySearch:
    def test_search_basic(self, tree):
        memory_write({"path": "/long/searchable", "content": "unique banana"})
        result = memory_search({"query": "banana"})
        assert "banana" in result.lower() or "Searchable" in result

    def test_search_no_results(self):
        result = memory_search({"query": "xyznonexistent"})
        assert "No memories" in result

    def test_search_no_query(self):
        result = memory_search({})
        assert "provide" in result.lower()

    def test_search_with_tier(self, tree):
        memory_write({"path": "/working/tier-search", "content": "working tier"})
        memory_write({"path": "/archival/tier-search2", "content": "archival tier"})
        result = memory_search({"query": "tier", "tier": "working"})
        assert "working" in result.lower()

    def test_search_with_tags(self, tree):
        memory_write(
            {
                "path": "/long/tag-search",
                "content": "tagged search",
                "tags": ["special"],
            }
        )
        result = memory_search({"tags": ["special"]})
        assert "Tag Search" in result or "tagged" in result.lower()

    def test_search_limit(self, tree):
        for i in range(5):
            memory_write({"path": f"/long/limited-{i}", "content": f"limited item {i}"})
        result = memory_search({"query": "limited", "limit": 2})
        assert "Found 2" in result


# ── memory_list ──


class TestMemoryList:
    def test_list_default(self, tree):
        memory_write({"path": "/long/list-item", "content": "list test"})
        result = memory_list({})
        assert "List Item" in result or "list" in result.lower()

    def test_list_by_tier(self, tree):
        memory_write({"path": "/working/w-item", "content": "working"})
        result = memory_list({"tier": "working"})
        assert "W Item" in result

    def test_list_all_tiers(self, tree):
        memory_write({"path": "/working/a", "content": "a"})
        memory_write({"path": "/long/b", "content": "b"})
        result = memory_list({"tier": "all"})
        assert "Memories" in result

    def test_list_empty_tier(self):
        result = memory_list({"tier": "archival"})
        assert "No memories" in result

    def test_list_by_tag(self, tree):
        memory_write(
            {
                "path": "/long/tag-list",
                "content": "tag list test",
                "tags": ["listtag"],
            }
        )
        result = memory_list({"tag": "listtag"})
        assert "Tag List" in result

    def test_list_limit(self, tree):
        for i in range(5):
            memory_write({"path": f"/long/lim-{i}", "content": f"lim {i}"})
        result = memory_list({"limit": 2})
        assert "Memories" in result


# ── memory_delete ──


class TestMemoryDelete:
    def test_delete_by_path(self, tree):
        memory_write({"path": "/long/deletable", "content": "delete me"})
        result = memory_delete({"path": "/long/deletable"})
        assert "Invalidated" in result

    def test_delete_by_id(self, tree):
        memory_write({"path": "/long/del-by-id", "content": "del by id"})
        entries = tree.search("del by id")
        result = memory_delete({"memory_id": entries[0].memory_id})
        assert "Invalidated" in result

    def test_delete_not_found(self):
        result = memory_delete({"path": "/long/ghost"})
        assert "not found" in result.lower()

    def test_delete_no_args(self):
        result = memory_delete({})
        assert "provide" in result.lower()

    def test_delete_with_reason(self, tree):
        memory_write({"path": "/long/reason-del", "content": "reason"})
        result = memory_delete({"path": "/long/reason-del", "reason": "outdated"})
        assert "outdated" in result

    def test_delete_already_invalidated(self, tree):
        memory_write({"path": "/long/double-del", "content": "double"})
        memory_delete({"path": "/long/double-del"})
        result = memory_delete({"path": "/long/double-del"})
        assert "already" in result.lower() or "not found" in result.lower()


# ── memory_link ──


class TestMemoryLink:
    def test_link_by_id(self, tree):
        memory_write({"path": "/long/link-a", "content": "a"})
        memory_write({"path": "/long/link-b", "content": "b"})
        a = tree.search("link-a")[0] if tree.search("link-a") else tree.search("Link A")[0]
        b = tree.search("link-b")[0] if tree.search("link-b") else tree.search("Link B")[0]
        result = memory_link(
            {
                "from_id": a.memory_id,
                "to_id": b.memory_id,
                "relation": "related",
            }
        )
        assert "Linked" in result

    def test_link_by_path(self, tree):
        memory_write({"path": "/long/path-a", "content": "pa"})
        memory_write({"path": "/long/path-b", "content": "pb"})
        result = memory_link(
            {
                "from_path": "/long/path-a",
                "to_path": "/long/path-b",
                "relation": "cites",
            }
        )
        assert "Linked" in result

    def test_link_not_found(self):
        result = memory_link(
            {
                "from_path": "/long/missing-a",
                "to_path": "/long/missing-b",
            }
        )
        assert "not found" in result.lower()

    def test_link_no_args(self):
        result = memory_link({})
        assert "provide" in result.lower()
