# tests/test_tx_analyzer.py
"""Tests for the TxAnalyzer module."""

from unittest.mock import MagicMock, patch

import pytest

from crashsense.core.tx_analyzer import TxAnalyzer


# ── Pattern matching tests ────────────────────────────────────────────────

class TestPatternMatching:
    """Test TxAnalyzer._match_patterns with various tx contexts."""

    def test_failed_tx_detected(self):
        ctx = {"status": "failed", "revert_reason": "", "function_called": "", "gas_utilisation_pct": 50, "value_wei": 0, "to": "0x1234"}
        patterns = TxAnalyzer._match_patterns(ctx)
        assert "TRANSACTION_FAILED" in patterns

    def test_out_of_gas(self):
        ctx = {"status": "failed", "revert_reason": "", "function_called": "", "gas_utilisation_pct": 99.5, "value_wei": 0, "to": "0x1234"}
        patterns = TxAnalyzer._match_patterns(ctx)
        assert "OUT_OF_GAS" in patterns

    def test_near_gas_limit(self):
        ctx = {"status": "failed", "revert_reason": "", "function_called": "", "gas_utilisation_pct": 96.0, "value_wei": 0, "to": "0x1234"}
        patterns = TxAnalyzer._match_patterns(ctx)
        assert "NEAR_GAS_LIMIT" in patterns

    def test_insufficient_balance_revert(self):
        ctx = {"status": "failed", "revert_reason": "ERC20: transfer amount exceeds balance", "function_called": "", "gas_utilisation_pct": 50, "value_wei": 0, "to": "0x1234"}
        patterns = TxAnalyzer._match_patterns(ctx)
        assert "INSUFFICIENT_BALANCE" in patterns

    def test_insufficient_allowance_revert(self):
        ctx = {"status": "failed", "revert_reason": "ERC20: transfer amount exceeds allowance", "function_called": "", "gas_utilisation_pct": 50, "value_wei": 0, "to": "0x1234"}
        patterns = TxAnalyzer._match_patterns(ctx)
        assert "INSUFFICIENT_ALLOWANCE" in patterns

    def test_slippage_uniswap_v2(self):
        ctx = {"status": "failed", "revert_reason": "UniswapV2: INSUFFICIENT_OUTPUT_AMOUNT", "function_called": "", "gas_utilisation_pct": 50, "value_wei": 0, "to": "0x1234"}
        patterns = TxAnalyzer._match_patterns(ctx)
        assert "SLIPPAGE_EXCEEDED" in patterns

    def test_deadline_expired(self):
        ctx = {"status": "failed", "revert_reason": "UniswapV2: EXPIRED", "function_called": "", "gas_utilisation_pct": 50, "value_wei": 0, "to": "0x1234"}
        patterns = TxAnalyzer._match_patterns(ctx)
        assert "DEADLINE_EXPIRED" in patterns

    def test_access_denied(self):
        ctx = {"status": "failed", "revert_reason": "Ownable: caller is not the owner", "function_called": "", "gas_utilisation_pct": 50, "value_wei": 0, "to": "0x1234"}
        patterns = TxAnalyzer._match_patterns(ctx)
        assert "ACCESS_DENIED" in patterns

    def test_paused_contract(self):
        ctx = {"status": "failed", "revert_reason": "Pausable: paused", "function_called": "", "gas_utilisation_pct": 50, "value_wei": 0, "to": "0x1234"}
        patterns = TxAnalyzer._match_patterns(ctx)
        assert "CONTRACT_PAUSED" in patterns

    def test_reentrancy(self):
        ctx = {"status": "failed", "revert_reason": "ReentrancyGuard: reentrant call", "function_called": "", "gas_utilisation_pct": 50, "value_wei": 0, "to": "0x1234"}
        patterns = TxAnalyzer._match_patterns(ctx)
        assert "REENTRANCY_GUARD" in patterns

    def test_swap_function_hint(self):
        ctx = {"status": "failed", "revert_reason": "", "function_called": "swapExactTokensForTokens", "gas_utilisation_pct": 50, "value_wei": 0, "to": "0x1234"}
        patterns = TxAnalyzer._match_patterns(ctx)
        assert "SWAP_FAILURE" in patterns

    def test_dedup_patterns(self):
        ctx = {"status": "failed", "revert_reason": "overflow overflow", "function_called": "", "gas_utilisation_pct": 50, "value_wei": 0, "to": "0x1234"}
        patterns = TxAnalyzer._match_patterns(ctx)
        # Should not have duplicates
        assert len(patterns) == len(set(patterns))


