"""
Comprehensive tests for Phase 13-21 modules.
Covers: sandboxing, consensus, knowledge_distillation, discovery,
        decentralized_memory, self_healing, task_dependencies, api_server.
"""

import json
import os
import tempfile
import time

import pytest


# ── Phase 14: Sandboxing ────────────────────────────────────────────────


class TestSandboxing:
    def setup_method(self):
        from brynq.sandboxing import SandboxManager, TrustLevel
        self.SandboxManager = SandboxManager
        self.TrustLevel = TrustLevel
        self.sm = SandboxManager(storage_dir=tempfile.mkdtemp())

    def test_create_sandbox_intern(self):
        sb = self.sm.create_sandbox("agent-1")
        assert sb["agent_id"] == "agent-1"
        assert sb["trust_level"] == "intern"
        assert sb["permissions"]["can_create_tasks"] is False
        assert sb["permissions"]["can_review"] is False

    def test_create_sandbox_with_name(self):
        sb = self.sm.create_sandbox("agent-2", agent_name="Nova")
        assert sb["agent_name"] == "Nova"
        assert sb["agent_id"] == "agent-2"

    def test_create_sandbox_core(self):
        sb = self.sm.create_sandbox("agent-3", trust_level="core")
        assert sb["trust_level"] == "core"
        assert sb["permissions"]["can_review"] is True
        assert sb["permissions"]["can_broadcast"] is True

    def test_promote(self):
        self.sm.create_sandbox("agent-4")
        result = self.sm.promote("agent-4", "contributor")
        assert result["ok"] is True
        assert result["old_level"] == "intern"
        assert result["new_level"] == "contributor"

    def test_promote_invalid_level(self):
        self.sm.create_sandbox("agent-5")
        result = self.sm.promote("agent-5", "superadmin")
        assert result["ok"] is False

    def test_promote_nonexistent(self):
        result = self.sm.promote("ghost", "contributor")
        assert result["ok"] is False

    def test_check_permission_post_message(self):
        self.sm.create_sandbox("agent-6")
        result = self.sm.check_permission("agent-6", "post_message", {"channel": "general"})
        assert result["allowed"] is True

    def test_check_permission_blocked_channel(self):
        self.sm.create_sandbox("agent-7")
        result = self.sm.check_permission("agent-7", "post_message", {"channel": "secret-ops"})
        assert result["allowed"] is False

    def test_check_permission_create_task_intern(self):
        self.sm.create_sandbox("agent-8")
        result = self.sm.check_permission("agent-8", "create_task")
        assert result["allowed"] is False

    def test_check_permission_create_task_contributor(self):
        self.sm.create_sandbox("agent-9", trust_level="contributor")
        result = self.sm.check_permission("agent-9", "create_task")
        assert result["allowed"] is True

    def test_unknown_action_denied(self):
        self.sm.create_sandbox("agent-10")
        result = self.sm.check_permission("agent-10", "launch_missiles")
        assert result["allowed"] is False

    def test_no_sandbox_unrestricted(self):
        result = self.sm.check_permission("unregistered-agent", "anything")
        assert result["allowed"] is True

    def test_record_violation(self):
        self.sm.create_sandbox("agent-11")
        result = self.sm.record_violation("agent-11", "spamming")
        assert result["ok"] is True
        assert result["total_violations"] == 1

    def test_record_task_completion(self):
        self.sm.create_sandbox("agent-12")
        result = self.sm.record_task_completion("agent-12", success=True)
        assert result["ok"] is True

    def test_auto_promote_after_5_tasks(self):
        """record_task_completion auto-promotes internally at 5 completions."""
        self.sm.create_sandbox("agent-13")
        for _ in range(5):
            self.sm.record_task_completion("agent-13", success=True)
        sb = self.sm.get_sandbox("agent-13")
        assert sb["trust_level"] == "contributor"

    def test_rate_limiting(self):
        self.sm.create_sandbox("agent-14", trust_level="intern")
        # Intern has max 20 messages/hour
        for i in range(20):
            r = self.sm.check_permission("agent-14", "post_message", {"channel": "general"})
            assert r["allowed"] is True, f"Message {i+1} should be allowed"
        r = self.sm.check_permission("agent-14", "post_message", {"channel": "general"})
        assert r["allowed"] is False


# ── Phase 17: Consensus ─────────────────────────────────────────────────


