"""Tests for Memory Inbox Processor — process, distribute, status, pipeline."""

from __future__ import annotations

import json
import pytest
from pathlib import Path
from unittest.mock import AsyncMock, MagicMock

import yaml

from vastian.tasks.memory_inbox import InboxProcessor, load_subscriptions


# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------


def _make_subscription_config(root: Path, mind: str = "work") -> Path:
    """Create a minimal memory-subscriptions.yaml at the expected location."""
    config_dir = root / "config"
    config_dir.mkdir(parents=True, exist_ok=True)
    config_path = config_dir / "memory-subscriptions.yaml"
    config_path.write_text(
        yaml.dump(
            {
                "subscriptions": {
                    mind: {
                        "recipients": ["henry", "liam"],
                        "distributor": "charles",
                        "raw_doc": f"docs/shared/{mind}/context.md",
                        "auto_sync": False,
                    },
                },
            }
        ),
        encoding="utf-8",
    )
    return config_path


def _seed_inbox_plans(root: Path, mind: str = "work", count: int = 2) -> None:
    """Write sample plan JSON files into state/memory-inbox/{mind}/plans/."""
    plans_dir = root / "state" / "memory-inbox" / mind / "plans"
    plans_dir.mkdir(parents=True, exist_ok=True)
    for i in range(count):
        (plans_dir / f"plan_{i}.json").write_text(
            json.dumps({"key_name": f"plan-{i}", "content": f"Plan {i} content"}),
            encoding="utf-8",
        )


def _seed_inbox_neurons(root: Path, mind: str = "work", count: int = 1) -> None:
    """Write sample neuron JSON files into state/memory-inbox/{mind}/neurons/."""
    neurons_dir = root / "state" / "memory-inbox" / mind / "neurons"
    neurons_dir.mkdir(parents=True, exist_ok=True)
    for i in range(count):
        (neurons_dir / f"neuron_{i}.json").write_text(
            json.dumps({"title": f"Neuron {i}", "body": f"Body text {i}"}),
            encoding="utf-8",
        )


# ---------------------------------------------------------------------------
# Tests: load_subscriptions
# ---------------------------------------------------------------------------


class TestLoadSubscriptions:
    def test_loads_valid_yaml(self, temp_dir: Path) -> None:
        config_path = _make_subscription_config(temp_dir, "work")
        subs = load_subscriptions(config_path)
        assert "work" in subs
        assert subs["work"]["distributor"] == "charles"
        assert "henry" in subs["work"]["recipients"]

    def test_missing_file_returns_empty(self, temp_dir: Path) -> None:
        subs = load_subscriptions(temp_dir / "nonexistent.yaml")
        assert subs == {}

    def test_malformed_yaml_returns_empty(self, temp_dir: Path) -> None:
        bad = temp_dir / "bad.yaml"
        bad.write_text(": : : not valid yaml [[[", encoding="utf-8")
        subs = load_subscriptions(bad)
        assert subs == {}


# ---------------------------------------------------------------------------
# Tests: InboxProcessor
# ---------------------------------------------------------------------------


class TestInboxProcessor:
    @pytest.fixture
    def project(self, temp_dir: Path) -> Path:
        """Set up a temp project with subscriptions, inbox, and docs dirs."""
        _make_subscription_config(temp_dir, "work")
        _seed_inbox_plans(temp_dir, "work", count=2)
        _seed_inbox_neurons(temp_dir, "work", count=1)
        (temp_dir / "docs").mkdir(exist_ok=True)
        return temp_dir

    @pytest.fixture
    def processor(self, project: Path) -> InboxProcessor:
        return InboxProcessor(orchestrator=None, project_root=project)

    def test_process_inbox_merges_plans_and_neurons(self, processor: InboxProcessor) -> None:
        result = processor.process_inbox("work")
        assert result["plans"] == 2
        assert result["neurons"] == 1
        assert result["raw_doc"] is not None

    def test_process_inbox_empty_returns_no_doc(self, temp_dir: Path) -> None:
        _make_subscription_config(temp_dir, "empty-mind")
        proc = InboxProcessor(project_root=temp_dir)
        result = proc.process_inbox("empty-mind")
        assert result["raw_doc"] is None
        assert result["plans"] == 0

    def test_process_inbox_writes_raw_context_doc(
        self, processor: InboxProcessor, project: Path
    ) -> None:
        processor.process_inbox("work")
        raw_doc = project / "docs" / "shared" / "work" / "context.md"
        assert raw_doc.exists()
        content = raw_doc.read_text(encoding="utf-8")
        assert "External Context: work" in content

    def test_raw_context_contains_plan_titles(
        self, processor: InboxProcessor, project: Path
    ) -> None:
        processor.process_inbox("work")
        raw_doc = project / "docs" / "shared" / "work" / "context.md"
        content = raw_doc.read_text(encoding="utf-8")
        assert "plan-0" in content
        assert "plan-1" in content

    def test_raw_context_contains_neuron_bodies(
        self, processor: InboxProcessor, project: Path
    ) -> None:
        processor.process_inbox("work")
        raw_doc = project / "docs" / "shared" / "work" / "context.md"
        content = raw_doc.read_text(encoding="utf-8")
        assert "Body text 0" in content


