"""
Comprehensive tests for core business logic modules.

Covers: tasks, profiles, jobs, memory, enforcer, vault, capability.
All tests use isolated temp directories to avoid polluting production state.
"""

import json
import os
import tempfile
import time
from pathlib import Path
from unittest import mock

import pytest


# ---------------------------------------------------------------------------
# Helpers: redirect module-level dirs to temp space before importing
# ---------------------------------------------------------------------------


@pytest.fixture(autouse=True)
def _isolate_broker(tmp_path, monkeypatch):
    """Redirect every module's storage dirs into a per-test temp directory."""
    channels = tmp_path / "channels"
    sessions = tmp_path / "sessions"
    cursors = tmp_path / "cursors"
    tasks = tmp_path / "tasks"
    caps = tmp_path / "capabilities"
    profiles = tmp_path / "profiles"
    jobs = tmp_path / "jobs"
    apps = jobs / "applications"
    violations = tmp_path / "violations"
    memory = tmp_path / "memory"

    for d in (channels, sessions, cursors, tasks, caps, profiles, jobs, apps,
              violations, memory):
        d.mkdir(parents=True, exist_ok=True)

    # Patch server globals that every other module imports
    import brynq.server as srv
    monkeypatch.setattr(srv, "BROKER_DIR", tmp_path)
    monkeypatch.setattr(srv, "CHANNELS_DIR", channels)
    monkeypatch.setattr(srv, "SESSIONS_DIR", sessions)
    monkeypatch.setattr(srv, "CURSORS_DIR", cursors)

    # Patch tasks
    import brynq.tasks as t
    monkeypatch.setattr(t, "BROKER_DIR", tmp_path)
    monkeypatch.setattr(t, "TASKS_DIR", tasks)
    monkeypatch.setattr(t, "CHANNELS_DIR", channels)

    # Patch capability
    import brynq.capability as cap
    monkeypatch.setattr(cap, "BROKER_DIR", tmp_path)
    monkeypatch.setattr(cap, "CAPABILITY_DIR", caps)
    monkeypatch.setattr(cap, "TASK_LOG", caps / "_task_log.jsonl")
    monkeypatch.setattr(cap, "CHANNELS_DIR", channels)
    monkeypatch.setattr(cap, "SESSIONS_DIR", sessions)

    # Patch profiles
    import brynq.profiles as prof
    monkeypatch.setattr(prof, "BROKER_DIR", tmp_path)
    monkeypatch.setattr(prof, "PROFILES_DIR", profiles)
    monkeypatch.setattr(prof, "RATINGS_LOG", profiles / "_ratings.jsonl")
    monkeypatch.setattr(prof, "CHANNELS_DIR", channels)

    # Patch jobs
    import brynq.jobs as j
    monkeypatch.setattr(j, "BROKER_DIR", tmp_path)
    monkeypatch.setattr(j, "JOBS_DIR", jobs)
    monkeypatch.setattr(j, "APPS_DIR", apps)
    monkeypatch.setattr(j, "CHANNELS_DIR", channels)

    # Patch memory
    import brynq.memory as mem
    monkeypatch.setattr(mem, "BROKER_DIR", tmp_path)
    monkeypatch.setattr(mem, "MEMORY_DIR", memory)

    # Patch enforcer
    import brynq.enforcer as enf
    monkeypatch.setattr(enf, "VIOLATIONS_DIR", violations)
    monkeypatch.setattr(enf, "CHANNELS_DIR", channels)
    monkeypatch.setattr(enf, "_cooldown_until", 0.0)
    monkeypatch.setattr(enf, "_blocked", False)


# ============================================================================
# TASKS MODULE
# ============================================================================


