from __future__ import annotations

import os
import time
from dataclasses import dataclass
from typing import Any, Callable

import pytest

from .helpers import ReleaseApiClient, ReleaseTestConfig


@dataclass(frozen=True)
class TenantSeed:
    tenant_code: str
    tenant_name: str


@dataclass(frozen=True)
class SeededUser:
    user_id: int
    sub: str
    username: str
    nickname: str
    role_key: str
    tenant_code: str
    token: str


@dataclass(frozen=True)
class DocumentTypeSeed:
    type_id: int
    type_code: str
    type_name: str


@dataclass(frozen=True)
class ReleaseDocumentSeed:
    document_id: int
    file_id: int | None
    type_id: int
    type_code: str
    tenant_code: str
    tenant_name: str
    file_name: str


@dataclass(frozen=True)
class ReleaseDatasetSeed:
    dataset_id: int
    dataset_name: str
    tenant_code: str
    tenant_name: str
    app_id: int | None
    app_name: str | None


def _env(name: str, default: str) -> str:
    return str(os.getenv(name, default)).strip()


@pytest.fixture(scope="session")
def release_config() -> ReleaseTestConfig:
    return ReleaseTestConfig(
        base_url=_env("LEAUDIT_TEST_BASE_URL", "http://127.0.0.1:8096").rstrip("/"),
        admin_username=_env("LEAUDIT_TEST_ADMIN_USERNAME", "000"),
        admin_password=_env("LEAUDIT_TEST_ADMIN_PASSWORD", "admin06111"),
        timeout_seconds=float(_env("LEAUDIT_TEST_TIMEOUT_SECONDS", "30")),
        tenant_a_code=_env("LEAUDIT_TEST_TENANT_A_CODE", "PTA01"),
        tenant_a_name=_env("LEAUDIT_TEST_TENANT_A_NAME", "Pytest租户A"),
        tenant_b_code=_env("LEAUDIT_TEST_TENANT_B_CODE", "PTB01"),
        tenant_b_name=_env("LEAUDIT_TEST_TENANT_B_NAME", "Pytest租户B"),
        module_name=_env("LEAUDIT_TEST_MODULE_NAME", f"Pytest发布验收入口模块-{time.time_ns()}"),
        module_path=_env("LEAUDIT_TEST_MODULE_PATH", "/documents"),
    )


@pytest.fixture(scope="session")
def anonymous_client(release_config: ReleaseTestConfig) -> ReleaseApiClient:
    client = ReleaseApiClient(release_config)
    yield client
    client.close()


@pytest.fixture(scope="session")
def admin_auth(release_config: ReleaseTestConfig, anonymous_client: ReleaseApiClient) -> dict[str, Any]:
    return anonymous_client.login_password(release_config.admin_username, release_config.admin_password)


@pytest.fixture(scope="session")
def admin_client(release_config: ReleaseTestConfig, admin_auth: dict[str, Any]) -> ReleaseApiClient:
    client = ReleaseApiClient(release_config, token=str(admin_auth["access_token"]))
    yield client
    client.close()


@pytest.fixture(scope="session")
def role_map(admin_client: ReleaseApiClient) -> dict[str, int]:
    response = admin_client.get("/api/v3/rbac/roles?page=1&page_size=200")
    items = ReleaseApiClient.json_data(response)["items"]
    mapping = {str(item["role_key"]): int(item["id"]) for item in items}
    for role_key in ("super_admin", "provincial_admin", "admin", "common"):
        assert role_key in mapping, f"缺少角色种子: {role_key}"
    return mapping


