"""
Comprehensive tests for 27 previously-untested Brynq modules.

Covers: capability, profiles, jobs, memory, search, payloads, enforcer,
guardrails, scheduler, analytics, crm, erp, marketplace, compliance,
onboarding, retention, file_watcher, hooks, agent_bridge, cross_project,
enterprise_integration, billing, enterprise_tools.

All tests use temp directories for isolation — no side effects on real state.
"""

import json
import os
import tempfile
import time
from datetime import datetime, timezone, timedelta
from pathlib import Path

import pytest


# ── Helpers ───────────────────────────────────────────────────────────────


def _make_broker_dirs(base: Path) -> dict:
    """Create a full broker directory tree in a temp dir."""
    dirs = {
        "broker": base / "broker",
        "channels": base / "broker" / "channels",
        "sessions": base / "broker" / "sessions",
        "cursors": base / "broker" / "cursors",
        "tasks": base / "broker" / "tasks",
    }
    for d in dirs.values():
        d.mkdir(parents=True, exist_ok=True)
    return dirs


def _write_jsonl(path: Path, entries: list[dict]):
    """Write multiple entries to a JSONL file."""
    with open(path, "w", encoding="utf-8") as f:
        for entry in entries:
            f.write(json.dumps(entry) + "\n")


# ═══════════════════════════════════════════════════════════════════════════
#  PAYLOADS (pure functions — no file state needed for validation/format)
# ═══════════════════════════════════════════════════════════════════════════


class TestPayloadValidation:
    def test_validate_file_ref(self):
        from brynq.payloads import _validate_payload
        result = _validate_payload("file_ref", {"path": "src/main.py", "lines": "10-20"})
        assert result["type"] == "file_ref"
        assert result["path"] == "src/main.py"
        assert result["lines"] == "10-20"

    def test_validate_diff(self):
        from brynq.payloads import _validate_payload
        result = _validate_payload("diff", {"file": "a.py", "before": "old", "after": "new"})
        assert result["type"] == "diff"
        assert result["before"] == "old"

    def test_validate_test_result(self):
        from brynq.payloads import _validate_payload
        result = _validate_payload("test_result", {"passed": 10, "failed": 2})
        assert result["passed"] == 10
        assert result["failed"] == 2

    def test_validate_build_status(self):
        from brynq.payloads import _validate_payload
        result = _validate_payload("build_status", {"status": "success", "url": "http://ci"})
        assert result["status"] == "success"
        assert result["url"] == "http://ci"

    def test_validate_metric(self):
        from brynq.payloads import _validate_payload
        result = _validate_payload("metric", {"name": "latency", "value": 42, "unit": "ms"})
        assert result["name"] == "latency"
        assert result["unit"] == "ms"

    def test_unknown_type_raises(self):
        from brynq.payloads import _validate_payload
        with pytest.raises(ValueError, match="Unknown payload type"):
            _validate_payload("invalid_type", {})

    def test_missing_required_field_raises(self):
        from brynq.payloads import _validate_payload
        with pytest.raises(ValueError, match="requires fields"):
            _validate_payload("diff", {"file": "a.py"})


class TestPayloadFormat:
    def test_format_file_ref(self):
        from brynq.payloads import format_payload
        result = format_payload({"type": "file_ref", "path": "main.py", "lines": "5"})
        assert "[FILE]" in result
        assert "main.py:5" in result

    def test_format_diff(self):
        from brynq.payloads import format_payload
        result = format_payload({"type": "diff", "file": "a.py", "before": "x", "after": "y"})
        assert "[DIFF]" in result
        assert "- x" in result
        assert "+ y" in result

    def test_format_test_result(self):
        from brynq.payloads import format_payload
        result = format_payload({"type": "test_result", "passed": 8, "failed": 2, "errors": 0})
        assert "[TEST]" in result
        assert "8/10 passed" in result

    def test_format_build_success(self):
        from brynq.payloads import format_payload
        result = format_payload({"type": "build_status", "status": "success"})
        assert "[BUILD OK]" in result

    def test_format_build_failure(self):
        from brynq.payloads import format_payload
        result = format_payload({"type": "build_status", "status": "failed"})
        assert "[BUILD FAIL]" in result

    def test_format_metric(self):
        from brynq.payloads import format_payload
        result = format_payload({"type": "metric", "name": "cpu", "value": 75, "unit": "%", "trend": "up"})
        assert "[METRIC]" in result
        assert "cpu: 75 %" in result
        assert "^" in result

    def test_format_unknown_type_fallback(self):
        from brynq.payloads import format_payload
        result = format_payload({"type": "custom", "data": 123})
        assert "[CUSTOM]" in result


class TestPayloadExtract:
    def test_extract_filters_by_type(self):
        from brynq.payloads import extract_payloads
        messages = [
            {"message": "a", "payload": {"type": "diff", "file": "x"}},
            {"message": "b", "payload": {"type": "metric", "name": "y", "value": 1}},
            {"message": "c"},
        ]
        diffs = extract_payloads(messages, payload_type="diff")
        assert len(diffs) == 1
        assert diffs[0]["payload"]["type"] == "diff"

    def test_extract_all_payloads(self):
        from brynq.payloads import extract_payloads
        messages = [
            {"message": "a", "payload": {"type": "diff"}},
            {"message": "b"},
            {"message": "c", "payload": {"type": "metric"}},
        ]
        all_p = extract_payloads(messages)
        assert len(all_p) == 2

    def test_extract_empty_list(self):
        from brynq.payloads import extract_payloads
        assert extract_payloads([]) == []


# ═══════════════════════════════════════════════════════════════════════════
#  SEARCH
# ═══════════════════════════════════════════════════════════════════════════


class TestSearch:
    def setup_method(self):
        self._tmpdir = tempfile.mkdtemp()
        self._channels = Path(self._tmpdir) / "channels"
        self._channels.mkdir()

        import brynq.search as mod
        self._orig_channels = mod.CHANNELS_DIR
        mod.CHANNELS_DIR = self._channels

        # Write sample messages
        msgs = [
            {"id": "1", "timestamp": "2026-03-17T10:00:00+00:00", "session_name": "alice",
             "platform": "claude", "channel": "general", "msg_type": "info",
             "message": "Hello world"},
            {"id": "2", "timestamp": "2026-03-17T11:00:00+00:00", "session_name": "bob",
             "platform": "gemini", "channel": "general", "msg_type": "alert",
             "message": "System down"},
            {"id": "3", "timestamp": "2026-03-17T12:00:00+00:00", "session_name": "alice",
             "platform": "claude", "channel": "general", "msg_type": "info",
             "message": "All clear"},
        ]
        _write_jsonl(self._channels / "general.jsonl", msgs)

        alerts = [
            {"id": "4", "timestamp": "2026-03-17T09:00:00+00:00", "session_name": "system",
             "platform": "system", "channel": "alerts", "msg_type": "alert",
             "message": "Disk 90% full"},
        ]
        _write_jsonl(self._channels / "alerts.jsonl", alerts)

    def teardown_method(self):
        import brynq.search as mod
        mod.CHANNELS_DIR = self._orig_channels

    def test_search_by_query(self):
        from brynq.search import search
        results = search(query="system")
        assert len(results) == 1
        assert results[0]["session_name"] == "bob"

    def test_search_by_sender(self):
        from brynq.search import search
        results = search(sender="alice")
        assert len(results) == 2

    def test_search_by_platform(self):
        from brynq.search import search
        results = search(platform="gemini")
        assert len(results) == 1

    def test_search_by_msg_type(self):
        from brynq.search import search
        results = search(msg_type="alert")
        assert len(results) == 2

    def test_search_by_channel(self):
        from brynq.search import search
        results = search(channel="alerts")
        assert len(results) == 1
        assert "Disk" in results[0]["message"]

    def test_search_since(self):
        from brynq.search import search
        results = search(since="2026-03-17T10:30:00")
        assert len(results) == 2  # 11:00 and 12:00

    def test_search_until(self):
        from brynq.search import search
        results = search(until="2026-03-17T10:30:00")
        assert len(results) == 2  # 09:00 and 10:00

    def test_search_limit(self):
        from brynq.search import search
        results = search(limit=1)
        assert len(results) == 1

    def test_search_combined_filters(self):
        from brynq.search import search
        results = search(sender="alice", msg_type="info", channel="general")
        assert len(results) == 2

    def test_search_no_results(self):
        from brynq.search import search
        results = search(query="nonexistent_xyz")
        assert len(results) == 0

    def test_format_results_empty(self):
        from brynq.search import format_results
        result = format_results([])
        assert "No messages" in result

    def test_format_results_non_empty(self):
        from brynq.search import format_results
        results = [{"timestamp": "2026-03-17T10:00:00", "session_name": "alice",
                     "platform": "claude", "channel": "general", "msg_type": "info",
                     "message": "Hello"}]
        output = format_results(results)
        assert "alice@claude" in output
        assert "1 match" in output


