"""
Tests for Hindsight Python client.

These tests require a running Hindsight API server.
"""

import os
import uuid
from datetime import datetime

import pytest

from hindsight_client import Hindsight

# Test configuration
HINDSIGHT_API_URL = os.getenv("HINDSIGHT_API_URL", "http://localhost:8888")


@pytest.fixture
def client():
    """Create a Hindsight client for testing."""
    with Hindsight(base_url=HINDSIGHT_API_URL) as client:
        yield client


@pytest.fixture
def bank_id():
    """Provide a unique test bank ID for each test."""
    return f"test_bank_{uuid.uuid4().hex[:12]}"


class TestRetain:
    """Tests for storing memories."""

    def test_retain_single_memory(self, client, bank_id):
        """Test storing a single memory."""
        response = client.retain(
            bank_id=bank_id,
            content="Alice loves artificial intelligence and machine learning",
        )

        assert response is not None
        assert response.success is True

    def test_retain_memory_with_context(self, client, bank_id):
        """Test storing a memory with context and timestamp."""
        response = client.retain(
            bank_id=bank_id,
            content="Bob went hiking in the mountains",
            timestamp=datetime(2024, 1, 15, 10, 30),
            context="outdoor activities",
        )

        assert response is not None
        assert response.success is True

    def test_retain_batch_memories(self, client, bank_id):
        """Test storing multiple memories in batch."""
        items = [
            {"content": "Charlie enjoys reading science fiction books"},
            {"content": "Diana is learning to play the guitar", "context": "hobbies"},
            {
                "content": "Eve completed a marathon last month",
                "event_date": datetime(2024, 10, 15),
            },
        ]

        response = client.retain_batch(
            bank_id=bank_id,
            items=items,
        )

        assert response is not None
        assert response.success is True
        assert response.items_count == 3


class TestRecall:
    """Tests for searching memories."""

    @pytest.fixture(autouse=True)
    def setup_memories(self, client, bank_id):
        """Setup: Store some test memories before search tests."""
        client.retain_batch(
            bank_id=bank_id,
            items=[
                {"content": "Alice loves programming in Python"},
                {"content": "Bob enjoys hiking and outdoor adventures"},
                {"content": "Charlie is interested in quantum physics"},
                {"content": "Diana plays the violin beautifully"},
            ],
        )

    def test_recall_basic(self, client, bank_id):
        """Test basic memory search."""
        response = client.recall(
            bank_id=bank_id,
            query="What does Alice like?",
        )

        assert response is not None
        assert response.results is not None
        assert len(response.results) > 0

        # Check that at least one result contains relevant information
        result_texts = [r.text for r in response.results]
        assert any("Alice" in text or "Python" in text or "programming" in text for text in result_texts)

    def test_recall_with_max_tokens(self, client, bank_id):
        """Test search with token limit."""
        response = client.recall(
            bank_id=bank_id,
            query="outdoor activities",
            max_tokens=1024,
        )

        assert response is not None
        assert response.results is not None

    def test_recall_full_featured(self, client, bank_id):
        """Test recall with all features."""
        response = client.recall(
            bank_id=bank_id,
            query="What are people's hobbies?",
            types=["world"],
            max_tokens=2048,
            trace=True,
        )

        assert response is not None
        assert response.results is not None


