# -*- coding: utf-8 -*-
"""Phase 1 end-to-end integration test.

Tests the full chain: ingest → type classify → store → consolidate → query → prompt cache.
This is the smoke test for all Phase 1 features (1A + 1B + 1C + 1B bridge) working together.
"""

import pytest

from adclaw.memory_agent.consolidate import ConsolidationEngine
from adclaw.memory_agent.embeddings import FakeEmbeddingPipeline
from adclaw.memory_agent.ingest import IngestAgent
from adclaw.memory_agent.models import AOMConfig, Memory
from adclaw.memory_agent.query import QueryAgent
from adclaw.memory_agent.store import MemoryStore
from adclaw.agents.prompt import CachedPromptBuilder, DynamicContext


@pytest.fixture
async def store():
    s = MemoryStore(":memory:", dimensions=32)
    await s.initialize()
    yield s
    await s.close()


@pytest.fixture
def embedder():
    return FakeEmbeddingPipeline(dimensions=32)


@pytest.fixture
def config():
    return AOMConfig(
        enabled=True,
        embedding_dimensions=32,
        importance_threshold=0.1,
        consolidation_min_interval_seconds=0,
        consolidation_min_new_memories=1,
        contradiction_detection_enabled=True,
    )


async def _llm_caller(prompt: str) -> str:
    """Fake LLM that handles all prompt types."""
    lower = prompt.lower()
    if "extract" in lower:
        return '{"entities": ["test"], "topics": ["email", "writing"], "importance": 0.7}'
    if "contradict" in lower or "winner" in lower:
        return "WINNER: B\nREASON: B is newer and more accurate."
    if "consolidat" in lower or "synthesiz" in lower or "cluster" in lower:
        return "INSIGHT: Combined knowledge about email best practices.\nIMPORTANCE: 0.8"
    if "memory" in lower or "question" in lower:
        return "Based on memories, always use formal tone and avoid emojis. [Memory #abc]"
    return "Test response"