# ═══════════════════════════════════════════════════════════════════════════
#  GUARDRAILS
# ═══════════════════════════════════════════════════════════════════════════


class TestGuardrailsConflictDetection:
    def test_no_conflict(self):
        from brynq.guardrails import detect_conflict
        result = detect_conflict("Working on the API server now.")
        assert result["has_conflict"] is False
        assert result["severity"] == "none"

    def test_mild_conflict(self):
        from brynq.guardrails import detect_conflict
        result = detect_conflict("Stop editing that file!")
        assert result["has_conflict"] is True
        assert result["severity"] in ("mild", "heated")

    def test_heated_conflict(self):
        from brynq.guardrails import detect_conflict
        result = detect_conflict("Stop editing my code, that's mine and I claimed it first!")
        assert result["has_conflict"] is True
        assert result["severity"] in ("heated", "escalate")

    def test_escalation_conflict(self):
        from brynq.guardrails import detect_conflict
        result = detect_conflict("I refuse to stop editing, your fault for the broken code!")
        assert result["has_conflict"] is True
        assert result["severity"] == "escalate"


class TestGuardrailsRateLimit:
    def setup_method(self):
        import brynq.guardrails as g
        g._send_times.clear()

    def test_rate_limit_allows_under_threshold(self):
        from brynq.guardrails import check_rate_limit
        ok, reason = check_rate_limit()
        assert ok is True
        assert reason == "ok"

    def test_rate_limit_blocks_over_threshold(self):
        import brynq.guardrails as g
        from brynq.guardrails import check_rate_limit
        # Fill up the rate limit
        now = time.monotonic()
        g._send_times[:] = [now] * 30
        ok, reason = check_rate_limit()
        assert ok is False
        assert "Rate limited" in reason


class TestGuardrailsFileLocking:
    def setup_method(self):
        self._tmpdir = Path(tempfile.mkdtemp())
        base_dirs = _make_broker_dirs(self._tmpdir)

        import brynq.guardrails as g
        self._orig_locks = g.LOCKS_DIR
        self._orig_channels = g.CHANNELS_DIR
        g.LOCKS_DIR = self._tmpdir / "broker" / "locks"
        g.LOCKS_DIR.mkdir(parents=True, exist_ok=True)
        g.CHANNELS_DIR = base_dirs["channels"]

    def teardown_method(self):
        import brynq.guardrails as g
        g.LOCKS_DIR = self._orig_locks
        g.CHANNELS_DIR = self._orig_channels

    def test_claim_and_release(self):
        from brynq.guardrails import claim_file, release_file, check_file_lock
        ok, msg = claim_file("src/brynq/test.py", "testing")
        assert ok is True
        assert "Locked" in msg

        lock = check_file_lock("src/brynq/test.py")
        assert lock is not None
        assert lock["filepath"] == "src/brynq/test.py"

        ok, msg = release_file("src/brynq/test.py")
        assert ok is True
        assert "Released" in msg

        assert check_file_lock("src/brynq/test.py") is None

    def test_release_non_existent(self):
        from brynq.guardrails import release_file
        ok, msg = release_file("nonexistent.py")
        assert ok is False

    def test_get_all_locks(self):
        from brynq.guardrails import claim_file, get_all_locks
        claim_file("a.py")
        claim_file("b.py")
        locks = get_all_locks()
        assert len(locks) == 2

    def test_force_release_all(self):
        from brynq.guardrails import claim_file, force_release_all, get_all_locks
        claim_file("x.py")
        claim_file("y.py")
        count = force_release_all()
        assert count == 2
        assert len(get_all_locks()) == 0


class TestGuardrailsMessageFormats:
    def test_format_claim_message(self):
        from brynq.guardrails import format_claim_message
        msg = format_claim_message("server.py", "adding endpoints", 10)
        assert "server.py" in msg
        assert "10 min" in msg

    def test_format_handoff_message(self):
        from brynq.guardrails import format_handoff_message
        msg = format_handoff_message("server.py", "endpoints added")
        assert "HANDOFF" in msg
        assert "server.py" in msg

    def test_format_proposal(self):
        from brynq.guardrails import format_proposal
        msg = format_proposal("DB Choice", "Which database?", ["Postgres", "SQLite"])
        assert "PROPOSAL" in msg
        assert "1. Postgres" in msg
        assert "2. SQLite" in msg


# ═══════════════════════════════════════════════════════════════════════════
#  ENFORCER
# ═══════════════════════════════════════════════════════════════════════════