class TestReflect:
    """Tests for thinking/reasoning operations."""

    @pytest.fixture(autouse=True)
    def setup_memories(self, client, bank_id):
        """Setup: Store some test memories and bank background."""
        client.create_bank(
            bank_id=bank_id,
            mission="I am a helpful AI assistant interested in technology and science.",
        )

        client.retain_batch(
            bank_id=bank_id,
            items=[
                {"content": "The Python programming language is great for data science"},
                {"content": "Machine learning models can recognize patterns in data"},
                {"content": "Neural networks are inspired by biological neurons"},
            ],
        )

    def test_reflect_basic(self, client, bank_id):
        """Test basic reflect operation."""
        response = client.reflect(
            bank_id=bank_id,
            query="What do you think about artificial intelligence?",
        )

        assert response is not None
        assert response.text is not None
        assert len(response.text) > 0

    def test_reflect_with_context(self, client, bank_id):
        """Test reflect with additional context."""
        response = client.reflect(
            bank_id=bank_id,
            query="Should I learn Python?",
            context="I'm interested in starting a career in data science",
        )

        assert response is not None
        assert response.text is not None
        assert len(response.text) > 0

    def test_reflect_with_max_tokens(self, client, bank_id):
        """Test reflect with custom max_tokens parameter."""
        response = client.reflect(
            bank_id=bank_id,
            query="What do you think about Python?",
            max_tokens=500,
        )

        assert response is not None
        assert response.text is not None
        assert len(response.text) > 0

    def test_reflect_with_structured_output(self, client, bank_id):
        """Test reflect with structured output via response_schema.

        When response_schema is provided, the response returns structured_output
        field parsed according to the provided JSON schema.
        """

        from pydantic import BaseModel

        # Define schema using Pydantic model
        class RecommendationResponse(BaseModel):
            recommendation: str
            reasons: list[str]
            confidence: str | None = None  # Optional for LLM flexibility

        response = client.reflect(
            bank_id=bank_id,
            query="What programming language should I learn for data science?",
            response_schema=RecommendationResponse.model_json_schema(),
            max_tokens=10000,
        )

        assert response is not None

        # Verify structured output is present and can be parsed into model
        assert response.structured_output is not None
        result = RecommendationResponse.model_validate(response.structured_output)
        assert result.recommendation
        assert isinstance(result.reasons, list)


class TestListMemories:
    """Tests for listing memories."""

    @pytest.fixture(autouse=True)
    def setup_memories(self, client, bank_id):
        """Setup: Store some test memories synchronously."""
        client.retain_batch(
            bank_id=bank_id,
            items=[{"content": f"Alice likes topic number {i}"} for i in range(5)],
            retain_async=False,  # Wait for fact extraction to complete
        )

    def test_list_all_memories(self, client, bank_id):
        """Test listing all memories."""
        response = client.list_memories(bank_id=bank_id)

        assert response is not None
        assert response.items is not None
        assert response.total is not None
        assert len(response.items) > 0

    def test_list_with_pagination(self, client, bank_id):
        """Test listing with pagination."""
        response = client.list_memories(
            bank_id=bank_id,
            limit=2,
            offset=0,
        )

        assert response is not None
        assert response.items is not None
        assert len(response.items) <= 2


class TestEndToEndWorkflow:
    """End-to-end workflow tests."""

    def test_complete_workflow(self, client):
        """Test a complete workflow: create bank, store, search, reflect."""
        workflow_bank_id = "workflow_test_" + datetime.now().strftime("%Y%m%d_%H%M%S")

        # 1. Create bank
        client.create_bank(
            bank_id=workflow_bank_id,
            mission="I am a software engineer who loves Python programming.",
        )

        # 2. Store memories
        store_response = client.retain_batch(
            bank_id=workflow_bank_id,
            items=[
                {"content": "I completed a project using FastAPI"},
                {"content": "I learned about async programming in Python"},
                {"content": "I enjoy working on open source projects"},
            ],
        )
        assert store_response.success is True

        # 3. Search for relevant memories
        search_results = client.recall(
            bank_id=workflow_bank_id,
            query="What programming technologies do I use?",
        )
        assert len(search_results.results) > 0

        # 4. Generate contextual answer
        reflect_response = client.reflect(
            bank_id=workflow_bank_id,
            query="What are my professional interests?",
        )
        assert reflect_response.text is not None
        assert len(reflect_response.text) > 0


class TestBankStats:
    """Tests for bank statistics endpoint."""

    @pytest.fixture(autouse=True)
    def setup_memories(self, client, bank_id):
        """Setup: Store some test memories."""
        client.retain_batch(
            bank_id=bank_id,
            items=[
                {"content": "Alice likes Python programming"},
                {"content": "Bob enjoys hiking in the mountains"},
            ],
            retain_async=False,
        )

    @pytest.mark.asyncio
    async def test_get_bank_stats(self, client, bank_id):
        """Test getting bank statistics."""
        from hindsight_client_api import ApiClient, Configuration
        from hindsight_client_api.api import BanksApi

        config = Configuration(host=HINDSIGHT_API_URL)
        api_client = ApiClient(config)
        api = BanksApi(api_client)
        stats = await api.get_agent_stats(bank_id=bank_id)

        assert stats is not None
        assert stats.bank_id == bank_id
        assert stats.total_nodes >= 0
        assert stats.total_links >= 0
        assert stats.total_documents >= 0
        assert isinstance(stats.nodes_by_fact_type, dict)
        assert isinstance(stats.links_by_link_type, dict)


