from __future__ import annotations

from datetime import date
import asyncio
import json
from pathlib import Path
import re
import time

import pytest
import httpx
from fastapi import HTTPException
from fastapi import FastAPI
from sqlalchemy import create_engine
from sqlalchemy.orm import Session
from sqlalchemy.pool import StaticPool

from app.api import chat
from app.models import Base
from app.models.ai_config import SystemConfig
from app.models.user import User
from app.services.chat_queue_runtime import ChatQueueLease
from app.services.chat_queue_service import ChatQueueConfig, QueueRequest
from app.services.chat_service import ChatService
from app.services.secret_service import ENCRYPTED_PREFIX, MASKED_SECRET, SecretService
from app.services.security_state_service import SecurityStateService
from app.services.auth_service import AuthService
from app.services.model_service import ModelClientService
from app.core.observability import RequestObservabilityMiddleware
from app.core.config import Settings


@pytest.fixture(autouse=True)
def reset_security_state(monkeypatch):
    monkeypatch.setattr("app.services.security_state_service.get_sync_redis_client", lambda: None)
    SecurityStateService._counters.clear()


def test_secret_is_encrypted_and_masked():
    encrypted = SecretService.encrypt("production-secret")
    assert encrypted.startswith(ENCRYPTED_PREFIX)
    assert "production-secret" not in encrypted
    assert SecretService.decrypt(encrypted) == "production-secret"
    assert SecretService.masked(encrypted) == MASKED_SECRET


def test_production_chat_capacity_defaults_cover_expected_burst():
    settings = Settings(_env_file=None)

    assert settings.chat_max_active_requests == 30
    assert settings.chat_max_queue_size == 90
    assert settings.chat_queue_timeout_seconds == 90


def test_migration_revision_ids_fit_default_alembic_version_column():
    versions_dir = Path(__file__).parents[1] / "alembic" / "versions"
    revision_pattern = re.compile(r'^revision\s*=\s*["\']([^"\']+)', re.MULTILINE)
    revision_ids = []
    for migration in versions_dir.glob("*.py"):
        match = revision_pattern.search(migration.read_text(encoding="utf-8"))
        if match:
            revision_ids.append((migration.name, match.group(1)))

    too_long = [(filename, revision) for filename, revision in revision_ids if len(revision) > 32]
    assert not too_long, f"Alembic revision ID 超过默认 VARCHAR(32): {too_long}"


def test_rate_limit_blocks_after_limit():
    SecurityStateService.enforce_limit("test:rate", limit=2, window_seconds=60, message="too many")
    SecurityStateService.enforce_limit("test:rate", limit=2, window_seconds=60, message="too many")
    with pytest.raises(HTTPException) as exc:
        SecurityStateService.enforce_limit("test:rate", limit=2, window_seconds=60, message="too many")
    assert exc.value.status_code == 429


def test_revoked_token_is_shared_security_state():
    SecurityStateService.revoke_token("token-id", None)
    assert SecurityStateService.is_token_revoked("token-id") is True


def test_daily_quota_resets_for_new_day():
    engine = create_engine(
        "sqlite:///:memory:",
        connect_args={"check_same_thread": False},
        poolclass=StaticPool,
    )
    Base.metadata.create_all(engine)
    with Session(engine) as db:
        user = User(
            id=1,
            phone="13800000001",
            name="测试用户",
            daily_chat_limit=10,
            daily_chat_used=10,
            daily_chat_reset_date=date(2020, 1, 1),
        )
        db.add(user)
        db.commit()
        prepared = ChatService.prepare_daily_quota(db, user)
        assert prepared.daily_chat_used == 0
        assert prepared.daily_chat_reset_date > date(2020, 1, 1)


