"""
Tests for Phase 44: Intelligent Task Routing — smart_routing.py.

Uses tmp_path + monkeypatch to isolate all filesystem operations.
"""

import json
import pytest
from datetime import datetime, timezone, timedelta


@pytest.fixture(autouse=True)
def _isolate(tmp_path, monkeypatch):
    """Redirect all smart_routing paths into tmp_path."""
    routing_dir = tmp_path / "routing"
    affinities_dir = routing_dir / "affinities"
    profiles_dir = tmp_path / "profiles"
    tasks_dir = tmp_path / "tasks"
    channels_dir = tmp_path / "channels"
    sessions_dir = tmp_path / "sessions"

    for d in (routing_dir, affinities_dir, profiles_dir, tasks_dir, channels_dir, sessions_dir):
        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_dir)
    monkeypatch.setattr(srv, "SESSIONS_DIR", sessions_dir)

    import brynq.smart_routing as sr
    monkeypatch.setattr(sr, "ROUTING_DIR", routing_dir)
    monkeypatch.setattr(sr, "HISTORY_FILE", routing_dir / "history.jsonl")
    monkeypatch.setattr(sr, "AFFINITIES_DIR", affinities_dir)
    monkeypatch.setattr(sr, "RULES_FILE", routing_dir / "rules.json")
    monkeypatch.setattr(sr, "PROFILES_DIR", profiles_dir)
    monkeypatch.setattr(sr, "TASKS_DIR", tasks_dir)

    # Clear in-memory state between tests
    sr._routing_rules.clear()
    sr._fallback_chains.clear()


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


def _make_profile(tmp_path, sid, skills, rep=4.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, skills=None, status="pending", priority="normal"):
    tasks = tmp_path / "tasks"
    tasks.mkdir(exist_ok=True)
    data = {
        "task_id": task_id,
        "title": title,
        "description": "",
        "status": status,
        "priority": priority,
        "channel": "tasks",
        "assignee_session": None,
        "assignee_name": None,
    }
    if skills:
        data["required_skills"] = skills
    (tasks / f"{task_id}.json").write_text(json.dumps(data))


def _make_active_task(tmp_path, task_id, assignee_session):
    """Create a task that is accepted/in-progress for load counting."""
    tasks = tmp_path / "tasks"
    tasks.mkdir(exist_ok=True)
    (tasks / f"{task_id}.json").write_text(json.dumps({
        "task_id": task_id,
        "title": "Active task",
        "status": "accepted",
        "assignee_session": assignee_session,
    }))


def _set_affinity(tmp_path, agent_id, skill, successes, failures, avg_minutes=20):
    affinities = tmp_path / "routing" / "affinities"
    affinities.mkdir(parents=True, exist_ok=True)
    path = affinities / f"{agent_id}.json"
    try:
        data = json.loads(path.read_text("utf-8"))
    except (FileNotFoundError, json.JSONDecodeError):
        data = {}
    data[skill.lower()] = {
        "successes": successes,
        "failures": failures,
        "avg_completion_minutes": avg_minutes,
        "total_tasks": successes + failures,
    }
    path.write_text(json.dumps(data, indent=2))


# ── 1. Route Task Tests ───────────────────────────────────────────────────