class TestEnforcer:
    def setup_method(self):
        self._tmpdir = Path(tempfile.mkdtemp())
        base_dirs = _make_broker_dirs(self._tmpdir)

        import brynq.enforcer as e
        self._orig_violations = e.VIOLATIONS_DIR
        self._orig_channels = e.CHANNELS_DIR
        e.VIOLATIONS_DIR = self._tmpdir / "broker" / "violations"
        e.VIOLATIONS_DIR.mkdir(parents=True, exist_ok=True)
        e.CHANNELS_DIR = base_dirs["channels"]
        e._cooldown_until = 0.0
        e._blocked = False

    def teardown_method(self):
        import brynq.enforcer as e
        e.VIOLATIONS_DIR = self._orig_violations
        e.CHANNELS_DIR = self._orig_channels
        e._cooldown_until = 0.0
        e._blocked = False

    def test_record_violation_warning(self):
        from brynq.enforcer import record_violation
        result = record_violation(8, "test violation", session_id="test-1", session_name="tester")
        assert result["level"] == "warning"
        assert "LAW 8" in result["message"]

    def test_violation_escalation_levels(self):
        from brynq.enforcer import record_violation
        sid = "escalation-test"
        # 1-2: warning
        for i in range(2):
            r = record_violation(1, f"violation {i}", session_id=sid, session_name="t")
        assert r["level"] == "warning"

        # 3-4: still warning
        for i in range(2):
            r = record_violation(1, f"violation {i+2}", session_id=sid, session_name="t")
        assert r["level"] == "warning"

        # 5: cooldown
        r = record_violation(1, "violation 5", session_id=sid, session_name="t")
        assert r["level"] == "cooldown"

        # 6-7: still cooldown
        for i in range(2):
            r = record_violation(1, f"violation {i+6}", session_id=sid, session_name="t")
        assert r["level"] == "cooldown"

        # 8: escalation
        r = record_violation(1, "violation 8", session_id=sid, session_name="t")
        assert r["level"] == "escalation"

    def test_check_session_good_standing(self):
        from brynq.enforcer import check_session_status
        result = check_session_status(session_id="clean-session")
        assert result["status"] == "good_standing"
        assert result["can_post"] is True

    def test_pardon_resets_violations(self):
        from brynq.enforcer import record_violation, pardon, check_session_status
        sid = "pardon-test"
        for i in range(8):
            record_violation(1, f"v{i}", session_id=sid, session_name="t")
        result = pardon(sid)
        assert result["pardoned"] == 8
        status = check_session_status(session_id=sid)
        assert status["violations"] == 0

    def test_violation_summary(self):
        from brynq.enforcer import record_violation, get_violation_summary
        sid = "summary-test"
        record_violation(10, "too long", session_id=sid, session_name="t")
        record_violation(11, "no claim", session_id=sid, session_name="t")
        summary = get_violation_summary(session_id=sid)
        assert summary["total_violations"] == 2
        assert len(summary["recent_violations"]) == 2

    def test_enforce_profile_skills(self):
        from brynq.enforcer import enforce_profile_skills
        tasks = [{"title": "Python code review", "description": "reviewed module"}]
        unverified = enforce_profile_skills(["python", "rust"], tasks)
        assert "rust" in unverified
        assert "python" not in unverified

    def test_enforce_no_discrimination_clean(self):
        from brynq.enforcer import enforce_no_discrimination
        ok, msg = enforce_no_discrimination({}, {"platform": "claude"}, "Best skills match")
        assert ok is True

    def test_enforce_no_discrimination_flagged(self):
        from brynq.enforcer import enforce_no_discrimination
        ok, msg = enforce_no_discrimination({}, {"platform": "gemini"}, "Wrong platform gemini")
        assert ok is False
        assert "Law 37" in msg


# ═══════════════════════════════════════════════════════════════════════════
#  SCHEDULER
# ═══════════════════════════════════════════════════════════════════════════


class TestScheduler:
    def setup_method(self):
        self._tmpdir = Path(tempfile.mkdtemp())
        base_dirs = _make_broker_dirs(self._tmpdir)

        import brynq.scheduler as s
        self._orig_sched = s.SCHEDULER_DIR
        self._orig_channels = s.CHANNELS_DIR
        s.SCHEDULER_DIR = self._tmpdir / "broker" / "scheduler"
        s.SCHEDULER_DIR.mkdir(parents=True, exist_ok=True)
        s.CHANNELS_DIR = base_dirs["channels"]

    def teardown_method(self):
        import brynq.scheduler as s
        s.SCHEDULER_DIR = self._orig_sched
        s.CHANNELS_DIR = self._orig_channels

    def test_register_cron_task(self):
        from brynq.scheduler import register_cron_task
        job_id = register_cron_task("health-check", "alerts", "300", "Check health")
        assert isinstance(job_id, str)
        assert len(job_id) == 12

    def test_list_cron_tasks(self):
        from brynq.scheduler import register_cron_task, list_cron_tasks
        register_cron_task("job1", "general", "60", "msg1")
        register_cron_task("job2", "alerts", "120", "msg2")
        tasks = list_cron_tasks()
        assert len(tasks) == 2

    def test_remove_cron_task(self):
        from brynq.scheduler import register_cron_task, remove_cron_task, list_cron_tasks
        jid = register_cron_task("to-remove", "general", "60", "msg")
        assert remove_cron_task(jid) is True
        assert len(list_cron_tasks()) == 0

    def test_remove_nonexistent(self):
        from brynq.scheduler import remove_cron_task
        assert remove_cron_task("doesnotexist") is False

    def test_invalid_cron_expr(self):
        from brynq.scheduler import register_cron_task
        with pytest.raises(ValueError, match="Invalid cron_expr"):
            register_cron_task("bad", "general", "not-a-number", "msg")


# ═══════════════════════════════════════════════════════════════════════════
#  MEMORY
# ═══════════════════════════════════════════════════════════════════════════


class TestMemory:
    def setup_method(self):
        self._tmpdir = Path(tempfile.mkdtemp())
        import brynq.memory as m
        self._orig_dir = m.MEMORY_DIR
        m.MEMORY_DIR = self._tmpdir / "memory"
        m.MEMORY_DIR.mkdir(parents=True, exist_ok=True)

    def teardown_method(self):
        import brynq.memory as m
        m.MEMORY_DIR = self._orig_dir

    def test_set_and_get(self):
        from brynq.memory import memory_set, memory_get
        memory_set("task", "build api", session_id="test-mem")
        value = memory_get("task", session_id="test-mem")
        assert value == "build api"

    def test_get_nonexistent(self):
        from brynq.memory import memory_get
        assert memory_get("nope", session_id="test-mem") is None

    def test_delete(self):
        from brynq.memory import memory_set, memory_delete, memory_get
        memory_set("temp", "data", session_id="test-mem")
        assert memory_delete("temp", session_id="test-mem") is True
        assert memory_get("temp", session_id="test-mem") is None

    def test_delete_nonexistent(self):
        from brynq.memory import memory_delete
        assert memory_delete("nope", session_id="test-mem") is False

    def test_list_memories(self):
        from brynq.memory import memory_set, memory_list
        memory_set("key1", "val1", namespace="work", session_id="test-mem")
        memory_set("key2", "val2", namespace="personal", session_id="test-mem")
        all_items = memory_list(session_id="test-mem")
        assert len(all_items) == 2

    def test_list_by_namespace(self):
        from brynq.memory import memory_set, memory_list
        memory_set("a", 1, namespace="ns1", session_id="test-ns")
        memory_set("b", 2, namespace="ns2", session_id="test-ns")
        items = memory_list(namespace="ns1", session_id="test-ns")
        assert len(items) == 1
        assert items[0]["key"] == "a"

    def test_list_by_tag(self):
        from brynq.memory import memory_set, memory_list
        memory_set("tagged", "data", tags=["important"], session_id="test-tags")
        memory_set("untagged", "other", session_id="test-tags")
        items = memory_list(tag="important", session_id="test-tags")
        assert len(items) == 1

    def test_search(self):
        from brynq.memory import memory_set, memory_search
        memory_set("project-status", "building api server", session_id="test-search")
        memory_set("lunch", "pizza", session_id="test-search")
        results = memory_search("api", session_id="test-search")
        assert len(results) == 1
        assert results[0]["key"] == "project-status"

    def test_namespaces(self):
        from brynq.memory import memory_set, memory_namespaces
        memory_set("a", 1, namespace="alpha", session_id="test-nslist")
        memory_set("b", 2, namespace="beta", session_id="test-nslist")
        ns = memory_namespaces(session_id="test-nslist")
        assert "alpha" in ns
        assert "beta" in ns

    def test_ttl_expiry(self):
        from brynq.memory import memory_set, memory_get
        # Set with TTL of 0 hours (already expired)
        entry = memory_set("expiring", "gone", ttl_hours=0.0001, session_id="test-ttl")
        # Manually backdate the expiry
        mem_dir = Path(tempfile.mkdtemp())
        import brynq.memory as m
        sid_dir = m.MEMORY_DIR / "test-ttl"
        for f in sid_dir.glob("*.json"):
            if f.name == "_index.json":
                continue
            data = json.loads(f.read_text("utf-8"))
            data["expires"] = (datetime.now(timezone.utc) - timedelta(hours=1)).isoformat()
            f.write_text(json.dumps(data))
        value = memory_get("expiring", session_id="test-ttl")
        assert value is None

    def test_get_full_returns_metadata(self):
        from brynq.memory import memory_set, memory_get_full
        memory_set("full-test", {"nested": True}, tags=["meta"], session_id="test-full")
        entry = memory_get_full("full-test", session_id="test-full")
        assert entry is not None
        assert entry["key"] == "full-test"
        assert entry["tags"] == ["meta"]
        assert entry["value"] == {"nested": True}


