# tests/test_memory.py
"""Tests for the MemoryStore module."""

import os
import tempfile

import pytest

from crashsense.core.memory import MemoryStore, DiagnosisRecord, AIAction


@pytest.fixture
def mem_store(tmp_path):
    """Create a MemoryStore with a temp SQLite database."""
    db_path = str(tmp_path / "test_memories.db")
    return MemoryStore(db_path)


class TestMemoryStore:
    def test_save_and_retrieve(self, mem_store):
        mem_store.save_diagnosis(
            tx_hash="0x" + "a" * 64,
            chain="ethereum",
            summary="Test summary",
            root_cause="Test root cause",
            suggested_fix="Test fix",
            status="failed",
            function_called="transfer",
        )
        result = mem_store.get_by_tx_hash("0x" + "a" * 64)
        assert result is not None
        assert result.chain == "ethereum"
        assert result.summary == "Test summary"
        assert result.root_cause == "Test root cause"
        assert result.frequency == 1

    def test_upsert_increments_frequency(self, mem_store):
        tx = "0x" + "b" * 64
        mem_store.save_diagnosis(tx, "ethereum", "s1", "r1", "f1")
        mem_store.save_diagnosis(tx, "ethereum", "s2", "r2", "f2")
        result = mem_store.get_by_tx_hash(tx)
        assert result.frequency == 2
        assert result.summary == "s2"  # updated

    def test_list_recent(self, mem_store):
        for i in range(5):
            mem_store.save_diagnosis(f"0x{'0' * 63}{i}", "ethereum", f"summary {i}", f"rc {i}", f"fix {i}")
        items = mem_store.list_recent(3)
        assert len(items) == 3

    def test_search(self, mem_store):
        mem_store.save_diagnosis("0x" + "c" * 64, "polygon", "slippage exceeded on uniswap", "slippage", "increase tolerance")
        mem_store.save_diagnosis("0x" + "d" * 64, "ethereum", "out of gas on transfer", "gas", "increase limit")
        results = mem_store.search("slippage")
        assert len(results) == 1
        assert "slippage" in results[0].summary.lower()

    def test_search_by_tx_hash(self, mem_store):
        tx = "0x" + "e" * 64
        mem_store.save_diagnosis(tx, "ethereum", "test", "test", "test")
        results = mem_store.search("eeee")
        assert len(results) >= 1

    def test_get_nonexistent(self, mem_store):
        result = mem_store.get_by_tx_hash("0x" + "f" * 64)
        assert result is None

    def test_prune(self, mem_store):
        for i in range(10):
            mem_store.save_diagnosis(f"0x{'0' * 63}{i}", "ethereum", f"s{i}", f"r{i}", f"f{i}")
        deleted = mem_store.prune(max_entries=5, retention_days=0)
        assert deleted >= 5
        remaining = mem_store.list_recent(100)
        assert len(remaining) <= 5


# ── New tests for expanded schema ─────────────────────────────────────────

class TestExpandedSchema:
    """Test the enriched fields added to DiagnosisRecord."""

    def test_save_with_patterns_and_remediation(self, mem_store):
        patterns = ["SLIPPAGE_EXCEEDED", "DEADLINE_EXPIRED"]
        remediation = [{"pattern": "SLIPPAGE_EXCEEDED", "title": "Increase slippage"}]
        rec = mem_store.save_diagnosis(
            tx_hash="0x" + "1" * 64,
            chain="polygon",
            summary="Slippage failure",
            root_cause="Price moved",
            suggested_fix="Increase tolerance",
            patterns=patterns,
            remediation=remediation,
        )
        d = mem_store.record_to_dict(rec)
        assert d["patterns"] == patterns
        assert d["remediation"] == remediation
        assert d["chain"] == "polygon"

    def test_save_with_tx_context_and_analysis(self, mem_store):
        tx_context = {"from": "0xSender", "to": "0xReceiver", "gas_used": 50000}
        analysis = {"explanation": "Tokens insufficient", "root_cause": "balance"}
        rec = mem_store.save_diagnosis(
            tx_hash="0x" + "2" * 64,
            chain="ethereum",
            summary="Balance failure",
            root_cause="Low balance",
            suggested_fix="Fund wallet",
            tx_context=tx_context,
            analysis=analysis,
            from_address="0xSender",
            to_address="0xReceiver",
            value_eth=1.5,
            gas_used=50000,
            gas_limit=100000,
            revert_reason="ERC20: transfer amount exceeds balance",
        )
        d = mem_store.record_to_dict(rec)
        assert d["from_address"] == "0xSender"
        assert d["to_address"] == "0xReceiver"
        assert d["value_eth"] == 1.5
        assert d["gas_used"] == 50000
        assert d["gas_limit"] == 100000
        assert d["revert_reason"] == "ERC20: transfer amount exceeds balance"
        assert d["tx_context"]["from"] == "0xSender"
        assert d["analysis"]["root_cause"] == "balance"

    def test_save_with_provider_and_model(self, mem_store):
        rec = mem_store.save_diagnosis(
            tx_hash="0x" + "3" * 64,
            chain="bsc",
            summary="Gas issue",
            root_cause="OOG",
            suggested_fix="Increase gas",
            provider="gemini",
            model="gemini-2.5-flash",
        )
        d = mem_store.record_to_dict(rec)
        assert d["provider"] == "gemini"
        assert d["model"] == "gemini-2.5-flash"

    def test_save_raw_input(self, mem_store):
        rec = mem_store.save_diagnosis(
            tx_hash="raw_abc123",
            summary="Reverted",
            root_cause="Slippage",
            suggested_fix="Retry",
            raw_input="execution reverted: slippage",
        )
        d = mem_store.record_to_dict(rec)
        assert d["raw_input"] == "execution reverted: slippage"