class TestConsensus:
    def setup_method(self):
        from brynq.consensus import ConsensusManager
        self.cm = ConsensusManager(storage_dir=tempfile.mkdtemp())

    def test_create_proposal(self):
        p = self.cm.create_proposal("Merge?", ["yes", "no"], "agent-1")
        assert "proposal_id" in p
        assert "expires_at" in p

    def test_vote_and_resolve(self):
        p = self.cm.create_proposal("Ship it?", ["yes", "no"], "agent-1", quorum=2)
        self.cm.vote(p["proposal_id"], "agent-1", "yes")
        self.cm.vote(p["proposal_id"], "agent-2", "yes")
        result = self.cm.resolve(p["proposal_id"])
        assert result["winner"] == "yes"
        assert result["quorum_met"] is True

    def test_quorum_not_met(self):
        p = self.cm.create_proposal("Big decision", ["a", "b"], "agent-1", quorum=3)
        self.cm.vote(p["proposal_id"], "agent-1", "a")
        result = self.cm.resolve(p["proposal_id"])
        assert result["quorum_met"] is False

    def test_tie_breaking(self):
        p = self.cm.create_proposal("Tie?", ["a", "b"], "agent-1", quorum=2)
        self.cm.vote(p["proposal_id"], "agent-1", "a")
        self.cm.vote(p["proposal_id"], "agent-2", "b")
        result = self.cm.resolve(p["proposal_id"])
        # Should have a winner via tie-breaking (earliest voter)
        assert result["winner"] in ("a", "b")

    def test_revoke_vote(self):
        p = self.cm.create_proposal("Revocable?", ["yes", "no"], "agent-1")
        self.cm.vote(p["proposal_id"], "agent-1", "yes")
        result = self.cm.revoke_vote(p["proposal_id"], "agent-1")
        assert result.get("ok", True)  # Should succeed

    def test_list_open(self):
        self.cm.create_proposal("Open 1", ["a", "b"], "agent-1")
        self.cm.create_proposal("Open 2", ["x", "y"], "agent-2")
        open_list = self.cm.list_open()
        assert len(open_list) >= 2

    def test_cancel_proposal(self):
        p = self.cm.create_proposal("Cancel me", ["yes", "no"], "agent-1")
        result = self.cm.cancel(p["proposal_id"])
        assert result is not None


# ── Phase 18: Knowledge Distillation ────────────────────────────────────


class TestKnowledgeDistillation:
    """API: publish_insight(agent_id, topic, content, confidence, tags)"""

    def setup_method(self):
        from brynq.knowledge_distillation import KnowledgeStore
        self.ks = KnowledgeStore(storage_dir=tempfile.mkdtemp())

    def test_publish_and_query(self):
        self.ks.publish_insight(
            "agent-1", "python-perf", "List comprehensions are faster than for loops",
            confidence=0.9, tags=["python", "performance"]
        )
        results = self.ks.query_topic("python-perf")
        assert len(results) > 0

    def test_search(self):
        self.ks.publish_insight(
            "agent-1", "docker-tips", "Use multi-stage builds to reduce image size",
            confidence=0.85, tags=["docker"]
        )
        results = self.ks.search("multi-stage builds")
        assert len(results) > 0

    def test_list_topics(self):
        self.ks.publish_insight("agent-1", "topic-a", "Content A")
        self.ks.publish_insight("agent-2", "topic-b", "Content B")
        topics = self.ks.list_topics()
        assert len(topics) >= 2

    def test_delete_insight(self):
        result = self.ks.publish_insight("agent-1", "deleteme", "Temporary insight")
        insight_id = result.get("insight_id", "")
        del_result = self.ks.delete_insight("deleteme", insight_id)
        assert del_result is not None

    def test_stats(self):
        self.ks.publish_insight("agent-1", "stat-test", "Content")
        stats = self.ks.stats()
        assert isinstance(stats, dict)

    def test_deduplication(self):
        self.ks.publish_insight("agent-1", "dedup", "Same content")
        self.ks.publish_insight("agent-1", "dedup", "Same content")
        results = self.ks.query_topic("dedup")
        assert len(results) <= 2


# ── Phase 19: Discovery ─────────────────────────────────────────────────


