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

"""Tests for the Macro Engine."""

import json
from unittest.mock import patch

import pytest

from familiar.core.files import FileStore
from familiar.core.macros import (
    MacroDefinition,
    MacroRegistry,
    MacroStep,
    get_macro_registry,
)


@pytest.fixture
def registry():
    return MacroRegistry()


@pytest.fixture
def file_store(tmp_path):
    return FileStore(files_dir=tmp_path / "files")


@pytest.fixture
def sample_file(tmp_path):
    """Create a sample file on disk."""
    f = tmp_path / "test_doc.pdf"
    f.write_bytes(b"%PDF-1.4 test content")
    return f


# ── Registration ──


class TestRegistration:
    def test_register_and_get(self, registry):
        macro = MacroDefinition(
            name="test_macro",
            label="Test",
            description="A test macro",
            job_classes=["helper"],
        )
        registry.register(macro)
        assert registry.get("test_macro") is not None
        assert registry.get("test_macro").label == "Test"

    def test_get_nonexistent(self, registry):
        assert registry.get("no_such_macro") is None

    def test_list_all(self, registry):
        registry.register(
            MacroDefinition(
                name="a",
                label="A",
                description="",
                job_classes=["helper"],
            )
        )
        registry.register(
            MacroDefinition(
                name="b",
                label="B",
                description="",
                job_classes=["chef"],
            )
        )
        assert len(registry.list_all()) == 2

    def test_list_for_job_class(self, registry):
        registry.register(
            MacroDefinition(
                name="a",
                label="A",
                description="",
                job_classes=["helper"],
            )
        )
        registry.register(
            MacroDefinition(
                name="b",
                label="B",
                description="",
                job_classes=["chef"],
            )
        )
        registry.register(
            MacroDefinition(
                name="c",
                label="C",
                description="",
                job_classes=["all"],
            )
        )
        helper_macros = registry.list_for_job_class("helper")
        assert len(helper_macros) == 2  # a + c (all)
        chef_macros = registry.list_for_job_class("chef")
        assert len(chef_macros) == 2  # b + c (all)

    def test_to_dict(self):
        macro = MacroDefinition(
            name="test",
            label="Test",
            description="desc",
            job_classes=["helper"],
            steps=[MacroStep(action="add_memory", params={"content": "hi"})],
        )
        d = macro.to_dict()
        assert d["name"] == "test"
        assert len(d["steps"]) == 1


# ── Execution ──


