import asyncio

from memory_system_api.schemas import (
    MemoryWriteRequest,
    MessageIngestRequest,
    ResourceUploadRequest,
    SearchRequest,
    SessionContextRequest,
)
from memory_system_api.service import MemorySystemService


class FakeOpenViking:
    def __init__(self, fail_on_append: bool = False):
        self.fail_on_append = fail_on_append
        self.calls = []

    async def create_user(self, user_id: str) -> dict:
        self.calls.append(("create_user", user_id))
        return {"account_id": f"{user_id}_account", "admin_user_id": user_id, "user_key": f"{user_id}-key"}

    def credential_for_user(
        self,
        user_id: str,
        user_key: str,
        agent_id: str | None = None,
    ) -> str:
        self.calls.append(("credential_for_user", user_id, user_key, agent_id))
        if user_key != f"{user_id}-key":
            raise PermissionError("Invalid user key")
        return f"key-{user_id}"

    async def ensure_session(self, user_key: str, session_id: str) -> dict:
        self.calls.append(("ensure_session", user_key, session_id))
        return {"session_id": session_id}

    async def append_message(self, user_key: str, session_id: str, role: str, content: str) -> dict:
        self.calls.append(("append_message", user_key, session_id, role, content))
        if self.fail_on_append:
            raise RuntimeError("openviking append failed")
        return {"message_count": len([call for call in self.calls if call[0] == "append_message"])}

    async def find(self, user_key: str, query: str, limit: int) -> dict:
        self.calls.append(("find", user_key, query, limit))
        await asyncio.sleep(0.01)
        return {"items": [{"source": "openviking-find"}]}

    async def search(
        self,
        user_key: str,
        query: str,
        limit: int,
        level: int = 2,
        score_threshold: float = 0.8,
        target_uri: str = "viking://user/memories",
    ) -> dict:
        self.calls.append(("search", user_key, query, limit, level, score_threshold, target_uri))
        await asyncio.sleep(0.01)
        return {"items": [{"source": "openviking-search"}]}

    async def search_profile_memories(self, user_key: str, query: str, limit: int, level: int) -> dict:
        self.calls.append(("search_profile_memories", user_key, query, limit, level))
        return {
            "status": "ok",
            "result": {
                "memories": [
                    {
                        "context_type": "memory",
                        "uri": "viking://user/tom/memories/preferences/coffee.md",
                        "level": 2,
                        "score": 0.91,
                        "abstract": "用户喜欢喝咖啡。",
                    }
                ],
                "resources": [],
                "skills": [],
                "total": 1,
            },
        }

    async def get_session_context(self, user_key: str, session_id: str) -> dict:
        self.calls.append(("get_session_context", user_key, session_id))
        return {
            "status": "ok",
            "result": {
                "latest_archive_overview": "# Working Memory\nUser likes coffee.",
                "pre_archive_abstracts": [],
                "messages": [],
                "estimatedTokens": 42,
                "stats": {"totalArchives": 1},
            },
        }

    async def commit_session(self, user_key: str, session_id: str) -> dict:
        self.calls.append(("commit_session", user_key, session_id))
        return {"status": "ok", "result": {"task_id": "task-1", "archive_uri": "archive-1"}}

    async def upload_temp_file(self, user_key: str, path) -> dict:
        self.calls.append(("upload_temp_file", user_key, str(path)))
        return {"status": "ok", "result": {"temp_file_id": "upload_report.pdf"}}

    async def add_resource(
        self,
        user_key: str,
        *,
        to: str,
        reason: str | None,
        wait: bool,
        directly_upload_media: bool,
        path: str | None = None,
        temp_file_id: str | None = None,
    ) -> dict:
        self.calls.append((
            "add_resource",
            user_key,
            path,
            temp_file_id,
            to,
            reason,
            wait,
            directly_upload_media,
        ))
        return {"status": "ok", "result": {"uri": to}}

    async def delete_resource(self, user_key: str, uri: str, recursive: bool = True) -> dict:
        self.calls.append(("delete_resource", user_key, uri, recursive))
        return {"status": "ok", "result": {"uri": uri, "estimated_deleted_count": 4}}

    async def list_memories(self, user_key: str, uri: str, recursive: bool = True) -> dict:
        self.calls.append(("list_memories", user_key, uri, recursive))
        return {"status": "ok", "result": {"children": [{"uri": "viking://user/memories/profile.md"}]}}

    async def read_memory(self, user_key: str, uri: str) -> dict:
        self.calls.append(("read_memory", user_key, uri))
        return {"status": "ok", "result": {"uri": uri, "content": "# Profile"}}

    async def write_memory(self, user_key: str, uri: str, content: str, mode: str, wait: bool = True) -> dict:
        self.calls.append(("write_memory", user_key, uri, content, mode, wait))
        return {"status": "ok", "result": {"uri": uri, "mode": mode}}

    async def delete_memory(self, user_key: str, uri: str, recursive: bool = False) -> dict:
        self.calls.append(("delete_memory", user_key, uri, recursive))
        return {"status": "ok", "result": {"uri": uri, "estimated_deleted_count": 1}}


