"""
Tests for Phase 31: Auto-Dispatch Engine.
"""

import json
import pytest


@pytest.fixture(autouse=True)
def _isolate(tmp_path, monkeypatch):
    channels = tmp_path / "channels"
    sessions = tmp_path / "sessions"
    tasks = tmp_path / "tasks"
    profiles = tmp_path / "profiles"
    dispatch = tmp_path / "dispatch"
    caps = tmp_path / "capabilities"
    for d in (channels, sessions, tasks, profiles, dispatch, caps):
        d.mkdir(parents=True, exist_ok=True)

    import brynq.server as srv
    monkeypatch.setattr(srv, "BROKER_DIR", tmp_path)
    monkeypatch.setattr(srv, "CHANNELS_DIR", channels)
    monkeypatch.setattr(srv, "SESSIONS_DIR", sessions)

    import brynq.auto_dispatch as ad
    monkeypatch.setattr(ad, "DISPATCH_DIR", dispatch)
    monkeypatch.setattr(ad, "RULES_FILE", dispatch / "rules.json")
    monkeypatch.setattr(ad, "HISTORY_FILE", dispatch / "history.jsonl")
    monkeypatch.setattr(ad, "TASKS_DIR", tasks)
    monkeypatch.setattr(ad, "PROFILES_DIR", profiles)
    monkeypatch.setattr(ad, "CAPABILITIES_DIR", caps)
    monkeypatch.setattr(ad, "CHANNELS_DIR", channels)


def _make_profile(tmp_path, sid, skills, rep=5.0, avail="available"):
    profiles = tmp_path / "profiles"
    profiles.mkdir(exist_ok=True)
    (profiles / f"{sid}.json").write_text(json.dumps({
        "session_id": sid, "session_name": sid, "platform": "claude",
        "skills": skills, "reputation": rep, "availability": avail,
    }))


def _make_task(tmp_path, task_id, title, desc="", status="pending", priority="normal"):
    tasks = tmp_path / "tasks"
    tasks.mkdir(exist_ok=True)
    (tasks / f"{task_id}.json").write_text(json.dumps({
        "task_id": task_id, "title": title, "description": desc,
        "status": status, "priority": priority, "channel": "tasks",
        "assignee_session": None, "assignee_name": None, "accepted_at": None,
    }))


class TestScoring:
    def test_skill_match(self, tmp_path):
        from brynq.auto_dispatch import score_agent_for_task
        profile = {"skills": ["python", "testing"], "reputation": 5.0, "availability": "available"}
        task = {"title": "Fix Python tests", "description": "testing python code"}
        score = score_agent_for_task(profile, task)
        assert score > 50  # Good match

    def test_no_skill_match(self, tmp_path):
        from brynq.auto_dispatch import score_agent_for_task
        profile = {"skills": ["rust"], "reputation": 5.0, "availability": "available"}
        task = {"title": "Fix Python tests", "description": "testing python code"}
        score = score_agent_for_task(profile, task)
        assert score < 50  # Poor skill match

    def test_busy_penalized(self, tmp_path):
        from brynq.auto_dispatch import score_agent_for_task
        avail = {"skills": ["python"], "reputation": 5.0, "availability": "available"}
        busy = {"skills": ["python"], "reputation": 5.0, "availability": "busy"}
        task = {"title": "Python task", "description": ""}
        assert score_agent_for_task(avail, task) > score_agent_for_task(busy, task)

    def test_high_rep_preferred(self, tmp_path):
        from brynq.auto_dispatch import score_agent_for_task
        high = {"skills": ["python"], "reputation": 5.0, "availability": "available"}
        low = {"skills": ["python"], "reputation": 2.0, "availability": "available"}
        task = {"title": "Python task", "description": ""}
        assert score_agent_for_task(high, task) > score_agent_for_task(low, task)


class TestFindBestAgent:
    def test_finds_best(self, tmp_path):
        _make_profile(tmp_path, "expert", ["python", "testing"], rep=5.0)
        _make_profile(tmp_path, "novice", ["go"], rep=3.0)
        from brynq.auto_dispatch import find_best_agent
        task = {"title": "Write Python tests", "description": "testing python"}
        best = find_best_agent(task)
        assert best is not None
        assert best["session_id"] == "expert"

    def test_skips_offline(self, tmp_path):
        _make_profile(tmp_path, "offline-bot", ["python"], avail="offline")
        from brynq.auto_dispatch import find_best_agent
        task = {"title": "Python task", "description": ""}
        assert find_best_agent(task) is None

    def test_no_profiles(self, tmp_path):
        from brynq.auto_dispatch import find_best_agent
        assert find_best_agent({"title": "task"}) is None


class TestDispatchTask:
    def test_dispatch_success(self, tmp_path):
        _make_profile(tmp_path, "worker", ["python", "api"])
        _make_task(tmp_path, "t1", "Build Python API", "python fastapi")
        from brynq.auto_dispatch import dispatch_task
        result = dispatch_task("t1")
        assert result["status"] == "dispatched"
        assert "worker" in result["assigned_to"]

    def test_dispatch_not_found(self):
        from brynq.auto_dispatch import dispatch_task
        result = dispatch_task("nonexistent")
        assert result["status"] == "error"

    def test_dispatch_not_pending(self, tmp_path):
        _make_task(tmp_path, "t2", "Done task", status="completed")
        from brynq.auto_dispatch import dispatch_task
        result = dispatch_task("t2")
        assert result["status"] == "skipped"

    def test_dispatch_escalates(self, tmp_path):
        _make_task(tmp_path, "t3", "Quantum computing task", "quantum physics simulation")
        from brynq.auto_dispatch import dispatch_task
        result = dispatch_task("t3")
        assert result["status"] == "escalated"


