"""
Tests for /echo/ask endpoint (WO-ECHO-ASK-02)

Tests covering:
- POST /echo/ask with mode=search returns evidence list and receipt
- mode=answer returns answer + confidence + receipt
- mode=receipt finds receipt content
- Receipt chain hash recomputes and matches
- Truncation limits hold
- Error handling when index unavailable
"""

import hashlib
import json
import pytest
from pathlib import Path
from unittest.mock import patch, MagicMock
from datetime import datetime, timezone

# Configure pytest-anyio for async tests
pytest_plugins = ('anyio',)


@pytest.fixture
def anyio_backend():
    return 'asyncio'


# Import models and helpers
from helix_cdc.echo.ops.models import (
    AskMode,
    AskRequest,
    AskResponse,
    AskEvidence,
    AskReceipt,
)
from helix_cdc.echo.ops.endpoints import (
    _compute_ask_chain_hash,
    _get_memory_index,
)
from helix_cdc.echo.ops.helpers import PROJECT_ROOT


class TestAskChainHash:
    """Tests for chain hash computation."""

    def test_chain_hash_deterministic(self):
        """Test that chain hash is deterministic."""
        hash1 = _compute_ask_chain_hash("qsha", "answer", 8, "rsha", "esha", "OK")
        hash2 = _compute_ask_chain_hash("qsha", "answer", 8, "rsha", "esha", "OK")
        assert hash1 == hash2

    def test_chain_hash_is_sha256(self):
        """Test that chain hash is valid SHA256."""
        result = _compute_ask_chain_hash("q", "answer", 8, "r", "e", "OK")
        assert len(result) == 64
        assert all(c in "0123456789abcdef" for c in result)

    def test_chain_hash_changes_with_input(self):
        """Test that chain hash changes when any input changes."""
        base = _compute_ask_chain_hash("q", "answer", 8, "r", "e", "OK")

        # Change each parameter
        assert base != _compute_ask_chain_hash("q2", "answer", 8, "r", "e", "OK")
        assert base != _compute_ask_chain_hash("q", "search", 8, "r", "e", "OK")
        assert base != _compute_ask_chain_hash("q", "answer", 10, "r", "e", "OK")
        assert base != _compute_ask_chain_hash("q", "answer", 8, "r2", "e", "OK")
        assert base != _compute_ask_chain_hash("q", "answer", 8, "r", "e2", "OK")
        assert base != _compute_ask_chain_hash("q", "answer", 8, "r", "e", "ERROR")


class TestAskRequestModel:
    """Tests for AskRequest model validation."""

    def test_basic_request(self):
        """Test basic request creation."""
        req = AskRequest(query="what is Se?")
        assert req.query == "what is Se?"
        assert req.k == 8
        assert req.mode == AskMode.ANSWER
        assert req.include_chunks is False

    def test_search_mode(self):
        """Test search mode request."""
        req = AskRequest(query="test", mode=AskMode.SEARCH, k=5)
        assert req.mode == AskMode.SEARCH
        assert req.k == 5

    def test_receipt_mode(self):
        """Test receipt mode request."""
        req = AskRequest(
            query="test",
            mode=AskMode.RECEIPT,
            receipt_id="abc123"
        )
        assert req.mode == AskMode.RECEIPT
        assert req.receipt_id == "abc123"

    def test_k_bounds(self):
        """Test k parameter bounds."""
        # Valid
        req = AskRequest(query="test", k=1)
        assert req.k == 1

        req = AskRequest(query="test", k=50)
        assert req.k == 50

        # Invalid (out of bounds)
        with pytest.raises(Exception):
            AskRequest(query="test", k=0)

        with pytest.raises(Exception):
            AskRequest(query="test", k=51)