class TestOperations:
    """Tests for operations endpoints."""

    @pytest.mark.asyncio
    async def test_list_operations(self, client, bank_id):
        """Test listing operations."""
        from hindsight_client_api import ApiClient, Configuration
        from hindsight_client_api.api import OperationsApi

        # First create an async operation using aretain_batch
        await client.aretain_batch(
            bank_id=bank_id,
            items=[{"content": "Test content for async operation"}],
            retain_async=True,
        )

        config = Configuration(host=HINDSIGHT_API_URL)
        api_client = ApiClient(config)
        api = OperationsApi(api_client)
        response = await api.list_operations(bank_id=bank_id)

        assert response is not None
        assert response.bank_id == bank_id
        assert isinstance(response.operations, list)


class TestDocuments:
    """Tests for document endpoints."""

    def test_delete_document(self, client, bank_id):
        """Test deleting a document."""
        import asyncio

        from hindsight_client_api import ApiClient, Configuration
        from hindsight_client_api.api import DocumentsApi

        # First create a document using sync retain
        doc_id = f"test-doc-{uuid.uuid4().hex[:8]}"
        client.retain(
            bank_id=bank_id,
            content="Test document content for deletion",
            document_id=doc_id,
        )

        # Use async API for delete (run in event loop)
        async def do_delete():
            config = Configuration(host=HINDSIGHT_API_URL)
            api_client = ApiClient(config)
            api = DocumentsApi(api_client)

            # Try to delete
            response = await api.delete_document(bank_id=bank_id, document_id=doc_id)
            return response

        response = asyncio.get_event_loop().run_until_complete(do_delete())

        assert response is not None
        assert response.success is True
        assert response.document_id == doc_id
        assert response.memory_units_deleted >= 0

    def test_get_document(self, client, bank_id):
        """Test getting a document."""
        import asyncio

        from hindsight_client_api import ApiClient, Configuration
        from hindsight_client_api.api import DocumentsApi

        # First create a document
        doc_id = f"test-doc-{uuid.uuid4().hex[:8]}"
        client.retain(
            bank_id=bank_id,
            content="Test document content for retrieval",
            document_id=doc_id,
        )

        async def do_get():
            config = Configuration(host=HINDSIGHT_API_URL)
            api_client = ApiClient(config)
            api = DocumentsApi(api_client)
            return await api.get_document(bank_id=bank_id, document_id=doc_id)

        document = asyncio.get_event_loop().run_until_complete(do_get())

        assert document is not None
        assert document.id == doc_id
        assert "Test document content" in document.original_text