class TestDispatchAll:
    def test_dispatch_multiple(self, tmp_path):
        _make_profile(tmp_path, "dev", ["python", "testing"])
        _make_task(tmp_path, "p1", "Python task 1", "python")
        _make_task(tmp_path, "p2", "Python task 2", "testing")
        _make_task(tmp_path, "done", "Already done", status="completed")
        from brynq.auto_dispatch import dispatch_all_pending
        result = dispatch_all_pending()
        assert result["dispatched"] >= 1

    def test_dispatch_empty(self):
        from brynq.auto_dispatch import dispatch_all_pending
        result = dispatch_all_pending()
        assert result["dispatched"] == 0


class TestRebalance:
    def test_requeue_stuck(self, tmp_path):
        from datetime import datetime, timezone, timedelta
        old_time = (datetime.now(timezone.utc) - timedelta(minutes=60)).isoformat()
        tasks = tmp_path / "tasks"
        (tasks / "stuck.json").write_text(json.dumps({
            "task_id": "stuck", "title": "Stuck task", "status": "accepted",
            "accepted_at": old_time, "assignee_session": "dead-agent",
            "assignee_name": "dead", "channel": "tasks",
        }))
        from brynq.auto_dispatch import rebalance_stuck_tasks
        result = rebalance_stuck_tasks(stuck_minutes=30)
        assert result["requeued"] == 1

    def test_no_stuck(self, tmp_path):
        from brynq.auto_dispatch import rebalance_stuck_tasks
        result = rebalance_stuck_tasks()
        assert result["requeued"] == 0


class TestRules:
    def test_set_and_get(self):
        from brynq.auto_dispatch import set_dispatch_rules, get_dispatch_rules
        set_dispatch_rules(min_score=30.0, max_concurrent_per_agent=5)
        rules = get_dispatch_rules()
        assert rules["min_score"] == 30.0
        assert rules["max_concurrent_per_agent"] == 5

    def test_defaults(self):
        from brynq.auto_dispatch import get_dispatch_rules
        rules = get_dispatch_rules()
        assert rules["auto_dispatch"] is True


class TestPullQueue:
    def test_pull_next_task(self, tmp_path):
        _make_task(tmp_path, "pull1", "Fix Python bug", "python debugging")
        from brynq.auto_dispatch import pull_next_task
        task = pull_next_task("agent-1", agent_skills=["python"])
        assert task is not None
        assert task["task_id"] == "pull1"
        assert task["status"] == "accepted"
        assert task["pulled"] is True

    def test_pull_empty_queue(self):
        from brynq.auto_dispatch import pull_next_task
        assert pull_next_task("agent-1") is None

    def test_pull_skips_accepted(self, tmp_path):
        _make_task(tmp_path, "taken", "Task", status="accepted")
        from brynq.auto_dispatch import pull_next_task
        assert pull_next_task("agent-1") is None

    def test_pull_prefers_matching_skills(self, tmp_path):
        _make_task(tmp_path, "py-task", "Python task", "python code")
        _make_task(tmp_path, "rs-task", "Rust task", "rust code")
        from brynq.auto_dispatch import pull_next_task
        task = pull_next_task("agent-1", agent_skills=["python"])
        assert task["task_id"] == "py-task"

    def test_pull_prefers_high_priority(self, tmp_path):
        _make_task(tmp_path, "low", "Low task", priority="low")
        _make_task(tmp_path, "urgent", "Urgent task", priority="urgent")
        from brynq.auto_dispatch import pull_next_task
        task = pull_next_task("agent-1")
        assert task["task_id"] == "urgent"

    def test_queue_depth(self, tmp_path):
        _make_task(tmp_path, "q1", "Task 1", priority="normal")
        _make_task(tmp_path, "q2", "Task 2", priority="high")
        _make_task(tmp_path, "q3", "Task 3", priority="normal")
        _make_task(tmp_path, "done", "Done", status="completed")
        from brynq.auto_dispatch import get_queue_depth
        depth = get_queue_depth()
        assert depth["total"] == 3
        assert depth["by_priority"]["normal"] == 2
        assert depth["by_priority"]["high"] == 1

    def test_queue_depth_empty(self):
        from brynq.auto_dispatch import get_queue_depth
        assert get_queue_depth()["total"] == 0


class TestHistory:
    def test_dispatch_logs(self, tmp_path):
        _make_profile(tmp_path, "logger", ["python"])
        _make_task(tmp_path, "log-task", "Python work", "python")
        from brynq.auto_dispatch import dispatch_task, get_dispatch_history
        dispatch_task("log-task")
        history = get_dispatch_history()
        assert len(history) >= 1

    def test_stats(self, tmp_path):
        _make_profile(tmp_path, "stat-agent", ["python"])
        _make_task(tmp_path, "st1", "Task", "python")
        from brynq.auto_dispatch import dispatch_task, get_dispatch_stats
        dispatch_task("st1")
        stats = get_dispatch_stats()
        assert stats["total_dispatches"] >= 1

    def test_empty_history(self):
        from brynq.auto_dispatch import get_dispatch_history
        assert get_dispatch_history() == []
