#!/usr/bin/env python3
"""
Test Echo Loop Receipt Chain

WO-ECHO-FIBPI3D-LOOP-01 Phase 3: Integration tests for the full
Echo->Tool->Echo loop with receipt chain verification.

Tests:
1. Geometry prompts trigger full loop with receipt
2. Router decision correctly detects geometry intent
3. Loop receipt has valid chain hash
4. Se metadata is present (non-blocking)
5. Receipt is saved to receipts/echo_loops/

Requires: Running basin_server on port 8001
"""

import hashlib
import json
import os
import pytest
import requests
from datetime import datetime
from pathlib import Path

# Configuration
ENDPOINT = os.environ.get("SPATIAL_ENDPOINT", "http://127.0.0.1:8001")
RECEIPTS_DIR = Path(__file__).parent.parent / "receipts" / "echo_loops"


def server_is_healthy():
    """Check if the server is running and healthy."""
    try:
        resp = requests.get(f"{ENDPOINT}/health", timeout=2)
        return resp.ok
    except requests.exceptions.RequestException:
        return False


# Skip all tests if server is not running
pytestmark = pytest.mark.skipif(
    not server_is_healthy(),
    reason=f"Echo server not running on {ENDPOINT}"
)


class TestGeometryDetection:
    """Test geometry intent detection via the loop endpoint."""

    def test_geometry_prompt_detected(self):
        """Verify geometry prompt triggers detection."""
        resp = requests.post(
            f"{ENDPOINT}/echo/geometry-loop",
            json={"prompt": "What is the distance between obj_0000 and obj_0001?"},
            timeout=10
        )
        assert resp.ok, f"Request failed: {resp.text}"

        data = resp.json()
        assert data["is_geometry"] is True
        assert data["router_decision"]["is_geometry"] is True
        assert data["router_decision"]["confidence"] > 0

    def test_non_geometry_prompt_rejected(self):
        """Verify non-geometry prompt is not detected."""
        resp = requests.post(
            f"{ENDPOINT}/echo/geometry-loop",
            json={"prompt": "Hello, how are you today?"},
            timeout=10
        )
        assert resp.ok

        data = resp.json()
        assert data["is_geometry"] is False
        assert data["loop_receipt"] is None

    def test_keyword_matching(self):
        """Verify keywords are correctly matched."""
        prompts_and_expected = [
            ("Find the 5 nearest objects to obj_0007", ["nearest", "objects"]),
            ("Is obj_0020 near obj_0025?", ["near"]),
            ("What is the distance between obj_0010 and obj_0050?", ["distance"]),
            ("Is obj_0005 to the left of obj_0015?", ["left of"]),
            ("Is obj_0030 above obj_0040?", ["above"]),
        ]

        for prompt, expected_keywords in prompts_and_expected:
            resp = requests.post(
                f"{ENDPOINT}/echo/geometry-loop",
                json={"prompt": prompt},
                timeout=10
            )
            assert resp.ok

            data = resp.json()
            assert data["is_geometry"] is True, f"Failed for: {prompt}"

            matched = data["router_decision"]["matched_keywords"]
            for kw in expected_keywords:
                assert any(kw in m for m in matched), \
                    f"Expected '{kw}' in matched keywords for '{prompt}', got {matched}"


class TestLoopReceipt:
    """Test that loop receipts are generated correctly."""

    def test_loop_receipt_generated(self):
        """Verify loop receipt is generated for geometry prompt."""
        resp = requests.post(
            f"{ENDPOINT}/echo/geometry-loop",
            json={
                "prompt": "Find the 5 nearest objects to obj_0000",
                "scene_id": "test_loop_receipt"
            },
            timeout=10
        )
        assert resp.ok

        data = resp.json()
        assert data["loop_receipt"] is not None

        receipt = data["loop_receipt"]
        assert receipt["loop_id"]
        assert receipt["timestamp_utc"]
        assert receipt["prompt_sha256"]
        assert receipt["tool_receipt_id"]
        assert receipt["chain_hash"]
        assert receipt["answer"]

    def test_loop_receipt_has_router_decision(self):
        """Verify loop receipt contains router decision."""
        resp = requests.post(
            f"{ENDPOINT}/echo/geometry-loop",
            json={"prompt": "What is the distance between obj_0010 and obj_0050?"},
            timeout=10
        )
        assert resp.ok

        receipt = resp.json()["loop_receipt"]
        router = receipt["router_decision"]

        assert router["is_geometry"] is True
        assert "matched_keywords" in router
        assert "confidence" in router

    def test_loop_receipt_has_tool_info(self):
        """Verify loop receipt contains tool call information."""
        resp = requests.post(
            f"{ENDPOINT}/echo/geometry-loop",
            json={"prompt": "Is obj_0020 near obj_0025 with threshold 10?"},
            timeout=10
        )
        assert resp.ok

        receipt = resp.json()["loop_receipt"]

        assert receipt["tool_endpoint"] == "/echo/spatial"
        assert "queries" in receipt["tool_request"]
        assert "scene_hash" in receipt["tool_response"]
        assert receipt["tool_scene_hash"]


