"""Tests for auto-generated contract error classes and decoder."""

import pytest
from web3 import Web3

from xccy.errors import (
    _NAME_TO_CLASS,
    _SELECTOR_TO_CLASS,
    ContractRevertError,
    decode_error,
    decode_error_selector,
    get_error_class,
    list_all_errors,
)
from xccy.exceptions import ContractError, XccyError

# ═══════════════════════════════════════════════════════════════════════════════
# Registry & Generation
# ═══════════════════════════════════════════════════════════════════════════════


class TestRegistry:
    """Test that the error registry is populated correctly from ABIs."""

    def test_registry_populated(self):
        """Should have 91 unique errors across all ABIs."""
        assert len(_SELECTOR_TO_CLASS) == 91

    def test_name_registry_populated(self):
        """Name-to-class registry should match selector registry."""
        assert len(_NAME_TO_CLASS) == 91

    def test_all_classes_are_subclass_of_contract_revert_error(self):
        for cls in _SELECTOR_TO_CLASS.values():
            assert issubclass(cls, ContractRevertError)

    def test_all_classes_are_subclass_of_contract_error(self):
        for cls in _SELECTOR_TO_CLASS.values():
            assert issubclass(cls, ContractError)

    def test_all_classes_are_subclass_of_xccy_error(self):
        for cls in _SELECTOR_TO_CLASS.values():
            assert issubclass(cls, XccyError)

    def test_all_classes_are_subclass_of_exception(self):
        for cls in _SELECTOR_TO_CLASS.values():
            assert issubclass(cls, Exception)

    def test_no_duplicate_selectors(self):
        """Each 4-byte selector should map to exactly one class."""
        selectors = list(_SELECTOR_TO_CLASS.keys())
        assert len(selectors) == len(set(selectors))

    def test_class_names_end_with_error(self):
        for cls in _SELECTOR_TO_CLASS.values():
            assert cls.__name__.endswith("Error")

    def test_classes_have_selector_attribute(self):
        for cls in _SELECTOR_TO_CLASS.values():
            assert hasattr(cls, "_selector")
            assert isinstance(cls._selector, bytes)
            assert len(cls._selector) == 4

    def test_classes_have_solidity_signature(self):
        for cls in _SELECTOR_TO_CLASS.values():
            assert hasattr(cls, "_solidity_signature")
            assert "(" in cls._solidity_signature
            assert cls._solidity_signature.endswith(")")


# ═══════════════════════════════════════════════════════════════════════════════
# Importability
# ═══════════════════════════════════════════════════════════════════════════════


class TestImports:
    """Test that generated classes are importable from xccy.errors."""

    def test_import_pool_expired_error(self):
        from xccy.errors import PoolExpiredError
        assert issubclass(PoolExpiredError, ContractRevertError)

    def test_import_margin_requirement_not_met_error(self):
        from xccy.errors import MarginRequirementNotMetError
        assert issubclass(MarginRequirementNotMetError, ContractRevertError)

    def test_import_not_enough_funds_error(self):
        from xccy.errors import NotEnoughFundsError
        assert issubclass(NotEnoughFundsError, ContractRevertError)

    def test_import_cannot_liquidate_error(self):
        from xccy.errors import CannotLiquidateError
        assert issubclass(CannotLiquidateError, ContractRevertError)

    def test_import_pool_not_initialized_error(self):
        from xccy.errors import PoolNotInitializedError
        assert issubclass(PoolNotInitializedError, ContractRevertError)

    def test_import_token_not_whitelisted_error(self):
        from xccy.errors import TokenNotWhitelistedError
        assert issubclass(TokenNotWhitelistedError, ContractRevertError)

    def test_import_vamm_locked_error(self):
        from xccy.errors import VAMMLockedError
        assert issubclass(VAMMLockedError, ContractRevertError)

    def test_import_contract_revert_error_from_exceptions(self):
        """ContractRevertError should also be importable from xccy.exceptions."""
        from xccy.exceptions import ContractRevertError as RevertErr
        assert RevertErr is ContractRevertError

    def test_import_from_top_level(self):
        """ContractRevertError should be importable from xccy."""
        from xccy import ContractRevertError as RevertErr
        assert RevertErr is ContractRevertError

    def test_import_decode_error_from_top_level(self):
        from xccy import decode_error as de
        assert de is decode_error

    def test_import_decode_error_selector_from_top_level(self):
        from xccy import decode_error_selector as des
        assert des is decode_error_selector


# ═══════════════════════════════════════════════════════════════════════════════
# Selectors
# ═══════════════════════════════════════════════════════════════════════════════


