from __future__ import annotations

import json

import pytest
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
from sqlalchemy.pool import StaticPool

from app.api.deps import CurrentUserContext
from app.db.base import Base
from app.models.agent_run import AgentRun
from app.models.tenant import Tenant
from app.services.knowledge import KnowledgeService
from app.services.knowledge_rag import KnowledgeRagService
from app.services.knowledge_run_scope import resolve_trusted_knowledge_run_tenant
from app.services.knowledge_scheduler import KnowledgeIndexScheduler
from app.services.knowledge_tenant_scope import PLATFORM_KNOWLEDGE_SCOPE


def _user(tenant_id: str) -> CurrentUserContext:
    return CurrentUserContext(
        username=f"admin-{tenant_id}",
        name=f"管理员 {tenant_id}",
        role_codes=["manager"],
        is_admin=True,
        tenant_id=tenant_id,
    )


def test_tenant_document_metadata_paths_and_reads_are_isolated(tmp_path, monkeypatch) -> None:
    monkeypatch.setattr(
        KnowledgeRagService,
        "get_document_status_map",
        lambda _self, _document_ids: {},
    )
    monkeypatch.setattr(KnowledgeRagService, "delete_document", lambda *_args: None)
    tenant_a = KnowledgeService(storage_root=tmp_path, tenant_id="tenant-a")
    tenant_b = KnowledgeService(storage_root=tmp_path, tenant_id="tenant-b")

    doc_a = tenant_a.upload_document("报销制度", "制度.txt", b"tenant-a", _user("tenant-a"))
    doc_b = tenant_b.upload_document("报销制度", "制度.txt", b"tenant-b", _user("tenant-b"))

    assert doc_a.id != doc_b.id
    path_a, _, _ = tenant_a.get_document_content(doc_a.id)
    path_b, _, _ = tenant_b.get_document_content(doc_b.id)
    assert path_a.read_bytes() == b"tenant-a"
    assert path_b.read_bytes() == b"tenant-b"
    assert path_a.is_relative_to(tmp_path / "knowledge" / "tenants" / "tenant-a")
    assert path_b.is_relative_to(tmp_path / "knowledge" / "tenants" / "tenant-b")
    with pytest.raises(FileNotFoundError):
        tenant_a.get_document_content(doc_b.id)
    with pytest.raises(FileNotFoundError):
        tenant_b.get_document_detail(doc_a.id)


def test_platform_documents_are_explicitly_read_only_and_visible_to_tenants(
    tmp_path,
    monkeypatch,
) -> None:
    legacy_folder = tmp_path / "knowledge" / "制度政策"
    legacy_folder.mkdir(parents=True)
    legacy_file = legacy_folder / "platform-doc__平台制度.txt"
    legacy_file.write_bytes(b"platform-policy")
    (tmp_path / "knowledge" / ".index.json").write_text(
        json.dumps(
            {
                "version": 1,
                "documents": [
                    {
                        "id": "platform-doc",
                        "folder": "制度政策",
                        "original_name": "平台制度.txt",
                        "stored_name": legacy_file.name,
                        "mime_type": "text/plain",
                        "extension": "txt",
                        "size_bytes": len(b"platform-policy"),
                        "sha256": "",
                        "created_at": "2026-07-17T00:00:00+00:00",
                        "updated_at": "2026-07-17T00:00:00+00:00",
                        "uploaded_by": "平台",
                        "version_number": 1,
                        "ingest_status": 1,
                    }
                ],
            },
            ensure_ascii=False,
        ),
        encoding="utf-8",
    )
    monkeypatch.setattr(
        KnowledgeRagService,
        "get_document_status_map",
        lambda _self, _document_ids: {},
    )
    platform = KnowledgeService(storage_root=tmp_path, scope=PLATFORM_KNOWLEDGE_SCOPE)
    platform.ensure_library_ready()
    tenant = KnowledgeService(storage_root=tmp_path, tenant_id="tenant-a")

    listed = {item.id: item for item in tenant.list_library().documents}
    assert listed["platform-doc"].scope == PLATFORM_KNOWLEDGE_SCOPE
    assert listed["platform-doc"].readOnly is True
    assert tenant.get_document_content("platform-doc")[0].read_bytes() == b"platform-policy"
    with pytest.raises(FileNotFoundError):
        tenant.delete_document("platform-doc")
    with pytest.raises(ValueError, match="只读"):
        platform.upload_document("制度政策", "new.txt", b"x", _user("tenant-a"))


