"""Tests for Phase 74: Multi-Agent Consensus Voting (25+ tests)."""

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

import pytest

import brynq.server as srv
import brynq.multi_consensus as mc


@pytest.fixture(autouse=True)
def isolate(tmp_path, monkeypatch):
    monkeypatch.setattr(srv, "BROKER_DIR", tmp_path)
    monkeypatch.setattr(mc, "VOTES_DIR", tmp_path / "votes")
    (tmp_path / "votes").mkdir(parents=True, exist_ok=True)


# ── create_vote ──────────────────────────────────────────────────────────


class TestCreateVote:
    def test_basic_create(self):
        v = mc.create_vote("deploy?", "Ship v2", ["approve", "reject"])
        assert v["topic"] == "deploy?"
        assert v["status"] == "open"
        assert v["options"] == ["approve", "reject"]
        assert "vote_id" in v

    def test_custom_approvals(self):
        v = mc.create_vote("t", "d", ["a", "b"], required_approvals=5)
        assert v["required_approvals"] == 5

    def test_tally_initialised(self):
        v = mc.create_vote("t", "d", ["a", "b", "c"])
        assert v["tally"] == {"a": 0, "b": 0, "c": 0}

    def test_too_few_options(self):
        result = mc.create_vote("t", "d", ["only"])
        assert "error" in result

    def test_unique_ids(self):
        v1 = mc.create_vote("t1", "d", ["a", "b"])
        v2 = mc.create_vote("t2", "d", ["a", "b"])
        assert v1["vote_id"] != v2["vote_id"]

    def test_initiator_id(self):
        v = mc.create_vote("t", "d", ["a", "b"], initiator_id="agent-x")
        assert v["initiator_id"] == "agent-x"

    def test_timeout_sets_expiry(self):
        v = mc.create_vote("t", "d", ["a", "b"], timeout_minutes=60)
        created = datetime.fromisoformat(v["created_at"])
        expires = datetime.fromisoformat(v["expires_at"])
        delta = (expires - created).total_seconds()
        assert 3500 < delta < 3700  # ~60 minutes


# ── cast_vote ────────────────────────────────────────────────────────────


class TestCastVote:
    def test_basic_cast(self):
        v = mc.create_vote("t", "d", ["yes", "no"])
        result = mc.cast_vote(v["vote_id"], "agent-1", "yes", "looks good")
        assert result["choice"] == "yes"
        assert result["tally"]["yes"] == 1

    def test_one_vote_per_agent(self):
        v = mc.create_vote("t", "d", ["yes", "no"])
        mc.cast_vote(v["vote_id"], "a1", "yes")
        mc.cast_vote(v["vote_id"], "a1", "no")  # change vote
        loaded = mc._load(v["vote_id"])
        assert loaded["tally"]["yes"] == 0
        assert loaded["tally"]["no"] == 1

    def test_invalid_choice(self):
        v = mc.create_vote("t", "d", ["yes", "no"])
        result = mc.cast_vote(v["vote_id"], "a1", "maybe")
        assert "error" in result

    def test_vote_nonexistent(self):
        result = mc.cast_vote("ghost", "a1", "yes")
        assert "error" in result

    def test_multiple_agents(self):
        v = mc.create_vote("t", "d", ["yes", "no"])
        mc.cast_vote(v["vote_id"], "a1", "yes")
        mc.cast_vote(v["vote_id"], "a2", "yes")
        mc.cast_vote(v["vote_id"], "a3", "no")
        loaded = mc._load(v["vote_id"])
        assert loaded["tally"]["yes"] == 2
        assert loaded["tally"]["no"] == 1

    def test_vote_on_cancelled(self):
        v = mc.create_vote("t", "d", ["yes", "no"])
        mc.cancel_vote(v["vote_id"], "nvm")
        result = mc.cast_vote(v["vote_id"], "a1", "yes")
        assert "error" in result

    def test_vote_on_expired(self):
        v = mc.create_vote("t", "d", ["yes", "no"], timeout_minutes=0)
        # Force expiry by backdating
        loaded = mc._load(v["vote_id"])
        loaded["expires_at"] = (
            datetime.now(timezone.utc) - timedelta(minutes=5)
        ).isoformat()
        mc._save(loaded)

        result = mc.cast_vote(v["vote_id"], "a1", "yes")
        assert "error" in result
        assert "expired" in result["error"].lower()


# ── get_vote_status ──────────────────────────────────────────────────────


class TestGetVoteStatus:
    def test_basic_status(self):
        v = mc.create_vote("t", "d", ["a", "b"])
        status = mc.get_vote_status(v["vote_id"])
        assert status["status"] == "open"
        assert status["tally"] == {"a": 0, "b": 0}

    def test_time_remaining(self):
        v = mc.create_vote("t", "d", ["a", "b"], timeout_minutes=30)
        status = mc.get_vote_status(v["vote_id"])
        assert status["time_remaining_seconds"] > 0

    def test_nonexistent(self):
        result = mc.get_vote_status("ghost")
        assert "error" in result