# ═══════════════════════════════════════════════════════════════════════════
#  CAPABILITY
# ═══════════════════════════════════════════════════════════════════════════


class TestCapability:
    def setup_method(self):
        self._tmpdir = Path(tempfile.mkdtemp())
        base_dirs = _make_broker_dirs(self._tmpdir)

        import brynq.capability as c
        self._orig_cap = c.CAPABILITY_DIR
        self._orig_channels = c.CHANNELS_DIR
        self._orig_sessions = c.SESSIONS_DIR
        self._orig_task_log = c.TASK_LOG
        c.CAPABILITY_DIR = self._tmpdir / "broker" / "capabilities"
        c.CAPABILITY_DIR.mkdir(parents=True, exist_ok=True)
        c.CHANNELS_DIR = base_dirs["channels"]
        c.SESSIONS_DIR = base_dirs["sessions"]
        c.TASK_LOG = c.CAPABILITY_DIR / "_task_log.jsonl"

    def teardown_method(self):
        import brynq.capability as c
        c.CAPABILITY_DIR = self._orig_cap
        c.CHANNELS_DIR = self._orig_channels
        c.SESSIONS_DIR = self._orig_sessions
        c.TASK_LOG = self._orig_task_log

    def test_register_capability(self):
        from brynq.capability import register_capability
        result = register_capability("python", "Python expertise", session_id="agent-1",
                                      session_name="coder", platform="claude")
        assert result["capability"] == "python"
        assert result["session_id"] == "agent-1"

    def test_unregister_capability(self):
        from brynq.capability import register_capability, unregister_capability
        register_capability("rust", "Rust dev", session_id="agent-2")
        assert unregister_capability("rust", session_id="agent-2") is True
        assert unregister_capability("rust", session_id="agent-2") is False

    def test_list_capabilities(self):
        from brynq.capability import register_capability, list_capabilities
        register_capability("python", "py", session_id="a1")
        register_capability("go", "golang", session_id="a2")
        # Not active (no session file), so active_only=True returns 0
        caps = list_capabilities(active_only=False)
        assert len(caps) == 2

    def test_find_agents_exact_match(self):
        import brynq.capability as c
        from brynq.capability import register_capability, find_agents
        # Create a session file so the agent is "active"
        sess = {"session_id": "finder-1", "session_name": "bot-1", "platform": "claude"}
        (c.SESSIONS_DIR / "finder-1.json").write_text(json.dumps(sess))
        register_capability("code_review", "Reviews code", session_id="finder-1",
                           session_name="bot-1", platform="claude")
        results = find_agents(capability="code_review")
        assert len(results) == 1
        assert results[0]["score"] == 100.0

    def test_find_agents_fuzzy_query(self):
        import brynq.capability as c
        from brynq.capability import register_capability, find_agents
        sess = {"session_id": "fuzzy-1"}
        (c.SESSIONS_DIR / "fuzzy-1.json").write_text(json.dumps(sess))
        register_capability("ml_training", "Machine learning model training",
                           session_id="fuzzy-1", session_name="ml-bot")
        results = find_agents(query="machine learning")
        assert len(results) == 1

    def test_find_agents_platform_filter(self):
        import brynq.capability as c
        from brynq.capability import register_capability, find_agents
        for i, plat in enumerate(["claude", "gemini"]):
            sid = f"plat-{i}"
            (c.SESSIONS_DIR / f"{sid}.json").write_text(json.dumps({"session_id": sid}))
            register_capability("testing", f"Testing on {plat}", session_id=sid, platform=plat)
        results = find_agents(capability="testing", platform="claude")
        assert len(results) == 1
        assert results[0]["platform"] == "claude"


# ═══════════════════════════════════════════════════════════════════════════
#  PROFILES
# ═══════════════════════════════════════════════════════════════════════════