class FakeEverOS:
    def __init__(self, fail_on_append: bool = False):
        self.fail_on_append = fail_on_append
        self.calls = []

    async def append_message(self, user_id: str, session_id: str, role: str, content: str) -> dict:
        self.calls.append(("append_message", user_id, session_id, role, content))
        if self.fail_on_append:
            raise RuntimeError("everos append failed")
        return {"status": "accumulated"}

    async def search(self, user_id: str, session_id: str | None, query: str, method: str, limit: int) -> dict:
        self.calls.append(("search", user_id, session_id, query, method, limit))
        await asyncio.sleep(0.01)
        return {"items": [{"source": f"everos-{method}"}]}

    async def flush(self, user_id: str, session_id: str) -> dict:
        self.calls.append(("flush", user_id, session_id))
        return {"status": "flushed"}

    async def get_profile(self, user_id: str) -> dict:
        self.calls.append(("get_profile", user_id))
        return {
            "data": {
                "episodes": [],
                "profiles": [
                    {
                        "id": "profile-1",
                        "user_id": user_id,
                        "profile_data": {"summary": "喜欢咖啡"},
                    }
                ],
                "agent_cases": [],
                "agent_skills": [],
                "total_count": 1,
                "count": 1,
            }
        }


class FakeEverOSWithVector(FakeEverOS):
    async def search(self, user_id: str, session_id: str | None, query: str, method: str, limit: int) -> dict:
        self.calls.append(("search", user_id, session_id, query, method, limit))
        return {
            "data": {
                "episodes": [{"id": "episode-1", "vector": [0.1, 0.2]}],
                "original_data": {
                    "episodes": {
                        "episode-1": {
                            "summary": "喜欢拿铁",
                            "vector": [0.1, 0.2],
                            "nested": {"vector": [0.3]},
                        }
                    }
                },
            }
        }


class FakeEverOSVerbose(FakeEverOS):
    async def search(self, user_id: str, session_id: str | None, query: str, method: str, limit: int) -> dict:
        self.calls.append(("search", user_id, session_id, query, method, limit))
        return {
            "data": {
                "episodes": [
                    {
                        "id": "episode-1",
                        "user_id": user_id,
                        "session_id": session_id,
                        "timestamp": "2026-05-22T07:50:51.750000Z",
                        "summary": "userB 在对话中表示自己喜欢拿铁。",
                        "subject": "UserB 表达对拿铁的喜好",
                        "episode": "userB 在对话中表示自己喜欢拿铁。",
                        "type": "Conversation",
                        "parent_id": "parent-1",
                        "score": 0.72,
                        "atomic_facts": [],
                    }
                ],
                "profiles": [],
                "raw_messages": [],
                "query": {
                    "text": query,
                    "method": method,
                    "filters_applied": {"user_id": user_id, "session_id": session_id},
                },
                "original_data": {
                    "episodes": {
                        "episode-1": {
                            "id": "episode-1",
                            "summary": "userB 在对话中表示自己喜欢拿铁。",
                            "episode": "userB 在对话中表示自己喜欢拿铁。",
                            "vector_model": "Qwen3-VL-Embedding-2B",
                        }
                    }
                },
            }
        }