class TestRouteTask:
    def test_route_returns_ranked_candidates(self, tmp_path):
        _make_profile(tmp_path, "alpha", ["python", "testing"], rep=5.0)
        _make_profile(tmp_path, "beta", ["python"], rep=3.0)
        from brynq.smart_routing import route_task
        result = route_task("Fix Python tests", ["python", "testing"])
        assert len(result) == 2
        assert result[0]["agent_id"] == "alpha"
        assert result[0]["score"] >= result[1]["score"]

    def test_route_empty_when_no_profiles(self, tmp_path):
        from brynq.smart_routing import route_task
        assert route_task("Any task", ["python"]) == []

    def test_route_excludes_offline(self, tmp_path):
        _make_profile(tmp_path, "offline-bot", ["python"], avail="offline")
        _make_profile(tmp_path, "available-bot", ["python"], avail="available")
        from brynq.smart_routing import route_task
        result = route_task("Python task", ["python"])
        ids = [c["agent_id"] for c in result]
        assert "offline-bot" not in ids
        assert "available-bot" in ids

    def test_route_excludes_exiled(self, tmp_path):
        _make_profile(tmp_path, "exiled-bot", ["python"], avail="exiled")
        from brynq.smart_routing import route_task
        result = route_task("Python task", ["python"])
        assert len(result) == 0

    def test_route_with_constraint_exclude_agents(self, tmp_path):
        _make_profile(tmp_path, "agent-a", ["python"])
        _make_profile(tmp_path, "agent-b", ["python"])
        from brynq.smart_routing import route_task
        result = route_task("Task", ["python"], constraints={"exclude_agents": ["agent-a"]})
        ids = [c["agent_id"] for c in result]
        assert "agent-a" not in ids
        assert "agent-b" in ids

    def test_route_with_constraint_platform(self, tmp_path):
        _make_profile(tmp_path, "claude-bot", ["python"])
        profiles = tmp_path / "profiles"
        # Override platform for one agent
        data = json.loads((profiles / "claude-bot.json").read_text("utf-8"))
        data["platform"] = "gemini"
        (profiles / "claude-bot.json").write_text(json.dumps(data))

        from brynq.smart_routing import route_task
        result = route_task("Task", ["python"], constraints={"platform": "cursor"})
        assert len(result) == 0

    def test_route_breakdown_present(self, tmp_path):
        _make_profile(tmp_path, "agent", ["python"], rep=4.0)
        from brynq.smart_routing import route_task
        result = route_task("Task", ["python"])
        assert len(result) == 1
        bd = result[0]["breakdown"]
        assert "skill_match" in bd
        assert "performance" in bd
        assert "availability" in bd
        assert "reputation" in bd

    def test_route_no_required_skills(self, tmp_path):
        _make_profile(tmp_path, "general", ["python"])
        from brynq.smart_routing import route_task
        result = route_task("Some generic task", [])
        assert len(result) == 1
        assert result[0]["breakdown"]["skill_match"] == 50.0

    def test_route_prefers_high_reputation(self, tmp_path):
        _make_profile(tmp_path, "reputable", ["python"], rep=5.0)
        _make_profile(tmp_path, "newbie", ["python"], rep=1.0)
        from brynq.smart_routing import route_task
        result = route_task("Python task", ["python"])
        assert result[0]["agent_id"] == "reputable"

    def test_route_prefers_historical_performer(self, tmp_path):
        _make_profile(tmp_path, "experienced", ["python"], rep=4.0)
        _make_profile(tmp_path, "fresh", ["python"], rep=4.0)
        _set_affinity(tmp_path, "experienced", "python", successes=10, failures=0)
        _set_affinity(tmp_path, "fresh", "python", successes=0, failures=5)
        from brynq.smart_routing import route_task
        result = route_task("Python task", ["python"])
        assert result[0]["agent_id"] == "experienced"


# ── 2. Routing History Tests ──────────────────────────────────────────────


class TestRoutingHistory:
    def test_record_routing(self, tmp_path):
        from brynq.smart_routing import record_routing
        entry = record_routing("task-1", "agent-a", 0.85)
        assert entry["task_id"] == "task-1"
        assert entry["agent_id"] == "agent-a"
        assert entry["score"] == 0.85
        assert "timestamp" in entry

    def test_record_outcome(self, tmp_path):
        from brynq.smart_routing import record_routing, record_outcome
        record_routing("task-2", "agent-b", 0.9)
        entry = record_outcome("task-2", success=True, completion_time_minutes=15)
        assert entry["success"] is True
        assert entry["completion_time_minutes"] == 15

    def test_record_outcome_updates_affinity(self, tmp_path):
        _make_task(tmp_path, "task-3", "Python fix", skills=["python"])
        from brynq.smart_routing import record_routing, record_outcome, get_agent_affinity
        record_routing("task-3", "agent-c", 0.8)
        record_outcome("task-3", success=True, completion_time_minutes=10)
        affinity = get_agent_affinity("agent-c", "python")
        assert affinity > 50.0  # Better than neutral after success


# ── 3. Agent Affinity Tests ──────────────────────────────────────────────


class TestAgentAffinity:
    def test_no_data_returns_neutral(self, tmp_path):
        from brynq.smart_routing import get_agent_affinity
        assert get_agent_affinity("unknown", "python") == 50.0

    def test_high_success_rate(self, tmp_path):
        _set_affinity(tmp_path, "star", "python", successes=10, failures=0)
        from brynq.smart_routing import get_agent_affinity
        affinity = get_agent_affinity("star", "python")
        assert affinity > 90.0

    def test_low_success_rate(self, tmp_path):
        _set_affinity(tmp_path, "struggling", "python", successes=0, failures=10)
        from brynq.smart_routing import get_agent_affinity
        affinity = get_agent_affinity("struggling", "python")
        assert affinity < 10.0

    def test_mixed_results(self, tmp_path):
        _set_affinity(tmp_path, "mixed", "python", successes=5, failures=5)
        from brynq.smart_routing import get_agent_affinity
        affinity = get_agent_affinity("mixed", "python")
        assert 40.0 <= affinity <= 60.0

    def test_fast_completion_bonus(self, tmp_path):
        # Use mixed results so scores are below the 100.0 cap, making the
        # +5 speed bonus visible.
        _set_affinity(tmp_path, "fast", "python", successes=7, failures=3, avg_minutes=10)
        _set_affinity(tmp_path, "slow", "python", successes=7, failures=3, avg_minutes=120)
        from brynq.smart_routing import get_agent_affinity
        fast_score = get_agent_affinity("fast", "python")
        slow_score = get_agent_affinity("slow", "python")
        assert fast_score > slow_score

    def test_unknown_skill_returns_neutral(self, tmp_path):
        _set_affinity(tmp_path, "agent", "python", successes=10, failures=0)
        from brynq.smart_routing import get_agent_affinity
        assert get_agent_affinity("agent", "rust") == 50.0