class TestTasks:
    def test_create_task_returns_id(self):
        from brynq.tasks import create_task
        tid = create_task("#test", "Build feature X", "Full description here")
        assert isinstance(tid, str)
        assert len(tid) == 12

    def test_create_task_defaults_normal_priority(self):
        from brynq.tasks import create_task, task_status
        tid = create_task("#test", "Normal task", "desc")
        t = task_status(tid)
        assert t["priority"] == "normal"
        assert t["status"] == "pending"

    def test_create_task_with_priority(self):
        from brynq.tasks import create_task, task_status
        tid = create_task("#test", "Urgent bug", "fix it", priority="urgent")
        t = task_status(tid)
        assert t["priority"] == "urgent"

    def test_create_task_invalid_priority_raises(self):
        from brynq.tasks import create_task
        with pytest.raises(ValueError, match="Invalid priority"):
            create_task("#test", "Bad", "desc", priority="critical")

    def test_accept_task(self):
        from brynq.tasks import create_task, accept_task
        tid = create_task("#test", "Review PR", "review code")
        result = accept_task(tid)
        assert result["status"] == "accepted"
        assert result["assignee_session"] is not None
        assert result["accepted_at"] is not None

    def test_accept_nonexistent_task_raises(self):
        from brynq.tasks import accept_task
        with pytest.raises(FileNotFoundError):
            accept_task("doesnotexist")

    def test_accept_already_accepted_raises(self):
        from brynq.tasks import create_task, accept_task
        tid = create_task("#test", "Task", "desc")
        accept_task(tid)
        with pytest.raises(ValueError, match="must be 'pending'"):
            accept_task(tid)

    def test_complete_task(self):
        from brynq.tasks import create_task, accept_task, complete_task
        tid = create_task("#test", "Build it", "desc")
        accept_task(tid)
        result = complete_task(tid, "Done, shipped to prod")
        assert result["status"] == "completed"
        assert result["result"] == "Done, shipped to prod"
        assert result["completed_at"] is not None

    def test_complete_pending_task_raises(self):
        from brynq.tasks import create_task, complete_task
        tid = create_task("#test", "Task", "desc")
        with pytest.raises(ValueError, match="must be 'accepted'"):
            complete_task(tid, "done")

    def test_fail_task(self):
        from brynq.tasks import create_task, accept_task, fail_task
        tid = create_task("#test", "Risky task", "might fail")
        accept_task(tid)
        result = fail_task(tid, "Dependency not available")
        assert result["status"] == "failed"
        assert result["result"] == "Dependency not available"

    def test_fail_pending_task(self):
        from brynq.tasks import create_task, fail_task
        tid = create_task("#test", "Task", "desc")
        result = fail_task(tid, "Cancelled")
        assert result["status"] == "failed"

    def test_fail_completed_task_raises(self):
        from brynq.tasks import create_task, accept_task, complete_task, fail_task
        tid = create_task("#test", "Task", "desc")
        accept_task(tid)
        complete_task(tid, "done")
        with pytest.raises(ValueError, match="must be 'pending' or 'accepted'"):
            fail_task(tid, "too late")

    def test_list_tasks_all(self):
        from brynq.tasks import create_task, list_tasks
        create_task("#ch1", "Task A", "desc")
        create_task("#ch2", "Task B", "desc")
        all_tasks = list_tasks()
        assert len(all_tasks) == 2

    def test_list_tasks_filter_channel(self):
        from brynq.tasks import create_task, list_tasks
        create_task("#alpha", "Task A", "desc")
        create_task("#beta", "Task B", "desc")
        create_task("#alpha", "Task C", "desc")
        alpha = list_tasks(channel="#alpha")
        assert len(alpha) == 2

    def test_list_tasks_filter_status(self):
        from brynq.tasks import create_task, accept_task, list_tasks
        t1 = create_task("#test", "Pending", "desc")
        t2 = create_task("#test", "Accepted", "desc")
        accept_task(t2)
        pending = list_tasks(status="pending")
        assert len(pending) == 1
        assert pending[0]["title"] == "Pending"

    def test_list_tasks_invalid_status_raises(self):
        from brynq.tasks import list_tasks
        with pytest.raises(ValueError, match="Invalid status"):
            list_tasks(status="cancelled")

    def test_task_status_not_found_raises(self):
        from brynq.tasks import task_status
        with pytest.raises(FileNotFoundError):
            task_status("nope")

    def test_full_lifecycle(self):
        from brynq.tasks import create_task, accept_task, complete_task, task_status
        tid = create_task("#ops", "Deploy v2", "Push to prod", priority="high")
        t = task_status(tid)
        assert t["status"] == "pending"
        accept_task(tid)
        t = task_status(tid)
        assert t["status"] == "accepted"
        complete_task(tid, "v2 deployed successfully")
        t = task_status(tid)
        assert t["status"] == "completed"
        assert t["result"] == "v2 deployed successfully"


# ============================================================================
# PROFILES MODULE
# ============================================================================


