"""Tests for TrustMem FastAPI server."""
import pytest
from fastapi.testclient import TestClient

from trustmem.sdk import TrustMem
from trustmem.server import app, _set_tm_for_testing, _set_api_key_for_testing
from trustmem.handler.persistent import PersistentHandler


@pytest.fixture
def client(mock_ai):
    """TestClient with in-memory TrustMem backend."""
    handler = PersistentHandler(database_url=":memory:", ai_client=mock_ai)
    tm = TrustMem._from_handler(handler)
    _set_tm_for_testing(tm)
    _set_api_key_for_testing(None)  # no auth by default
    with TestClient(app) as c:
        c._tm = tm  # expose TrustMem instance for data seeding
        yield c
    _set_tm_for_testing(None)


@pytest.fixture
def auth_client(mock_ai):
    """TestClient with API key authentication enabled."""
    handler = PersistentHandler(database_url=":memory:", ai_client=mock_ai)
    tm = TrustMem._from_handler(handler)
    _set_tm_for_testing(tm)
    _set_api_key_for_testing("test-secret-key")
    with TestClient(app) as c:
        c._tm = tm  # expose TrustMem instance for data seeding
        yield c
    _set_tm_for_testing(None)
    _set_api_key_for_testing(None)


# ==============================================================
# Health & Version
# ==============================================================
class TestHealth:
    def test_health(self, client):
        r = client.get("/health")
        assert r.status_code == 200
        assert r.json()["status"] == "ok"

    def test_version(self, client):
        r = client.get("/v1/version")
        assert r.status_code == 200
        assert "version" in r.json()


# ==============================================================
# Add
# ==============================================================
class TestAdd:
    def test_add_messages(self, client):
        r = client.post("/v1/memories", json={
            "data": [
                {"role": "user", "content": "I like sushi"},
                {"role": "assistant", "content": "Noted!"},
            ],
            "user_id": "user1",
        })
        assert r.status_code == 200
        assert "stats" in r.json()

    def test_add_with_user_id(self, client):
        r = client.post("/v1/memories", json={
            "data": [{"role": "user", "content": "Agent specific"}],
            "user_id": "user-a",
        })
        assert r.status_code == 200
        assert "stats" in r.json()

    def test_add_with_tags(self, client):
        r = client.post("/v1/memories", json={
            "data": [{"role": "user", "content": "Tagged memory"}],
            "user_id": "user1",
            "tags": ["food", "preference"],
        })
        assert r.status_code == 200

    def test_add_string_returns_422(self, client):
        """String data is no longer accepted — must be list[dict]."""
        r = client.post("/v1/memories", json={"data": "Hello world"})
        assert r.status_code == 422


# ==============================================================
# Search
# ==============================================================
class TestSearch:
    def test_search_empty(self, client):
        r = client.post("/v1/memories/search", json={
            "query": "nothing",
            "visibility": "organization",
        })
        assert r.status_code == 200
        assert "results" in r.json()
        assert "relations" in r.json()

    def test_search_after_add(self, client):
        client._tm._handler.add_memory("The sky is blue")
        r = client.post("/v1/memories/search", json={
            "query": "sky",
            "visibility": "organization",
        })
        assert r.status_code == 200
        assert isinstance(r.json()["results"], list)


# ==============================================================
# Get All
# ==============================================================
class TestGetAll:
    def test_get_all_empty(self, client):
        r = client.get("/v1/memories", params={"visibility": "organization"})
        assert r.status_code == 200
        assert r.json()["results"] == []

    def test_get_all_after_add(self, client):
        client._tm._handler.add_memory("First")
        client._tm._handler.add_memory("Second")
        r = client.get("/v1/memories", params={"visibility": "organization"})
        assert len(r.json()["results"]) == 2

    def test_get_all_with_pagination(self, client):
        for i in range(5):
            client._tm._handler.add_memory(f"Memory {i}")
        r = client.get("/v1/memories", params={
            "limit": 2, "offset": 0, "visibility": "organization",
        })
        assert len(r.json()["results"]) == 2