# ── 4. Load Balancing Tests ──────────────────────────────────────────────


class TestLoadBalancing:
    def test_workload_empty(self, tmp_path):
        from brynq.smart_routing import get_workload
        result = get_workload()
        assert result == {}

    def test_workload_counts_active_tasks(self, tmp_path):
        _make_active_task(tmp_path, "t1", "agent-a")
        _make_active_task(tmp_path, "t2", "agent-a")
        _make_active_task(tmp_path, "t3", "agent-b")
        from brynq.smart_routing import get_workload
        result = get_workload()
        assert result["agent-a"] == 2
        assert result["agent-b"] == 1

    def test_workload_for_specific_agent(self, tmp_path):
        _make_active_task(tmp_path, "t1", "agent-a")
        _make_active_task(tmp_path, "t2", "agent-b")
        from brynq.smart_routing import get_workload
        result = get_workload("agent-a")
        assert result == {"agent-a": 1}

    def test_workload_ignores_completed(self, tmp_path):
        tasks = tmp_path / "tasks"
        (tasks / "done.json").write_text(json.dumps({
            "task_id": "done", "status": "completed", "assignee_session": "agent-a",
        }))
        from brynq.smart_routing import get_workload
        result = get_workload("agent-a")
        assert result == {"agent-a": 0}

    def test_least_loaded_basic(self, tmp_path):
        _make_profile(tmp_path, "busy-bot", ["python"])
        _make_profile(tmp_path, "idle-bot", ["python"])
        _make_active_task(tmp_path, "t1", "busy-bot")
        _make_active_task(tmp_path, "t2", "busy-bot")
        from brynq.smart_routing import get_least_loaded
        result = get_least_loaded()
        assert result is not None
        assert result["agent_id"] == "idle-bot"
        assert result["active_tasks"] == 0

    def test_least_loaded_with_skill_filter(self, tmp_path):
        _make_profile(tmp_path, "py-dev", ["python"])
        _make_profile(tmp_path, "rs-dev", ["rust"])
        from brynq.smart_routing import get_least_loaded
        result = get_least_loaded(skill="rust")
        assert result is not None
        assert result["agent_id"] == "rs-dev"

    def test_least_loaded_no_match(self, tmp_path):
        _make_profile(tmp_path, "py-only", ["python"])
        from brynq.smart_routing import get_least_loaded
        result = get_least_loaded(skill="haskell")
        assert result is None

    def test_least_loaded_no_profiles(self, tmp_path):
        from brynq.smart_routing import get_least_loaded
        assert get_least_loaded() is None


# ── 5. Routing Rules Tests ───────────────────────────────────────────────


class TestRoutingRules:
    def test_add_and_list_rules(self, tmp_path):
        from brynq.smart_routing import add_rule, list_rules
        add_rule("no-newbies", lambda p, t, s, pr: p.get("reputation", 0) < 2.0, "reject")
        rules = list_rules()
        assert len(rules) == 1
        assert rules[0]["name"] == "no-newbies"
        assert rules[0]["action"] == "reject"

    def test_remove_rule(self, tmp_path):
        from brynq.smart_routing import add_rule, remove_rule, list_rules
        add_rule("temp-rule", lambda p, t, s, pr: True, "prefer")
        assert remove_rule("temp-rule") is True
        assert list_rules() == []

    def test_remove_nonexistent_rule(self, tmp_path):
        from brynq.smart_routing import remove_rule
        assert remove_rule("ghost") is False

    def test_reject_rule_excludes_agent(self, tmp_path):
        _make_profile(tmp_path, "low-rep", ["python"], rep=1.0)
        _make_profile(tmp_path, "high-rep", ["python"], rep=5.0)
        from brynq.smart_routing import add_rule, route_task
        add_rule("no-low-rep", lambda p, t, s, pr: p.get("reputation", 0) < 2.0, "reject")
        result = route_task("Python task", ["python"])
        ids = [c["agent_id"] for c in result]
        assert "low-rep" not in ids
        assert "high-rep" in ids

    def test_prefer_rule_adds_reason(self, tmp_path):
        _make_profile(tmp_path, "star", ["security"], rep=5.0)
        from brynq.smart_routing import add_rule, route_task
        add_rule(
            "security-expert",
            lambda p, t, s, pr: "security" in [sk.lower() for sk in p.get("skills", [])],
            "prefer",
        )
        result = route_task("Security audit", ["security"])
        assert any("Preferred by rule" in r for r in result[0]["reasons"])