class TestSelectors:
    """Test that computed selectors match known values."""

    def test_pool_expired_selector(self):
        from xccy.errors import PoolExpiredError
        assert PoolExpiredError._selector == bytes.fromhex("398b36db")

    def test_margin_requirement_not_met_selector(self):
        from xccy.errors import MarginRequirementNotMetError
        expected = Web3.keccak(text="MarginRequirementNotMet(uint256,uint256)")[:4]
        assert MarginRequirementNotMetError._selector == expected

    def test_not_enough_funds_selector(self):
        from xccy.errors import NotEnoughFundsError
        expected = Web3.keccak(text="NotEnoughFunds(uint256,uint256)")[:4]
        assert NotEnoughFundsError._selector == expected

    def test_cannot_liquidate_selector(self):
        from xccy.errors import CannotLiquidateError
        expected = Web3.keccak(text="CannotLiquidate()")[:4]
        assert CannotLiquidateError._selector == expected

    def test_selector_matches_solidity_signature(self):
        """For every registered class, selector should match keccak of signature."""
        for selector, cls in _SELECTOR_TO_CLASS.items():
            expected = Web3.keccak(text=cls._solidity_signature)[:4]
            assert selector == expected, (
                f"{cls.__name__}: expected {expected.hex()}, got {selector.hex()}"
            )


# ═══════════════════════════════════════════════════════════════════════════════
# decode_error
# ═══════════════════════════════════════════════════════════════════════════════


class TestDecodeError:
    """Test the decode_error function."""

    def test_decode_pool_expired_hex_string(self):
        from xccy.errors import PoolExpiredError
        err = decode_error("0x398b36db")
        assert isinstance(err, PoolExpiredError)
        assert err.error_name == "PoolExpired"
        assert err.error_selector == "0x398b36db"
        assert err.error_args == {}

    def test_decode_pool_expired_no_prefix(self):
        from xccy.errors import PoolExpiredError
        err = decode_error("398b36db")
        assert isinstance(err, PoolExpiredError)

    def test_decode_pool_expired_bytes(self):
        from xccy.errors import PoolExpiredError
        err = decode_error(bytes.fromhex("398b36db"))
        assert isinstance(err, PoolExpiredError)

    def test_decode_margin_requirement_not_met(self):
        from xccy.errors import MarginRequirementNotMetError
        selector = Web3.keccak(text="MarginRequirementNotMet(uint256,uint256)")[:4]
        # Encode two uint256 params
        import eth_abi
        params = eth_abi.encode(["uint256", "uint256"], [1000, 500])
        raw = "0x" + selector.hex() + params.hex()

        err = decode_error(raw)
        assert isinstance(err, MarginRequirementNotMetError)
        assert err.error_name == "MarginRequirementNotMet"
        assert err.margin_requirement == 1000
        assert err.total_margin_value == 500
        assert err.error_args == {
            "margin_requirement": 1000,
            "total_margin_value": 500,
        }

    def test_decode_not_enough_funds(self):
        from xccy.errors import NotEnoughFundsError
        selector = Web3.keccak(text="NotEnoughFunds(uint256,uint256)")[:4]
        import eth_abi
        params = eth_abi.encode(["uint256", "uint256"], [2000, 100])
        raw = selector + params

        err = decode_error(raw)
        assert isinstance(err, NotEnoughFundsError)
        assert err.requested == 2000
        assert err.available == 100

    def test_decode_unknown_selector(self):
        err = decode_error("0xdeadbeef")
        assert isinstance(err, ContractRevertError)
        assert err.error_name == "Unknown"
        assert err.error_selector == "0xdeadbeef"

    def test_decode_too_short(self):
        err = decode_error("0xab")
        assert isinstance(err, ContractRevertError)
        assert err.error_name == "Unknown"

    def test_decode_empty(self):
        err = decode_error("0x")
        assert isinstance(err, ContractRevertError)
        assert err.error_name == "Unknown"

    def test_decode_parameterized_error_missing_data(self):
        """If selector matches but data is too short for params, still returns typed error."""
        from xccy.errors import MarginRequirementNotMetError
        selector = Web3.keccak(text="MarginRequirementNotMet(uint256,uint256)")[:4]
        err = decode_error(selector)
        assert isinstance(err, MarginRequirementNotMetError)
        assert err.error_args == {}

    def test_decoded_error_has_raw_data(self):
        err = decode_error("0x398b36db")
        assert err.raw_data == "0x398b36db"


# ═══════════════════════════════════════════════════════════════════════════════
# decode_error_selector
# ═══════════════════════════════════════════════════════════════════════════════


