from unittest.mock import MagicMock, patch

import pytest

from GramAddict.core.resonance_engine import ResonanceEngine


@pytest.fixture
def engine():
    # Patch the databases at the source to prevent any real Qdrant connection
    with (
        patch("GramAddict.core.resonance_engine.ContentMemoryDB") as mock_cm_cls,
        patch("GramAddict.core.resonance_engine.PersonaMemoryDB"),
    ):
        # Create a single consistent mock instance for ContentMemory
        mock_cm = MagicMock()
        mock_cm_cls.return_value = mock_cm

        # KEY: Ensure cache lookups return None to avoid fake hits with MagicMocks
        mock_cm.get_cached_evaluation.return_value = None

        # Mock embedding return to ensure truthy checks pass
        mock_cm._get_embedding.return_value = [0.1] * 1536

        # Initialize
        eng = ResonanceEngine(my_username="test_user", persona_interests=["fitness", "travel"])

        # MANUALLY FORCE VALID STATE
        eng._persona_vector = [0.1] * 1536
        eng.content_memory = mock_cm  # Re-enforce the mock

        return eng


def test_resonance_calculation_happy_path(engine):
    """Verifies that resonance is calculated correctly for matching content."""
    post_data = {
        "username": "fitness_junkie",
        "description": "Amazing morning workout session #fitness #gym",
        "caption": "No pain no gain",
    }

    # 1. Provide Real Matching Vectors (exactly the same = 1.0 similarity)
    # The real _persona_vector is [0.1]*1536 (from fixture).
    # Returning the same vector for the content.
    engine.content_memory._get_embedding.return_value = [0.1] * 1536

    # 2. Real Math Logic
    score = engine.calculate_resonance(post_data)
    # Cosine Similarity 1.0 -> Normalization (1.0 - 0.15)/0.30 -> capped to 1.000
    assert score == 1.0
    assert engine.judge_interaction(score) is True


def test_resonance_calculation_low_match(engine):
    """Verifies low score for non-matching content."""
    post_data = {
        "username": "politics_daily",
        "description": "New tax law discussed in parliament",
        "caption": "Breaking news",
    }

    # Provide Orthogonal/Opposite Vectors (-0.1 to differ from 0.1)
    engine.content_memory._get_embedding.return_value = [-0.1] * 1536

    score = engine.calculate_resonance(post_data)
    # Similarity will be low/negative -> Final score 0.0
    assert score == 0.0
    assert engine.judge_interaction(score) is False


def test_resonance_no_content(engine):
    """Empty content should return neutral score (0.5)."""
    post_data = {"username": "ghost", "description": "", "caption": ""}
    score = engine.calculate_resonance(post_data)
    assert score == 0.5


def test_resonance_caching(engine):
    """Verify that ContentMemoryDB cache is checked first."""
    post_data = {"username": "test", "description": "Some recycled content", "caption": "Again"}

    # Reset mock to verify it's not called
    engine.content_memory._get_embedding.reset_mock()
    engine.content_memory._get_embedding.return_value = [0.1] * 1536

    # Mock cache hit
    engine.content_memory.get_cached_evaluation.return_value = {"classification": "high"}

    score = engine.calculate_resonance(post_data)
    assert score == 0.85  # 'high' classification from cache

    # Should not have called embedding for the post
    engine.content_memory._get_embedding.assert_not_called()