class TestRawPatternMatching:
    """Test TxAnalyzer._match_raw_patterns."""

    def test_execution_reverted(self):
        patterns = TxAnalyzer._match_raw_patterns("Error: execution reverted")
        assert "EXECUTION_REVERTED" in patterns

    def test_out_of_gas_raw(self):
        patterns = TxAnalyzer._match_raw_patterns("transaction out of gas")
        assert "OUT_OF_GAS" in patterns

    def test_nonce_too_low_raw(self):
        patterns = TxAnalyzer._match_raw_patterns("nonce too low")
        assert "NONCE_TOO_LOW" in patterns

    def test_multiple_patterns_raw(self):
        patterns = TxAnalyzer._match_raw_patterns("insufficient balance and slippage exceeded nonce too low")
        assert "INSUFFICIENT_BALANCE" in patterns
        assert "SLIPPAGE_EXCEEDED" in patterns
        assert "NONCE_TOO_LOW" in patterns


# ── Diagnosis integration (mocked) ───────────────────────────────────────

class TestDiagnoseIntegration:
    """Test the full diagnose pipeline with mocked dependencies."""

    @patch("crashsense.core.tx_analyzer.ChainFetcher")
    @patch("crashsense.core.tx_analyzer.LLMAdapter")
    def test_diagnose_success(self, MockLLM, MockFetcher):
        mock_fetcher = MockFetcher.return_value
        mock_fetcher.fetch_full_context.return_value = {
            "tx_hash": "0x" + "a" * 64,
            "chain": "ethereum",
            "from": "0x" + "1" * 40,
            "to": "0x" + "2" * 40,
            "value_wei": 0,
            "value_eth": 0.0,
            "input_data": "0xa9059cbb",
            "function_called": "transfer(address,uint256)",
            "gas_limit": 100000,
            "gas_used": 50000,
            "gas_utilisation_pct": 50.0,
            "gas_price_gwei": 20.0,
            "effective_gas_price_gwei": 20.0,
            "tx_fee_eth": 0.001,
            "status": "failed",
            "revert_reason": "ERC20: transfer amount exceeds balance",
            "block_number": 12345678,
            "logs_count": 0,
            "log_summaries": [],
            "nonce": 42,
            "contract_address": None,
        }
        mock_fetcher.chain_info = {"currency": "ETH"}

        mock_llm = MockLLM.return_value
        mock_llm.analyze.return_value = {
            "explanation": "Your balance is too low.",
            "root_cause": "Insufficient token balance",
            "suggested_fix": "Get more tokens.",
        }
        mock_llm.retrieve.return_value = []

        analyzer = TxAnalyzer(provider="gemini")
        result = analyzer.diagnose("0x" + "a" * 64)

        assert "analysis" in result
        assert "tx_context" in result
        assert "patterns_matched" in result
        assert "INSUFFICIENT_BALANCE" in result["patterns_matched"]

    @patch("crashsense.core.tx_analyzer.ChainFetcher")
    @patch("crashsense.core.tx_analyzer.LLMAdapter")
    def test_diagnose_fetch_error(self, MockLLM, MockFetcher):
        mock_fetcher = MockFetcher.return_value
        mock_fetcher.fetch_full_context.return_value = {"error": "Transaction not found"}

        analyzer = TxAnalyzer(provider="gemini")
        result = analyzer.diagnose("0x" + "b" * 64)

        assert result["analysis"]["root_cause"] == "fetch_error"

    @patch("crashsense.core.tx_analyzer.LLMAdapter")
    def test_diagnose_raw(self, MockLLM):
        mock_llm = MockLLM.return_value
        mock_llm.analyze.return_value = {
            "explanation": "Nonce is out of sequence.",
            "root_cause": "Nonce mismatch",
            "suggested_fix": "Clear activity data.",
        }
        mock_llm.retrieve.return_value = []

        analyzer = TxAnalyzer(provider="gemini")
        result = analyzer.diagnose_raw("Error: nonce too low for account 0x123")

        assert "NONCE_TOO_LOW" in result["patterns_matched"]
        assert result["analysis"]["root_cause"] == "Nonce mismatch"