class TestEntities:
    """Tests for entity endpoints."""

    @pytest.fixture(autouse=True)
    def setup_memories(self, client, bank_id):
        """Setup: Store memories that will generate entities."""
        client.retain_batch(
            bank_id=bank_id,
            items=[
                {"content": "Alice works at Google as a software engineer"},
                {"content": "Bob is friends with Alice and works at Microsoft"},
            ],
            retain_async=False,
        )

    def test_list_entities(self, client, bank_id):
        """Test listing entities."""
        import asyncio

        from hindsight_client_api import ApiClient, Configuration
        from hindsight_client_api.api import EntitiesApi

        async def do_list():
            config = Configuration(host=HINDSIGHT_API_URL)
            api_client = ApiClient(config)
            api = EntitiesApi(api_client)
            return await api.list_entities(bank_id=bank_id)

        response = asyncio.get_event_loop().run_until_complete(do_list())

        assert response is not None
        assert response.items is not None
        assert isinstance(response.items, list)
        # Verify pagination fields
        assert response.total is not None
        assert response.limit is not None
        assert response.offset is not None
        assert response.offset == 0
        assert response.limit == 100  # default limit

    def test_list_entities_with_pagination(self, client, bank_id):
        """Test listing entities with pagination parameters."""
        import asyncio

        from hindsight_client_api import ApiClient, Configuration
        from hindsight_client_api.api import EntitiesApi

        async def do_list_paginated():
            config = Configuration(host=HINDSIGHT_API_URL)
            api_client = ApiClient(config)
            api = EntitiesApi(api_client)

            # Test with custom limit
            response = await api.list_entities(bank_id=bank_id, limit=5, offset=0)
            assert response.limit == 5
            assert response.offset == 0
            assert len(response.items) <= 5

            # Test with offset
            response_offset = await api.list_entities(bank_id=bank_id, limit=1, offset=1)
            assert response_offset.offset == 1
            assert response_offset.limit == 1

            return response

        asyncio.get_event_loop().run_until_complete(do_list_paginated())

    def test_get_entity(self, client, bank_id):
        """Test getting a specific entity."""
        import asyncio

        from hindsight_client_api import ApiClient, Configuration
        from hindsight_client_api.api import EntitiesApi

        async def do_test():
            config = Configuration(host=HINDSIGHT_API_URL)
            api_client = ApiClient(config)
            api = EntitiesApi(api_client)

            # First list entities to get an ID
            list_response = await api.list_entities(bank_id=bank_id)

            if list_response.items and len(list_response.items) > 0:
                entity_id = list_response.items[0].id

                # Get the entity
                entity = await api.get_entity(bank_id=bank_id, entity_id=entity_id)
                return entity_id, entity
            return None, None

        entity_id, entity = asyncio.get_event_loop().run_until_complete(do_test())

        if entity_id:
            assert entity is not None
            assert entity.id == entity_id



class TestTags:
    """Tests for tags filtering functionality."""

    @pytest.fixture(autouse=True)
    def setup_memories(self, client, bank_id):
        """Setup: Store memories with different tags."""
        client.retain_batch(
            bank_id=bank_id,
            items=[
                {"content": "Alice presented the Q3 roadmap at the Monday standup", "tags": ["project_x", "meetings"]},
                {"content": "Bob wrote the architecture document for the new auth system", "tags": ["project_x", "docs"]},
                {"content": "Charlie led the sprint planning session for the mobile app", "tags": ["project_y", "meetings"]},
                {"content": "Diana announced the company picnic for next Friday", "tags": ["company"]},
                {"content": "Eve mentioned she likes pineapple on pizza"},  # no tags
            ],
            retain_async=False,
        )

    def test_recall_with_tags_any(self, client, bank_id):
        """Test recall with tags using 'any' match (includes untagged)."""
        response = client.recall(
            bank_id=bank_id,
            query="What has everyone been working on?",
            tags=["project_x"],
            tags_match="any",
            max_tokens=16000,
        )

        assert response is not None
        assert response.results is not None
        assert len(response.results) > 0
        # 'any' mode: results should include items matching the tag or untagged items
        result_tags = [set(r.tags) if r.tags else set() for r in response.results]
        assert any("project_x" in tags for tags in result_tags)

    def test_recall_with_tags_any_strict(self, client, bank_id):
        """Test recall with tags using 'any_strict' match (excludes untagged)."""
        response = client.recall(
            bank_id=bank_id,
            query="What has everyone been working on?",
            tags=["project_x"],
            tags_match="any_strict",
            max_tokens=16000,
        )

        assert response is not None
        assert response.results is not None
        assert len(response.results) > 0
        # any_strict: every result must have the project_x tag
        for r in response.results:
            assert r.tags is not None
            assert "project_x" in r.tags

    def test_recall_with_tags_all_strict(self, client, bank_id):
        """Test recall with tags using 'all_strict' match (AND matching)."""
        response = client.recall(
            bank_id=bank_id,
            query="What has everyone been working on?",
            tags=["project_x", "meetings"],
            tags_match="all_strict",
            max_tokens=16000,
        )

        assert response is not None
        assert response.results is not None
        assert len(response.results) > 0
        # all_strict: every result must have BOTH tags
        for r in response.results:
            assert r.tags is not None
            assert "project_x" in r.tags
            assert "meetings" in r.tags

    def test_recall_with_multiple_tags_any(self, client, bank_id):
        """Test recall with multiple tags using 'any_strict' match (OR)."""
        response = client.recall(
            bank_id=bank_id,
            query="What has everyone been working on?",
            tags=["project_x", "project_y"],
            tags_match="any_strict",
            max_tokens=16000,
        )

        assert response is not None
        assert response.results is not None
        assert len(response.results) > 0
        # any_strict with multiple tags: every result must have at least one of the tags
        for r in response.results:
            assert r.tags is not None
            assert "project_x" in r.tags or "project_y" in r.tags

    def test_reflect_with_tags(self, client, bank_id):
        """Test reflect with tags filtering."""
        response = client.reflect(
            bank_id=bank_id,
            query="Summarize project X activities",
            tags=["project_x"],
            tags_match="any_strict",
        )

        assert response is not None
        assert response.text is not None
        assert len(response.text) > 0

    def test_retain_with_tags(self, client, bank_id):
        """Test storing a memory with tags."""
        response = client.retain(
            bank_id=bank_id,
            content="Frank deployed the billing microservice to production on Tuesday",
            tags=["project_z", "features"],
        )

        assert response is not None
        assert response.success is True

        # Verify we can recall it with the tag
        recall_response = client.recall(
            bank_id=bank_id,
            query="What did Frank deploy?",
            tags=["project_z"],
            tags_match="any_strict",
            max_tokens=16000,
        )
        assert recall_response is not None
        assert len(recall_response.results) > 0
        # any_strict: every result must have the project_z tag
        for r in recall_response.results:
            assert r.tags is not None
            assert "project_z" in r.tags

    def test_retain_batch_with_document_tags(self, client, bank_id):
        """Test batch retain with document-level tags."""
        response = client.retain_batch(
            bank_id=bank_id,
            items=[
                {"content": "First item in batch"},
                {"content": "Second item in batch"},
            ],
            document_tags=["batch_import", "test_data"],
            retain_async=False,
        )

        assert response is not None
        assert response.success is True
        assert response.items_count == 2