class TestAskResponseModel:
    """Tests for AskResponse model."""

    def test_ok_response(self):
        """Test OK response creation."""
        resp = AskResponse(
            status="OK",
            mode=AskMode.ANSWER,
            query="test query",
            answer="test answer",
            confidence="HIGH",
            evidence=[],
            receipt_path="receipts/echo_ask/test.json",
            chain_hash="a" * 64,
        )
        assert resp.status == "OK"
        assert resp.answer == "test answer"

    def test_error_response(self):
        """Test error response."""
        resp = AskResponse(
            status="ERROR",
            mode=AskMode.ANSWER,
            query="test",
            evidence=[],
            receipt_path="receipts/echo_ask/test.json",
            chain_hash="a" * 64,
            error="Something went wrong",
        )
        assert resp.status == "ERROR"
        assert resp.error == "Something went wrong"

    def test_evidence_items(self):
        """Test evidence items in response."""
        evidence = [
            AskEvidence(
                source="file.py:L10-L20",
                sha256="abc123",
                snippet="some code here",
                source_type="code",
                score=0.85,
            )
        ]
        resp = AskResponse(
            status="OK",
            mode=AskMode.SEARCH,
            query="test",
            evidence=evidence,
            receipt_path="test.json",
            chain_hash="a" * 64,
        )
        assert len(resp.evidence) == 1
        assert resp.evidence[0].source == "file.py:L10-L20"
        assert resp.evidence[0].score == 0.85


class TestAskReceiptModel:
    """Tests for AskReceipt model."""

    def test_receipt_structure(self):
        """Test receipt has all required fields."""
        receipt = AskReceipt(
            timestamp_utc=datetime.now(timezone.utc).isoformat(),
            receipt_id="abc12345",
            query="test query",
            query_sha256="q" * 64,
            mode="answer",
            k=8,
            result_sha256="r" * 64,
            evidence_sha256="e" * 64,
            answer_preview="test answer",
            evidence_preview=["file1.py", "file2.py"],
            status="OK",
            chain_hash="c" * 64,
        )

        assert receipt.receipt_type == "echo_ask"
        assert receipt.work_order == "WO-ECHO-ASK-02"
        assert receipt.query == "test query"
        assert receipt.mode == "answer"
        assert receipt.k == 8
        assert receipt.status == "OK"

    def test_receipt_chain_hash_verifiable(self):
        """Test that receipt chain hash can be recomputed."""
        query = "test query"
        mode = "answer"
        k = 8
        result = "test answer"
        evidence = []

        query_sha = hashlib.sha256(query.encode()).hexdigest()
        result_sha = hashlib.sha256(result.encode()).hexdigest()
        evidence_sha = hashlib.sha256(json.dumps(evidence).encode()).hexdigest()

        chain_hash = _compute_ask_chain_hash(
            query_sha, mode, k, result_sha, evidence_sha, "OK"
        )

        receipt = AskReceipt(
            timestamp_utc=datetime.now(timezone.utc).isoformat(),
            receipt_id="test123",
            query=query,
            query_sha256=query_sha,
            mode=mode,
            k=k,
            result_sha256=result_sha,
            evidence_sha256=evidence_sha,
            status="OK",
            chain_hash=chain_hash,
        )

        # Verify chain hash
        recomputed = _compute_ask_chain_hash(
            receipt.query_sha256,
            receipt.mode,
            receipt.k,
            receipt.result_sha256,
            receipt.evidence_sha256,
            receipt.status,
        )

        assert recomputed == receipt.chain_hash