def _ensure_tenant(admin_client: ReleaseApiClient, tenant_code: str, tenant_name: str) -> TenantSeed:
    detail_response = admin_client.get(f"/api/v3/tenants/{tenant_code}", expected_status=None)
    body = detail_response.json()
    desired_payload = {
        "tenant_name": tenant_name,
        "tenant_short_name": tenant_name,
        "tenant_type": "CUSTOM",
        "parent_tenant_code": None,
        "display_order": 500,
        "is_public": False,
        "can_host_entry_module": True,
        "can_host_documents": True,
        "can_host_rag": True,
        "can_host_templates": True,
        "feature_keys": ["home.entry_module", "documents.upload", "rag.dataset"],
        "alias_values": [tenant_name],
        "ext": {"created_by": "pytest-release"},
    }
    if detail_response.status_code == 404:
        create_payload = {
            "tenant_code": tenant_code,
            "is_enabled": True,
            **desired_payload,
        }
        admin_client.post("/api/v3/tenants", json=create_payload, expected_status=200)
    else:
        assert detail_response.status_code == 200, body
        admin_client.put(f"/api/v3/tenants/{tenant_code}", json=desired_payload, expected_status=200)
        admin_client.patch(f"/api/v3/tenants/{tenant_code}/status", json={"is_enabled": True}, expected_status=200)
    return TenantSeed(tenant_code=tenant_code, tenant_name=tenant_name)


@pytest.fixture(scope="session")
def tenant_a(admin_client: ReleaseApiClient, release_config: ReleaseTestConfig) -> TenantSeed:
    return _ensure_tenant(admin_client, release_config.tenant_a_code, release_config.tenant_a_name)


@pytest.fixture(scope="session")
def tenant_b(admin_client: ReleaseApiClient, release_config: ReleaseTestConfig) -> TenantSeed:
    return _ensure_tenant(admin_client, release_config.tenant_b_code, release_config.tenant_b_name)


def _ensure_user(
    *,
    anonymous_client: ReleaseApiClient,
    admin_client: ReleaseApiClient,
    role_map: dict[str, int],
    sub: str,
    username: str,
    nickname: str,
    role_key: str,
    tenant: TenantSeed,
) -> SeededUser:
    login_data = anonymous_client.login_oauth(
        sub=sub,
        username=username,
        nickname=nickname,
        ou_id=f"pytest-{tenant.tenant_code.lower()}",
        ou_name=f"{tenant.tenant_name}测试组织",
        area=tenant.tenant_name,
    )
    user_info = login_data["user_info"]
    user_id = int(user_info["user_id"])
    admin_client.post(
        f"/api/v3/rbac/users/{user_id}/roles",
        json={"role_ids": [role_map[role_key]]},
        expected_status=200,
    )
    admin_client.put(
        f"/api/v3/rbac/users/{user_id}/tenant",
        json={"tenant_code": tenant.tenant_code},
        expected_status=200,
    )
    refreshed = anonymous_client.login_oauth(
        sub=sub,
        username=username,
        nickname=nickname,
        ou_id=f"pytest-{tenant.tenant_code.lower()}",
        ou_name=f"{tenant.tenant_name}测试组织",
        area=tenant.tenant_name,
    )
    refreshed_user = refreshed["user_info"]
    assert str(refreshed_user.get("tenant_code") or "") == tenant.tenant_code
    return SeededUser(
        user_id=user_id,
        sub=sub,
        username=username,
        nickname=nickname,
        role_key=role_key,
        tenant_code=tenant.tenant_code,
        token=str(refreshed["access_token"]),
    )


@pytest.fixture(scope="session")
def tenant_admin_user(
    anonymous_client: ReleaseApiClient,
    admin_client: ReleaseApiClient,
    role_map: dict[str, int],
    tenant_a: TenantSeed,
) -> SeededUser:
    return _ensure_user(
        anonymous_client=anonymous_client,
        admin_client=admin_client,
        role_map=role_map,
        sub="pytest-admin-pta01",
        username="pytest_admin_pta01",
        nickname="Pytest租户A管理员",
        role_key="admin",
        tenant=tenant_a,
    )


@pytest.fixture(scope="session")
def tenant_admin_user_b(
    anonymous_client: ReleaseApiClient,
    admin_client: ReleaseApiClient,
    role_map: dict[str, int],
    tenant_b: TenantSeed,
) -> SeededUser:
    return _ensure_user(
        anonymous_client=anonymous_client,
        admin_client=admin_client,
        role_map=role_map,
        sub="pytest-admin-ptb01",
        username="pytest_admin_ptb01",
        nickname="Pytest租户B管理员",
        role_key="admin",
        tenant=tenant_b,
    )


