"""Tests for Phase 71: Dynamic Role Reassignment (25+ tests)."""

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

import pytest

import brynq.server as srv
import brynq.dynamic_roles as dr


@pytest.fixture(autouse=True)
def isolate(tmp_path, monkeypatch):
    """Redirect all storage to a temp directory."""
    monkeypatch.setattr(srv, "BROKER_DIR", tmp_path)
    monkeypatch.setattr(dr, "ROLES_DIR", tmp_path / "dynamic_roles")
    (tmp_path / "dynamic_roles").mkdir(parents=True, exist_ok=True)


# ── assign_role ───────────────────────────────────────────────────────────


class TestAssignRole:
    def test_basic_assign(self):
        result = dr.assign_role("swarm1", "agent-a", "lead", "coordinate work")
        assert result["role"] == "lead"
        assert result["status"] == "active"
        assert "assigned_at" in result

    def test_assign_creates_progress_entry(self):
        dr.assign_role("swarm1", "agent-a", "lead")
        state = dr._load_state("swarm1")
        assert "agent-a" in state["progress"]
        assert "last_update" in state["progress"]["agent-a"]

    def test_assign_overwrites_existing_role(self):
        dr.assign_role("swarm1", "agent-a", "lead")
        dr.assign_role("swarm1", "agent-a", "reviewer", "review PRs")
        role = dr.get_role("swarm1", "agent-a")
        assert role["role"] == "reviewer"
        assert role["task_description"] == "review PRs"

    def test_multiple_agents(self):
        dr.assign_role("swarm1", "a1", "lead")
        dr.assign_role("swarm1", "a2", "worker")
        dr.assign_role("swarm1", "a3", "reviewer")
        state = dr._load_state("swarm1")
        assert len(state["roles"]) == 3

    def test_assign_empty_task_description(self):
        result = dr.assign_role("swarm1", "agent-a", "lead")
        role = dr.get_role("swarm1", "agent-a")
        assert role["task_description"] == ""


# ── get_role ──────────────────────────────────────────────────────────────


class TestGetRole:
    def test_get_existing(self):
        dr.assign_role("swarm1", "agent-a", "lead", "do stuff")
        role = dr.get_role("swarm1", "agent-a")
        assert role["role"] == "lead"
        assert role["task_description"] == "do stuff"

    def test_get_nonexistent_agent(self):
        assert dr.get_role("swarm1", "ghost") is None

    def test_get_nonexistent_swarm(self):
        assert dr.get_role("no-swarm", "agent-a") is None


# ── record_progress ──────────────────────────────────────────────────────


class TestRecordProgress:
    def test_basic_record(self):
        dr.assign_role("s1", "a1", "worker")
        result = dr.record_progress("s1", "a1", "step 1 done")
        assert "last_update" in result

    def test_message_appended(self):
        dr.assign_role("s1", "a1", "worker")
        dr.record_progress("s1", "a1", "step 1")
        dr.record_progress("s1", "a1", "step 2")
        state = dr._load_state("s1")
        msgs = state["progress"]["a1"]["messages"]
        assert len(msgs) == 2
        assert msgs[0]["message"] == "step 1"
        assert msgs[1]["message"] == "step 2"

    def test_record_without_prior_assign(self):
        result = dr.record_progress("s1", "new-agent", "hello")
        assert result["agent_id"] == "new-agent"

    def test_empty_message_skips_append(self):
        dr.assign_role("s1", "a1", "worker")
        dr.record_progress("s1", "a1", "")
        state = dr._load_state("s1")
        assert len(state["progress"]["a1"]["messages"]) == 0


# ── check_stuck ──────────────────────────────────────────────────────────


class TestCheckStuck:
    def test_no_stuck(self):
        dr.assign_role("s1", "a1", "worker")
        dr.record_progress("s1", "a1", "active")
        stuck = dr.check_stuck("s1", timeout_minutes=20)
        assert stuck == []

    def test_stuck_detected(self, monkeypatch):
        dr.assign_role("s1", "a1", "worker")
        # Backdate progress
        state = dr._load_state("s1")
        old = (datetime.now(timezone.utc) - timedelta(minutes=30)).isoformat()
        state["progress"]["a1"]["last_update"] = old
        dr._save_state(state)

        stuck = dr.check_stuck("s1", timeout_minutes=20)
        assert len(stuck) == 1
        assert stuck[0]["agent_id"] == "a1"
        assert stuck[0]["minutes_since_update"] >= 20

    def test_custom_timeout(self, monkeypatch):
        dr.assign_role("s1", "a1", "worker")
        state = dr._load_state("s1")
        old = (datetime.now(timezone.utc) - timedelta(minutes=5)).isoformat()
        state["progress"]["a1"]["last_update"] = old
        dr._save_state(state)

        assert len(dr.check_stuck("s1", timeout_minutes=3)) == 1
        assert len(dr.check_stuck("s1", timeout_minutes=10)) == 0

    def test_empty_swarm(self):
        assert dr.check_stuck("empty-swarm") == []