class TestDiscovery:
    def setup_method(self):
        from brynq.discovery import AgentDiscovery
        self.ad = AgentDiscovery(storage_dir=tempfile.mkdtemp())

    def test_register_and_get(self):
        self.ad.register_agent("agent-1", capabilities=["python", "review"], platform="claude")
        agent = self.ad.get_agent("agent-1")
        assert agent is not None
        assert "python" in agent.get("capabilities", [])

    def test_find_by_capability(self):
        self.ad.register_agent("agent-2", capabilities=["python", "ml"], platform="gemini")
        self.ad.register_agent("agent-3", capabilities=["javascript"], platform="cursor")
        results = self.ad.find_agents_by_capability("python")
        ids = [a.get("agent_id", a.get("id", "")) for a in results]
        assert "agent-2" in ids

    def test_find_by_hardware(self):
        self.ad.register_agent("agent-4", capabilities=["ml"], hardware={"gpu": "A100", "ram_gb": 64})
        results = self.ad.find_agents_by_hardware("gpu")
        assert len(results) >= 1

    def test_unregister(self):
        self.ad.register_agent("agent-5", capabilities=["testing"])
        self.ad.unregister_agent("agent-5")
        agent = self.ad.get_agent("agent-5")
        assert agent is None

    def test_no_duplicates_on_reregister(self):
        self.ad.register_agent("agent-6", capabilities=["python"])
        self.ad.register_agent("agent-6", capabilities=["python", "go"])
        agents = self.ad.list_agents()
        count = sum(1 for a in agents if a.get("agent_id", a.get("id", "")) == "agent-6")
        assert count == 1

    def test_heartbeat(self):
        self.ad.register_agent("agent-7", capabilities=["python"])
        result = self.ad.heartbeat("agent-7")
        assert result is not None

    def test_stats(self):
        self.ad.register_agent("agent-8", capabilities=["test"])
        stats = self.ad.stats()
        assert isinstance(stats, dict)


# ── Phase 13: Decentralized Memory ──────────────────────────────────────


class TestDecentralizedMemory:
    def setup_method(self):
        """Point memory at a temp dir to avoid polluting real state."""
        import brynq.decentralized_memory as dm
        self._orig_dir = dm.MEMORY_DIR
        from pathlib import Path
        dm.MEMORY_DIR = Path(tempfile.mkdtemp())
        self._orig_index = getattr(dm, '_INDEX_FILE', None)
        if hasattr(dm, '_INDEX_FILE'):
            dm._INDEX_FILE = dm.MEMORY_DIR / "_index.json"

    def teardown_method(self):
        import brynq.decentralized_memory as dm
        dm.MEMORY_DIR = self._orig_dir
        if self._orig_index is not None:
            dm._INDEX_FILE = self._orig_index

    def test_store_and_retrieve(self):
        from brynq.decentralized_memory import store_memory, retrieve_memory
        mid = store_memory("Test content for retrieval", "agent-1")
        result = retrieve_memory(mid)
        assert result is not None

    def test_store_with_metadata_dict(self):
        from brynq.decentralized_memory import store_memory, retrieve_memory
        mid = store_memory("Content with meta", "agent-1", metadata={"topic": "testing"})
        result = retrieve_memory(mid)
        assert result is not None

    def test_store_with_metadata_string_no_crash(self):
        """Regression test: metadata as string should not crash _rebuild_index."""
        from brynq.decentralized_memory import store_memory, list_memories
        store_memory("Content", "agent-1", metadata="not-a-dict")
        # Should not crash
        mems = list_memories()
        assert isinstance(mems, list)

    def test_list_memories(self):
        from brynq.decentralized_memory import store_memory, list_memories
        store_memory("Memory 1", "agent-1")
        store_memory("Memory 2", "agent-2")
        mems = list_memories()
        assert len(mems) >= 2

    def test_delete_memory(self):
        from brynq.decentralized_memory import store_memory, delete_memory, retrieve_memory
        mid = store_memory("Delete me", "agent-1")
        delete_memory(mid)
        result = retrieve_memory(mid)
        assert result is None or result == []

    def test_memory_stats(self):
        from brynq.decentralized_memory import store_memory, memory_stats
        store_memory("Stats test", "agent-1")
        stats = memory_stats()
        assert isinstance(stats, dict)


# ── Phase 16: Self-Healing ──────────────────────────────────────────────