def test_capture_includes_exception_type_when_message_is_empty():
    service = MemorySystemService(openviking=FakeOpenViking(), everos=FakeEverOS())

    class EmptyError(Exception):
        pass

    async def fail():
        raise EmptyError()

    response = asyncio.run(service._capture(fail))

    assert response.status == "failed"
    assert response.error == "EmptyError"


def test_create_user_delegates_to_openviking_only():
    openviking = FakeOpenViking()
    everos = FakeEverOS()
    service = MemorySystemService(openviking=openviking, everos=everos)

    response = asyncio.run(service.create_user("alice"))

    assert response.status == "success"
    assert response.account == {"account_id": "alice_account", "admin_user_id": "alice", "user_key": "alice-key"}
    assert openviking.calls == [("create_user", "alice")]
    assert everos.calls == []


def test_upload_resource_with_url_delegates_directly_to_openviking_add_resource():
    openviking = FakeOpenViking()
    service = MemorySystemService(openviking=openviking, everos=FakeEverOS())

    response = asyncio.run(service.upload_resource(ResourceUploadRequest(
        user_id="tom",
        user_key="tom-key",
        path="https://example.com/images/photo.png",
        to="viking://resources/tom/images/photo.png",
        reason="上传远程图片",
    )))

    assert response.status == "success"
    assert response.resource == {"status": "ok", "result": {"uri": "viking://resources/tom/images/photo.png"}}
    assert openviking.calls == [
        ("credential_for_user", "tom", "tom-key", None),
        (
            "add_resource",
            "key-tom",
            "https://example.com/images/photo.png",
            None,
            "viking://resources/tom/images/photo.png",
            "上传远程图片",
            True,
            True,
        ),
    ]


def test_upload_resource_with_local_path_uploads_temp_file_first(tmp_path):
    path = tmp_path / "report.pdf"
    path.write_bytes(b"pdf-bytes")
    openviking = FakeOpenViking()
    service = MemorySystemService(openviking=openviking, everos=FakeEverOS())

    response = asyncio.run(service.upload_resource(ResourceUploadRequest(
        user_id="tom",
        user_key="tom-key",
        path=str(path),
        to="viking://resources/tom/files/report.pdf",
        reason="上传本地文件",
    )))

    assert response.status == "success"
    assert response.resource == {"status": "ok", "result": {"uri": "viking://resources/tom/files/report.pdf"}}
    assert openviking.calls == [
        ("credential_for_user", "tom", "tom-key", None),
        ("upload_temp_file", "key-tom", str(path)),
        (
            "add_resource",
            "key-tom",
            None,
            "upload_report.pdf",
            "viking://resources/tom/files/report.pdf",
            "上传本地文件",
            True,
            True,
        ),
    ]


def test_delete_resource_delegates_to_openviking_only():
    openviking = FakeOpenViking()
    everos = FakeEverOS()
    service = MemorySystemService(openviking=openviking, everos=everos)

    response = asyncio.run(service.delete_resource(
        user_id="tom",
        user_key="tom-key",
        uri="viking://resources/tom/files/report.pdf",
        recursive=True,
    ))

    assert response.status == "success"
    assert response.resource == {
        "status": "ok",
        "result": {"uri": "viking://resources/tom/files/report.pdf", "estimated_deleted_count": 4},
    }
    assert openviking.calls == [
        ("credential_for_user", "tom", "tom-key", None),
        ("delete_resource", "key-tom", "viking://resources/tom/files/report.pdf", True),
    ]
    assert everos.calls == []