# ── 6. Routing Stats Tests ───────────────────────────────────────────────


class TestRoutingStats:
    def test_stats_empty(self, tmp_path):
        from brynq.smart_routing import get_routing_stats
        stats = get_routing_stats()
        assert stats["total_routed"] == 0
        assert stats["success_rate"] == 0.0

    def test_stats_after_routing(self, tmp_path):
        from brynq.smart_routing import record_routing, record_outcome, get_routing_stats
        record_routing("t1", "agent-a", 0.9)
        record_routing("t2", "agent-a", 0.85)
        record_outcome("t1", success=True, completion_time_minutes=10)
        record_outcome("t2", success=False, completion_time_minutes=60)
        stats = get_routing_stats(hours=1)
        assert stats["total_routed"] == 2
        assert stats["success_rate"] == 0.5
        assert stats["routing_failures"] == 1
        assert len(stats["top_agents"]) == 1
        assert stats["top_agents"][0]["agent_id"] == "agent-a"

    def test_stats_respects_time_window(self, tmp_path):
        from brynq.smart_routing import record_routing, get_routing_stats, HISTORY_FILE
        import brynq.smart_routing as sr
        # Write an old entry directly
        old_ts = (datetime.now(timezone.utc) - timedelta(hours=48)).isoformat()
        old_entry = {"timestamp": old_ts, "task_id": "old", "agent_id": "x", "score": 0.5}
        with open(sr.HISTORY_FILE, "a", encoding="utf-8") as f:
            f.write(json.dumps(old_entry) + "\n")

        record_routing("recent", "agent-b", 0.9)
        stats = get_routing_stats(hours=1)
        assert stats["total_routed"] == 1  # Only the recent one


# ── 7. Fallback Chain Tests ──────────────────────────────────────────────


class TestFallbackChain:
    def test_set_and_get_chain(self, tmp_path):
        from brynq.smart_routing import set_fallback_chain, get_fallback_chain
        set_fallback_chain(["python"], ["agent-a", "agent-b", "agent-c"])
        chain = get_fallback_chain(["python"])
        assert chain == ["agent-a", "agent-b", "agent-c"]

    def test_get_chain_empty(self, tmp_path):
        from brynq.smart_routing import get_fallback_chain
        assert get_fallback_chain(["unknown_skill"]) == []

    def test_chain_key_is_order_independent(self, tmp_path):
        from brynq.smart_routing import set_fallback_chain, get_fallback_chain
        set_fallback_chain(["python", "testing"], ["a1", "a2"])
        chain = get_fallback_chain(["testing", "python"])
        assert chain == ["a1", "a2"]

    def test_route_with_fallback_uses_candidates(self, tmp_path):
        _make_profile(tmp_path, "primary", ["python"], rep=5.0)
        from brynq.smart_routing import route_with_fallback
        result = route_with_fallback("Task", ["python"])
        assert result is not None
        assert result["agent_id"] == "primary"

    def test_route_with_fallback_skips_overloaded(self, tmp_path):
        _make_profile(tmp_path, "overloaded", ["python"], rep=5.0)
        _make_profile(tmp_path, "backup", ["python"], rep=3.0)
        # Give overloaded agent 5+ tasks
        for i in range(6):
            _make_active_task(tmp_path, f"busy-{i}", "overloaded")
        from brynq.smart_routing import route_with_fallback
        result = route_with_fallback("Task", ["python"])
        assert result is not None
        assert result["agent_id"] == "backup"

    def test_route_with_fallback_uses_chain(self, tmp_path):
        # No profiles at all — rely purely on fallback chain
        from brynq.smart_routing import set_fallback_chain, route_with_fallback
        set_fallback_chain(["python"], ["fallback-1", "fallback-2"])
        result = route_with_fallback("Task", ["python"])
        assert result is not None
        assert result["agent_id"] == "fallback-1"
        assert "fallback chain" in result["reasons"][0].lower()

    def test_route_with_fallback_returns_none_when_exhausted(self, tmp_path):
        from brynq.smart_routing import route_with_fallback
        result = route_with_fallback("Task", ["nonexistent_skill"])
        assert result is None