def test_unscoped_knowledge_services_fail_closed(tmp_path) -> None:
    with pytest.raises(ValueError, match="显式提供"):
        KnowledgeService(storage_root=tmp_path)
    with pytest.raises(ValueError, match="显式提供"):
        KnowledgeRagService(storage_root=tmp_path)


def test_lightrag_workspace_cache_and_local_chunks_are_tenant_namespaced(tmp_path) -> None:
    service_a = KnowledgeRagService(storage_root=tmp_path, tenant_id="tenant-a")
    service_b = KnowledgeRagService(storage_root=tmp_path, tenant_id="tenant-b")
    assert service_a.storage_scope.workspace != service_b.storage_scope.workspace
    assert service_a.storage_scope.runtime_cache_key != service_b.storage_scope.runtime_cache_key
    assert "tenant-a" not in service_a.storage_scope.workspace

    for service, marker in ((service_a, "A 租户专属限额"), (service_b, "B 租户专属限额")):
        workspace = service.storage_scope.lightrag_root / service.storage_scope.workspace
        workspace.mkdir(parents=True)
        (workspace / "kv_store_text_chunks.json").write_text(
            json.dumps(
                {
                    f"chunk-{marker[0]}": {
                        "_id": f"chunk-{marker[0]}",
                        "full_doc_id": f"doc-{marker[0]}",
                        "chunk_order_index": 1,
                        "file_path": f"/tmp/doc-{marker[0]}__制度.txt",
                        "content": f"报销限额规定：{marker}，提交前必须校验。",
                    }
                },
                ensure_ascii=False,
            ),
            encoding="utf-8",
        )

    result_a = service_a.query_knowledge("A 租户专属限额是什么？", limit=2)
    result_b = service_b.query_knowledge("B 租户专属限额是什么？", limit=2)
    assert "A 租户专属限额" in result_a["hits"][0]["content"]
    assert "B 租户专属限额" in result_b["hits"][0]["content"]
    assert all("B 租户" not in item["content"] for item in result_a["hits"])
    assert all("A 租户" not in item["content"] for item in result_b["hits"])


def test_scheduler_enumerates_only_active_tenant_registry_rows(monkeypatch) -> None:
    assert "tenants" in Base.metadata.tables
    engine = create_engine(
        "sqlite+pysqlite:///:memory:",
        connect_args={"check_same_thread": False},
        poolclass=StaticPool,
    )
    Tenant.__table__.create(bind=engine)
    factory = sessionmaker(bind=engine, autoflush=False, autocommit=False)
    with factory() as db:
        db.add_all(
            [
                Tenant(tenant_id="tenant-a", tenant_code="A", name="A", status="active"),
                Tenant(
                    tenant_id="tenant-b",
                    tenant_code="B",
                    name="B",
                    status="suspended",
                ),
            ]
        )
        db.commit()

    seen_tenants: list[str] = []

    class FakeDispatch:
        def __init__(self, _db) -> None:
            pass

        def queue_sync(self, *, current_user, **_kwargs):
            seen_tenants.append(current_user.tenant_id)
            return type(
                "Result",
                (),
                {
                    "agent_run_id": "",
                    "document_ids": [],
                    "reused": False,
                    "summary": "no changes",
                },
            )()

    monkeypatch.setattr(
        "app.services.knowledge_scheduler.get_session_factory",
        lambda: factory,
    )
    monkeypatch.setattr(
        "app.services.knowledge_scheduler.KnowledgeSyncDispatchService",
        FakeDispatch,
    )
    KnowledgeIndexScheduler()._run_incremental_sync()
    assert seen_tenants == ["tenant-a"]


def test_index_worker_rederives_tenant_from_trusted_agent_run() -> None:
    engine = create_engine(
        "sqlite+pysqlite:///:memory:",
        connect_args={"check_same_thread": False},
        poolclass=StaticPool,
    )
    AgentRun.__table__.create(bind=engine)
    factory = sessionmaker(bind=engine, autoflush=False, autocommit=False)
    with factory() as db:
        run = AgentRun(
            run_id="run-tenant-bound",
            agent="hermes",
            source="schedule",
            route_json={"tenant_id": "tenant-a"},
            ontology_json={"tenant_id": "tenant-a"},
            permission_level="read",
            status="running",
        )
        db.add(run)
        db.commit()
        assert resolve_trusted_knowledge_run_tenant(db, run.run_id) == "tenant-a"

        run.ontology_json = {"tenant_id": "tenant-b"}
        db.commit()
        with pytest.raises(ValueError, match="冲突"):
            resolve_trusted_knowledge_run_tenant(db, run.run_id)