def test_list_memories_delegates_to_openviking_only():
    openviking = FakeOpenViking()
    everos = FakeEverOS()
    service = MemorySystemService(openviking=openviking, everos=everos)

    response = asyncio.run(service.list_memories(
        user_id="tom",
        user_key="tom-key",
        uri="viking://user/memories",
        recursive=True,
    ))

    assert response.status == "success"
    assert response.memory == {"status": "ok", "result": {"children": [{"uri": "viking://user/memories/profile.md"}]}}
    assert openviking.calls == [
        ("credential_for_user", "tom", "tom-key", None),
        ("list_memories", "key-tom", "viking://user/memories", True),
    ]
    assert everos.calls == []


def test_read_memory_delegates_to_openviking_only():
    openviking = FakeOpenViking()
    everos = FakeEverOS()
    service = MemorySystemService(openviking=openviking, everos=everos)

    response = asyncio.run(service.read_memory(
        user_id="tom",
        user_key="tom-key",
        uri="viking://user/memories/profile.md",
    ))

    assert response.status == "success"
    assert response.memory == {"status": "ok", "result": {"uri": "viking://user/memories/profile.md", "content": "# Profile"}}
    assert openviking.calls == [
        ("credential_for_user", "tom", "tom-key", None),
        ("read_memory", "key-tom", "viking://user/memories/profile.md"),
    ]
    assert everos.calls == []


def test_write_memory_delegates_to_openviking_content_write_only():
    openviking = FakeOpenViking()
    everos = FakeEverOS()
    service = MemorySystemService(openviking=openviking, everos=everos)

    response = asyncio.run(service.write_memory(MemoryWriteRequest(
        user_id="tom",
        user_key="tom-key",
        uri="viking://user/memories/profile.md",
        content="# Profile\n\nLikes Python.",
        mode="replace",
        wait=True,
    )))

    assert response.status == "success"
    assert response.memory == {"status": "ok", "result": {"uri": "viking://user/memories/profile.md", "mode": "replace"}}
    assert openviking.calls == [
        ("credential_for_user", "tom", "tom-key", None),
        ("write_memory", "key-tom", "viking://user/memories/profile.md", "# Profile\n\nLikes Python.", "replace", True),
    ]
    assert everos.calls == []


def test_delete_memory_delegates_to_openviking_only_and_defaults_non_recursive():
    openviking = FakeOpenViking()
    everos = FakeEverOS()
    service = MemorySystemService(openviking=openviking, everos=everos)

    response = asyncio.run(service.delete_memory(
        user_id="tom",
        user_key="tom-key",
        uri="viking://user/memories/preferences/python.md",
    ))

    assert response.status == "success"
    assert response.memory == {
        "status": "ok",
        "result": {"uri": "viking://user/memories/preferences/python.md", "estimated_deleted_count": 1},
    }
    assert openviking.calls == [
        ("credential_for_user", "tom", "tom-key", None),
        ("delete_memory", "key-tom", "viking://user/memories/preferences/python.md", False),
    ]
    assert everos.calls == []


def test_search_removes_vectors_from_items_and_backend_results():
    service = MemorySystemService(openviking=FakeOpenViking(), everos=FakeEverOSWithVector())

    response = asyncio.run(service.search(
        SearchRequest(user_id="tom", user_key="tom-key", session_id="sess-1", query="咖啡偏好", use_llm=False, limit=5),
    ))

    assert response.items == [
        {"source_backend": "openviking", "source": "openviking-search"},
        {"source_backend": "everos", "memory_type": "episode", "id": "episode-1"},
    ]
    assert not _has_key(response.backends["everos"].result, "vector")


def test_search_returns_compact_items_and_backend_diagnostics_without_duplicate_raw_payloads():
    service = MemorySystemService(openviking=FakeOpenViking(), everos=FakeEverOSVerbose())

    response = asyncio.run(service.search(
        SearchRequest(user_id="tom", user_key="tom-key", session_id="sess-1", query="我喜欢喝什么？", use_llm=True),
    ))

    assert response.items == [
        {"source_backend": "openviking", "source": "openviking-search"},
        {
            "source_backend": "everos",
            "memory_type": "episode",
            "id": "episode-1",
            "user_id": "tom",
            "session_id": "sess-1",
            "timestamp": "2026-05-22T07:50:51.750000Z",
            "summary": "userB 在对话中表示自己喜欢拿铁。",
            "score": 0.72,
        },
    ]
    assert response.backends["everos"].result == {
        "counts": {"episodes": 1, "profiles": 0, "raw_messages": 0},
        "query": {
            "text": "我喜欢喝什么？",
            "method": "agentic",
            "filters_applied": {"user_id": "tom", "session_id": "sess-1"},
        },
    }
    assert not _has_key(response.backends["everos"].result, "original_data")