class TestDecodeErrorSelector:
    """Test the decode_error_selector function."""

    def test_known_selector_hex_string(self):
        assert decode_error_selector("0x398b36db") == "PoolExpired"

    def test_known_selector_bytes(self):
        assert decode_error_selector(bytes.fromhex("398b36db")) == "PoolExpired"

    def test_unknown_selector(self):
        assert decode_error_selector("0xdeadbeef") is None


# ═══════════════════════════════════════════════════════════════════════════════
# list_all_errors
# ═══════════════════════════════════════════════════════════════════════════════


class TestListAllErrors:
    """Test the list_all_errors function."""

    def test_returns_sorted_list(self):
        errors = list_all_errors()
        assert errors == sorted(errors)

    def test_contains_known_errors(self):
        errors = list_all_errors()
        assert "PoolExpired" in errors
        assert "MarginRequirementNotMet" in errors
        assert "NotEnoughFunds" in errors
        assert "CannotLiquidate" in errors

    def test_count_matches_registry(self):
        assert len(list_all_errors()) == len(_NAME_TO_CLASS)


# ═══════════════════════════════════════════════════════════════════════════════
# get_error_class
# ═══════════════════════════════════════════════════════════════════════════════


class TestGetErrorClass:
    """Test the get_error_class function."""

    def test_known_error(self):
        from xccy.errors import PoolExpiredError
        assert get_error_class("PoolExpired") is PoolExpiredError

    def test_unknown_error(self):
        assert get_error_class("NoSuchError") is None


# ═══════════════════════════════════════════════════════════════════════════════
# Exception Hierarchy
# ═══════════════════════════════════════════════════════════════════════════════


class TestExceptionHierarchy:
    """Test that the exception hierarchy works for except clauses."""

    def test_catch_specific_error(self):
        from xccy.errors import PoolExpiredError
        err = decode_error("0x398b36db")
        with pytest.raises(PoolExpiredError):
            raise err

    def test_catch_contract_revert_error(self):
        """ContractRevertError should catch all generated errors."""
        err = decode_error("0x398b36db")
        with pytest.raises(ContractRevertError):
            raise err

    def test_catch_contract_error(self):
        err = decode_error("0x398b36db")
        with pytest.raises(ContractError):
            raise err

    def test_catch_xccy_error(self):
        err = decode_error("0x398b36db")
        with pytest.raises(XccyError):
            raise err

    def test_catch_as_base_exception(self):
        err = decode_error("0x398b36db")
        caught = False
        try:
            raise err
        except Exception:
            caught = True
        assert caught

    def test_specific_except_does_not_catch_other(self):
        """PoolExpiredError should not catch MarginRequirementNotMetError."""
        import eth_abi

        from xccy.errors import MarginRequirementNotMetError, PoolExpiredError
        selector = Web3.keccak(text="MarginRequirementNotMet(uint256,uint256)")[:4]
        params = eth_abi.encode(["uint256", "uint256"], [100, 50])
        err = decode_error(selector + params)

        assert isinstance(err, MarginRequirementNotMetError)
        assert not isinstance(err, PoolExpiredError)


# ═══════════════════════════════════════════════════════════════════════════════
# String Representation
# ═══════════════════════════════════════════════════════════════════════════════


class TestStringRepresentation:
    """Test __str__ and __repr__ output."""

    def test_str_no_args(self):
        err = decode_error("0x398b36db")
        s = str(err)
        assert "PoolExpired()" in s
        assert "[0x398b36db]" in s

    def test_str_includes_description(self):
        err = decode_error("0x398b36db")
        s = str(err)
        assert "Pool has expired" in s

    def test_str_with_args(self):
        import eth_abi

        selector = Web3.keccak(text="MarginRequirementNotMet(uint256,uint256)")[:4]
        params = eth_abi.encode(["uint256", "uint256"], [1000, 500])
        err = decode_error(selector + params)

        s = str(err)
        assert "MarginRequirementNotMet" in s
        assert "1000" in s
        assert "500" in s

    def test_repr(self):
        err = decode_error("0x398b36db")
        r = repr(err)
        assert "PoolExpiredError" in r
        assert "error_name=" in r

    def test_str_unknown_error(self):
        err = decode_error("0xdeadbeef")
        s = str(err)
        assert "Unknown" in s
        assert "0xdeadbeef" in s


# ═══════════════════════════════════════════════════════════════════════════════
# Version
# ═══════════════════════════════════════════════════════════════════════════════


class TestVersion:
    """Test version bump."""

    def test_version_is_0_4_0(self):
        import xccy
        assert xccy.__version__ == "0.4.0"