# ==============================================================
# Delete
# ==============================================================
class TestDelete:
    def test_delete_existing(self, client):
        result = client._tm._handler.add_memory("To delete")
        mid = result["memory_id"]
        r2 = client.delete(f"/v1/memories/{mid}")
        assert r2.status_code == 200
        assert r2.json()["deleted"] is True

    def test_delete_nonexistent(self, client):
        r = client.delete("/v1/memories/nonexistent-id")
        assert r.status_code == 404


# ==============================================================
# Auth: No auth mode (default)
# ==============================================================
class TestNoAuth:
    def test_no_auth_allows_all(self, client):
        r = client.post("/v1/memories", json={
            "data": [{"role": "user", "content": "No auth needed"}],
            "user_id": "user1",
        })
        assert r.status_code == 200


# ==============================================================
# Auth: API key mode
# ==============================================================
class TestAuth:
    def test_health_no_auth_required(self, auth_client):
        r = auth_client.get("/health")
        assert r.status_code == 200

    def test_no_key_returns_401(self, auth_client):
        r = auth_client.post("/v1/memories", json={
            "data": [{"role": "user", "content": "Denied"}],
            "user_id": "user1",
        })
        assert r.status_code == 401

    def test_wrong_key_returns_401(self, auth_client):
        r = auth_client.post(
            "/v1/memories",
            json={
                "data": [{"role": "user", "content": "Denied"}],
                "user_id": "user1",
            },
            headers={"Authorization": "Bearer wrong-key"},
        )
        assert r.status_code == 401

    def test_correct_key_allows_access(self, auth_client):
        r = auth_client.post(
            "/v1/memories",
            json={
                "data": [{"role": "user", "content": "Allowed"}],
                "user_id": "user1",
            },
            headers={"Authorization": "Bearer test-secret-key"},
        )
        assert r.status_code == 200
        assert "stats" in r.json()

    def test_search_with_auth(self, auth_client):
        auth_client._tm._handler.add_memory("Auth memory")
        r = auth_client.post(
            "/v1/memories/search",
            json={"query": "auth", "visibility": "organization"},
            headers={"Authorization": "Bearer test-secret-key"},
        )
        assert r.status_code == 200

    def test_get_all_with_auth(self, auth_client):
        r = auth_client.get(
            "/v1/memories",
            params={"visibility": "organization"},
            headers={"Authorization": "Bearer test-secret-key"},
        )
        assert r.status_code == 200

    def test_delete_with_auth(self, auth_client):
        result = auth_client._tm._handler.add_memory("To delete")
        mid = result["memory_id"]
        r2 = auth_client.delete(
            f"/v1/memories/{mid}",
            headers={"Authorization": "Bearer test-secret-key"},
        )
        assert r2.status_code == 200
        assert r2.json()["deleted"] is True

    def test_version_requires_auth(self, auth_client):
        r = auth_client.get("/v1/version")
        assert r.status_code == 401


class TestPlayground:
    def test_playground_returns_html(self, client):
        r = client.get("/playground")
        assert r.status_code == 200
        assert "text/html" in r.headers["content-type"]
        assert "TrustMem Playground" in r.text

    def test_playground_no_auth_required(self, auth_client):
        """Playground should be accessible without auth (like /health)."""
        r = auth_client.get("/playground")
        assert r.status_code == 200