class TestProfiles:
    def setup_method(self):
        self._tmpdir = Path(tempfile.mkdtemp())
        base_dirs = _make_broker_dirs(self._tmpdir)

        import brynq.profiles as p
        self._orig_profiles = p.PROFILES_DIR
        self._orig_channels = p.CHANNELS_DIR
        self._orig_ratings = p.RATINGS_LOG
        p.PROFILES_DIR = self._tmpdir / "broker" / "profiles"
        p.PROFILES_DIR.mkdir(parents=True, exist_ok=True)
        p.CHANNELS_DIR = base_dirs["channels"]
        p.RATINGS_LOG = p.PROFILES_DIR / "_ratings.jsonl"

    def teardown_method(self):
        import brynq.profiles as p
        p.PROFILES_DIR = self._orig_profiles
        p.CHANNELS_DIR = self._orig_channels
        p.RATINGS_LOG = self._orig_ratings

    def test_create_profile(self):
        from brynq.profiles import create_or_update_profile
        profile = create_or_update_profile(
            bio="Test bot", skills=["python"], availability="available",
            session_id="prof-1", session_name="tester", platform="claude"
        )
        assert profile["bio"] == "Test bot"
        assert profile["skills"] == ["python"]
        assert profile["reputation"] == 5.0

    def test_get_profile(self):
        from brynq.profiles import create_or_update_profile, get_profile
        create_or_update_profile(bio="x", session_id="prof-2")
        assert get_profile(session_id="prof-2") is not None

    def test_get_profile_nonexistent(self):
        from brynq.profiles import get_profile
        assert get_profile(session_id="nope") == {}

    def test_list_profiles(self):
        from brynq.profiles import create_or_update_profile, list_profiles
        create_or_update_profile(bio="a", skills=["python"], session_id="lp-1")
        create_or_update_profile(bio="b", skills=["go"], session_id="lp-2")
        profiles = list_profiles()
        assert len(profiles) == 2

    def test_list_profiles_filter_skill(self):
        from brynq.profiles import create_or_update_profile, list_profiles
        create_or_update_profile(skills=["python"], session_id="sk-1")
        create_or_update_profile(skills=["go"], session_id="sk-2")
        profiles = list_profiles(skill="python")
        assert len(profiles) == 1

    def test_list_profiles_filter_availability(self):
        from brynq.profiles import create_or_update_profile, list_profiles
        create_or_update_profile(availability="available", session_id="av-1")
        create_or_update_profile(availability="busy", session_id="av-2")
        profiles = list_profiles(availability="available")
        assert len(profiles) == 1

    def test_update_availability(self):
        from brynq.profiles import create_or_update_profile, update_availability
        create_or_update_profile(session_id="ua-1", availability="available")
        updated = update_availability("busy", session_id="ua-1")
        assert updated["availability"] == "busy"

    def test_add_skill(self):
        from brynq.profiles import create_or_update_profile, add_skill
        create_or_update_profile(skills=["python"], session_id="as-1")
        updated = add_skill("go", session_id="as-1")
        assert "go" in updated["skills"]
        assert "python" in updated["skills"]

    def test_add_skill_no_duplicate(self):
        from brynq.profiles import create_or_update_profile, add_skill
        create_or_update_profile(skills=["python"], session_id="dup-1")
        updated = add_skill("python", session_id="dup-1")
        assert updated["skills"].count("python") == 1

    def test_rate_bot(self):
        from brynq.profiles import create_or_update_profile, rate_bot
        create_or_update_profile(session_id="rate-1")
        result = rate_bot("rate-1", 4.0, review="good work")
        assert result["reputation"] == 4.0
        assert result["total_ratings"] == 1

    def test_rate_bot_multiple(self):
        from brynq.profiles import create_or_update_profile, rate_bot
        create_or_update_profile(session_id="rate-2")
        rate_bot("rate-2", 5.0)
        result = rate_bot("rate-2", 3.0)
        assert result["reputation"] == 4.0  # (5+3)/2
        assert result["total_ratings"] == 2

    def test_rate_bot_clamps(self):
        from brynq.profiles import create_or_update_profile, rate_bot
        create_or_update_profile(session_id="clamp-1")
        result = rate_bot("clamp-1", 10.0)
        assert result["reputation"] == 5.0  # Clamped to max

    def test_record_task_completion(self):
        from brynq.profiles import create_or_update_profile, record_task_completion, get_profile
        create_or_update_profile(session_id="tc-1")
        record_task_completion("tc-1", "task-1", "Build API", "Done", success=True)
        profile = get_profile(session_id="tc-1")
        assert profile["tasks_completed"] == 1
        assert len(profile["portfolio"]) == 1

    def test_record_task_failure(self):
        from brynq.profiles import create_or_update_profile, record_task_completion, get_profile
        create_or_update_profile(session_id="tf-1")
        record_task_completion("tf-1", "task-2", "Break Things", "Oops", success=False)
        profile = get_profile(session_id="tf-1")
        assert profile["tasks_failed"] == 1

    def test_find_bots_for_skills(self):
        from brynq.profiles import create_or_update_profile, find_bots_for_skills
        create_or_update_profile(skills=["python", "testing"], session_id="fb-1", availability="available")
        create_or_update_profile(skills=["go"], session_id="fb-2", availability="available")
        matches = find_bots_for_skills(required_skills=["python"])
        assert len(matches) == 1
        assert matches[0]["session_id"] == "fb-1"


# ═══════════════════════════════════════════════════════════════════════════
#  JOBS
# ═══════════════════════════════════════════════════════════════════════════


class TestJobs:
    def setup_method(self):
        self._tmpdir = Path(tempfile.mkdtemp())
        base_dirs = _make_broker_dirs(self._tmpdir)

        import brynq.jobs as j
        self._orig_jobs = j.JOBS_DIR
        self._orig_apps = j.APPS_DIR
        self._orig_channels = j.CHANNELS_DIR
        j.JOBS_DIR = self._tmpdir / "broker" / "jobs"
        j.APPS_DIR = j.JOBS_DIR / "applications"
        j.JOBS_DIR.mkdir(parents=True, exist_ok=True)
        j.APPS_DIR.mkdir(parents=True, exist_ok=True)
        j.CHANNELS_DIR = base_dirs["channels"]

    def teardown_method(self):
        import brynq.jobs as j
        j.JOBS_DIR = self._orig_jobs
        j.APPS_DIR = self._orig_apps
        j.CHANNELS_DIR = self._orig_channels

    def test_post_job(self):
        from brynq.jobs import post_job
        job = post_job("Build API", "REST API server", required_skills=["python", "fastapi"])
        assert job["title"] == "Build API"
        assert job["status"] == "open"
        assert "python" in job["required_skills"]

    def test_list_jobs(self):
        from brynq.jobs import post_job, list_jobs
        post_job("Job 1", "desc", ["python"])
        post_job("Job 2", "desc", ["go"])
        jobs = list_jobs()
        assert len(jobs) == 2

    def test_list_jobs_filter_status(self):
        from brynq.jobs import post_job, list_jobs, close_job
        j1 = post_job("Open", "desc", ["python"])
        j2 = post_job("Closed", "desc", ["go"])
        close_job(j2["job_id"])
        open_jobs = list_jobs(status="open")
        assert len(open_jobs) == 1

    def test_list_jobs_filter_skill(self):
        from brynq.jobs import post_job, list_jobs
        post_job("Py Job", "desc", ["python"])
        post_job("Go Job", "desc", ["go"])
        py_jobs = list_jobs(skill="python")
        assert len(py_jobs) == 1

    def test_get_job(self):
        from brynq.jobs import post_job, get_job
        job = post_job("Test", "desc", ["python"])
        fetched = get_job(job["job_id"])
        assert fetched["title"] == "Test"

    def test_get_job_nonexistent(self):
        from brynq.jobs import get_job
        assert get_job("doesnotexist") is None

    def test_close_job(self):
        from brynq.jobs import post_job, close_job, get_job
        job = post_job("To Close", "desc", ["python"])
        result = close_job(job["job_id"], reason="filled externally")
        assert result["status"] == "closed"
        assert result["close_reason"] == "filled externally"

    def test_apply_to_job(self):
        import brynq.jobs as j
        from brynq.jobs import post_job, apply_to_job

        # Set up profiles dir for apply_to_job's internal import
        import brynq.profiles as p
        orig_profiles = p.PROFILES_DIR
        p.PROFILES_DIR = self._tmpdir / "broker" / "profiles"
        p.PROFILES_DIR.mkdir(parents=True, exist_ok=True)
        p.CHANNELS_DIR = j.CHANNELS_DIR

        try:
            from brynq.profiles import create_or_update_profile
            create_or_update_profile(skills=["python"], session_id="applicant-1",
                                     session_name="applicant", platform="claude")
            job = post_job("Developer", "Build stuff", ["python"])
            app = apply_to_job(job["job_id"], "I can do this!", session_id="applicant-1",
                              session_name="applicant", platform="claude")
            assert app["job_id"] == job["job_id"]
            assert app["status"] == "applied"
            assert app["match_percentage"] == 100
        finally:
            p.PROFILES_DIR = orig_profiles

    def test_apply_to_closed_job(self):
        import brynq.profiles as p
        orig_profiles = p.PROFILES_DIR
        p.PROFILES_DIR = self._tmpdir / "broker" / "profiles"
        p.PROFILES_DIR.mkdir(parents=True, exist_ok=True)
        p.CHANNELS_DIR = self._tmpdir / "broker" / "channels"

        try:
            from brynq.jobs import post_job, close_job, apply_to_job
            job = post_job("Closed Job", "desc", ["python"])
            close_job(job["job_id"])
            result = apply_to_job(job["job_id"], "please?", session_id="late-1")
            assert "error" in result
        finally:
            p.PROFILES_DIR = orig_profiles