class TestDeleteBank:
    """Tests for bank deletion."""

    def test_delete_bank(self, client):
        """Test deleting a bank."""
        import asyncio

        from hindsight_client_api import ApiClient, Configuration
        from hindsight_client_api.api import BanksApi

        # Create a unique bank for this test
        bank_id = f"test_bank_delete_{uuid.uuid4().hex[:12]}"

        # Create bank with some data
        client.create_bank(
            bank_id=bank_id,
            mission="This bank will be deleted",
        )
        client.retain(
            bank_id=bank_id,
            content="Some memory to store",
        )

        async def do_delete():
            config = Configuration(host=HINDSIGHT_API_URL)
            api_client = ApiClient(config)
            api = BanksApi(api_client)
            return await api.delete_bank(bank_id=bank_id)

        response = asyncio.get_event_loop().run_until_complete(do_delete())

        assert response is not None
        assert response.success is True

        # Verify bank data is deleted - memories should be gone
        memories = client.list_memories(bank_id=bank_id)
        assert memories.total == 0


class TestMission:
    """Tests for mission operations."""

    def test_set_mission(self, client, bank_id):
        """Test setting a bank's mission."""
        response = client.set_mission(
            bank_id=bank_id,
            mission="Be a helpful PM tracking sprint progress and team capacity",
        )

        assert response is not None
        assert response.bank_id == bank_id
        assert response.mission == "Be a helpful PM tracking sprint progress and team capacity"