def test_session_context_combines_openviking_context_and_everos_search_items():
    openviking = FakeOpenViking()
    everos = FakeEverOSVerbose()
    service = MemorySystemService(openviking=openviking, everos=everos)

    response = asyncio.run(
        service.get_session_context(
            "sess-1",
            SessionContextRequest(user_id="tom", user_key="tom-key", query="我喜欢喝什么？", limit=5),
        )
    )

    assert response.status == "success"
    assert response.context == {
        "latest_archive_overview": "# Working Memory\nUser likes coffee.",
        "pre_archive_abstracts": [],
        "messages": [],
        "estimatedTokens": 42,
        "stats": {"totalArchives": 1},
    }
    assert response.items == [
        {
            "source_backend": "everos",
            "memory_type": "episode",
            "id": "episode-1",
            "user_id": "tom",
            "session_id": "sess-1",
            "timestamp": "2026-05-22T07:50:51.750000Z",
            "summary": "userB 在对话中表示自己喜欢拿铁。",
            "score": 0.72,
        }
    ]
    assert ("credential_for_user", "tom", "tom-key", "sess-1") in openviking.calls
    assert ("get_session_context", "key-tom", "sess-1") in openviking.calls
    assert ("search", "tom", "sess-1", "我喜欢喝什么？", "hybrid", 5) in everos.calls


def test_profile_combines_everos_profile_and_openviking_memory_search():
    openviking = FakeOpenViking()
    everos = FakeEverOS()
    service = MemorySystemService(openviking=openviking, everos=everos)

    response = asyncio.run(service.get_profile("tom", "tom-key", query="我想喝东西", limit=10, level=2))

    assert response.status == "success"
    assert response.profile == {
        "data": {
            "episodes": [],
            "profiles": [
                {
                    "id": "profile-1",
                    "user_id": "tom",
                    "profile_data": {"summary": "喜欢咖啡"},
                }
            ],
            "agent_cases": [],
            "agent_skills": [],
            "total_count": 1,
            "count": 1,
        }
    }
    assert response.items == [
        {
            "source_backend": "openviking",
            "context_type": "memory",
            "uri": "viking://user/tom/memories/preferences/coffee.md",
            "level": 2,
            "score": 0.91,
            "abstract": "用户喜欢喝咖啡。",
        }
    ]
    assert response.backends["everos"].result == {
        "total_count": 1,
        "count": 1,
        "counts": {
            "episodes": 0,
            "profiles": 1,
            "agent_cases": 0,
            "agent_skills": 0,
        },
    }
    assert "profiles" not in response.backends["everos"].result
    assert ("credential_for_user", "tom", "tom-key", None) in openviking.calls
    assert ("search_profile_memories", "key-tom", "我想喝东西", 10, 2) in openviking.calls
    assert everos.calls == [("get_profile", "tom")]


def _has_key(value, key: str) -> bool:
    if isinstance(value, dict):
        return key in value or any(_has_key(item, key) for item in value.values())
    if isinstance(value, list):
        return any(_has_key(item, key) for item in value)
    return False