class TestChainHash:
    """Test chain hash computation and verification."""

    def test_chain_hash_present(self):
        """Verify chain hash is present in receipt."""
        resp = requests.post(
            f"{ENDPOINT}/echo/geometry-loop",
            json={"prompt": "Find the 3 closest neighbors to obj_0042"},
            timeout=10
        )
        assert resp.ok

        receipt = resp.json()["loop_receipt"]
        assert receipt["chain_hash"]
        assert len(receipt["chain_hash"]) == 64  # SHA256 hex

    def test_chain_hash_verifiable(self):
        """Verify chain hash can be recomputed."""
        resp = requests.post(
            f"{ENDPOINT}/echo/geometry-loop",
            json={"prompt": "What is the distance between obj_0001 and obj_0099?"},
            timeout=10
        )
        assert resp.ok

        receipt = resp.json()["loop_receipt"]

        # Recompute chain hash
        prompt_sha = receipt["prompt_sha256"]
        router = receipt["router_decision"]
        tool_receipt_id = receipt["tool_receipt_id"]
        answer_sha = receipt["answer_sha256"]

        data = f"{prompt_sha}|{json.dumps(router, sort_keys=True)}|{tool_receipt_id}|{answer_sha}"
        expected_hash = hashlib.sha256(data.encode()).hexdigest()

        assert receipt["chain_hash"] == expected_hash, \
            f"Chain hash mismatch: expected {expected_hash}, got {receipt['chain_hash']}"

    def test_prompt_sha_matches(self):
        """Verify prompt SHA256 matches actual prompt."""
        prompt = "Is obj_0005 to the left of obj_0015?"

        resp = requests.post(
            f"{ENDPOINT}/echo/geometry-loop",
            json={"prompt": prompt},
            timeout=10
        )
        assert resp.ok

        receipt = resp.json()["loop_receipt"]

        expected_sha = hashlib.sha256(prompt.encode()).hexdigest()
        assert receipt["prompt_sha256"] == expected_sha


class TestSeMetadata:
    """Test that Se metrics are included in loop receipt."""

    def test_se_metadata_present(self):
        """Verify Se metadata is in loop receipt."""
        resp = requests.post(
            f"{ENDPOINT}/echo/geometry-loop",
            json={"prompt": "Find the 10 nearest objects to obj_0050"},
            timeout=10
        )
        assert resp.ok

        receipt = resp.json()["loop_receipt"]
        se = receipt["se_metadata"]

        assert se is not None
        assert "H" in se
        assert "C" in se
        assert "D" in se
        assert "Se" in se
        assert "regime" in se
        assert "classification_mode" in se

    def test_se_is_non_blocking(self):
        """Verify loop completes even with various Se values."""
        # Various prompts should all complete successfully
        prompts = [
            "Find the 5 nearest objects to obj_0000",
            "What is the distance between obj_0001 and obj_0002?",
            "Is obj_0010 above obj_0020?",
        ]

        for prompt in prompts:
            resp = requests.post(
                f"{ENDPOINT}/echo/geometry-loop",
                json={"prompt": prompt},
                timeout=10
            )
            assert resp.ok, f"Failed for prompt: {prompt}"
            assert resp.json()["loop_receipt"]["status"] == "PASS"