# ==============================================================
# Admin: add-raw
# ==============================================================
class TestAdminAddRaw:
    def test_add_raw_basic(self, client):
        r = client.post("/v1/admin/add-raw", json={
            "content": "Direct insert fact",
            "visibility": "organization",
        })
        assert r.status_code == 200
        data = r.json()
        assert data["memory_id"]
        assert data["confidence"] == "medium"
        assert data["source_trust"] == "derived"

    def test_add_raw_with_embedding(self, client):
        r = client.post("/v1/admin/add-raw", json={
            "content": "Pre-embedded fact",
            "embedding": [0.1] * 768,
            "confidence": "high",
            "source_trust": "verified",
            "visibility": "organization",
        })
        assert r.status_code == 200
        data = r.json()
        assert data["confidence"] == "high"
        assert data["source_trust"] == "verified"

    def test_add_raw_searchable(self, client):
        client.post("/v1/admin/add-raw", json={
            "content": "Bob likes green tea",
            "user_id": "user1",
        })
        r = client.post("/v1/memories/search", json={
            "query": "tea preferences",
            "user_id": "user1",
        })
        assert r.status_code == 200
        contents = [m["content"] for m in r.json()["results"]]
        assert "Bob likes green tea" in contents

    def test_add_raw_empty_content_400(self, client):
        r = client.post("/v1/admin/add-raw", json={"content": ""})
        assert r.status_code == 400

    def test_add_raw_requires_auth(self, auth_client):
        r = auth_client.post("/v1/admin/add-raw", json={
            "content": "should fail",
        })
        assert r.status_code == 401


class TestSearchFilters:
    """search endpoint with metadata filters."""

    def test_search_with_filters(self, client):
        """Metadata filters narrow search results."""
        client.post("/v1/admin/add-raw", json={
            "content": "CRM record from SF",
            "user_id": "user1",
            "metadata": {"source": "salesforce"},
        })
        client.post("/v1/admin/add-raw", json={
            "content": "CRM record from HB",
            "user_id": "user1",
            "metadata": {"source": "hubspot"},
        })
        r = client.post("/v1/memories/search", json={
            "query": "CRM",
            "user_id": "user1",
            "filters": {"source": "salesforce"},
        })
        assert r.status_code == 200
        results = r.json()["results"]
        assert len(results) == 1
        assert results[0]["content"] == "CRM record from SF"

    def test_search_without_filters_returns_all(self, client):
        """Without filters, all matching memories are returned."""
        client.post("/v1/admin/add-raw", json={
            "content": "Record alpha",
            "user_id": "user1",
            "metadata": {"source": "salesforce"},
        })
        client.post("/v1/admin/add-raw", json={
            "content": "Record beta",
            "user_id": "user1",
            "metadata": {"source": "hubspot"},
        })
        r = client.post("/v1/memories/search", json={
            "query": "record",
            "user_id": "user1",
        })
        assert r.status_code == 200
        assert len(r.json()["results"]) == 2


class TestMultiTenantEndpoints:
    """Multi-tenant parameter support in server endpoints."""

    def test_add_with_multitenant_params(self, client):
        """POST /v1/memories accepts multi-tenant params."""
        resp = client.post("/v1/memories", json={
            "data": [{"role": "user", "content": "multi-tenant test"}],
            "user_id": "user1",
        })
        assert resp.status_code == 200
        assert "stats" in resp.json()

    def test_search_with_multitenant_params(self, client):
        """POST /v1/memories/search accepts multi-tenant params."""
        client.post("/v1/admin/add-raw", json={
            "content": "org-scoped data",
            "user_id": "user1",
        })
        resp = client.post("/v1/memories/search", json={
            "query": "org-scoped",
            "user_id": "user1",
        })
        assert resp.status_code == 200
        assert "results" in resp.json()

    def test_get_all_with_multitenant_params(self, client):
        """GET /v1/memories accepts multi-tenant query params."""
        client.post("/v1/admin/add-raw", json={
            "content": "tenant data",
            "user_id": "user1",
        })
        resp = client.get("/v1/memories", params={
            "user_id": "user1",
        })
        assert resp.status_code == 200
        assert "results" in resp.json()

    def test_add_raw_with_multitenant_params(self, client):
        """POST /v1/admin/add-raw accepts multi-tenant params."""
        resp = client.post("/v1/admin/add-raw", json={
            "content": "raw tenant data",
            "user_id": "user1",
        })
        assert resp.status_code == 200
        data = resp.json()
        assert data["memory_id"]

    def test_list_all_admin_endpoint(self, client):
        """GET /v1/admin/list-all returns memories without visibility filter."""
        client.post("/v1/admin/add-raw", json={
            "content": "admin list test",
            "visibility": "organization",
        })
        resp = client.get("/v1/admin/list-all", params={})
        assert resp.status_code == 200
        data = resp.json()
        assert "memories" in data

    def test_list_all_with_filters(self, client):
        """GET /v1/admin/list-all accepts filtering params."""
        client.post("/v1/admin/add-raw", json={
            "content": "filtered data",
            "user_id": "user1",
        })
        resp = client.get("/v1/admin/list-all", params={
            "user_id": "user1",
            "limit": 50,
            "offset": 0,
        })
        assert resp.status_code == 200
        data = resp.json()
        assert "memories" in data


