# tests/test_chain_fetcher.py
"""Tests for the ChainFetcher module (all network calls mocked)."""

from unittest.mock import MagicMock, patch

import pytest

from crashsense.core.chain_fetcher import ChainFetcher, _hex_to_int, FUNCTION_SELECTORS, ERROR_SELECTORS, PANIC_CODES


# ── hex_to_int tests ──────────────────────────────────────────────────────

class TestHexToInt:
    def test_hex_string(self):
        assert _hex_to_int("0x10") == 16

    def test_zero(self):
        assert _hex_to_int("0x0") == 0

    def test_int_passthrough(self):
        assert _hex_to_int(42) == 42

    def test_empty_string(self):
        assert _hex_to_int("") == 0

    def test_large_hex(self):
        assert _hex_to_int("0xffffffff") == 4294967295


# ── Function selector decoding ────────────────────────────────────────────

class TestFunctionSelector:
    def test_transfer(self):
        assert ChainFetcher.decode_function_selector("0xa9059cbb0000") == "transfer(address,uint256)"

    def test_approve(self):
        assert ChainFetcher.decode_function_selector("0x095ea7b30000") == "approve(address,uint256)"

    def test_plain_transfer(self):
        assert ChainFetcher.decode_function_selector("0x") == "plain ETH transfer"

    def test_empty(self):
        assert ChainFetcher.decode_function_selector("") == "plain ETH transfer"

    def test_unknown(self):
        result = ChainFetcher.decode_function_selector("0xdeadbeef0000")
        assert result.startswith("unknown(")


# ── Revert data decoding ─────────────────────────────────────────────────

class TestRevertDecoding:
    def test_empty_revert(self):
        result = ChainFetcher._decode_revert_data("0x")
        assert "without a reason" in result.lower()

    def test_error_string(self):
        # Error(string) encoding for "Insufficient balance"
        # selector: 08c379a0
        # offset: 32 (0x20)
        # length: 20 (0x14) = "Insufficient balance" = 20 chars
        offset = "0000000000000000000000000000000000000000000000000000000000000020"
        length = "0000000000000000000000000000000000000000000000000000000000000014"
        msg_hex = "496e73756666696369656e742062616c616e636500000000000000000000000000"  # padded
        data = "0x08c379a0" + offset + length + msg_hex[:40]  # 20 bytes = 40 hex chars
        result = ChainFetcher._decode_revert_data(data)
        assert "Insufficient balance" in result

    def test_panic_overflow(self):
        # Panic(0x11) = arithmetic overflow
        code = "0000000000000000000000000000000000000000000000000000000000000011"
        data = "0x4e487b71" + code
        result = ChainFetcher._decode_revert_data(data)
        assert "overflow" in result.lower() or "Panic" in result

    def test_raw_fallback(self):
        result = ChainFetcher._decode_revert_data("0xdeadbeef1234")
        assert "Raw revert data" in result or "Custom error" in result


# ── Log summarisation ─────────────────────────────────────────────────────

class TestLogSummary:
    TRANSFER_TOPIC = "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef"
    APPROVAL_TOPIC = "0x8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925"

    def test_transfer_event(self):
        logs = [{"topics": [self.TRANSFER_TOPIC], "address": "0xtoken"}]
        result = ChainFetcher._summarise_logs(logs)
        assert len(result) == 1
        assert result[0]["event"] == "Transfer"

    def test_approval_event(self):
        logs = [{"topics": [self.APPROVAL_TOPIC], "address": "0xtoken"}]
        result = ChainFetcher._summarise_logs(logs)
        assert result[0]["event"] == "Approval"

    def test_empty_logs(self):
        assert ChainFetcher._summarise_logs([]) == []

    def test_unknown_event(self):
        logs = [{"topics": ["0x1234567890abcdef" + "0" * 48], "address": "0xaddr"}]
        result = ChainFetcher._summarise_logs(logs)
        assert result[0]["event"].startswith("Event(")


# ── fetch_full_context (mocked RPC) ──────────────────────────────────────

class TestFetchContext:
    @patch("crashsense.core.chain_fetcher.ChainFetcher._rpc_call")
    @patch("crashsense.core.chain_fetcher.load_config")
    @patch("crashsense.core.chain_fetcher.get_chain_info")
    def test_tx_not_found(self, mock_chain_info, mock_config, mock_rpc):
        mock_config.return_value = {"chain": {"name": "ethereum", "explorer_api_key": ""}}
        mock_chain_info.return_value = {"rpc_url": "http://test", "chain_id": 1, "currency": "ETH"}
        mock_rpc.return_value = None

        fetcher = ChainFetcher()
        result = fetcher.fetch_full_context("0x" + "a" * 64)
        assert "error" in result

    @patch("crashsense.core.chain_fetcher.ChainFetcher._rpc_call")
    @patch("crashsense.core.chain_fetcher.load_config")
    @patch("crashsense.core.chain_fetcher.get_chain_info")
    def test_successful_fetch(self, mock_chain_info, mock_config, mock_rpc):
        mock_config.return_value = {"chain": {"name": "ethereum", "explorer_api_key": ""}, "rag": {"enabled": False}}
        mock_chain_info.return_value = {"rpc_url": "http://test", "chain_id": 1, "currency": "ETH", "name": "ethereum"}

        tx = {
            "from": "0x" + "1" * 40,
            "to": "0x" + "2" * 40,
            "value": "0xde0b6b3a7640000",  # 1 ETH
            "input": "0xa9059cbb",
            "gas": "0x30d40",  # 200000
            "gasPrice": "0x4a817c800",  # 20 gwei
            "blockNumber": "0xbc614e",
            "nonce": "0x2a",
        }
        receipt = {
            "status": "0x0",  # failed
            "gasUsed": "0x1e848",  # 125000
            "effectiveGasPrice": "0x4a817c800",
            "logs": [],
        }

        def rpc_side_effect(method, params, timeout=30):
            if method == "eth_getTransactionByHash":
                return tx
            elif method == "eth_getTransactionReceipt":
                return receipt
            elif method == "eth_call":
                raise RuntimeError("execution reverted: insufficient balance")
            return None

        mock_rpc.side_effect = rpc_side_effect

        fetcher = ChainFetcher()
        result = fetcher.fetch_full_context("0x" + "a" * 64)

        assert result["status"] == "failed"
        assert result["gas_limit"] == 200000
        assert result["gas_used"] == 125000
        assert result["value_eth"] == 1.0
        assert "insufficient balance" in result["revert_reason"].lower()


# ── Constants sanity checks ───────────────────────────────────────────────

class TestConstants:
    def test_function_selectors_format(self):
        for sel in FUNCTION_SELECTORS:
            assert sel.startswith("0x") and len(sel) == 10

    def test_error_selectors_format(self):
        for sel in ERROR_SELECTORS:
            assert sel.startswith("0x")

    def test_panic_codes_are_ints(self):
        for code in PANIC_CODES:
            assert isinstance(code, int)
