"""Test fixtures — PostgreSQL + live uvicorn server.

PostgreSQL 需要本地运行（需先启动 pg_ctl 或 docker compose）。
脚本启动时会自动运行 Alembic 迁移 + TRUNCATE 所有表（保留 alembic_version）。
"""

import os
import sys

sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..'))

# Windows ProactorEventLoop 在测试间关闭导致 asyncpg socket 写入时报
# "'NoneType' object has no attribute 'send'"。切换到 SelectorEventLoop 解决。
import asyncio
if sys.platform == 'win32':
    asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy())

# ── Database URL resolution ──
# 优先使用环境变量（如 CI 传入），否则默认本地开发 PG
_DEFAULT_ASYNC_DSN = "postgresql+asyncpg://scilit:scilit_dev@localhost:5432/scilit_test"
_ASYNC_DSN = os.environ.get("DATABASE_URL") or _DEFAULT_ASYNC_DSN
# 同步 DSN：去掉 +asyncpg 后缀
_SYNC_DSN = _ASYNC_DSN.replace("+asyncpg", "")

os.environ.setdefault("DATABASE_URL", _ASYNC_DSN)
os.environ["JWT_SECRET"] = "test-secret-32-bytes-long-for-testing-!@"
os.environ["DEBUG"] = "false"
os.environ["TESTING"] = "true"
os.environ["SPECIALTY"] = "oncology"
os.environ["SMTP_HOST"] = ""

# ── Alembic 迁移 + 表清理（模块加载时同步执行一次）──────────────
from alembic.config import Config
from alembic import command
_alembic_cfg = Config(os.path.join(os.path.dirname(__file__), "..", "alembic", "alembic.ini"))
_alembic_cfg.set_main_option("sqlalchemy.url", _SYNC_DSN)
command.upgrade(_alembic_cfg, "head")

from sqlalchemy import create_engine, text as _text
_sync_engine = create_engine(_SYNC_DSN)
with _sync_engine.begin() as _conn:
    _tables = [r[0] for r in _conn.execute(
        _text("SELECT tablename FROM pg_tables WHERE schemaname='public' AND tablename!='alembic_version'")
    ).all()]
    if _tables:
        _conn.execute(_text(f"TRUNCATE TABLE {', '.join(_tables)} CASCADE"))
_sync_engine.dispose()

# ── Per-module DB truncation (prevents cross-module data leaks) ──
import pathlib as _pathlib
_last_test_module = [None]


def pytest_runtest_protocol(item, nextitem):
    """Truncate all tables when transitioning between test modules."""
    current = _pathlib.Path(str(item.path)).resolve() if hasattr(item, 'path') else None
    if current and current != _last_test_module[0]:
        if _last_test_module[0] is not None:
            try:
                _e = create_engine(_SYNC_DSN)
                with _e.begin() as _c:
                    _t = [r[0] for r in _c.execute(
                        _text("SELECT tablename FROM pg_tables WHERE schemaname='public' AND tablename!='alembic_version'")
                    ).all()]
                    if _t:
                        _c.execute(_text(f"TRUNCATE TABLE {', '.join(_t)} CASCADE"))
                _e.dispose()
            except Exception:
                pass
        _last_test_module[0] = current
    return None


import socket
import threading
import time
from contextlib import closing

import pytest
import pytest_asyncio
import uvicorn
from httpx import AsyncClient

from app.config import settings
from app.main import app as _real_app

# ── 测试用 async engine（与 server 的 engine 分离，避免连接冲突）──
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine

_test_engine = create_async_engine(
    _ASYNC_DSN,
    echo=False,
    pool_size=5,
    max_overflow=5,
)
_test_async_session = async_sessionmaker(_test_engine, class_=AsyncSession, expire_on_commit=False)


def _find_free_port() -> int:
    with closing(socket.socket(socket.AF_INET, socket.SOCK_STREAM)) as s:
        s.bind(('127.0.0.1', 0))
        s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
        return s.getsockname()[1]


# ── Fixtures ────────────────────────────────────────────────────

@pytest.fixture(scope="session")
def server_port() -> int:
    return _find_free_port()


@pytest.fixture(scope="session")
def live_server(server_port):
    """在后台线程启动 uvicorn 真实 HTTP 服务器（含全部中间件）。

    使用独立的事件循环，与测试线程完全隔离，彻底避免 asyncpg 事件循环冲突。
    """
    config = uvicorn.Config(
        app=_real_app,
        host="127.0.0.1",
        port=server_port,
        log_level="error",
    )
    server = uvicorn.Server(config)

    thread = threading.Thread(target=server.run, daemon=True)
    thread.start()

    # 轮询 /health 直到服务器就绪（同步轮询，不涉及事件循环）
    import httpx
    for _ in range(100):
        try:
            with httpx.Client() as hc:
                r = hc.get(f"http://127.0.0.1:{server_port}/health", timeout=1)
                if r.status_code == 200:
                    break
        except Exception:
            pass
        time.sleep(0.1)
    else:
        raise RuntimeError("Live server failed to start")

    yield server

    server.should_exit = True
    thread.join(timeout=10)


@pytest_asyncio.fixture(scope="function")
async def client(live_server, server_port):
    """指向 live server 的 HTTP 客户端。"""
    async with AsyncClient(base_url=f"http://127.0.0.1:{server_port}") as ac:
        yield ac


@pytest_asyncio.fixture(scope="function")
async def db():
    """async DB 会话，供测试做直连数据库操作。

    使用与 server 不同的 engine，不会产生连接冲突。
    变更会立即提交到数据库（server 可见）。
    """
    session = _test_async_session()
    try:
        yield session
        await session.commit()
    except Exception:
        await session.rollback()
        raise
    finally:
        await session.close()


@pytest_asyncio.fixture(scope="function")
async def auth_headers(client):
    """注册一个新用户 → 返回 Authorization headers"""
    import uuid
    email = f"t{uuid.uuid4().hex[:6]}@test.cn"
    resp = await client.post("/api/v1/auth/register", json={
        "email": email, "password": "Test1234", "display_name": "TestDoctor"
    })
    assert resp.status_code == 200, f"Register failed: {resp.text}"
    data = resp.json()
    return {"Authorization": f"Bearer {data['token']['access_token']}"}