def test_ingest_splits_user_and_assistant_messages():
    openviking = FakeOpenViking()
    everos = FakeEverOS()
    service = MemorySystemService(openviking=openviking, everos=everos)

    response = asyncio.run(service.ingest_messages(
        MessageIngestRequest(
            user_id="tom",
            user_key="tom-key",
            session_id="sess-1",
            user_message="我喜欢拿铁",
            assistant_message="我记住了",
        )
    ))

    assert response.status == "success"
    assert response.message_count == 2
    assert openviking.calls == [
        ("credential_for_user", "tom", "tom-key", "sess-1"),
        ("ensure_session", "key-tom", "sess-1"),
        ("append_message", "key-tom", "sess-1", "user", "我喜欢拿铁"),
        ("append_message", "key-tom", "sess-1", "assistant", "我记住了"),
    ]
    assert everos.calls == [
        ("append_message", "tom", "sess-1", "user", "我喜欢拿铁"),
        ("append_message", "tom", "sess-1", "assistant", "我记住了"),
    ]


def test_ingest_requires_at_least_one_message():
    service = MemorySystemService(openviking=FakeOpenViking(), everos=FakeEverOS())

    try:
        asyncio.run(
            service.ingest_messages(
                MessageIngestRequest(user_id="tom", user_key="tom-key", session_id="sess-1"),
            )
        )
    except ValueError as exc:
        assert "at least one message" in str(exc)
    else:
        raise AssertionError("expected ValueError")


def test_ingest_returns_partial_success_when_one_backend_fails():
    service = MemorySystemService(openviking=FakeOpenViking(fail_on_append=True), everos=FakeEverOS())

    response = asyncio.run(service.ingest_messages(
        MessageIngestRequest(user_id="tom", user_key="tom-key", session_id="sess-1", user_message="hello"),
    ))

    assert response.status == "partial_success"
    assert response.backends["openviking"].status == "failed"
    assert response.backends["everos"].status == "success"


def test_commit_uses_user_key_without_account_id():
    openviking = FakeOpenViking()
    everos = FakeEverOS()
    service = MemorySystemService(openviking=openviking, everos=everos)

    response = asyncio.run(service.commit_session("tom", "tom-key", "sess-1"))

    assert response.status == "success"
    assert openviking.calls == [
        ("credential_for_user", "tom", "tom-key", "sess-1"),
        ("commit_session", "key-tom", "sess-1"),
    ]
    assert everos.calls == [("flush", "tom", "sess-1")]


def test_search_uses_openviking_search_and_hybrid_without_llm():
    openviking = FakeOpenViking()
    everos = FakeEverOS()
    service = MemorySystemService(openviking=openviking, everos=everos)

    response = asyncio.run(service.search(
        SearchRequest(
            user_id="tom",
            user_key="tom-key",
            session_id="sess-1",
            query="咖啡偏好",
            use_llm=False,
            limit=5,
            level=3,
            score_threshold=0.7,
            target_uri="viking://user/custom/memories",
        ),
    ))

    assert response.status == "success"
    assert response.items == [
        {"source_backend": "openviking", "source": "openviking-search"},
        {"source_backend": "everos", "source": "everos-hybrid"},
    ]
    assert ("credential_for_user", "tom", "tom-key", "sess-1") in openviking.calls
    assert ("search", "key-tom", "咖啡偏好", 5, 3, 0.7, "viking://user/custom/memories") in openviking.calls
    assert ("search", "tom", "sess-1", "咖啡偏好", "hybrid", 5) in everos.calls


def test_search_uses_search_and_agentic_with_llm():
    openviking = FakeOpenViking()
    everos = FakeEverOS()
    service = MemorySystemService(openviking=openviking, everos=everos)

    response = asyncio.run(service.search(
        SearchRequest(user_id="tom", user_key="tom-key", session_id="sess-1", query="咖啡偏好", use_llm=True, limit=5),
    ))

    assert response.status == "success"
    assert response.items == [
        {"source_backend": "openviking", "source": "openviking-search"},
        {"source_backend": "everos", "source": "everos-agentic"},
    ]
    assert ("credential_for_user", "tom", "tom-key", "sess-1") in openviking.calls
    assert ("search", "key-tom", "咖啡偏好", 5, 2, 0.8, "viking://user/memories") in openviking.calls
    assert ("search", "tom", "sess-1", "咖啡偏好", "agentic", 5) in everos.calls