class TestAIActions:
    """Test the granular AI action logging."""

    def test_log_and_retrieve_actions(self, mem_store):
        rec = mem_store.save_diagnosis(
            tx_hash="0x" + "4" * 64,
            chain="ethereum",
            summary="Test",
            root_cause="Test",
            suggested_fix="Test",
        )
        mem_store.log_action(rec.id, "pattern_match", "Matched 2 patterns", {"patterns": ["A", "B"]})
        mem_store.log_action(rec.id, "llm_call", "Provider: gemini", {"model": "gemini-2.5-flash"})
        mem_store.log_action(rec.id, "remediation", "2 fixes applied", {"count": 2})

        actions = mem_store.get_actions(rec.id)
        assert len(actions) == 3
        assert actions[0].action_type == "pattern_match"
        assert actions[1].action_type == "llm_call"
        assert actions[2].action_type == "remediation"

    def test_action_to_dict(self, mem_store):
        rec = mem_store.save_diagnosis(
            tx_hash="0x" + "5" * 64,
            summary="t", root_cause="t", suggested_fix="t",
        )
        action = mem_store.log_action(rec.id, "fetch", "Fetched tx", {"chain": "ethereum"})
        d = mem_store.action_to_dict(action)
        assert d["action_type"] == "fetch"
        assert d["data"]["chain"] == "ethereum"
        assert "created_at" in d

    def test_no_actions(self, mem_store):
        actions = mem_store.get_actions(9999)
        assert actions == []


class TestGetAllPagination:
    """Test paginated history retrieval."""

    def test_pagination(self, mem_store):
        for i in range(12):
            mem_store.save_diagnosis(f"0x{i:064x}", "ethereum", f"s{i}", f"r{i}", f"f{i}")
        page1 = mem_store.get_all(page=1, per_page=5)
        assert page1["total"] == 12
        assert page1["page"] == 1
        assert page1["per_page"] == 5
        assert page1["pages"] == 3
        assert len(page1["items"]) == 5

        page3 = mem_store.get_all(page=3, per_page=5)
        assert len(page3["items"]) == 2

    def test_empty_db(self, mem_store):
        result = mem_store.get_all()
        assert result["total"] == 0
        assert result["items"] == []


class TestGetStats:
    """Test aggregate statistics."""

    def test_stats(self, mem_store):
        mem_store.save_diagnosis("0x" + "a" * 64, "ethereum", "s", "r", "f", provider="gemini", status="failed")
        mem_store.save_diagnosis("0x" + "b" * 64, "polygon", "s", "r", "f", provider="openai", status="failed")
        mem_store.save_diagnosis("0x" + "c" * 64, "ethereum", "s", "r", "f", provider="gemini", status="reverted")
        stats = mem_store.get_stats()
        assert stats["total_diagnoses"] == 3
        assert stats["by_chain"]["ethereum"] == 2
        assert stats["by_chain"]["polygon"] == 1
        assert stats["by_provider"]["gemini"] == 2
        assert stats["by_provider"]["openai"] == 1


class TestRecordToDict:
    """Test serialisation helper."""

    def test_full_round_trip(self, mem_store):
        rec = mem_store.save_diagnosis(
            tx_hash="0x" + "6" * 64,
            chain="arbitrum",
            summary="Arb test",
            root_cause="Gas",
            suggested_fix="Retry",
            explanation="Ran out of gas on L2",
            patterns=["OUT_OF_GAS"],
            remediation=[{"pattern": "OUT_OF_GAS", "title": "Increase gas"}],
            provider="claude",
            model="claude-sonnet-4-20250514",
            status="failed",
            function_called="swap",
            from_address="0xAA",
            to_address="0xBB",
            value_eth=0.01,
            gas_used=21000,
            gas_limit=50000,
            revert_reason="out of gas",
            tx_context={"chain": "arbitrum"},
            analysis={"root_cause": "Gas"},
            raw_input="",
        )
        d = mem_store.record_to_dict(rec)
        assert d["id"] == rec.id
        assert d["tx_hash"] == "0x" + "6" * 64
        assert d["chain"] == "arbitrum"
        assert d["explanation"] == "Ran out of gas on L2"
        assert d["patterns"] == ["OUT_OF_GAS"]
        assert d["provider"] == "claude"
        assert d["model"] == "claude-sonnet-4-20250514"
        assert d["created_at"] is not None