# ── check_result ─────────────────────────────────────────────────────────


class TestCheckResult:
    def test_pending(self):
        v = mc.create_vote("t", "d", ["yes", "no"], required_approvals=3)
        result = mc.check_result(v["vote_id"])
        assert result["result"] == "pending"

    def test_approved_on_quorum(self):
        v = mc.create_vote("t", "d", ["yes", "no"], required_approvals=2)
        mc.cast_vote(v["vote_id"], "a1", "yes")
        mc.cast_vote(v["vote_id"], "a2", "yes")
        result = mc.check_result(v["vote_id"])
        assert result["result"] == "approved"
        assert result["winning_option"] == "yes"

    def test_tie(self):
        v = mc.create_vote("t", "d", ["a", "b"], required_approvals=2)
        mc.cast_vote(v["vote_id"], "a1", "a")
        mc.cast_vote(v["vote_id"], "a2", "b")
        mc.cast_vote(v["vote_id"], "a3", "a")
        mc.cast_vote(v["vote_id"], "a4", "b")
        result = mc.check_result(v["vote_id"])
        assert result["result"] in ("tie", "approved")  # both hit 2

    def test_timeout_with_leader(self):
        v = mc.create_vote("t", "d", ["yes", "no"], required_approvals=10, timeout_minutes=1)
        mc.cast_vote(v["vote_id"], "a1", "yes")
        # Force expire
        loaded = mc._load(v["vote_id"])
        loaded["expires_at"] = (
            datetime.now(timezone.utc) - timedelta(minutes=5)
        ).isoformat()
        mc._save(loaded)

        result = mc.check_result(v["vote_id"])
        assert result["result"] == "approved"
        assert result["winning_option"] == "yes"

    def test_timeout_no_votes(self):
        v = mc.create_vote("t", "d", ["yes", "no"], timeout_minutes=1)
        loaded = mc._load(v["vote_id"])
        loaded["expires_at"] = (
            datetime.now(timezone.utc) - timedelta(minutes=5)
        ).isoformat()
        mc._save(loaded)

        result = mc.check_result(v["vote_id"])
        assert result["result"] == "expired"

    def test_already_resolved(self):
        v = mc.create_vote("t", "d", ["yes", "no"], required_approvals=1)
        mc.cast_vote(v["vote_id"], "a1", "yes")
        mc.check_result(v["vote_id"])
        # Call again
        result = mc.check_result(v["vote_id"])
        assert result["result"] == "approved"

    def test_nonexistent(self):
        result = mc.check_result("ghost")
        assert "error" in result


# ── list_votes ───────────────────────────────────────────────────────────


class TestListVotes:
    def test_empty(self):
        assert mc.list_votes() == []

    def test_list_all(self):
        mc.create_vote("a", "d", ["x", "y"])
        mc.create_vote("b", "d", ["x", "y"])
        assert len(mc.list_votes()) == 2

    def test_filter_by_status(self):
        v1 = mc.create_vote("a", "d", ["x", "y"])
        mc.create_vote("b", "d", ["x", "y"])
        mc.cancel_vote(v1["vote_id"])

        assert len(mc.list_votes(status="open")) == 1
        assert len(mc.list_votes(status="cancelled")) == 1


# ── cancel_vote ──────────────────────────────────────────────────────────


class TestCancelVote:
    def test_cancel(self):
        v = mc.create_vote("t", "d", ["a", "b"])
        result = mc.cancel_vote(v["vote_id"], "changed mind")
        assert result["status"] == "cancelled"
        assert result["cancel_reason"] == "changed mind"

    def test_cancel_nonexistent(self):
        result = mc.cancel_vote("ghost")
        assert "error" in result

    def test_cancel_already_cancelled(self):
        v = mc.create_vote("t", "d", ["a", "b"])
        mc.cancel_vote(v["vote_id"])
        result = mc.cancel_vote(v["vote_id"])
        assert "error" in result

    def test_cancel_resolved(self):
        v = mc.create_vote("t", "d", ["a", "b"], required_approvals=1)
        mc.cast_vote(v["vote_id"], "a1", "a")
        mc.check_result(v["vote_id"])
        result = mc.cancel_vote(v["vote_id"])
        assert "error" in result


# ── get_voting_history ───────────────────────────────────────────────────


class TestGetVotingHistory:
    def test_empty(self):
        assert mc.get_voting_history() == []

    def test_all_events(self):
        v = mc.create_vote("t", "d", ["a", "b"])
        mc.cast_vote(v["vote_id"], "agent-1", "a")
        mc.cast_vote(v["vote_id"], "agent-2", "b")
        events = mc.get_voting_history()
        assert len(events) >= 3  # created + 2 votes

    def test_filter_by_agent(self):
        v = mc.create_vote("t", "d", ["a", "b"])
        mc.cast_vote(v["vote_id"], "agent-1", "a")
        mc.cast_vote(v["vote_id"], "agent-2", "b")
        events = mc.get_voting_history(agent_id="agent-1")
        assert all(e["agent_id"] == "agent-1" for e in events)
        assert len(events) == 1