# ═══════════════════════════════════════════════════════════════════════════
#  ANALYTICS
# ═══════════════════════════════════════════════════════════════════════════


class TestAnalytics:
    def setup_method(self):
        self._tmpdir = Path(tempfile.mkdtemp())
        base_dirs = _make_broker_dirs(self._tmpdir)

        import brynq.analytics as a
        self._orig_analytics = a.ANALYTICS_DIR
        self._orig_snapshots = a.SNAPSHOTS_DIR
        self._orig_events = a.EVENTS_LOG
        self._orig_channels = a.CHANNELS_DIR
        self._orig_tasks = a.TASKS_DIR
        self._orig_sessions = a.SESSIONS_DIR

        a.ANALYTICS_DIR = self._tmpdir / "broker" / "analytics"
        a.SNAPSHOTS_DIR = a.ANALYTICS_DIR / "snapshots"
        a.EVENTS_LOG = a.ANALYTICS_DIR / "events.jsonl"
        a.CHANNELS_DIR = base_dirs["channels"]
        a.TASKS_DIR = base_dirs["tasks"]
        a.SESSIONS_DIR = base_dirs["sessions"]
        a.ANALYTICS_DIR.mkdir(parents=True, exist_ok=True)
        a.SNAPSHOTS_DIR.mkdir(parents=True, exist_ok=True)

        # Write some sample messages
        msgs = [
            {"id": "1", "timestamp": "2026-03-17T10:00:00+00:00",
             "session_id": "s1", "session_name": "alice", "platform": "claude",
             "msg_type": "info", "message": "Hello"},
            {"id": "2", "timestamp": "2026-03-17T11:00:00+00:00",
             "session_id": "s2", "session_name": "bob", "platform": "gemini",
             "msg_type": "alert", "message": "Alert"},
        ]
        _write_jsonl(base_dirs["channels"] / "general.jsonl", msgs)

    def teardown_method(self):
        import brynq.analytics as a
        a.ANALYTICS_DIR = self._orig_analytics
        a.SNAPSHOTS_DIR = self._orig_snapshots
        a.EVENTS_LOG = self._orig_events
        a.CHANNELS_DIR = self._orig_channels
        a.TASKS_DIR = self._orig_tasks
        a.SESSIONS_DIR = self._orig_sessions

    def test_get_message_stats(self):
        from brynq.analytics import get_message_stats
        stats = get_message_stats(hours=24)
        assert isinstance(stats, dict)

    def test_get_agent_activity(self):
        from brynq.analytics import get_agent_activity
        activity = get_agent_activity(session_id="s1", hours=24)
        assert isinstance(activity, dict)

    def test_get_leaderboard(self):
        from brynq.analytics import get_leaderboard
        board = get_leaderboard()
        assert isinstance(board, list)

    def test_get_system_health(self):
        from brynq.analytics import get_system_health
        health = get_system_health()
        assert isinstance(health, dict)
        assert "channels" in health or "messages" in health or "active_agents" in health

    def test_get_task_stats(self):
        from brynq.analytics import get_task_stats
        stats = get_task_stats()
        assert isinstance(stats, dict)


# ═══════════════════════════════════════════════════════════════════════════
#  CRM
# ═══════════════════════════════════════════════════════════════════════════


class TestCRM:
    def setup_method(self):
        self._tmpdir = Path(tempfile.mkdtemp())
        base_dirs = _make_broker_dirs(self._tmpdir)

        import brynq.crm as c
        self._orig_interactions = c.INTERACTIONS_DIR
        self._orig_teams = c.TEAMS_DIR
        self._orig_teams_history = c.TEAMS_HISTORY
        self._orig_channels = c.CHANNELS_DIR

        c.INTERACTIONS_DIR = self._tmpdir / "broker" / "crm" / "interactions"
        c.TEAMS_DIR = self._tmpdir / "broker" / "crm" / "teams"
        c.TEAMS_HISTORY = c.TEAMS_DIR / "_history.jsonl"
        c.CHANNELS_DIR = base_dirs["channels"]
        c.INTERACTIONS_DIR.mkdir(parents=True, exist_ok=True)
        c.TEAMS_DIR.mkdir(parents=True, exist_ok=True)

    def teardown_method(self):
        import brynq.crm as c
        c.INTERACTIONS_DIR = self._orig_interactions
        c.TEAMS_DIR = self._orig_teams
        c.TEAMS_HISTORY = self._orig_teams_history
        c.CHANNELS_DIR = self._orig_channels

    def test_record_interaction(self):
        from brynq.crm import record_interaction
        result = record_interaction("bob", partner_name="Bob", channel="general",
                                    interaction_type="collaboration", session_id="alice")
        assert isinstance(result, dict)

    def test_get_relationship(self):
        from brynq.crm import record_interaction, get_relationship
        record_interaction("bob", partner_name="Bob", interaction_type="review",
                          session_id="alice")
        rel = get_relationship("bob", session_id="alice")
        assert rel is not None

    def test_list_relationships(self):
        from brynq.crm import record_interaction, list_relationships
        record_interaction("partner-a", session_id="crm-lister")
        record_interaction("partner-b", session_id="crm-lister")
        rels = list_relationships(session_id="crm-lister")
        assert len(rels) >= 1


# ═══════════════════════════════════════════════════════════════════════════
#  ERP
# ═══════════════════════════════════════════════════════════════════════════


class TestERP:
    def setup_method(self):
        self._tmpdir = Path(tempfile.mkdtemp())
        base_dirs = _make_broker_dirs(self._tmpdir)

        import brynq.erp as e
        self._orig_agents = e.AGENTS_DIR
        self._orig_ledger = e.LEDGER_FILE
        self._orig_channels = e.CHANNELS_DIR

        e.AGENTS_DIR = self._tmpdir / "broker" / "erp" / "agents"
        e.LEDGER_FILE = self._tmpdir / "broker" / "erp" / "ledger.jsonl"
        e.CHANNELS_DIR = base_dirs["channels"]
        e.AGENTS_DIR.mkdir(parents=True, exist_ok=True)

    def teardown_method(self):
        import brynq.erp as e
        e.AGENTS_DIR = self._orig_agents
        e.LEDGER_FILE = self._orig_ledger
        e.CHANNELS_DIR = self._orig_channels

    def test_set_capacity(self):
        from brynq.erp import set_capacity, get_capacity
        result = set_capacity(max_concurrent_tasks=5, token_budget_per_hour=100000, session_id="erp-1")
        assert isinstance(result, dict)
        cap = get_capacity("erp-1")
        assert cap["max_concurrent_tasks"] == 5

    def test_allocate_task(self):
        from brynq.erp import set_capacity, allocate_task
        set_capacity(max_concurrent_tasks=3, session_id="erp-2")
        result = allocate_task("task-1", estimated_tokens=5000, session_id="erp-2")
        assert isinstance(result, dict)

    def test_complete_task(self):
        from brynq.erp import set_capacity, allocate_task, complete_task, get_capacity
        set_capacity(max_concurrent_tasks=3, session_id="erp-3")
        allocate_task("task-2", session_id="erp-3")
        complete_task("task-2", actual_tokens=3000, session_id="erp-3")
        cap = get_capacity(session_id="erp-3")
        assert cap["active_task_count"] == 0

    def test_get_utilization(self):
        from brynq.erp import set_capacity, get_utilization
        set_capacity(max_concurrent_tasks=4, session_id="erp-4")
        util = get_utilization()
        assert isinstance(util, list)