class TestSelfHealing:
    def test_health_summary(self):
        from brynq.self_healing import health_summary
        report = health_summary()
        assert isinstance(report, dict)
        assert "stale_sessions" in report
        assert "stuck_tasks" in report
        assert "orphan_locks" in report

    def test_auto_heal(self):
        from brynq.self_healing import auto_heal
        result = auto_heal()
        assert isinstance(result, dict)

    def test_scan_for_stale_sessions(self):
        from brynq.self_healing import scan_for_stale_sessions
        result = scan_for_stale_sessions()
        assert isinstance(result, list)


# ── Phase 15: Task Dependencies ─────────────────────────────────────────


class TestTaskDependencies:
    """Tasks must exist as JSON files before dependencies can be added."""

    def setup_method(self):
        from brynq.task_dependencies import DependencyManager
        self.tmpdir = tempfile.mkdtemp()
        self.dm = DependencyManager(broker_dir=self.tmpdir)

    def _create_task(self, task_id, status="pending"):
        """Create a minimal task file so DependencyManager can find it."""
        task_data = {
            "id": task_id,
            "title": f"Task {task_id}",
            "status": status,
            "dependencies": [],
        }
        path = self.dm.tasks_dir / f"{task_id}.json"
        path.write_text(json.dumps(task_data), encoding="utf-8")

    def test_add_dependency(self):
        self._create_task("build")
        self._create_task("test")
        result = self.dm.add_dependency("test", "build")
        assert result.get("ok") is True
        deps = self.dm.get_dependencies("test")
        assert "build" in deps.get("direct", [])

    def test_get_ready_tasks(self):
        self._create_task("build")
        self._create_task("test")
        self._create_task("deploy")
        self.dm.add_dependency("test", "build")
        self.dm.add_dependency("deploy", "test")
        ready = self.dm.get_ready_tasks()
        ready_ids = [t.get("id", t) if isinstance(t, dict) else t for t in ready]
        assert "build" in ready_ids

    def test_execution_order(self):
        self._create_task("build")
        self._create_task("test")
        self._create_task("deploy")
        self.dm.add_dependency("test", "build")
        self.dm.add_dependency("deploy", "test")
        result = self.dm.execution_order(["build", "test", "deploy"])
        order = result.get("order", result) if isinstance(result, dict) else result
        assert order.index("build") < order.index("test")
        assert order.index("test") < order.index("deploy")

    def test_get_dependents(self):
        self._create_task("build")
        self._create_task("test")
        self.dm.add_dependency("test", "build")
        dependents = self.dm.get_dependents("build")
        dep_ids = [d.get("id", d) if isinstance(d, dict) else d for d in dependents]
        assert "test" in dep_ids

    def test_remove_dependency(self):
        self._create_task("build")
        self._create_task("test")
        self.dm.add_dependency("test", "build")
        self.dm.remove_dependency("test", "build")
        deps = self.dm.get_dependencies("test")
        assert "build" not in deps.get("direct", [])

    def test_is_task_ready(self):
        self._create_task("build")
        self._create_task("test")
        self.dm.add_dependency("test", "build")
        build_ready = self.dm.is_task_ready("build")
        test_ready = self.dm.is_task_ready("test")
        assert build_ready.get("ready", build_ready) is True
        assert test_ready.get("ready", test_ready) is False


# ── Phase 21: API Server ────────────────────────────────────────────────


class TestAPIServer:
    def test_app_imports(self):
        from brynq.api_server import app
        assert app.title == "Brynq Platform API"

    def test_route_count(self):
        from brynq.api_server import app
        routes = [r for r in app.routes if hasattr(r, "methods")]
        assert len(routes) >= 15  # Should have 19+ functional routes

    def test_has_auth_route(self):
        from brynq.api_server import app
        paths = [r.path for r in app.routes if hasattr(r, "path")]
        assert "/auth/register" in paths

    def test_has_channels_routes(self):
        from brynq.api_server import app
        paths = [r.path for r in app.routes if hasattr(r, "path")]
        assert "/channels" in paths
        assert "/channels/{channel}/messages" in paths

    def test_has_tasks_routes(self):
        from brynq.api_server import app
        paths = [r.path for r in app.routes if hasattr(r, "path")]
        assert "/tasks" in paths

    def test_has_health_route(self):
        from brynq.api_server import app
        paths = [r.path for r in app.routes if hasattr(r, "path")]
        assert "/health" in paths