class TestMemoryIndexIntegration:
    """Tests for MemoryIndex integration (mocked)."""

    @pytest.fixture
    def mock_memory_index(self):
        """Create a mock MemoryIndex."""
        mock = MagicMock()

        # Mock search result
        mock_chunk = MagicMock()
        mock_chunk.id = "chunk1"
        mock_chunk.text = "This is test content from the index"
        mock_chunk.source_path = "helix_cdc/test.py"
        mock_chunk.source_type = "code"
        mock_chunk.sha256 = "abc123"
        mock_chunk.relevance_score = 0.75

        mock.search.return_value = [mock_chunk]

        # Mock answer result
        mock_answer = MagicMock()
        mock_answer.result = "Based on 1 code source."
        mock_answer.confidence = "MEDIUM"
        mock_answer.evidence = [{
            "source": "helix_cdc/test.py",
            "quote": "This is test content",
            "type": "code",
            "score": 0.75,
        }]
        mock.answer.return_value = mock_answer

        # Mock get_receipt result
        mock.get_receipt.return_value = {
            "receipt_type": "test_execution",
            "status": "PASS",
            "_path": "receipts/test.json",
            "_sha256": "xyz789",
        }

        return mock

    def test_search_mode_with_mock(self, mock_memory_index):
        """Test search mode with mocked index."""
        chunks = mock_memory_index.search("test query", k=5)
        assert len(chunks) == 1
        assert chunks[0].source_path == "helix_cdc/test.py"

    def test_answer_mode_with_mock(self, mock_memory_index):
        """Test answer mode with mocked index."""
        result = mock_memory_index.answer("what is Se?", k=8)
        assert result.confidence == "MEDIUM"
        assert len(result.evidence) == 1

    def test_receipt_mode_with_mock(self, mock_memory_index):
        """Test receipt mode with mocked index."""
        receipt = mock_memory_index.get_receipt("abc123")
        assert receipt["status"] == "PASS"
        assert receipt["receipt_type"] == "test_execution"


class TestTruncationLimits:
    """Tests for output truncation."""

    def test_query_truncation_in_receipt(self):
        """Test that long queries are truncated in receipts."""
        long_query = "a" * 1000
        receipt = AskReceipt(
            timestamp_utc=datetime.now(timezone.utc).isoformat(),
            receipt_id="test",
            query=long_query[:500],  # Should be truncated
            query_sha256="q" * 64,
            mode="answer",
            k=8,
            result_sha256="r" * 64,
            evidence_sha256="e" * 64,
            status="OK",
            chain_hash="c" * 64,
        )
        assert len(receipt.query) <= 500

    def test_answer_preview_truncation(self):
        """Test that answer preview is truncated."""
        long_answer = "b" * 500
        receipt = AskReceipt(
            timestamp_utc=datetime.now(timezone.utc).isoformat(),
            receipt_id="test",
            query="test",
            query_sha256="q" * 64,
            mode="answer",
            k=8,
            result_sha256="r" * 64,
            evidence_sha256="e" * 64,
            answer_preview=long_answer[:200],  # Should be truncated
            status="OK",
            chain_hash="c" * 64,
        )
        assert len(receipt.answer_preview) <= 200

    def test_evidence_preview_limit(self):
        """Test that evidence preview is limited."""
        many_sources = [f"file{i}.py" for i in range(20)]
        receipt = AskReceipt(
            timestamp_utc=datetime.now(timezone.utc).isoformat(),
            receipt_id="test",
            query="test",
            query_sha256="q" * 64,
            mode="search",
            k=8,
            result_sha256="r" * 64,
            evidence_sha256="e" * 64,
            evidence_preview=many_sources[:5],  # Should be limited
            status="OK",
            chain_hash="c" * 64,
        )
        assert len(receipt.evidence_preview) <= 5