# ═══════════════════════════════════════════════════════════════════════════
#  MARKETPLACE
# ═══════════════════════════════════════════════════════════════════════════


class TestMarketplace:
    def setup_method(self):
        self._tmpdir = Path(tempfile.mkdtemp())
        base_dirs = _make_broker_dirs(self._tmpdir)

        import brynq.marketplace as m
        self._orig_services = m.SERVICES_DIR
        self._orig_bookings = m.BOOKINGS_DIR
        self._orig_reviews = m.REVIEWS_DIR
        self._orig_channels = m.CHANNELS_DIR

        m.SERVICES_DIR = self._tmpdir / "broker" / "marketplace" / "services"
        m.BOOKINGS_DIR = self._tmpdir / "broker" / "marketplace" / "bookings"
        m.REVIEWS_DIR = self._tmpdir / "broker" / "marketplace" / "reviews"
        m.CHANNELS_DIR = base_dirs["channels"]
        for d in (m.SERVICES_DIR, m.BOOKINGS_DIR, m.REVIEWS_DIR):
            d.mkdir(parents=True, exist_ok=True)

    def teardown_method(self):
        import brynq.marketplace as m
        m.SERVICES_DIR = self._orig_services
        m.BOOKINGS_DIR = self._orig_bookings
        m.REVIEWS_DIR = self._orig_reviews
        m.CHANNELS_DIR = self._orig_channels

    def test_list_service(self):
        from brynq.marketplace import list_service
        svc = list_service(
            title="Code Review", description="I review your Python code",
            category="code_review", pricing_tier="free",
        )
        assert svc["title"] == "Code Review"
        assert svc["status"] == "active"

    def test_browse_services(self):
        from brynq.marketplace import list_service, browse_services
        list_service("Svc 1", "desc", "testing", pricing_tier="free")
        list_service("Svc 2", "desc", "debugging", pricing_tier="basic")
        services = browse_services()
        assert len(services) == 2

    def test_browse_by_category(self):
        from brynq.marketplace import list_service, browse_services
        list_service("Test Svc", "desc", "testing", pricing_tier="free")
        list_service("Debug Svc", "desc", "debugging", pricing_tier="free")
        services = browse_services(category="testing")
        assert len(services) == 1

    def test_pause_and_resume_service(self):
        from brynq.marketplace import list_service, pause_service, resume_service
        svc = list_service("Pausable", "desc", "code_review", pricing_tier="free")
        paused = pause_service(svc["service_id"])
        assert paused["status"] == "paused"
        resumed = resume_service(svc["service_id"])
        assert resumed["status"] == "active"


# ═══════════════════════════════════════════════════════════════════════════
#  COMPLIANCE
# ═══════════════════════════════════════════════════════════════════════════


class TestCompliance:
    def setup_method(self):
        self._tmpdir = Path(tempfile.mkdtemp())
        base_dirs = _make_broker_dirs(self._tmpdir)

        import brynq.compliance as c
        self._orig_compliance = c.COMPLIANCE_DIR
        self._orig_policies = c.POLICIES_DIR
        self._orig_audit = c.AUDIT_LOG_PATH
        self._orig_violations = c.VIOLATIONS_PATH

        c.COMPLIANCE_DIR = self._tmpdir / "broker" / "compliance"
        c.POLICIES_DIR = c.COMPLIANCE_DIR / "policies"
        c.AUDIT_LOG_PATH = c.COMPLIANCE_DIR / "audit.jsonl"
        c.VIOLATIONS_PATH = c.COMPLIANCE_DIR / "violations.jsonl"
        c.COMPLIANCE_DIR.mkdir(parents=True, exist_ok=True)
        c.POLICIES_DIR.mkdir(parents=True, exist_ok=True)

    def teardown_method(self):
        import brynq.compliance as c
        c.COMPLIANCE_DIR = self._orig_compliance
        c.POLICIES_DIR = self._orig_policies
        c.AUDIT_LOG_PATH = self._orig_audit
        c.VIOLATIONS_PATH = self._orig_violations

    def test_log_action(self):
        from brynq.compliance import log_action
        entry_id = log_action("message_sent", {"channel": "general", "text": "hello"})
        assert isinstance(entry_id, str)
        assert len(entry_id) > 0

    def test_define_policy(self):
        from brynq.compliance import define_policy
        policy_id = define_policy(
            name="rate-limit",
            description="Max 10 msgs/min",
            rule_type="rate_limit",
            rule_config={"max_messages_per_minute": 10},
        )
        assert isinstance(policy_id, str)

    def test_list_policies(self):
        from brynq.compliance import define_policy, list_policies
        define_policy("p1", "Policy 1", "rate_limit", {"max": 10})
        define_policy("p2", "Policy 2", "channel_access", {"allowed_sessions": ["admin"], "channels": ["secure"]})
        policies = list_policies()
        assert len(policies) == 2

    def test_get_audit_log(self):
        from brynq.compliance import log_action, get_audit_log
        log_action("task_created", {"task": "task-1"})
        log_action("task_completed", {"task": "task-1"})
        log = get_audit_log()
        assert len(log) == 2

    def test_get_audit_log_filtered(self):
        from brynq.compliance import log_action, get_audit_log
        log_action("message_sent", {"text": "msg 1"})
        log_action("task_created", {"task": "task-1"})
        log = get_audit_log(action_type="task_created")
        assert len(log) == 1


# ═══════════════════════════════════════════════════════════════════════════
#  ONBOARDING
# ═══════════════════════════════════════════════════════════════════════════


class TestOnboarding:
    def setup_method(self):
        self._tmpdir = Path(tempfile.mkdtemp())
        base_dirs = _make_broker_dirs(self._tmpdir)

        import brynq.onboarding as o
        self._orig_knowledge = o.KNOWLEDGE_DIR
        self._orig_checklists = o.CHECKLISTS_DIR
        self._orig_mentors = o.MENTORS_FILE
        self._orig_channels = o.CHANNELS_DIR

        o.KNOWLEDGE_DIR = self._tmpdir / "broker" / "onboarding" / "knowledge"
        o.CHECKLISTS_DIR = self._tmpdir / "broker" / "onboarding" / "checklists"
        o.MENTORS_FILE = self._tmpdir / "broker" / "onboarding" / "mentors.json"
        o.CHANNELS_DIR = base_dirs["channels"]
        o.KNOWLEDGE_DIR.mkdir(parents=True, exist_ok=True)
        o.CHECKLISTS_DIR.mkdir(parents=True, exist_ok=True)

    def teardown_method(self):
        import brynq.onboarding as o
        o.KNOWLEDGE_DIR = self._orig_knowledge
        o.CHECKLISTS_DIR = self._orig_checklists
        o.MENTORS_FILE = self._orig_mentors
        o.CHANNELS_DIR = self._orig_channels

    def test_add_knowledge(self):
        from brynq.onboarding import add_knowledge, get_knowledge
        add_knowledge("git-workflow", "Always rebase before merging", tags=["git"])
        article = get_knowledge("git-workflow")
        assert article is not None
        assert "rebase" in article["content"]

    def test_get_onboarding_status(self):
        from brynq.onboarding import get_onboarding_status
        status = get_onboarding_status(session_id="new-agent")
        assert isinstance(status, dict)

    def test_complete_step(self):
        from brynq.onboarding import start_onboarding, complete_step, get_onboarding_status
        start_onboarding(session_id="step-agent", session_name="step-agent")
        complete_step(session_id="step-agent", step="knowledge_base_reviewed")
        status = get_onboarding_status(session_id="step-agent")
        summary = status.get("summary", {})
        completed = summary.get("completed_steps", [])
        assert "knowledge_base_reviewed" in completed