class TestReceiptPersistence:
    """Test that receipts are saved to disk."""

    def test_receipt_saved(self):
        """Verify receipt is saved to receipts/echo_loops/."""
        resp = requests.post(
            f"{ENDPOINT}/echo/geometry-loop",
            json={"prompt": "Find the nearest object to obj_0099"},
            timeout=10
        )
        assert resp.ok

        data = resp.json()
        receipt_path = data.get("receipt_path")

        assert receipt_path is not None
        assert "echo_loops" in receipt_path

        # Verify file exists (adjust path to be absolute)
        base_path = Path(__file__).parent.parent
        full_path = base_path / receipt_path

        assert full_path.exists(), f"Receipt file not found: {full_path}"

        # Verify content
        with open(full_path) as f:
            saved_receipt = json.load(f)

        assert saved_receipt["loop_id"] == data["loop_receipt"]["loop_id"]
        assert saved_receipt["chain_hash"] == data["loop_receipt"]["chain_hash"]

    def test_receipt_has_correct_structure(self):
        """Verify saved receipt has all required fields."""
        resp = requests.post(
            f"{ENDPOINT}/echo/geometry-loop",
            json={"prompt": "What is the distance between obj_0025 and obj_0075?"},
            timeout=10
        )
        assert resp.ok

        data = resp.json()
        receipt_path = data.get("receipt_path")

        base_path = Path(__file__).parent.parent
        full_path = base_path / receipt_path

        with open(full_path) as f:
            receipt = json.load(f)

        # Required fields
        required_fields = [
            "loop_id", "timestamp_utc", "work_order",
            "prompt", "prompt_sha256",
            "router_decision",
            "tool_endpoint", "tool_request", "tool_response",
            "tool_receipt_id", "tool_scene_hash",
            "answer", "answer_sha256",
            "chain_hash", "se_metadata", "status"
        ]

        for field in required_fields:
            assert field in receipt, f"Missing required field: {field}"


class TestQueryParsing:
    """Test that prompts are correctly parsed into queries."""

    def test_k_nearest_parsing(self):
        """Verify k_nearest query parsing."""
        resp = requests.post(
            f"{ENDPOINT}/echo/geometry-loop",
            json={"prompt": "Find the 7 nearest objects to obj_0042"},
            timeout=10
        )
        assert resp.ok

        receipt = resp.json()["loop_receipt"]
        queries = receipt["tool_request"]["queries"]

        assert len(queries) >= 1
        assert queries[0]["query_type"] == "k_nearest"
        assert queries[0]["a"] == "obj_0042"
        assert queries[0]["k"] == 7

    def test_distance_parsing(self):
        """Verify distance query parsing."""
        resp = requests.post(
            f"{ENDPOINT}/echo/geometry-loop",
            json={"prompt": "What is the distance between obj_0010 and obj_0050?"},
            timeout=10
        )
        assert resp.ok

        receipt = resp.json()["loop_receipt"]
        queries = receipt["tool_request"]["queries"]

        assert len(queries) >= 1
        assert queries[0]["query_type"] == "distance"
        assert queries[0]["a"] == "obj_0010"
        assert queries[0]["b"] == "obj_0050"

    def test_near_parsing(self):
        """Verify near query parsing."""
        resp = requests.post(
            f"{ENDPOINT}/echo/geometry-loop",
            json={"prompt": "Is obj_0020 near obj_0025? Use a threshold of 5.0 units"},
            timeout=10
        )
        assert resp.ok

        receipt = resp.json()["loop_receipt"]
        queries = receipt["tool_request"]["queries"]

        assert len(queries) >= 1
        assert queries[0]["query_type"] == "near"


class TestDeterminism:
    """Test that loop results are deterministic."""

    def test_same_prompt_same_results(self):
        """Verify same prompt produces same tool results."""
        prompt = "Find the 5 nearest objects to obj_0000"

        resp1 = requests.post(
            f"{ENDPOINT}/echo/geometry-loop",
            json={"prompt": prompt, "scene_id": "determinism_test"},
            timeout=10
        )
        resp2 = requests.post(
            f"{ENDPOINT}/echo/geometry-loop",
            json={"prompt": prompt, "scene_id": "determinism_test"},
            timeout=10
        )

        assert resp1.ok and resp2.ok

        receipt1 = resp1.json()["loop_receipt"]
        receipt2 = resp2.json()["loop_receipt"]

        # Scene hash should be the same
        assert receipt1["tool_scene_hash"] == receipt2["tool_scene_hash"]

        # Tool response results should match (excluding latency)
        results1 = receipt1["tool_response"]["results"]
        results2 = receipt2["tool_response"]["results"]

        for r1, r2 in zip(results1, results2):
            assert r1["query_type"] == r2["query_type"]
            assert r1["result"] == r2["result"]


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