class TestSearchRawEndpoint:
    """POST /v1/memories/search-raw endpoint."""

    def test_search_raw_basic(self, client):
        client.post("/v1/admin/add-raw", json={
            "content": "Raw search test",
            "user_id": "user1",
        })
        r = client.post("/v1/memories/search-raw", json={
            "query": "raw search",
            "user_id": "user1",
        })
        assert r.status_code == 200
        data = r.json()
        assert len(data["results"]) == 1
        assert "relations" not in data

    def test_search_raw_with_filters(self, client):
        client.post("/v1/admin/add-raw", json={
            "content": "SF data",
            "user_id": "user1",
            "metadata": {"source": "salesforce"},
        })
        client.post("/v1/admin/add-raw", json={
            "content": "HB data",
            "user_id": "user1",
            "metadata": {"source": "hubspot"},
        })
        r = client.post("/v1/memories/search-raw", json={
            "query": "data",
            "user_id": "user1",
            "filters": {"source": "salesforce"},
        })
        assert r.status_code == 200
        results = r.json()["results"]
        assert len(results) == 1
        assert results[0]["content"] == "SF data"


# ==============================================================
# memory_type
# ==============================================================
class TestMemoryType:
    def test_search_with_memory_type(self, client):
        """POST /v1/memories/search with memory_type parameter."""
        tm = client._tm
        tm.add_raw("semantic fact about cats", memory_type="semantic", user_id="user1")
        tm.add_raw("document about cats", memory_type="document", user_id="user1")

        r = client.post("/v1/memories/search", json={
            "query": "cats",
            "user_id": "user1",
            "memory_type": "semantic",
        })
        assert r.status_code == 200
        results = r.json()["results"]
        # All results should be semantic type
        for res in results:
            mem = tm.get(res["memory_id"])
            assert mem["memory_type"] == "semantic"

    def test_add_raw_with_memory_type(self, client):
        """POST /v1/admin/add-raw with memory_type parameter."""
        r = client.post("/v1/admin/add-raw", json={
            "content": "document chunk",
            "memory_type": "document",
            "user_id": "user1",
        })
        assert r.status_code == 200
        mid = r.json()["memory_id"]
        tm = client._tm
        mem = tm.get(mid)
        assert mem["memory_type"] == "document"

    def test_add_raw_default_memory_type(self, client):
        """POST /v1/admin/add-raw defaults to 'semantic'."""
        r = client.post("/v1/admin/add-raw", json={
            "content": "plain fact",
            "user_id": "user1",
        })
        assert r.status_code == 200
        mid = r.json()["memory_id"]
        tm = client._tm
        mem = tm.get(mid)
        assert mem["memory_type"] == "semantic"

    def test_get_all_with_memory_type(self, client):
        """GET /v1/memories with memory_type query param."""
        tm = client._tm
        tm.add_raw("sem1", memory_type="semantic", user_id="user1")
        tm.add_raw("doc1", memory_type="document", user_id="user1")

        r = client.get("/v1/memories", params={
            "user_id": "user1",
            "memory_type": "document",
        })
        assert r.status_code == 200
        results = r.json()["results"]
        assert all(m["memory_type"] == "document" for m in results)