@pytest.fixture(scope="session")
def tenant_common_user_a(
    anonymous_client: ReleaseApiClient,
    admin_client: ReleaseApiClient,
    role_map: dict[str, int],
    tenant_a: TenantSeed,
) -> SeededUser:
    return _ensure_user(
        anonymous_client=anonymous_client,
        admin_client=admin_client,
        role_map=role_map,
        sub="pytest-common-pta01",
        username="pytest_common_pta01",
        nickname="Pytest租户A普通用户",
        role_key="common",
        tenant=tenant_a,
    )


@pytest.fixture(scope="session")
def tenant_common_user_b(
    anonymous_client: ReleaseApiClient,
    admin_client: ReleaseApiClient,
    role_map: dict[str, int],
    tenant_b: TenantSeed,
) -> SeededUser:
    return _ensure_user(
        anonymous_client=anonymous_client,
        admin_client=admin_client,
        role_map=role_map,
        sub="pytest-common-ptb01",
        username="pytest_common_ptb01",
        nickname="Pytest租户B普通用户",
        role_key="common",
        tenant=tenant_b,
    )


@pytest.fixture
def tenant_admin_api(release_config: ReleaseTestConfig, tenant_admin_user: SeededUser) -> ReleaseApiClient:
    client = ReleaseApiClient(release_config, token=tenant_admin_user.token)
    yield client
    client.close()


@pytest.fixture
def tenant_admin_api_b(release_config: ReleaseTestConfig, tenant_admin_user_b: SeededUser) -> ReleaseApiClient:
    client = ReleaseApiClient(release_config, token=tenant_admin_user_b.token)
    yield client
    client.close()


@pytest.fixture
def common_api_a(release_config: ReleaseTestConfig, tenant_common_user_a: SeededUser) -> ReleaseApiClient:
    client = ReleaseApiClient(release_config, token=tenant_common_user_a.token)
    yield client
    client.close()


@pytest.fixture
def common_api_b(release_config: ReleaseTestConfig, tenant_common_user_b: SeededUser) -> ReleaseApiClient:
    client = ReleaseApiClient(release_config, token=tenant_common_user_b.token)
    yield client
    client.close()


def _find_module_by_name(admin_client: ReleaseApiClient, module_name: str) -> dict[str, Any] | None:
    response = admin_client.get(f"/api/v3/entry-modules?page=1&page_size=200&name={module_name}".replace("page_size", "page_size"), expected_status=None)
    items = ReleaseApiClient.json_data(response)["items"]
    for item in items:
        if str(item.get("name") or "") == module_name:
            return item
    return None


@pytest.fixture
def release_entry_module(
    admin_client: ReleaseApiClient,
    release_config: ReleaseTestConfig,
    tenant_b: TenantSeed,
) -> dict[str, Any]:
    module_name = release_config.module_name
    existing = _find_module_by_name(admin_client, module_name)
    payload = {
        "name": module_name,
        "description": "pytest release acceptance only",
        "path": release_config.module_path,
        "route_path": release_config.module_path,
        "tenants": [
            {
                "tenant_code": tenant_b.tenant_code,
                "tenant_name": tenant_b.tenant_name,
                "enabled": True,
                "sort_order": 1,
            }
        ],
    }
    created = False
    if existing:
        module_id = int(existing["id"])
        response = admin_client.put(f"/api/v3/entry-modules/{module_id}", json=payload, expected_status=200)
    else:
        response = admin_client.post("/api/v3/entry-modules", json=payload, expected_status=None)
        if response.status_code == 200:
            created = True
        else:
            retried = False
            for attempt in range(1, 4):
                module_name = f"{release_config.module_name}-{attempt}"
                payload["name"] = module_name
                existing = _find_module_by_name(admin_client, module_name)
                if existing:
                    module_id = int(existing["id"])
                    response = admin_client.put(f"/api/v3/entry-modules/{module_id}", json=payload, expected_status=200)
                    retried = True
                    break
                response = admin_client.post("/api/v3/entry-modules", json=payload, expected_status=None)
                if response.status_code == 200:
                    created = True
                    retried = True
                    break
            assert retried, response.text
    module = ReleaseApiClient.json_data(response)
    try:
        yield module
    finally:
        if created:
            admin_client.delete(f"/api/v3/entry-modules/{int(module['id'])}", expected_status=200)