class TestPhase1EndToEnd:
    """Full chain smoke test: ingest → classify → consolidate → query → prompt."""

    async def test_full_chain(self, store, embedder, config, tmp_path):
        """The main integration test — exercises all Phase 1 features."""

        # ---- Step 1: Ingest memories of different types ----
        ingest = IngestAgent(store, embedder, _llm_caller, config)

        # Wire consolidation engine notification
        engine = ConsolidationEngine(store, embedder, _llm_caller, config)
        ingest._on_memory_inserted = engine.notify_new_memory

        # Feedback memory
        mem_fb = await ingest.ingest(
            "Don't use emojis in client emails because they look unprofessional",
            skip_llm=True,
        )
        assert mem_fb.memory_type == "feedback"
        assert "feedback_structure" in mem_fb.metadata

        # Project memory
        mem_proj = await ingest.ingest(
            "Campaign launch deadline is Q2 2026, SEO report deliverable",
            skip_llm=True,
        )
        assert mem_proj.memory_type == "project"

        # Reference memory
        mem_ref = await ingest.ingest(
            "Brand guidelines at https://drive.google.com/brand-guide",
            skip_llm=True,
        )
        assert mem_ref.memory_type == "reference"

        # User memories
        mem_user1 = await ingest.ingest(
            "I prefer formal tone in all client communications",
            skip_llm=True,
        )
        assert mem_user1.memory_type == "user"

        mem_user2 = await ingest.ingest(
            "I always review drafts twice before sending",
            skip_llm=True,
        )
        assert mem_user2.memory_type == "user"

        # ---- Step 2: Verify gate logic ----
        # 5 memories ingested → engine should have been notified
        assert engine._new_memory_count == 5
        assert engine._should_run() is True

        # ---- Step 3: Run consolidation cycle ----
        results = await engine.run_consolidation_cycle()
        # Counter reset after successful cycle
        assert engine._new_memory_count == 0

        # ---- Step 4: Verify store state ----
        stats = await store.get_stats()
        assert stats["total_memories"] == 5

        # ---- Step 5: Ingest contradicting memories (with explicit topics for detection) ----
        mem_contra = Memory(
            content="The API endpoint is active and working correctly",
            topics=["api", "endpoint"],
            importance=0.6,
        )
        await store.insert_memory(mem_contra, embedding=await embedder.embed(mem_contra.content))
        mem_contra2 = Memory(
            content="The API endpoint is broken and deprecated",
            topics=["api", "endpoint"],
            importance=0.6,
        )
        await store.insert_memory(mem_contra2, embedding=await embedder.embed(mem_contra2.content))

        # Run contradiction detection
        engine.notify_new_memory()  # manual notify for the test
        await engine._detect_contradictions([mem_contra, mem_contra2])

        # One should be superseded (B wins per our fake LLM)
        loser = await store.get_memory(mem_contra.id)
        assert loser.is_deleted == 1
        assert loser.superseded_by == mem_contra2.id

        winner = await store.get_memory(mem_contra2.id)
        assert winner.is_deleted == 0

        # ---- Step 6: Query with feedback boost ----
        query_agent = QueryAgent(store, embedder, _llm_caller, config)
        result = await query_agent.query("How should I write emails?", skip_synthesis=True)

        # Should have results
        assert len(result.citations) > 0
        # Feedback should be boosted — ranked higher than non-feedback
        feedback_citations = [c for c in result.citations if c.memory.memory_type == "feedback"]
        if feedback_citations and len(result.citations) >= 2:
            fb_idx = next(i for i, c in enumerate(result.citations) if c.memory.memory_type == "feedback")
            non_fb_idxs = [i for i, c in enumerate(result.citations) if c.memory.memory_type != "feedback"]
            if non_fb_idxs:
                assert fb_idx < min(non_fb_idxs), "Feedback should be ranked higher than non-feedback"

        # ---- Step 7: Prompt caching ----
        (tmp_path / "AGENTS.md").write_text("Marketing agent instructions: write SEO content.")
        (tmp_path / "SOUL.md").write_text("I am a helpful marketing assistant.")

        builder = CachedPromptBuilder(working_dir=tmp_path)

        # First build
        dynamic = DynamicContext(
            env_context="Platform: linux",
            team_summary="3 agents: researcher, writer, SEO specialist",
        )
        prompt1 = builder.build(dynamic=dynamic)
        assert "Marketing agent instructions" in prompt1
        assert "Platform: linux" in prompt1
        assert "# Team Summary" in prompt1

        # Static cache should hit
        static1 = builder.static_prompt
        static2 = builder.static_prompt
        assert static1 is static2  # same object = cache hit

        # ---- Step 8: File change invalidates cache ----
        (tmp_path / "AGENTS.md").write_text("Updated: focus on email marketing campaigns.")
        builder.invalidate()
        prompt2 = builder.build(dynamic=dynamic)
        assert "email marketing campaigns" in prompt2
        assert "Marketing agent instructions" not in prompt2

        # ---- Step 9: Verify no memory type mixing in consolidation ----
        # The consolidation should not have merged feedback with project memories
        consolidations = await store.list_consolidations(limit=50)
        for cons in consolidations:
            # Get types of source memories
            types = set()
            for mid in cons.memory_ids:
                mem = await store.get_memory(mid)
                if mem:
                    types.add(mem.memory_type)
            # All memories in a consolidation should be the same type
            assert len(types) <= 1, (
                f"Cross-type consolidation detected: types={types}, "
                f"memory_ids={cons.memory_ids}"
            )

    async def test_empty_store_graceful(self, store, embedder, config, tmp_path):
        """All Phase 1 components handle empty store gracefully."""
        engine = ConsolidationEngine(store, embedder, _llm_caller, config)

        # Gate blocks on empty store
        assert engine._should_run() is False

        # Consolidation on empty store
        engine.notify_new_memory()
        results = await engine.run_consolidation_cycle()
        assert results == []

        # Query on empty store
        query_agent = QueryAgent(store, embedder, _llm_caller, config)
        result = await query_agent.query("anything")
        assert result.answer == "No relevant memories found."

        # Prompt builder with no files
        builder = CachedPromptBuilder(working_dir=tmp_path)
        prompt = builder.build()
        # Should return default prompt (no AGENTS.md)
        assert "helpful assistant" in prompt.lower()

    async def test_ingest_notify_consolidation_wiring(self, store, embedder, config):
        """Verify IngestAgent → ConsolidationEngine notification wiring."""
        engine = ConsolidationEngine(store, embedder, _llm_caller, config)
        ingest = IngestAgent(store, embedder, _llm_caller, config)
        ingest._on_memory_inserted = engine.notify_new_memory

        assert engine._new_memory_count == 0
        await ingest.ingest("Test memory one", skip_llm=True)
        assert engine._new_memory_count == 1
        await ingest.ingest("Test memory two", skip_llm=True)
        assert engine._new_memory_count == 2

        # Dedup should NOT increment counter
        await ingest.ingest("Test memory one", skip_llm=True)  # duplicate hash
        assert engine._new_memory_count == 2  # unchanged