class TestExecution:
    def test_execute_unknown_macro(self, registry):
        result = registry.execute("nonexistent", {})
        assert result.success is False
        assert "Unknown macro" in result.error

    def test_execute_add_memory(self, registry, tmp_path):
        registry.register(
            MacroDefinition(
                name="note",
                label="Note",
                description="Add a note",
                job_classes=["all"],
                steps=[
                    MacroStep(
                        action="add_memory",
                        params={"content": "Remember: {text}"},
                    ),
                ],
            )
        )

        import familiar.core.paths as paths_mod

        original = paths_mod.DATA_DIR
        try:
            paths_mod.DATA_DIR = tmp_path
            result = registry.execute("note", {"text": "test note"})
        finally:
            paths_mod.DATA_DIR = original

        assert result.success is True
        assert result.steps_completed == 1

    def test_execute_store_file(self, registry, file_store, sample_file):
        registry.register(
            MacroDefinition(
                name="store",
                label="Store",
                description="Store a file",
                job_classes=["all"],
                steps=[
                    MacroStep(
                        action="store_file",
                        params={
                            "file_path": str(sample_file),
                            "job_class": "test",
                            "record_type": "docs",
                            "record_id": "r1",
                        },
                        output_key="stored",
                    ),
                ],
            )
        )

        with patch(
            "familiar.core.files.get_file_store",
            return_value=file_store,
        ):
            result = registry.execute("store", {})

        assert result.success is True
        assert "stored" in result.outputs
        assert result.outputs["stored"]["filename"] == "test_doc.pdf"

    def test_execute_query_files(self, registry, file_store, sample_file):
        # Pre-store a file
        file_store.store(
            data=sample_file.read_bytes(),
            filename="doc.pdf",
            job_class="test",
            record_type="docs",
            record_id="r1",
        )

        registry.register(
            MacroDefinition(
                name="query",
                label="Query",
                description="Query files",
                job_classes=["all"],
                steps=[
                    MacroStep(
                        action="query_files",
                        params={
                            "record_type": "docs",
                            "record_id": "r1",
                        },
                        output_key="files",
                    ),
                ],
            )
        )

        with patch(
            "familiar.core.files.get_file_store",
            return_value=file_store,
        ):
            result = registry.execute("query", {})

        assert result.success is True
        assert len(result.outputs["files"]) == 1

    def test_execute_multi_step(self, registry, file_store, sample_file, tmp_path):
        """Test a macro with multiple chained steps."""
        # Pre-store two files
        r1 = file_store.store(
            sample_file.read_bytes(),
            "a.pdf",
            "test",
            "docs",
            "r1",
        )
        r2 = file_store.store(
            sample_file.read_bytes(),
            "b.pdf",
            "test",
            "docs",
            "r1",
        )

        registry.register(
            MacroDefinition(
                name="bundle",
                label="Bundle",
                description="Query and zip",
                job_classes=["all"],
                steps=[
                    MacroStep(
                        action="query_files",
                        params={"record_type": "docs", "record_id": "r1"},
                        output_key="files",
                    ),
                    MacroStep(
                        action="zip_bundle",
                        params={"output_name": "test_bundle.zip"},
                        output_key="bundle",
                    ),
                ],
            )
        )

        downloads_dir = tmp_path / "downloads"
        downloads_dir.mkdir()

        import familiar.core.paths as paths_mod

        original = paths_mod.DATA_DIR
        try:
            paths_mod.DATA_DIR = tmp_path
            with patch(
                "familiar.core.files.get_file_store",
                return_value=file_store,
            ):
                result = registry.execute("bundle", {})
        finally:
            paths_mod.DATA_DIR = original

        assert result.success is True
        assert result.steps_completed == 2
        assert result.outputs["bundle"]["file_count"] == 2

    def test_execute_step_failure(self, registry):
        registry.register(
            MacroDefinition(
                name="fail",
                label="Fail",
                description="Will fail",
                job_classes=["all"],
                steps=[
                    MacroStep(
                        action="store_file",
                        params={"file_path": "/nonexistent/file.pdf"},
                    ),
                ],
            )
        )

        result = registry.execute("fail", {})
        assert result.success is False
        assert result.failed_step == 0

    def test_param_resolution(self, registry, file_store, sample_file):
        registry.register(
            MacroDefinition(
                name="resolve",
                label="Resolve",
                description="Test param resolution",
                job_classes=["all"],
                steps=[
                    MacroStep(
                        action="store_file",
                        params={
                            "file_path": "{path}",
                            "job_class": "{jc}",
                            "record_type": "docs",
                            "record_id": "{rid}",
                        },
                        output_key="stored",
                    ),
                ],
            )
        )

        with patch(
            "familiar.core.files.get_file_store",
            return_value=file_store,
        ):
            result = registry.execute(
                "resolve",
                {
                    "path": str(sample_file),
                    "jc": "nonprofit",
                    "rid": "g1",
                },
            )

        assert result.success is True
        assert result.outputs["stored"]["job_class"] == "nonprofit"

    def test_delete_file_step(self, registry, file_store, sample_file):
        rec = file_store.store(
            sample_file.read_bytes(),
            "doc.pdf",
            "test",
            "docs",
            "r1",
        )

        registry.register(
            MacroDefinition(
                name="remove",
                label="Remove",
                description="Delete a file",
                job_classes=["all"],
                steps=[
                    MacroStep(
                        action="delete_file",
                        params={"file_id": "{fid}"},
                        output_key="result",
                    ),
                ],
            )
        )

        with patch(
            "familiar.core.files.get_file_store",
            return_value=file_store,
        ):
            result = registry.execute("remove", {"fid": rec.file_id})

        assert result.success is True
        assert result.outputs["result"]["deleted"] is True
        assert file_store.get(rec.file_id) is None


# ── Built-in Macros ──


class TestBuiltins:
    def test_builtins_registered(self):
        reg = get_macro_registry()
        assert len(reg.list_all()) > 0

    def test_nonprofit_macros(self):
        reg = get_macro_registry()
        np_macros = reg.list_for_job_class("nonprofit_director")
        names = [m.name for m in np_macros]
        assert "attach_grant_document" in names
        assert "attach_donor_receipt" in names
        # Universal macros also included
        assert "export_record_files" in names

    def test_social_worker_macros(self):
        reg = get_macro_registry()
        sw_macros = reg.list_for_job_class("social_worker")
        names = [m.name for m in sw_macros]
        assert "attach_case_document" in names
        assert "export_case_bundle" in names

    def test_all_builtins_have_steps(self):
        reg = get_macro_registry()
        for macro in reg.list_all():
            assert len(macro.steps) > 0, f"{macro.name} has no steps"

    def test_all_builtins_have_valid_actions(self):
        valid = {
            "store_file",
            "query_files",
            "update_record",
            "add_memory",
            "zip_bundle",
            "delete_file",
        }
        reg = get_macro_registry()
        for macro in reg.list_all():
            for step in macro.steps:
                assert step.action in valid, f"{macro.name} has invalid action: {step.action}"
