import hashlib
import json
from unittest.mock import MagicMock

import pytest

from skytale_sdk.auction import (
    AuctionResult,
    Commitment,
    Reveal,
    SealedBidAuction,
    _compute_hash,
)


def mock_manager():
    mgr = MagicMock()
    mgr.send = MagicMock()
    mgr.receive = MagicMock(return_value=[])
    return mgr


class TestComputeHash:
    def test_deterministic(self):
        salt = bytes.fromhex("aa" * 32)
        h1 = _compute_hash(100, salt, {})
        h2 = _compute_hash(100, salt, {})
        assert h1 == h2

    def test_different_bids_different_hash(self):
        salt = bytes.fromhex("bb" * 32)
        h1 = _compute_hash(100, salt, {})
        h2 = _compute_hash(200, salt, {})
        assert h1 != h2

    def test_different_salts_different_hash(self):
        h1 = _compute_hash(100, bytes(32), {})
        h2 = _compute_hash(100, bytes([1] * 32), {})
        assert h1 != h2

    def test_is_sha256_hex(self):
        h = _compute_hash(42, bytes(32), {"asset": "ETH"})
        assert len(h) == 64
        int(h, 16)  # valid hex

    def test_metadata_affects_hash(self):
        salt = bytes(32)
        h1 = _compute_hash(100, salt, {})
        h2 = _compute_hash(100, salt, {"asset": "BTC"})
        assert h1 != h2


class TestCommitment:
    def test_commit_returns_commitment(self):
        mgr = mock_manager()
        auction = SealedBidAuction(mgr, "org/auction/1")
        c = auction.commit(1500)
        assert isinstance(c, Commitment)
        assert c.bid_amount == 1500
        assert len(c.salt) == 32
        assert len(c.commitment_hash) == 64

    def test_commit_sends_to_channel(self):
        mgr = mock_manager()
        auction = SealedBidAuction(mgr, "org/auction/1")
        auction.commit(1500)
        mgr.send.assert_called_once()
        channel, msg = mgr.send.call_args.args
        assert channel == "org/auction/1"
        data = json.loads(msg)
        assert data["type"] == "commit"
        assert len(data["hash"]) == 64

    def test_commit_with_metadata(self):
        mgr = mock_manager()
        auction = SealedBidAuction(mgr, "ch")
        c = auction.commit(500, metadata={"asset": "ETH/USDC"})
        assert c.metadata == {"asset": "ETH/USDC"}

    def test_commitment_hash_is_verifiable(self):
        mgr = mock_manager()
        auction = SealedBidAuction(mgr, "ch")
        c = auction.commit(1000, metadata={"x": 1})
        expected = _compute_hash(c.bid_amount, c.salt, c.metadata)
        assert c.commitment_hash == expected

    def test_two_commits_different_salts(self):
        mgr = mock_manager()
        auction = SealedBidAuction(mgr, "ch")
        c1 = auction.commit(100)
        c2 = auction.commit(100)
        assert c1.salt != c2.salt
        assert c1.commitment_hash != c2.commitment_hash


class TestReveal:
    def test_reveal_sends_to_channel(self):
        mgr = mock_manager()
        auction = SealedBidAuction(mgr, "ch")
        c = auction.commit(2000)
        mgr.send.reset_mock()
        auction.reveal(c)
        mgr.send.assert_called_once()
        channel, msg = mgr.send.call_args.args
        assert channel == "ch"
        data = json.loads(msg)
        assert data["type"] == "reveal"
        assert data["bid"] == 2000
        assert data["hash"] == c.commitment_hash
        assert data["salt"] == c.salt.hex()


class TestCollect:
    def test_collects_commits(self):
        mgr = mock_manager()
        auction = SealedBidAuction(mgr, "ch")
        commit_msg = json.dumps({"type": "commit", "hash": "abc123"})
        mgr.receive.return_value = [commit_msg]
        auction.collect(timeout=1.0)
        assert "abc123" in auction._commitments

    def test_collects_valid_reveal(self):
        mgr = mock_manager()
        auction = SealedBidAuction(mgr, "ch")
        salt = bytes(32)
        bid = 500
        meta = {"x": 1}
        h = _compute_hash(bid, salt, meta)
        reveal_msg = json.dumps({
            "type": "reveal",
            "bid": bid,
            "salt": salt.hex(),
            "hash": h,
            "meta": meta,
            "sender": "agent-1",
        })
        mgr.receive.return_value = [reveal_msg]
        auction.collect(timeout=1.0)
        assert len(auction._reveals) == 1
        assert auction._reveals[0].valid is True

    def test_detects_invalid_reveal(self):
        mgr = mock_manager()
        auction = SealedBidAuction(mgr, "ch")
        reveal_msg = json.dumps({
            "type": "reveal",
            "bid": 500,
            "salt": "00" * 32,
            "hash": "wrong_hash",
            "meta": {},
        })
        mgr.receive.return_value = [reveal_msg]
        auction.collect(timeout=1.0)
        assert len(auction._reveals) == 1
        assert auction._reveals[0].valid is False

    def test_skips_malformed_messages(self):
        mgr = mock_manager()
        auction = SealedBidAuction(mgr, "ch")
        mgr.receive.return_value = ["not json {{{", ""]
        auction.collect(timeout=1.0)
        assert len(auction._reveals) == 0
        assert len(auction._commitments) == 0


class TestResolve:
    def _add_valid_reveal(self, auction, sender, bid):
        salt = bytes(32)
        h = _compute_hash(bid, salt, {})
        auction._reveals.append(Reveal(
            sender=sender,
            bid_amount=bid,
            salt_hex=salt.hex(),
            commitment_hash=h,
            metadata={},
            valid=True,
        ))

    def _add_invalid_reveal(self, auction, sender, bid):
        auction._reveals.append(Reveal(
            sender=sender,
            bid_amount=bid,
            salt_hex="00" * 32,
            commitment_hash="bad",
            metadata={},
            valid=False,
        ))

    def test_highest_wins(self):
        mgr = mock_manager()
        auction = SealedBidAuction(mgr, "ch", highest_wins=True)
        self._add_valid_reveal(auction, "alice", 100)
        self._add_valid_reveal(auction, "bob", 200)
        result = auction.resolve()
        assert result.winner == "bob"
        assert result.winning_bid == 200
        assert len(result.reveals) == 2

    def test_lowest_wins(self):
        mgr = mock_manager()
        auction = SealedBidAuction(mgr, "ch", highest_wins=False)
        self._add_valid_reveal(auction, "alice", 100)
        self._add_valid_reveal(auction, "bob", 200)
        result = auction.resolve()
        assert result.winner == "alice"
        assert result.winning_bid == 100

    def test_no_valid_reveals(self):
        mgr = mock_manager()
        auction = SealedBidAuction(mgr, "ch")
        self._add_invalid_reveal(auction, "alice", 100)
        result = auction.resolve()
        assert result.winner is None
        assert result.winning_bid == 0
        assert len(result.invalid_reveals) == 1

    def test_empty_auction(self):
        mgr = mock_manager()
        auction = SealedBidAuction(mgr, "ch")
        result = auction.resolve()
        assert result.winner is None
        assert result.winning_bid == 0

    def test_invalid_reveals_separated(self):
        mgr = mock_manager()
        auction = SealedBidAuction(mgr, "ch")
        self._add_valid_reveal(auction, "alice", 300)
        self._add_invalid_reveal(auction, "bob", 500)
        result = auction.resolve()
        assert result.winner == "alice"
        assert len(result.reveals) == 1
        assert len(result.invalid_reveals) == 1
