import pytest
from fastapi.routing import APIRoute
from fastapi.testclient import TestClient

from flyway.api.deps import CSRF_HEADER, current_actor, get_db
from flyway.api.main import PUBLIC_PATHS, create_app
from flyway.auth.service import create_user


@pytest.fixture
def client(factory):
    app = create_app()

    def override():
        with factory() as s:
            yield s

    app.dependency_overrides[get_db] = override
    with TestClient(app, base_url="https://testserver") as c:
        yield c


@pytest.fixture
def users(factory):
    with factory() as s, s.begin():
        create_user(s, "boss", "boss-secret", admin=True)
        create_user(s, "sam", "sam-secret")


def _login(client, name, pw):
    r = client.post("/api/auth/login", json={"username": name, "password": pw})
    assert r.status_code == 200, r.text
    return r.json()["csrf_token"]


def _depends_on(dependant, target) -> bool:
    return any(d.call is target or _depends_on(d, target) for d in dependant.dependencies)


def test_every_non_public_route_requires_login():
    app = create_app()
    for route in app.routes:
        if isinstance(route, APIRoute) and route.path not in PUBLIC_PATHS:
            assert _depends_on(route.dependant, current_actor), route.path


def test_unauthenticated_request_gets_401(client):
    assert client.get("/api/auth/me").status_code == 401


def test_login_sets_secure_http_only_cookie(client, users, monkeypatch):
    from flyway.config import settings
    monkeypatch.setattr(settings, "cookie_secure", True)  # 生产默认；本机 .env 为 http 开发关闭了它
    r = client.post("/api/auth/login", json={"username": "sam", "password": "sam-secret"})
    cookie = r.headers["set-cookie"].lower()
    assert "httponly" in cookie and "secure" in cookie and "samesite=lax" in cookie
    assert client.get("/api/auth/me").json()["username"] == "sam"


def test_bad_login_returns_400_with_message(client, users):
    r = client.post("/api/auth/login", json={"username": "sam", "password": "nope"})
    assert r.status_code == 400 and r.json()["message"] == "Invalid user name or password."


def test_write_without_csrf_is_rejected(client, users):
    csrf = _login(client, "sam", "sam-secret")
    assert client.post("/api/auth/logout").status_code == 403
    assert client.post("/api/auth/logout", headers={CSRF_HEADER: csrf}).status_code == 204
    assert client.get("/api/auth/me").status_code == 401


def test_staff_gets_403_on_admin_route_and_admin_gets_200(client, users):
    _login(client, "sam", "sam-secret")
    assert client.get("/api/users").status_code == 403
    _login(client, "boss", "boss-secret")
    names = [u["username"] for u in client.get("/api/users").json()["items"]]
    assert names == ["boss", "sam"]


def test_api_docs_are_not_public(client):
    for path in ("/docs", "/redoc", "/openapi.json"):
        assert client.get(path).status_code == 404, path


def _age_session(factory, hours_left):
    import datetime as dt

    from flyway.models import AppSession
    with factory() as s, s.begin():
        for row in s.query(AppSession):
            row.expires_at = dt.datetime.now(dt.UTC) + dt.timedelta(hours=hours_left)


def test_session_slides_when_more_than_half_used(client, users, factory):
    from flyway.models import AppSession
    _login(client, "sam", "sam-secret")
    _age_session(factory, 30)  # 未过半：不续期、不重发 Cookie
    r = client.get("/api/auth/me")
    assert r.status_code == 200 and "set-cookie" not in r.headers
    _age_session(factory, 10)  # 已过半：续到 48 小时，重发会话 Cookie（未勾选记住我 → 无 Max-Age）
    r = client.get("/api/auth/me")
    assert "flyway_session=" in r.headers["set-cookie"] and "max-age" not in r.headers["set-cookie"].lower()
    with factory() as s:
        import datetime as dt
        left = s.query(AppSession).one().expires_at - dt.datetime.now(dt.UTC)
        assert left > dt.timedelta(hours=47)


def test_remember_me_cookie_is_renewed_with_max_age(client, users, factory):
    r = client.post("/api/auth/login", json={"username": "sam", "password": "sam-secret", "remember_me": True})
    assert "max-age=172800" in r.headers["set-cookie"].lower()
    _age_session(factory, 1)
    r = client.get("/api/parcels/attentions.xlsx")  # 直接返回 Response 的接口也要重发 Cookie
    assert r.status_code == 200 and "max-age=172800" in r.headers["set-cookie"].lower()
