"""Deep tests for consensus.py — voting, quorum, tie-breaking, expiry."""

import tempfile
import time

import pytest

from brynq.consensus import ConsensusManager


@pytest.fixture
def cm():
    return ConsensusManager(storage_dir=tempfile.mkdtemp())


class TestProposalCreation:
    def test_create_basic(self, cm):
        r = cm.create_proposal("Ship?", ["yes", "no"], "agent-1")
        assert "proposal_id" in r
        assert r["quorum"] == 1

    def test_create_with_quorum(self, cm):
        r = cm.create_proposal("Big decision", ["a", "b", "c"], "agent-1", quorum=3)
        assert r["quorum"] == 3

    def test_create_needs_2_options(self, cm):
        r = cm.create_proposal("Only one", ["yes"], "agent-1")
        assert "error" in r

    def test_dedup_options(self, cm):
        r = cm.create_proposal("Dupes?", ["a", "b", "a", "c", "b"], "agent-1")
        p = cm.get_proposal(r["proposal_id"])
        assert p["options"] == ["a", "b", "c"]

    def test_create_with_duration(self, cm):
        r = cm.create_proposal("Quick", ["a", "b"], "agent-1", duration_sec=10)
        assert r["expires_at"] > time.time()


class TestVoting:
    def test_vote_valid(self, cm):
        p = cm.create_proposal("Vote?", ["yes", "no"], "a1")
        r = cm.vote(p["proposal_id"], "a1", "yes")
        assert r["ok"] is True
        assert r["total_votes"] == 1

    def test_vote_invalid_option(self, cm):
        p = cm.create_proposal("Vote?", ["yes", "no"], "a1")
        r = cm.vote(p["proposal_id"], "a1", "maybe")
        assert r["ok"] is False

    def test_vote_change(self, cm):
        p = cm.create_proposal("Change?", ["a", "b"], "a1")
        cm.vote(p["proposal_id"], "a1", "a")
        r = cm.vote(p["proposal_id"], "a1", "b")
        assert r["ok"] is True
        assert r.get("changed_from") == "a"

    def test_vote_on_resolved(self, cm):
        p = cm.create_proposal("Done", ["a", "b"], "a1", quorum=1)
        cm.vote(p["proposal_id"], "a1", "a")
        cm.resolve(p["proposal_id"])
        r = cm.vote(p["proposal_id"], "a2", "b")
        assert r["ok"] is False

    def test_vote_nonexistent(self, cm):
        r = cm.vote("fake-id", "a1", "yes")
        assert r["ok"] is False


class TestRevocation:
    def test_revoke(self, cm):
        p = cm.create_proposal("Revoke?", ["a", "b"], "a1")
        cm.vote(p["proposal_id"], "a1", "a")
        r = cm.revoke_vote(p["proposal_id"], "a1")
        assert r["ok"] is True
        assert r["revoked"] == "a"
        assert r["total_votes"] == 0

    def test_revoke_no_vote(self, cm):
        p = cm.create_proposal("No vote", ["a", "b"], "a1")
        r = cm.revoke_vote(p["proposal_id"], "a1")
        assert r["ok"] is False

    def test_revoke_on_closed(self, cm):
        p = cm.create_proposal("Closed", ["a", "b"], "a1", quorum=1)
        cm.vote(p["proposal_id"], "a1", "a")
        cm.resolve(p["proposal_id"])
        r = cm.revoke_vote(p["proposal_id"], "a1")
        assert r["ok"] is False


class TestResolution:
    def test_clear_winner(self, cm):
        p = cm.create_proposal("Clear", ["a", "b"], "a1", quorum=2)
        cm.vote(p["proposal_id"], "a1", "a")
        cm.vote(p["proposal_id"], "a2", "a")
        r = cm.resolve(p["proposal_id"])
        assert r["winner"] == "a"
        assert r["quorum_met"] is True

    def test_quorum_not_met(self, cm):
        p = cm.create_proposal("Need 5", ["a", "b"], "a1", quorum=5)
        cm.vote(p["proposal_id"], "a1", "a")
        r = cm.resolve(p["proposal_id"])
        assert r["quorum_met"] is False
        assert r["winner"] == ""

    def test_no_votes(self, cm):
        p = cm.create_proposal("Empty", ["a", "b"], "a1", quorum=1)
        r = cm.resolve(p["proposal_id"])
        assert r["total_votes"] == 0

    def test_tie_breaking(self, cm):
        p = cm.create_proposal("Tie", ["a", "b"], "a1", quorum=2)
        cm.vote(p["proposal_id"], "a1", "a")
        time.sleep(0.01)
        cm.vote(p["proposal_id"], "a2", "b")
        r = cm.resolve(p["proposal_id"])
        assert r["winner"] in ("a", "b")
        assert r.get("tie_broken") is True

    def test_already_resolved(self, cm):
        p = cm.create_proposal("Done", ["a", "b"], "a1", quorum=1)
        cm.vote(p["proposal_id"], "a1", "a")
        cm.resolve(p["proposal_id"])
        r = cm.resolve(p["proposal_id"])
        assert r.get("already_resolved") is True

    def test_three_way_vote(self, cm):
        p = cm.create_proposal("3way", ["a", "b", "c"], "a1", quorum=3)
        cm.vote(p["proposal_id"], "a1", "a")
        cm.vote(p["proposal_id"], "a2", "b")
        cm.vote(p["proposal_id"], "a3", "a")
        r = cm.resolve(p["proposal_id"])
        assert r["winner"] == "a"
        assert r["tally"]["a"] == 2


class TestCancellation:
    def test_cancel(self, cm):
        p = cm.create_proposal("Cancel", ["a", "b"], "a1")
        r = cm.cancel(p["proposal_id"], "admin")
        assert r["ok"] is True

    def test_cancel_already_resolved(self, cm):
        p = cm.create_proposal("Done", ["a", "b"], "a1", quorum=1)
        cm.vote(p["proposal_id"], "a1", "a")
        cm.resolve(p["proposal_id"])
        r = cm.cancel(p["proposal_id"])
        assert r["ok"] is False


class TestListing:
    def test_list_open(self, cm):
        cm.create_proposal("Open 1", ["a", "b"], "a1")
        cm.create_proposal("Open 2", ["x", "y"], "a2")
        open_list = cm.list_open()
        assert len(open_list) >= 2

    def test_list_resolved(self, cm):
        p = cm.create_proposal("Resolve me", ["a", "b"], "a1", quorum=1)
        cm.vote(p["proposal_id"], "a1", "a")
        cm.resolve(p["proposal_id"])
        resolved = cm.list_resolved()
        assert len(resolved) >= 1

    def test_get_proposal(self, cm):
        p = cm.create_proposal("Details", ["a", "b"], "a1")
        detail = cm.get_proposal(p["proposal_id"])
        assert detail is not None
        assert "current_tally" in detail
        assert "time_remaining" in detail

    def test_get_nonexistent(self, cm):
        assert cm.get_proposal("fake") is None