@pytest.fixture(scope="session")
def release_document_type(admin_client: ReleaseApiClient) -> DocumentTypeSeed:
    response = admin_client.get("/api/document-types")
    items = ReleaseApiClient.json_data(response)
    assert isinstance(items, list), items
    for item in items:
        type_id = int(item.get("id") or 0)
        type_code = str(item.get("code") or "").strip()
        type_name = str(item.get("name") or "").strip()
        if type_id > 0 and type_code:
            return DocumentTypeSeed(type_id=type_id, type_code=type_code, type_name=type_name or type_code)
    pytest.skip("当前环境没有可用于发布验收的文档类型")


@pytest.fixture
def make_release_document(
    admin_client: ReleaseApiClient,
    release_document_type: DocumentTypeSeed,
) -> Callable[..., ReleaseDocumentSeed]:
    created_document_ids: list[int] = []

    def _make_document(
        *,
        client: ReleaseApiClient,
        tenant: TenantSeed,
        file_name: str,
        content: bytes | None = None,
        content_type: str = "text/plain",
    ) -> ReleaseDocumentSeed:
        payload_bytes = content or f"pytest release document for {tenant.tenant_code} / {file_name}\n".encode("utf-8")
        response = client.post(
            "/api/upload",
            data={
                "typeId": str(release_document_type.type_id),
                "typeCode": release_document_type.type_code,
                "tenant_code": tenant.tenant_code,
                "region": tenant.tenant_name,
                "fileRole": "primary",
                "autoRun": "false",
                "speed": "normal",
            },
            files={
                "file": (file_name, payload_bytes, content_type),
            },
            expected_status=200,
        )
        data = ReleaseApiClient.json_data(response)
        document_id = int(data["documentId"])
        created_document_ids.append(document_id)
        return ReleaseDocumentSeed(
            document_id=document_id,
            file_id=int(data["fileId"]) if data.get("fileId") is not None else None,
            type_id=release_document_type.type_id,
            type_code=release_document_type.type_code,
            tenant_code=tenant.tenant_code,
            tenant_name=tenant.tenant_name,
            file_name=file_name,
        )

    try:
        yield _make_document
    finally:
        for document_id in reversed(created_document_ids):
            admin_client.delete(f"/api/documents/{document_id}", expected_status=None)


@pytest.fixture
def make_release_dataset(
    admin_client: ReleaseApiClient,
) -> Callable[..., ReleaseDatasetSeed]:
    created_dataset_ids: list[int] = []

    def _make_dataset(
        *,
        tenant: TenantSeed,
        dataset_name: str,
        is_public: bool = False,
        is_default: bool = False,
    ) -> ReleaseDatasetSeed:
        response = admin_client.post(
            "/api/v3/rag/datasets/admin",
            json={
                "tenant_code": tenant.tenant_code,
                "tenant_name": tenant.tenant_name,
                "area": tenant.tenant_name,
                "name": dataset_name,
                "description": f"{dataset_name} - pytest release",
                "is_public": is_public,
                "is_default": is_default,
                "status": 1,
                "sort_order": 0,
            },
            expected_status=200,
        )
        data = ReleaseApiClient.json_data(response)
        dataset_id = int(data["id"])
        created_dataset_ids.append(dataset_id)
        return ReleaseDatasetSeed(
            dataset_id=dataset_id,
            dataset_name=str(data.get("name") or dataset_name),
            tenant_code=str(data.get("tenantCode") or tenant.tenant_code),
            tenant_name=str(data.get("tenantName") or tenant.tenant_name),
            app_id=int(data["appId"]) if data.get("appId") is not None else None,
            app_name=str(data.get("appName") or "") or None,
        )

    try:
        yield _make_dataset
    finally:
        for dataset_id in reversed(created_dataset_ids):
            admin_client.delete(f"/api/v3/rag/datasets/admin/{dataset_id}", expected_status=None)