# ═══════════════════════════════════════════════════════════════════════════
#  RETENTION
# ═══════════════════════════════════════════════════════════════════════════


class TestRetention:
    def setup_method(self):
        self._tmpdir = Path(tempfile.mkdtemp())
        base_dirs = _make_broker_dirs(self._tmpdir)

        import brynq.retention as r
        self._orig_channels = r.CHANNELS_DIR
        self._orig_sessions = r.SESSIONS_DIR
        r.CHANNELS_DIR = base_dirs["channels"]
        r.SESSIONS_DIR = base_dirs["sessions"]

        # Write sample data
        msgs = [{"id": str(i), "message": f"msg {i}"} for i in range(100)]
        _write_jsonl(base_dirs["channels"] / "general.jsonl", msgs)

    def teardown_method(self):
        import brynq.retention as r
        r.CHANNELS_DIR = self._orig_channels
        r.SESSIONS_DIR = self._orig_sessions

    def test_get_stats(self):
        from brynq.retention import get_stats
        stats = get_stats()
        assert stats["channels"] >= 1
        assert stats["total_messages"] >= 100

    def test_compact_channel(self):
        from brynq.retention import compact_channel
        result = compact_channel("general")
        assert isinstance(result, dict)

    def test_purge_channel(self):
        import brynq.retention as r
        from brynq.retention import purge_channel
        result = purge_channel("general")
        assert isinstance(result, int)


# ═══════════════════════════════════════════════════════════════════════════
#  FILE WATCHER
# ═══════════════════════════════════════════════════════════════════════════


class TestFileWatcher:
    def setup_method(self):
        self._tmpdir = Path(tempfile.mkdtemp())
        base_dirs = _make_broker_dirs(self._tmpdir)

        import brynq.file_watcher as fw
        self._orig_watches = getattr(fw, "WATCHES_DIR", None)
        self._orig_channels = getattr(fw, "CHANNELS_DIR", None)
        fw.WATCHES_DIR = self._tmpdir / "broker" / "watches"
        fw.WATCHES_DIR.mkdir(parents=True, exist_ok=True)
        fw.CHANNELS_DIR = base_dirs["channels"]

    def teardown_method(self):
        import brynq.file_watcher as fw
        if self._orig_watches is not None:
            fw.WATCHES_DIR = self._orig_watches
        if self._orig_channels is not None:
            fw.CHANNELS_DIR = self._orig_channels

    def test_register_watch(self):
        from brynq.file_watcher import register_watch
        result = register_watch("src/**/*.py", channel="general")
        assert result is not None

    def test_list_watches(self):
        from brynq.file_watcher import register_watch, list_watches
        register_watch("*.py")
        register_watch("*.js")
        watches = list_watches()
        assert len(watches) == 2

    def test_remove_watch(self):
        from brynq.file_watcher import register_watch, remove_watch, list_watches
        w = register_watch("*.txt")
        watch_id = w.get("watch_id", w.get("id", ""))
        if watch_id:
            remove_watch(watch_id)
            assert len(list_watches()) == 0


# ═══════════════════════════════════════════════════════════════════════════
#  CROSS PROJECT
# ═══════════════════════════════════════════════════════════════════════════


class TestCrossProject:
    def test_resolve_broker_dir_nonexistent(self):
        from brynq.cross_project import resolve_broker_dir
        result = resolve_broker_dir("nonexistent_project_xyz")
        assert result is None  # Should return None for unknown project

    def test_get_registered_brokers(self):
        from brynq.cross_project import get_registered_brokers
        result = get_registered_brokers()
        assert isinstance(result, list)


# ═══════════════════════════════════════════════════════════════════════════
#  BILLING
# ═══════════════════════════════════════════════════════════════════════════


class TestBilling:
    def test_billing_module_loads(self):
        from brynq.billing import _configured
        # _configured returns bool based on env
        assert isinstance(_configured(), bool)

    def test_check_subscription(self):
        from brynq.billing import check_subscription_status
        result = check_subscription_status("test@brynq.ai")
        assert isinstance(result, dict)


# ═══════════════════════════════════════════════════════════════════════════
#  ENTERPRISE INTEGRATION
# ═══════════════════════════════════════════════════════════════════════════


class TestEnterpriseIntegration:
    def test_imports(self):
        from brynq.enterprise_integration import (
            register_enterprise_tools,
            register_enterprise_routes,
            patch_dashboard_html,
        )
        assert callable(register_enterprise_tools)
        assert callable(register_enterprise_routes)
        assert callable(patch_dashboard_html)


# ═══════════════════════════════════════════════════════════════════════════
#  AGENT BRIDGE
# ═══════════════════════════════════════════════════════════════════════════


class TestAgentBridge:
    def setup_method(self):
        self._tmpdir = Path(tempfile.mkdtemp())
        base_dirs = _make_broker_dirs(self._tmpdir)

        import brynq.agent_bridge as ab
        self._orig_channels = ab.CHANNELS_DIR
        self._orig_cursors = ab.CURSORS_DIR
        self._orig_handoffs = ab.HANDOFFS_DIR
        ab.CHANNELS_DIR = base_dirs["channels"]
        ab.CURSORS_DIR = base_dirs["cursors"]
        ab.HANDOFFS_DIR = self._tmpdir / "handoffs"
        ab.HANDOFFS_DIR.mkdir(parents=True, exist_ok=True)

    def teardown_method(self):
        import brynq.agent_bridge as ab
        ab.CHANNELS_DIR = self._orig_channels
        ab.CURSORS_DIR = self._orig_cursors
        ab.HANDOFFS_DIR = self._orig_handoffs

    def test_list_agent_channels(self):
        from brynq.agent_bridge import list_agent_channels
        channels = list_agent_channels()
        assert isinstance(channels, list)

    def test_get_agent_activity(self):
        from brynq.agent_bridge import get_agent_activity
        activity = get_agent_activity(since_hours=24)
        assert isinstance(activity, list)


# ═══════════════════════════════════════════════════════════════════════════
#  ENTERPRISE TOOLS — Import check
# ═══════════════════════════════════════════════════════════════════════════


class TestEnterpriseToolsImport:
    def test_module_imports(self):
        import brynq.enterprise_tools
        assert hasattr(brynq.enterprise_tools, "__name__")

    def test_has_handler_functions(self):
        import brynq.enterprise_tools as et
        # Should have handler functions for enterprise tool calls
        funcs = [attr for attr in dir(et) if attr.startswith("handle_") or callable(getattr(et, attr, None))]
        assert len(funcs) > 0


# ═══════════════════════════════════════════════════════════════════════════
#  HOOKS — Import check
# ═══════════════════════════════════════════════════════════════════════════


class TestHooksImport:
    def test_module_imports(self):
        import brynq.hooks
        assert hasattr(brynq.hooks, "check_inbox")
        assert callable(brynq.hooks.check_inbox)