def test_extract_and_learn_comments_llm_kwargs(engine):
    """Verifies that query_llm is called with correct kwargs to prevent 'multiple values for argument' exception."""
    configs = MagicMock()
    configs.args = MagicMock()
    configs.args.ai_condenser_model = "test-model"
    configs.args.ai_condenser_url = "http://test-url"

    # Mock XML dump containing some fake comments
    xml_content = """<?xml version='1.0' encoding='UTF-8' standalone='yes' ?>
    <hierarchy>
        <node package="com.instagram.android" class="android.widget.TextView" text="Omg this is such a cool post! I love the lighting." resource-id="comment_text" />
        <node package="com.instagram.android" class="android.widget.TextView" text="Reply" />
    </hierarchy>
    """

    with (
        patch(
            "builtins.open",
            return_value=MagicMock(
                __enter__=MagicMock(return_value=MagicMock(read=MagicMock(return_value=xml_content)))
            ),
        ),
        patch("os.path.exists", return_value=True),
        patch("GramAddict.core.resonance_engine.query_llm", autospec=True) as mock_query,
        patch("GramAddict.core.resonance_engine.CommentMemoryDB"),
    ):
        mock_query.return_value = {"response": '["Omg this is such a cool post! I love the lighting."]'}

        # This should naturally pass if kwargs are valid, or raise TypeError if it's the bug
        configs.args.ai_learn_comments = True
        configs.args.ai_vibe = "friendly"
        configs.args.ai_blacklist_topics = "nsfw"

        engine.extract_and_learn_comments(xml_hierarchy=xml_content, configs=configs, author="test_author")

        # We can also assert that query_llm was indeed called correctly
        mock_query.assert_called_once()
        args, kwargs = mock_query.call_args

        # The prompt is the first positional argue
        # We want to ensure that "url" and "model" are correctly mapped, and no duplicate positional argument is provided
        # that overlaps with "url". If prompt is pos 0, 'url' parameter from query_llm is also pos 0.
        # This assertion will fail if Python raises the TypeError first.


def test_resonance_math_normalization(engine):
    """Verifies that the normalization math for text-embedding-3-small allows natural matches to score HIGH."""
    # text-embedding-3-small real matches are typically around 0.45-0.55 raw cosine.
    # We want a raw cosine similarity of 0.45 to yield a normalized score >= 0.85 (High resonance)
    # The current math returns around 0.25 (Low relevance), which effectively blocks all Autonomous likes/comments.

    post_data = {
        "username": "perfect_match",
        "description": "This is a mathematically perfect match for the persona",
        "caption": "",
    }

    # THE MATHEMATICAL TRICK:
    # To get raw cosine 0.45 with a persona vector of [0.1]*1536:
    # We need a content vector such that sum(a*b)/(norm(a)*norm(b)) = 0.45

    persona_vec = [0.1] * 1536
    # Create a vector that is partially aligned
    content_vec = [0.1] * 691 + [0.0] * 845  # 691/1536 is approx 0.45

    engine._persona_vector = persona_vec
    engine.content_memory._get_embedding.return_value = content_vec

    score = engine.calculate_resonance(post_data)
    # (0.45 - 0.15) / 0.30 = 1.0 (Previously it was failing)
    assert score >= 0.85


def test_extract_and_learn_comments_lenient_prompt():
    """
    Test that the Condenser prompt is lenient enough to not return empty lists constantly.
    We verify the prompt contains the lenient phrasing instead of 'perfectly match'.
    """
    engine = ResonanceEngine(my_username="test_bot")

    # Mock configs for comment learning
    configs = MagicMock()
    configs.args.ai_learn_comments = True
    configs.args.ai_vibe = "friendly, authentic"
    configs.args.ai_blacklist_topics = "crypto, spam"

    # Minimal XML
    xml = """<?xml version='1.0' encoding='UTF-8' standalone='yes' ?>
    <hierarchy rotation="0">
        <node package="com.instagram.android" resource-id="comment_text" index="0" text="This lighting trick is insane!" content-desc=""/>
        <node package="com.instagram.android" resource-id="like_button" index="1" text="Like" content-desc=""/>
    </hierarchy>
    """

    with patch("GramAddict.core.resonance_engine.query_llm") as mock_llm:
        mock_llm.return_value = {"response": '["This lighting trick is insane!"]'}

        # Act
        with patch("GramAddict.core.resonance_engine.CommentMemoryDB"):
            engine.extract_and_learn_comments(xml_hierarchy=xml, configs=configs)

            # Assert
            assert mock_llm.call_count == 1
            call_kwargs = mock_llm.call_args.kwargs
            prompt = call_kwargs.get("prompt", "")

            # Ensure we are using the lenient mapping theorem
            assert "generally match this vibe" in prompt
            assert "perfectly match the vibe" not in prompt

            # Verify the parsed comments were still passed
            assert "This lighting trick is insane!" in prompt