# ── get_available_agents ─────────────────────────────────────────────────


class TestGetAvailableAgents:
    def test_all_active(self):
        dr.assign_role("s1", "a1", "lead")
        dr.assign_role("s1", "a2", "worker")
        avail = dr.get_available_agents("s1")
        assert avail == []  # both active

    def test_idle_agent(self):
        dr.assign_role("s1", "a1", "lead")
        state = dr._load_state("s1")
        state["roles"]["a2"] = {"role": "worker", "status": "idle"}
        dr._save_state(state)
        avail = dr.get_available_agents("s1")
        assert "a2" in avail

    def test_completed_agent(self):
        dr.assign_role("s1", "a1", "worker")
        state = dr._load_state("s1")
        state["roles"]["a1"]["status"] = "completed"
        dr._save_state(state)
        avail = dr.get_available_agents("s1")
        assert "a1" in avail

    def test_empty_swarm(self):
        assert dr.get_available_agents("empty") == []


# ── reassign_role ────────────────────────────────────────────────────────


class TestReassignRole:
    def test_basic_reassign(self):
        dr.assign_role("s1", "a1", "lead", "build feature X")
        dr.assign_role("s1", "a2", "idle-worker")
        result = dr.reassign_role("s1", "a1", "a2", reason="a1 stuck")
        assert result["from_agent"] == "a1"
        assert result["to_agent"] == "a2"
        assert result["role"] == "lead"

        # a1 should be marked reassigned
        role_a1 = dr.get_role("s1", "a1")
        assert role_a1["status"] == "reassigned"

        # a2 should have the lead role
        role_a2 = dr.get_role("s1", "a2")
        assert role_a2["role"] == "lead"
        assert role_a2["inherited_from"] == "a1"

    def test_reassign_nonexistent(self):
        result = dr.reassign_role("s1", "ghost", "a2")
        assert "error" in result

    def test_reassign_recorded_in_history(self):
        dr.assign_role("s1", "a1", "lead")
        dr.reassign_role("s1", "a1", "a2", reason="timeout")
        history = dr.get_reassignment_history("s1")
        assert len(history) == 1
        assert history[0]["reason"] == "timeout"


# ── auto_reassign_stuck ─────────────────────────────────────────────────


class TestAutoReassignStuck:
    def test_auto_reassign(self):
        dr.assign_role("s1", "a1", "lead", "feature X")
        # Make a1 stuck
        state = dr._load_state("s1")
        old = (datetime.now(timezone.utc) - timedelta(minutes=30)).isoformat()
        state["progress"]["a1"]["last_update"] = old
        # Add an available agent
        state["roles"]["a2"] = {"role": "standby", "status": "idle"}
        state["progress"]["a2"] = {"last_update": datetime.now(timezone.utc).isoformat(), "messages": []}
        dr._save_state(state)

        results = dr.auto_reassign_stuck("s1", timeout_minutes=20)
        assert len(results) == 1
        assert results[0]["to_agent"] == "a2"

    def test_no_available_replacement(self):
        dr.assign_role("s1", "a1", "lead")
        state = dr._load_state("s1")
        old = (datetime.now(timezone.utc) - timedelta(minutes=30)).isoformat()
        state["progress"]["a1"]["last_update"] = old
        dr._save_state(state)

        results = dr.auto_reassign_stuck("s1", timeout_minutes=20)
        assert results == []

    def test_empty_swarm(self):
        results = dr.auto_reassign_stuck("empty")
        assert results == []


# ── get_reassignment_history ─────────────────────────────────────────────


class TestReassignmentHistory:
    def test_empty_history(self):
        assert dr.get_reassignment_history("no-swarm") == []

    def test_multiple_reassignments(self):
        dr.assign_role("s1", "a1", "lead")
        dr.assign_role("s1", "a2", "worker")
        dr.assign_role("s1", "a3", "reviewer")

        dr.reassign_role("s1", "a1", "a2", reason="r1")
        dr.reassign_role("s1", "a2", "a3", reason="r2")
        history = dr.get_reassignment_history("s1")
        assert len(history) == 2
        assert history[0]["reason"] == "r1"
        assert history[1]["reason"] == "r2"