class TestAsyncRecall:
    """Tests for async recall with full feature parity."""

    @pytest.fixture(autouse=True)
    async def setup_memories(self, client, bank_id):
        """Setup: Store some test memories before search tests."""
        await client.aretain_batch(
            bank_id=bank_id,
            items=[
                {"content": "Alice loves programming in Python"},
                {"content": "Bob enjoys hiking and outdoor adventures"},
                {"content": "Charlie is interested in quantum physics"},
            ],
        )

    @pytest.mark.asyncio
    async def test_arecall_returns_recall_response(self, client, bank_id):
        """Test that arecall returns a RecallResponse (not a list)."""
        from hindsight_client_api.models.recall_response import RecallResponse

        response = await client.arecall(
            bank_id=bank_id,
            query="What does Alice like?",
        )

        assert isinstance(response, RecallResponse)
        assert response.results is not None
        assert len(response.results) > 0

    @pytest.mark.asyncio
    async def test_arecall_with_include_chunks(self, client, bank_id):
        """Test arecall with include_chunks returns raw text chunks."""
        response = await client.arecall(
            bank_id=bank_id,
            query="programming",
            include_chunks=True,
        )

        assert response is not None
        assert response.results is not None
        # chunks should be present (may be empty dict if not yet consolidated)
        assert response.chunks is not None

    @pytest.mark.asyncio
    async def test_arecall_with_include_entities(self, client, bank_id):
        """Test arecall with include_entities."""
        response = await client.arecall(
            bank_id=bank_id,
            query="Alice",
            include_entities=True,
            max_entity_tokens=500,
        )

        assert response is not None
        assert response.results is not None

    @pytest.mark.asyncio
    async def test_arecall_with_trace(self, client, bank_id):
        """Test arecall with trace enabled."""
        response = await client.arecall(
            bank_id=bank_id,
            query="outdoor activities",
            trace=True,
        )

        assert response is not None
        assert response.results is not None

    @pytest.mark.asyncio
    async def test_arecall_full_featured(self, client, bank_id):
        """Test arecall with all parameters."""
        response = await client.arecall(
            bank_id=bank_id,
            query="What are people's interests?",
            types=["world"],
            max_tokens=2048,
            budget="high",
            trace=True,
            include_chunks=True,
            max_chunk_tokens=4096,
            include_entities=True,
            max_entity_tokens=500,
        )

        assert response is not None
        assert response.results is not None
        assert response.chunks is not None


class TestAsyncReflect:
    """Tests for async reflect with full feature parity."""

    @pytest.fixture(autouse=True)
    async def setup_memories(self, client, bank_id):
        """Setup: Store some test memories and bank background."""
        await client.acreate_bank(
            bank_id=bank_id,
            mission="I am a helpful AI assistant interested in technology and science.",
        )

        await client.aretain_batch(
            bank_id=bank_id,
            items=[
                {"content": "The Python programming language is great for data science"},
                {"content": "Machine learning models can recognize patterns in data"},
            ],
        )

    @pytest.mark.asyncio
    async def test_areflect_with_max_tokens(self, client, bank_id):
        """Test areflect with max_tokens parameter."""
        response = await client.areflect(
            bank_id=bank_id,
            query="What do you think about Python?",
            max_tokens=500,
        )

        assert response is not None
        assert response.text is not None
        assert len(response.text) > 0

    @pytest.mark.asyncio
    async def test_areflect_with_structured_output(self, client, bank_id):
        """Test areflect with response_schema for structured output."""
        from pydantic import BaseModel

        class RecommendationResponse(BaseModel):
            recommendation: str
            reasons: list[str]
            confidence: str | None = None

        response = await client.areflect(
            bank_id=bank_id,
            query="What programming language should I learn for data science?",
            response_schema=RecommendationResponse.model_json_schema(),
            max_tokens=10000,
        )

        assert response is not None
        assert response.structured_output is not None
        result = RecommendationResponse.model_validate(response.structured_output)
        assert result.recommendation
        assert isinstance(result.reasons, list)


class TestAsyncDeleteBank:
    """Tests for async bank deletion."""

    @pytest.mark.asyncio
    async def test_adelete_bank(self, client):
        """Test deleting a bank using the async method."""
        bank_id = f"test_bank_adelete_{uuid.uuid4().hex[:12]}"

        # Create bank with some data using async API
        await client.acreate_bank(
            bank_id=bank_id,
            mission="This bank will be deleted via async",
        )
        await client.aretain(
            bank_id=bank_id,
            content="Some memory to store",
        )

        # Delete using async method
        response = await client.adelete_bank(bank_id=bank_id)

        assert response is not None
        assert response.success is True