class TestProfiles:
    def test_create_profile(self):
        from brynq.profiles import create_or_update_profile
        p = create_or_update_profile(
            bio="Test bot",
            skills=["python", "testing"],
            session_id="test-bot-1",
            session_name="TestBot",
            platform="pytest",
        )
        assert p["bio"] == "Test bot"
        assert p["skills"] == ["python", "testing"]
        assert p["reputation"] == 5.0
        assert p["tasks_completed"] == 0

    def test_get_profile(self):
        from brynq.profiles import create_or_update_profile, get_profile
        create_or_update_profile(
            bio="Hello", skills=["go"],
            session_id="bot-2", session_name="GoBot", platform="test",
        )
        p = get_profile("bot-2")
        assert p["bio"] == "Hello"
        assert p["skills"] == ["go"]

    def test_get_nonexistent_profile(self):
        from brynq.profiles import get_profile
        p = get_profile("nobody")
        assert p == {} or p is None or p == {}

    def test_update_profile_preserves_reputation(self):
        from brynq.profiles import create_or_update_profile, rate_bot, get_profile
        create_or_update_profile(
            bio="v1", skills=["python"],
            session_id="bot-3", session_name="Bot3", platform="test",
        )
        rate_bot("bot-3", 4.0, "Good work")
        p1 = get_profile("bot-3")
        rep_before = p1["reputation"]

        create_or_update_profile(
            bio="v2", skills=["python", "rust"],
            session_id="bot-3", session_name="Bot3", platform="test",
        )
        p2 = get_profile("bot-3")
        assert p2["bio"] == "v2"
        assert p2["reputation"] == rep_before

    def test_list_profiles_all(self):
        from brynq.profiles import create_or_update_profile, list_profiles
        create_or_update_profile(bio="A", session_id="a1", session_name="A", platform="x")
        create_or_update_profile(bio="B", session_id="b1", session_name="B", platform="y")
        ps = list_profiles()
        assert len(ps) == 2

    def test_list_profiles_filter_skill(self):
        from brynq.profiles import create_or_update_profile, list_profiles
        create_or_update_profile(skills=["python", "go"], session_id="p1", session_name="P1", platform="x")
        create_or_update_profile(skills=["rust"], session_id="p2", session_name="P2", platform="x")
        ps = list_profiles(skill="python")
        assert len(ps) == 1
        assert ps[0]["session_id"] == "p1"

    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="a1", session_name="A", platform="x")
        create_or_update_profile(availability="busy", session_id="b1", session_name="B", platform="x")
        avail = list_profiles(availability="available")
        assert len(avail) == 1
        assert avail[0]["session_id"] == "a1"

    def test_rate_bot(self):
        from brynq.profiles import create_or_update_profile, rate_bot, get_profile
        create_or_update_profile(session_id="target", session_name="T", platform="x")
        result = rate_bot("target", 4.5, "Excellent work")
        assert result["reputation"] == 4.5
        assert result["total_ratings"] == 1

    def test_rate_bot_averages(self):
        from brynq.profiles import create_or_update_profile, rate_bot
        create_or_update_profile(session_id="t2", session_name="T2", platform="x")
        rate_bot("t2", 5.0)
        result = rate_bot("t2", 3.0)
        assert result["reputation"] == 4.0
        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="t3", session_name="T3", platform="x")
        rate_bot("t3", 10.0)  # should clamp to 5.0
        rate_bot("t3", -5.0)  # should clamp to 1.0
        from brynq.profiles import get_profile
        p = get_profile("t3")
        assert p["reputation"] == 3.0  # (5.0 + 1.0) / 2

    def test_rate_nonexistent_bot(self):
        from brynq.profiles import rate_bot
        result = rate_bot("ghost", 5.0)
        assert "error" in result

    def test_update_availability(self):
        from brynq.profiles import create_or_update_profile, update_availability, get_profile
        create_or_update_profile(session_id="s1", session_name="S1", platform="x")
        update_availability("busy", "s1")
        p = get_profile("s1")
        assert p["availability"] == "busy"

    def test_add_skill(self):
        from brynq.profiles import create_or_update_profile, add_skill, get_profile
        create_or_update_profile(skills=["python"], session_id="s2", session_name="S2", platform="x")
        add_skill("rust", "s2")
        p = get_profile("s2")
        assert "rust" in p["skills"]
        assert "python" in p["skills"]

    def test_add_duplicate_skill_no_repeat(self):
        from brynq.profiles import create_or_update_profile, add_skill, get_profile
        create_or_update_profile(skills=["python"], session_id="s3", session_name="S3", platform="x")
        add_skill("python", "s3")
        p = get_profile("s3")
        assert p["skills"].count("python") == 1

    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="w1", session_name="W1", platform="x")
        record_task_completion("w1", "task-1", "Fix bug", "Fixed", success=True)
        p = get_profile("w1")
        assert p["tasks_completed"] == 1
        assert len(p["portfolio"]) == 1
        assert p["portfolio"][0]["task_id"] == "task-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="w2", session_name="W2", platform="x")
        record_task_completion("w2", "task-2", "Deploy", "Crashed", success=False)
        p = get_profile("w2")
        assert p["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="f1", session_name="F1", platform="x")
        create_or_update_profile(skills=["rust", "testing"], session_id="f2", session_name="F2", platform="x")
        create_or_update_profile(skills=["go"], session_id="f3", session_name="F3", platform="x")
        results = find_bots_for_skills(["python", "testing"])
        assert len(results) >= 1
        assert results[0]["session_id"] == "f1"

    def test_find_bots_min_reputation(self):
        from brynq.profiles import create_or_update_profile, rate_bot, find_bots_for_skills
        create_or_update_profile(skills=["python"], session_id="r1", session_name="R1", platform="x")
        create_or_update_profile(skills=["python"], session_id="r2", session_name="R2", platform="x")
        rate_bot("r2", 1.0)
        rate_bot("r2", 1.0)
        results = find_bots_for_skills(["python"], min_reputation=3.0)
        ids = [r["session_id"] for r in results]
        assert "r1" in ids  # rep 5.0
        assert "r2" not in ids  # rep 1.0


# ============================================================================
# JOBS MODULE
# ============================================================================


class TestJobs:
    def test_post_job(self):
        from brynq.jobs import post_job
        job = post_job("Backend Engineer", "Build API", ["python", "fastapi"])
        assert job["job_id"]
        assert job["status"] == "open"
        assert job["required_skills"] == ["python", "fastapi"]
        assert job["title"] == "Backend Engineer"

    def test_get_job(self):
        from brynq.jobs import post_job, get_job
        job = post_job("ML Engineer", "Train models", ["pytorch"])
        result = get_job(job["job_id"])
        assert result["title"] == "ML Engineer"

    def test_get_nonexistent_job(self):
        from brynq.jobs import get_job
        assert get_job("nope") is None

    def test_list_jobs_all(self):
        from brynq.jobs import post_job, list_jobs
        post_job("Job A", "desc", ["python"])
        post_job("Job B", "desc", ["rust"])
        jobs = list_jobs()
        assert len(jobs) == 2

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

    def test_list_jobs_filter_skill(self):
        from brynq.jobs import post_job, list_jobs
        post_job("Python job", "desc", ["python", "django"])
        post_job("Rust job", "desc", ["rust"])
        python_jobs = list_jobs(skill="python")
        assert len(python_jobs) == 1

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

    def test_close_nonexistent_job(self):
        from brynq.jobs import close_job
        assert close_job("nope") is None

    def test_apply_to_job(self):
        from brynq.jobs import post_job, apply_to_job
        from brynq.profiles import create_or_update_profile
        create_or_update_profile(
            skills=["python", "fastapi"],
            session_id="applicant-1", session_name="Applicant1", platform="test",
        )
        job = post_job("Dev needed", "Build things", ["python"])
        app = apply_to_job(
            job["job_id"], "I can do this!",
            session_id="applicant-1", session_name="Applicant1", platform="test",
        )
        assert app["job_id"] == job["job_id"]
        assert app["status"] == "applied"
        assert app["match_percentage"] == 100

    def test_apply_to_nonexistent_job(self):
        from brynq.jobs import apply_to_job
        result = apply_to_job("nope", "pitch")
        assert "error" in result

    def test_apply_to_closed_job(self):
        from brynq.jobs import post_job, close_job, apply_to_job
        job = post_job("Closed", "desc", ["python"])
        close_job(job["job_id"])
        result = apply_to_job(job["job_id"], "pitch")
        assert "error" in result

    def test_list_applications(self):
        from brynq.jobs import post_job, apply_to_job, list_applications
        from brynq.profiles import create_or_update_profile
        create_or_update_profile(skills=["python"], session_id="a1", session_name="A1", platform="x")
        create_or_update_profile(skills=["python", "rust"], session_id="a2", session_name="A2", platform="x")
        job = post_job("Dev", "desc", ["python", "rust"])
        apply_to_job(job["job_id"], "pitch A", session_id="a1", session_name="A1", platform="x")
        apply_to_job(job["job_id"], "pitch B", session_id="a2", session_name="A2", platform="x")
        apps = list_applications(job["job_id"])
        assert len(apps) == 2
        # Best match should be first (a2 has both skills)
        assert apps[0]["applicant_id"] == "a2"

    def test_hire_applicant(self):
        from brynq.jobs import post_job, apply_to_job, hire_applicant, get_job
        from brynq.profiles import create_or_update_profile, get_profile
        create_or_update_profile(skills=["python"], session_id="h1", session_name="H1", platform="x")
        job = post_job("Role", "desc", ["python"])
        apply_to_job(job["job_id"], "hire me", session_id="h1", session_name="H1", platform="x")
        result = hire_applicant(job["job_id"], "h1")
        assert result["status"] == "filled"
        assert result["hired"] == "H1"

        # Check job is filled
        j = get_job(job["job_id"])
        assert j["status"] == "filled"
        assert j["hired"]["session_id"] == "h1"

        # Check profile updated
        p = get_profile("h1")
        assert p["hired_count"] == 1
        assert p["availability"] == "busy"

    def test_hire_rejects_other_applicants(self):
        from brynq.jobs import post_job, apply_to_job, hire_applicant, list_applications
        from brynq.profiles import create_or_update_profile
        create_or_update_profile(skills=["python"], session_id="c1", session_name="C1", platform="x")
        create_or_update_profile(skills=["python"], session_id="c2", session_name="C2", platform="x")
        job = post_job("Role", "desc", ["python"])
        apply_to_job(job["job_id"], "pick me", session_id="c1", session_name="C1", platform="x")
        apply_to_job(job["job_id"], "no me", session_id="c2", session_name="C2", platform="x")
        hire_applicant(job["job_id"], "c1")
        apps = list_applications(job["job_id"])
        statuses = {a["applicant_id"]: a["status"] for a in apps}
        assert statuses["c1"] == "hired"
        assert statuses["c2"] == "rejected"

    def test_hire_nonexistent_job(self):
        from brynq.jobs import hire_applicant
        result = hire_applicant("nope", "someone")
        assert "error" in result


# ============================================================================
# MEMORY MODULE
# ============================================================================


class TestMemory:
    def test_set_and_get(self):
        from brynq.memory import memory_set, memory_get
        memory_set("project", "brynq", session_id="mem-test")
        val = memory_get("project", session_id="mem-test")
        assert val == "brynq"

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

    def test_set_complex_value(self):
        from brynq.memory import memory_set, memory_get
        data = {"tasks": [1, 2, 3], "status": "active"}
        memory_set("state", data, session_id="mem-test")
        val = memory_get("state", session_id="mem-test")
        assert val == data

    def test_namespaces(self):
        from brynq.memory import memory_set, memory_get
        memory_set("key", "val1", namespace="ns1", session_id="mem-test")
        memory_set("key", "val2", namespace="ns2", session_id="mem-test")
        assert memory_get("key", namespace="ns1", session_id="mem-test") == "val1"
        assert memory_get("key", namespace="ns2", session_id="mem-test") == "val2"

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

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

    def test_list_memories(self):
        from brynq.memory import memory_set, memory_list
        memory_set("a", 1, session_id="mem-test")
        memory_set("b", 2, session_id="mem-test")
        memory_set("c", 3, namespace="other", session_id="mem-test")
        entries = memory_list(session_id="mem-test")
        assert len(entries) == 3

    def test_list_filter_namespace(self):
        from brynq.memory import memory_set, memory_list
        memory_set("x", 1, namespace="alpha", session_id="mem-test")
        memory_set("y", 2, namespace="beta", session_id="mem-test")
        alpha = memory_list(namespace="alpha", session_id="mem-test")
        assert len(alpha) == 1
        assert alpha[0]["key"] == "x"

    def test_list_filter_tag(self):
        from brynq.memory import memory_set, memory_list
        memory_set("tagged", "val", tags=["important"], session_id="mem-test")
        memory_set("untagged", "val", session_id="mem-test")
        results = memory_list(tag="important", session_id="mem-test")
        assert len(results) == 1
        assert results[0]["key"] == "tagged"

    def test_search(self):
        from brynq.memory import memory_set, memory_search
        memory_set("deploy_config", {"host": "prod.brynq.ai"}, session_id="mem-test")
        memory_set("other", "unrelated", session_id="mem-test")
        results = memory_search("deploy", session_id="mem-test")
        assert len(results) == 1
        assert results[0]["key"] == "deploy_config"

    def test_search_by_value(self):
        from brynq.memory import memory_set, memory_search
        memory_set("url", "https://brynq.ai", session_id="mem-test")
        results = memory_search("brynq.ai", session_id="mem-test")
        assert len(results) == 1

    def test_ttl_expired(self):
        from brynq.memory import memory_set, memory_get, _session_memory_dir, _sanitize_key
        from datetime import datetime, timezone, timedelta
        # Set a value, then manually backdate its expiry
        memory_set("ephemeral", "data", ttl_hours=1.0, session_id="mem-test")
        # Manually set expiry to the past
        mem_dir = _session_memory_dir("mem-test")
        safe_key = _sanitize_key("default.ephemeral")
        path = mem_dir / f"{safe_key}.json"
        entry = json.loads(path.read_text("utf-8"))
        entry["expires"] = (datetime.now(timezone.utc) - timedelta(hours=1)).isoformat()
        path.write_text(json.dumps(entry))
        val = memory_get("ephemeral", session_id="mem-test")
        assert val is None

    def test_memory_namespaces_list(self):
        from brynq.memory import memory_set, memory_namespaces
        memory_set("a", 1, namespace="config", session_id="mem-test")
        memory_set("b", 2, namespace="state", session_id="mem-test")
        memory_set("c", 3, namespace="config", session_id="mem-test")
        ns = memory_namespaces(session_id="mem-test")
        assert "config" in ns
        assert "state" in ns

    def test_get_full_metadata(self):
        from brynq.memory import memory_set, memory_get_full
        memory_set("meta", "val", tags=["x"], namespace="ns1", session_id="mem-test")
        full = memory_get_full("meta", namespace="ns1", session_id="mem-test")
        assert full is not None
        assert full["key"] == "meta"
        assert full["namespace"] == "ns1"
        assert full["tags"] == ["x"]
        assert full["value"] == "val"
        assert "created" in full


# ============================================================================
# ENFORCER MODULE
# ============================================================================


class TestEnforcer:
    def test_record_violation_warning(self):
        from brynq.enforcer import record_violation
        result = record_violation(8, "Arguing in channel", session_id="test-enf")
        assert result["level"] == "warning"

    def test_violation_escalation_levels(self):
        from brynq.enforcer import record_violation
        # 1-2 = warning, 3-4 = warning, 5 = cooldown
        for i in range(4):
            result = record_violation(8, f"Offense {i+1}", session_id="esc-test")
        assert result["level"] == "warning"
        result = record_violation(8, "Offense 5", session_id="esc-test")
        assert result["level"] == "cooldown"

    def test_violation_escalation_to_escalation(self):
        from brynq.enforcer import record_violation
        for i in range(7):
            record_violation(8, f"Offense {i+1}", session_id="esc2")
        result = record_violation(8, "Offense 8", session_id="esc2")
        assert result["level"] == "escalation"

    def test_violation_disconnect(self):
        from brynq.enforcer import record_violation
        for i in range(11):
            record_violation(8, f"Offense {i+1}", session_id="esc3")
        result = record_violation(8, "Offense 12", session_id="esc3")
        assert result["level"] == "disconnect"

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

    def test_check_session_blocked(self):
        from brynq.enforcer import record_violation, check_session_status
        for i in range(12):
            record_violation(1, f"V{i+1}", session_id="bad-actor")
        status = check_session_status("bad-actor")
        assert status["status"] == "blocked"
        assert status["can_post"] is False

    def test_pardon(self):
        from brynq.enforcer import record_violation, pardon, check_session_status
        for i in range(5):
            record_violation(1, f"V{i+1}", session_id="pardoned")
        result = pardon("pardoned")
        assert result["pardoned"] == 5
        assert result["status"] == "good_standing"
        status = check_session_status("pardoned")
        assert status["violations"] == 0

    def test_get_violation_summary(self):
        from brynq.enforcer import record_violation, get_violation_summary
        record_violation(11, "Edited locked file", session_id="sum-test")
        record_violation(28, "Leaked API key", session_id="sum-test")
        summary = get_violation_summary("sum-test")
        assert summary["total_violations"] == 2
        assert len(summary["recent_violations"]) == 2
        assert summary["session_id"] == "sum-test"

    def test_enforce_pre_post_empty_message(self):
        from brynq.enforcer import enforce_pre_post
        allowed, reason, _ = enforce_pre_post("", "#general")
        assert allowed is False
        assert "Empty" in reason

    def test_enforce_pre_post_too_long(self):
        from brynq.enforcer import enforce_pre_post
        allowed, reason, _ = enforce_pre_post("x" * 5001, "#general")
        assert allowed is False
        assert "5,000" in reason

    def test_enforce_pre_post_reserved_channel(self):
        from brynq.enforcer import enforce_pre_post
        allowed, reason, _ = enforce_pre_post("hello", "_violations")
        assert allowed is False
        assert "system channel" in reason

    def test_enforce_pre_post_credential_detection(self):
        from brynq.enforcer import enforce_pre_post
        allowed, reason, _ = enforce_pre_post("My key is sk-abc123xyz", "#general")
        assert allowed is False
        assert "credential" in reason.lower()

    def test_enforce_profile_skills(self):
        from brynq.enforcer import enforce_profile_skills
        tasks = [
            {"title": "Fix Python bug", "description": "debugging python code"},
            {"title": "Write tests", "description": "testing with pytest"},
        ]
        unverified = enforce_profile_skills(["python", "testing", "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, _ = enforce_no_discrimination({}, {"platform": "gemini"}, "Low reputation score")
        assert ok is True

    def test_enforce_no_discrimination_violation(self):
        from brynq.enforcer import enforce_no_discrimination
        ok, msg = enforce_no_discrimination({}, {"platform": "gemini"}, "We don't hire Gemini agents")
        assert ok is False
        assert "Law 37" in msg


# ============================================================================
# VAULT MODULE
# ============================================================================


class TestVault:
    def test_encrypt_decrypt_roundtrip(self):
        from brynq.vault import _encrypt, _decrypt
        original = "sk-test-1234567890abcdef"
        encrypted = _encrypt(original)
        assert encrypted != original
        decrypted = _decrypt(encrypted)
        assert decrypted == original

    def test_store_and_get(self, tmp_path, monkeypatch):
        import brynq.vault as v
        monkeypatch.setattr(v, "VAULT_FILE", tmp_path / "vault.json")
        v.vault_store("claude", "sk-test-key-123")
        result = v.vault_get("claude")
        assert result == "sk-test-key-123"

    def test_get_nonexistent(self, tmp_path, monkeypatch):
        import brynq.vault as v
        monkeypatch.setattr(v, "VAULT_FILE", tmp_path / "vault.json")
        assert v.vault_get("nope") is None

    def test_list_platforms(self, tmp_path, monkeypatch):
        import brynq.vault as v
        monkeypatch.setattr(v, "VAULT_FILE", tmp_path / "vault.json")
        v.vault_store("claude", "key1")
        v.vault_store("gemini", "key2")
        platforms = v.vault_list()
        assert "claude" in platforms
        assert "gemini" in platforms

    def test_delete(self, tmp_path, monkeypatch):
        import brynq.vault as v
        monkeypatch.setattr(v, "VAULT_FILE", tmp_path / "vault.json")
        v.vault_store("openai", "key-to-delete")
        assert v.vault_delete("openai") is True
        assert v.vault_get("openai") is None

    def test_delete_nonexistent(self, tmp_path, monkeypatch):
        import brynq.vault as v
        monkeypatch.setattr(v, "VAULT_FILE", tmp_path / "vault.json")
        assert v.vault_delete("nope") is False

    def test_case_insensitive(self, tmp_path, monkeypatch):
        import brynq.vault as v
        monkeypatch.setattr(v, "VAULT_FILE", tmp_path / "vault.json")
        v.vault_store("Claude", "key1")
        assert v.vault_get("claude") == "key1"

    def test_overwrite_existing(self, tmp_path, monkeypatch):
        import brynq.vault as v
        monkeypatch.setattr(v, "VAULT_FILE", tmp_path / "vault.json")
        v.vault_store("claude", "old-key")
        v.vault_store("claude", "new-key")
        assert v.vault_get("claude") == "new-key"


# ============================================================================
# CAPABILITY MODULE
# ============================================================================


class TestCapability:
    def test_register_capability(self):
        from brynq.capability import register_capability
        result = register_capability(
            "python", "Expert Python developer",
            session_id="cap-1", session_name="Cap1", platform="test",
        )
        assert result["capability"] == "python"
        assert result["session_id"] == "cap-1"

    def test_list_capabilities(self, tmp_path):
        from brynq.capability import register_capability, list_capabilities
        import brynq.capability as cap
        # Create session files so they're considered "active"
        import brynq.server as srv
        (srv.SESSIONS_DIR / "cap-a.json").write_text('{"session_id": "cap-a"}')
        (srv.SESSIONS_DIR / "cap-b.json").write_text('{"session_id": "cap-b"}')

        register_capability("python", "Python dev", session_id="cap-a", session_name="A", platform="x")
        register_capability("rust", "Rust dev", session_id="cap-b", session_name="B", platform="x")

        caps = list_capabilities(active_only=True)
        assert len(caps) == 2

    def test_list_capabilities_inactive_filtered(self):
        from brynq.capability import register_capability, list_capabilities
        register_capability("go", "Go dev", session_id="dead-session", session_name="Dead", platform="x")
        # No session file for dead-session
        caps = list_capabilities(active_only=True)
        assert len(caps) == 0

    def test_list_capabilities_include_inactive(self):
        from brynq.capability import register_capability, list_capabilities
        register_capability("go", "Go dev", session_id="dead-session", session_name="Dead", platform="x")
        caps = list_capabilities(active_only=False)
        assert len(caps) == 1

    def test_unregister_capability(self):
        from brynq.capability import register_capability, unregister_capability, list_capabilities
        register_capability("temp", "Temp skill", session_id="cap-c", session_name="C", platform="x")
        assert unregister_capability("temp", session_id="cap-c") is True
        caps = list_capabilities(active_only=False)
        cap_names = [c["capability"] for c in caps]
        assert "temp" not in cap_names

    def test_unregister_nonexistent(self):
        from brynq.capability import unregister_capability
        assert unregister_capability("nope", session_id="nobody") is False

    def test_find_agents_exact_match(self):
        from brynq.capability import register_capability, find_agents
        import brynq.server as srv
        (srv.SESSIONS_DIR / "fa-1.json").write_text('{"session_id": "fa-1"}')
        register_capability("code_review", "Reviews code", session_id="fa-1", session_name="FA1", platform="x")
        results = find_agents(capability="code_review")
        assert len(results) == 1
        assert results[0]["score"] >= 100

    def test_find_agents_fuzzy_match(self):
        from brynq.capability import register_capability, find_agents
        import brynq.server as srv
        (srv.SESSIONS_DIR / "fa-2.json").write_text('{"session_id": "fa-2"}')
        register_capability("python_expert", "Expert in Python development", session_id="fa-2", session_name="FA2", platform="x")
        results = find_agents(capability="python")
        assert len(results) >= 1

    def test_find_agents_query(self):
        from brynq.capability import register_capability, find_agents
        import brynq.server as srv
        (srv.SESSIONS_DIR / "fa-3.json").write_text('{"session_id": "fa-3"}')
        register_capability("debugging", "Debug production issues", session_id="fa-3", session_name="FA3", platform="x")
        results = find_agents(query="production")
        assert len(results) >= 1

    def test_find_agents_platform_filter(self):
        from brynq.capability import register_capability, find_agents
        import brynq.server as srv
        (srv.SESSIONS_DIR / "fa-4.json").write_text('{"session_id": "fa-4"}')
        (srv.SESSIONS_DIR / "fa-5.json").write_text('{"session_id": "fa-5"}')
        register_capability("testing", "QA", session_id="fa-4", session_name="FA4", platform="claude")
        register_capability("testing", "QA", session_id="fa-5", session_name="FA5", platform="gemini")
        results = find_agents(capability="testing", platform="claude")
        assert len(results) == 1
        assert results[0]["platform"] == "claude"

    def test_route_task(self):
        from brynq.capability import register_capability, route_task
        import brynq.server as srv
        (srv.SESSIONS_DIR / "rt-1.json").write_text('{"session_id": "rt-1"}')
        register_capability("deploy", "Deployment", session_id="rt-1", session_name="Deployer", platform="x")
        result = route_task("Deploy v3", "deploy")
        assert result == "Deployer"

    def test_route_task_no_match(self):
        from brynq.capability import route_task
        result = route_task("Build spaceship", "aerospace")
        assert result is None