# ---------------------------------------------------------------------------
# Tests: Distribute
# ---------------------------------------------------------------------------


class TestDistribute:
    @pytest.fixture
    def project(self, temp_dir: Path) -> Path:
        _make_subscription_config(temp_dir, "work")
        _seed_inbox_plans(temp_dir, "work", count=1)
        (temp_dir / "docs").mkdir(exist_ok=True)
        return temp_dir

    @pytest.mark.asyncio
    async def test_distribute_offline_writes_briefs(self, project: Path) -> None:
        proc = InboxProcessor(orchestrator=None, project_root=project)
        result = proc.process_inbox("work")
        raw_text = result.pop("raw_text", None)
        paths = await proc.distribute("work", raw_text)
        assert len(paths) == 2  # henry and liam
        for p in paths:
            assert Path(p).exists()

    @pytest.mark.asyncio
    async def test_distribute_no_subscription_returns_empty(self, project: Path) -> None:
        proc = InboxProcessor(orchestrator=None, project_root=project)
        paths = await proc.distribute("nonexistent-mind")
        assert paths == []

    @pytest.mark.asyncio
    async def test_distribute_via_agent_uses_orchestrator(self, project: Path) -> None:
        mock_orch = MagicMock()
        mock_orch.handle_user_message_direct = AsyncMock(return_value="Tailored brief for you.")
        proc = InboxProcessor(orchestrator=mock_orch, project_root=project)
        result = proc.process_inbox("work")
        raw_text = result.pop("raw_text", None)
        paths = await proc.distribute("work", raw_text)
        assert len(paths) == 2
        assert mock_orch.handle_user_message_direct.call_count == 2

    @pytest.mark.asyncio
    async def test_distribute_fallback_on_agent_error(self, project: Path) -> None:
        mock_orch = MagicMock()
        mock_orch.handle_user_message_direct = AsyncMock(side_effect=RuntimeError("Agent down"))
        proc = InboxProcessor(orchestrator=mock_orch, project_root=project)
        result = proc.process_inbox("work")
        raw_text = result.pop("raw_text", None)
        # Should not raise — falls back to offline
        paths = await proc.distribute("work", raw_text)
        assert len(paths) == 2  # still writes briefs via fallback


# ---------------------------------------------------------------------------
# Tests: get_status
# ---------------------------------------------------------------------------


class TestGetStatus:
    def test_status_reports_pending_counts(self, temp_dir: Path) -> None:
        _make_subscription_config(temp_dir, "work")
        _seed_inbox_plans(temp_dir, "work", count=3)
        _seed_inbox_neurons(temp_dir, "work", count=2)
        proc = InboxProcessor(project_root=temp_dir)
        status = proc.get_status()
        assert status["minds"]["work"]["pending_plans"] == 3
        assert status["minds"]["work"]["pending_neurons"] == 2
        assert "henry" in status["minds"]["work"]["recipients"]

    def test_status_with_no_subscriptions(self, temp_dir: Path) -> None:
        # Write an empty subscriptions file
        config_dir = temp_dir / "config"
        config_dir.mkdir(parents=True, exist_ok=True)
        (config_dir / "memory-subscriptions.yaml").write_text(
            yaml.dump({"subscriptions": {}}), encoding="utf-8"
        )
        proc = InboxProcessor(project_root=temp_dir)
        status = proc.get_status()
        assert status["minds"] == {}


# ---------------------------------------------------------------------------
# Tests: Full pipeline
# ---------------------------------------------------------------------------


class TestPullAndDistribute:
    @pytest.mark.asyncio
    async def test_full_pipeline_sync(self, temp_dir: Path) -> None:
        _make_subscription_config(temp_dir, "work")
        _seed_inbox_plans(temp_dir, "work", count=2)
        _seed_inbox_neurons(temp_dir, "work", count=1)
        (temp_dir / "docs").mkdir(exist_ok=True)

        proc = InboxProcessor(orchestrator=None, project_root=temp_dir)
        result = await proc.pull_and_distribute("work")

        assert result["plans"] == 2
        assert result["neurons"] == 1
        assert result["raw_doc"] is not None
        assert len(result["briefs_written"]) == 2
        assert result["recipients"] == ["henry", "liam"]