def test_queued_chat_reports_position_and_completes(monkeypatch):
    config = ChatQueueConfig(1, 10, 10, 60)
    queued = ChatQueueLease("memory", QueueRequest("queued", token=object(), position=2, active_count=1, waiting_count=2))
    acquired = ChatQueueLease("memory", QueueRequest("acquired", token=queued.request.token, active_count=1))
    released = False

    async def request_slot(_config):
        return queued

    async def wait_slot(_lease, _config):
        return acquired

    async def release_slot(_lease):
        nonlocal released
        released = True

    async def stream_answer(*_args, **_kwargs):
        yield "回答"

    monkeypatch.setattr(chat, "load_chat_queue_config", lambda _db: config)
    monkeypatch.setattr(chat, "request_chat_slot", request_slot)
    monkeypatch.setattr(chat, "wait_for_chat_slot", wait_slot)
    monkeypatch.setattr(chat, "release_chat_slot", release_slot)
    monkeypatch.setattr(chat.ChatStreamService, "stream_answer_async", stream_answer)
    monkeypatch.setattr(chat.ReasoningPolicyService, "is_visible", lambda _db: False)

    payload = type("Payload", (), {"sessionId": 1, "message": "问题", "retry": False})()
    async def collect_events():
        return [item async for item in chat._chat_stream(payload, object(), object())]

    events = asyncio.run(collect_events())
    decoded = [json.loads(item.removeprefix("data: ")) for item in events if item != "data: [DONE]\n\n"]
    assert decoded[0]["type"] == "queued"
    assert any(item["type"] == "content" and item["content"] == "回答" for item in decoded)
    assert released is True


def test_chat_streaming_response_disables_proxy_buffering():
    payload = type("Payload", (), {"sessionId": 1, "message": "问题"})()
    response = chat.completions(payload, object(), object())

    assert response.headers["cache-control"] == "no-cache, no-transform"
    assert response.headers["x-accel-buffering"] == "no"
    assert response.media_type == "text/event-stream"


def test_sensitive_system_config_never_returns_plaintext():
    from app.api.admin_settings import _config_dict

    config = SystemConfig(config_key="feishu_app_secret", config_value="plain-secret")
    result = _config_dict(config)
    assert result["configValue"] == MASKED_SECRET


def test_request_id_is_returned_without_breaking_response():
    app = FastAPI()
    app.add_middleware(RequestObservabilityMiddleware)

    @app.get("/test")
    def endpoint():
        return {"ok": True}

    async def make_request():
        transport = httpx.ASGITransport(app=app)
        async with httpx.AsyncClient(transport=transport, base_url="http://test") as client:
            return await client.get("/test", headers={"X-Request-ID": "trace-123"})

    response = asyncio.run(make_request())
    assert response.status_code == 200
    assert response.headers["X-Request-ID"] == "trace-123"
    assert response.json() == {"ok": True}


def test_agent_debug_model_call_does_not_block_event_loop(monkeypatch):
    def blocking_debug(*_args, **_kwargs):
        time.sleep(0.15)
        return {"ok": True}

    monkeypatch.setattr(ModelClientService, "debug_model", blocking_debug)

    async def verify_concurrency():
        started = time.perf_counter()
        task = asyncio.create_task(ModelClientService.debug_model_async(object(), object(), {}))
        await asyncio.sleep(0.02)
        event_loop_delay = time.perf_counter() - started
        result = await task
        return event_loop_delay, result

    delay, result = asyncio.run(verify_concurrency())
    assert delay < 0.10
    assert result == {"ok": True}


def test_readiness_reports_dependency_state(monkeypatch):
    from app.api import health

    monkeypatch.setattr(health, "_database_ready", lambda: True)
    monkeypatch.setattr(health, "_redis_ready", lambda: True)
    result = health.ready()
    assert result["data"]["status"] == "ready"
    assert result["data"]["checks"] == {"database": True, "redis": True}


def test_sms_captcha_is_required_only_after_first_send(monkeypatch):
    engine = create_engine(
        "sqlite:///:memory:",
        connect_args={"check_same_thread": False},
        poolclass=StaticPool,
    )
    Base.metadata.create_all(engine)
    monkeypatch.setattr("app.services.auth_service.SmsCodeService.send_code", lambda *_args: None)
    monkeypatch.setattr("app.services.auth_service.CaptchaService.verify", lambda *_args: None)

    with Session(engine) as db:
        db.add(User(id=2, phone="13800000002", name="按需验证用户", daily_chat_limit=10, daily_chat_used=0))
        db.commit()
        AuthService.send_sms_code(db, "13800000002", None, None, "127.0.0.1")
        with pytest.raises(HTTPException) as exc:
            AuthService.send_sms_code(db, "13800000002", None, None, "127.0.0.1")
        assert exc.value.status_code == 428