class TestEndpointWithTestClient:
    """Integration tests using FastAPI TestClient."""

    @pytest.fixture
    def client(self):
        """Create test client with mocked memory index."""
        from fastapi.testclient import TestClient
        from helix_cdc.echo.ops.endpoints import router
        from fastapi import FastAPI

        app = FastAPI()
        app.include_router(router)

        return TestClient(app)

    def test_ask_endpoint_no_index(self, client):
        """Test /echo/ask returns NO_INDEX when index unavailable."""
        # Reset the global state to simulate no index
        import helix_cdc.echo.ops.endpoints as ep
        ep._memory_index = None
        ep._memory_index_error = "Index not found"

        response = client.post("/echo/ask", json={
            "query": "test query",
            "mode": "answer",
        })

        assert response.status_code == 200
        data = response.json()
        assert data["status"] == "NO_INDEX"
        assert "receipt_path" in data
        assert data["chain_hash"] is not None

        # Reset
        ep._memory_index = None
        ep._memory_index_error = None

    def test_ask_endpoint_with_mock(self, client):
        """Test /echo/ask with mocked MemoryIndex."""
        import helix_cdc.echo.ops.endpoints as ep

        # Create mock
        mock = MagicMock()
        mock_answer = MagicMock()
        mock_answer.result = "Test answer"
        mock_answer.confidence = "HIGH"
        mock_answer.evidence = [{
            "source": "test.py",
            "quote": "code snippet",
            "type": "code",
            "score": 0.9,
        }]
        mock.answer.return_value = mock_answer

        # Inject mock
        ep._memory_index = mock
        ep._memory_index_error = None

        try:
            response = client.post("/echo/ask", json={
                "query": "what is Se?",
                "mode": "answer",
                "k": 5,
            })

            assert response.status_code == 200
            data = response.json()
            assert data["status"] == "OK"
            assert data["answer"] == "Test answer"
            assert data["confidence"] == "HIGH"
            assert len(data["evidence"]) == 1
            assert data["evidence"][0]["source"] == "test.py"
            assert "receipt_path" in data
            assert "chain_hash" in data

            # Verify receipt file was created
            receipt_path = PROJECT_ROOT / data["receipt_path"]
            if receipt_path.exists():
                with open(receipt_path) as f:
                    receipt = json.load(f)
                assert receipt["receipt_type"] == "echo_ask"
                assert receipt["work_order"] == "WO-ECHO-ASK-02"

                # Verify chain hash
                recomputed = _compute_ask_chain_hash(
                    receipt["query_sha256"],
                    receipt["mode"],
                    receipt["k"],
                    receipt["result_sha256"],
                    receipt["evidence_sha256"],
                    receipt["status"],
                )
                assert recomputed == receipt["chain_hash"]

                # Cleanup
                receipt_path.unlink()

        finally:
            # Reset
            ep._memory_index = None
            ep._memory_index_error = None

    def test_search_mode(self, client):
        """Test mode=search returns chunks."""
        import helix_cdc.echo.ops.endpoints as ep

        mock = MagicMock()
        mock_chunk = MagicMock()
        mock_chunk.id = "c1"
        mock_chunk.text = "chunk content"
        mock_chunk.source_path = "file.py"
        mock_chunk.source_type = "code"
        mock_chunk.sha256 = "abc"
        mock_chunk.relevance_score = 0.8
        mock.search.return_value = [mock_chunk]

        ep._memory_index = mock
        ep._memory_index_error = None

        try:
            response = client.post("/echo/ask", json={
                "query": "test",
                "mode": "search",
            })

            assert response.status_code == 200
            data = response.json()
            assert data["status"] == "OK"
            assert data["mode"] == "search"
            assert len(data["evidence"]) == 1
            assert data["evidence"][0]["source"] == "file.py"

        finally:
            ep._memory_index = None
            ep._memory_index_error = None

    def test_receipt_mode(self, client):
        """Test mode=receipt finds receipt."""
        import helix_cdc.echo.ops.endpoints as ep

        mock = MagicMock()
        mock.get_receipt.return_value = {
            "receipt_type": "test",
            "status": "PASS",
            "_path": "receipts/test.json",
            "_sha256": "xyz",
        }

        ep._memory_index = mock
        ep._memory_index_error = None

        try:
            response = client.post("/echo/ask", json={
                "query": "find receipt",
                "mode": "receipt",
                "receipt_id": "abc123",
            })

            assert response.status_code == 200
            data = response.json()
            assert data["status"] == "OK"
            assert data["mode"] == "receipt"
            assert data["receipt_data"] is not None
            assert data["receipt_data"]["status"] == "PASS"

        finally:
            ep._memory_index = None
            ep._memory_index_error = None


if __name__ == "__main__":
    pytest.main([__file__, "-v"])
