"""パス正規化と方針判定。"""

from __future__ import annotations

import dataclasses
import json

import pytest

from app import errors, policy
from app.config import PATH_POLICY_ALLOWLIST


@pytest.mark.parametrize(
    ("raw", "expected"),
    [
        ("/slots", "/slots"),
        ("/slots/", "/slots"),
        ("/slots/0", "/slots/0"),
        ("//slots", "/slots"),
        ("/SLOTS", "/slots"),
        ("/v1/../slots", "/slots"),
        ("/%2e%2e/slots", "/slots"),
        ("/%252e%252e/slots", "/slots"),
        ("/v1%2fchat%2fcompletions", "/v1/chat/completions"),
        ("/./v1/./chat/completions", "/v1/chat/completions"),
        ("/v1/chat/completions/", "/v1/chat/completions"),
        ("/", "/"),
        ("", "/"),
        ("/a/b/../../slots", "/slots"),
    ],
)
def test_normalize_path(raw: str, expected: str) -> None:
    assert policy.normalize_path(raw) == expected


@pytest.mark.parametrize(
    ("path", "denied"),
    [
        ("/slots", True),
        ("/slots/", True),
        ("/slots/0", True),
        ("/slotsfoo", False),
        ("/slots-manager", False),
        ("/props", True),
        ("/props/x", True),
        ("/propsx", False),
        ("/metrics", True),
        ("/metricsfoo", False),
        ("/lora-adapters", True),
        ("/lora-adapters/1", True),
        ("/lora-adaptersfoo", False),
        ("/v1/chat/completions", False),
    ],
)
def test_deny_boundary(path: str, denied: bool) -> None:
    """前方一致ではなくパス境界で判定する。"""

    assert policy.is_denied(policy.normalize_path(path)) is denied


def test_denied_path_raises_404(settings) -> None:
    with pytest.raises(errors.GatewayError) as excinfo:
        policy.check_path(policy.normalize_path("/slots/0"), settings)
    assert excinfo.value.status_code == 404


def test_denylist_allows_everything_else(settings) -> None:
    policy.check_path(policy.normalize_path("/unknown/path"), settings)


def test_allowlist_rejects_unlisted(settings) -> None:
    strict = dataclasses.replace(settings, path_policy=PATH_POLICY_ALLOWLIST)
    policy.check_path("/v1/chat/completions", strict)
    policy.check_path("/v1/models", strict)
    policy.check_path("/completion", strict)
    policy.check_path("/apply-template", strict)
    with pytest.raises(errors.GatewayError) as excinfo:
        policy.check_path("/unknown/path", strict)
    assert excinfo.value.status_code == 404


def test_allowlist_still_denies_denylist_paths(settings) -> None:
    """許可リスト方式でも拒否リストは併せて効く。"""

    strict = dataclasses.replace(settings, path_policy=PATH_POLICY_ALLOWLIST)
    with pytest.raises(errors.GatewayError):
        policy.check_path("/slots", strict)


@pytest.mark.parametrize(
    "path",
    [
        "/v1/chat/completions",
        "/v1/completions",
        "/v1/embeddings",
        "/v1/rerank",
        "/v1/reranking",
        "/completion",
        "/completions",
        "/infill",
        "/v1/infill",
        "/apply-template",
        "/v1/audio/transcriptions",
    ],
)
def test_model_check_paths(path: str) -> None:
    """旧来の別名を含む全ての生成系パスが検査対象である。"""

    assert policy.requires_model_check(policy.normalize_path(path))
    assert policy.requires_model_check(policy.normalize_path(path + "/"))


def test_extract_model_json() -> None:
    body = json.dumps({"model": "qwen3-8b", "messages": []}).encode()
    assert policy.extract_model(body, "application/json") == "qwen3-8b"


def test_extract_model_returns_none_for_unparsable() -> None:
    assert policy.extract_model(b"model=qwen3-8b", "application/x-www-form-urlencoded") is None
    assert policy.extract_model(b"", "application/json") is None
    assert policy.extract_model(b'{"messages": []}', "application/json") is None


def test_extract_model_multipart() -> None:
    body = (
        b"--BOUNDARY\r\n"
        b'Content-Disposition: form-data; name="file"; filename="a.wav"\r\n'
        b"Content-Type: audio/wav\r\n\r\n"
        b"RIFFDATA\r\n"
        b"--BOUNDARY\r\n"
        b'Content-Disposition: form-data; name="model"\r\n\r\n'
        b"whisper-1\r\n"
        b"--BOUNDARY--\r\n"
    )
    assert policy.extract_model(body, "multipart/form-data; boundary=BOUNDARY") == "whisper-1"


def test_check_admission_grammar(settings) -> None:
    body = json.dumps({"model": "m", "grammar": "x" * (settings.max_grammar_bytes + 1)}).encode()
    with pytest.raises(errors.GatewayError) as excinfo:
        policy.check_admission(body, settings)
    assert excinfo.value.status_code == 400


def test_check_admission_n_probs(settings) -> None:
    body = json.dumps({"model": "m", "n_probs": settings.max_n_probs + 1}).encode()
    with pytest.raises(errors.GatewayError) as excinfo:
        policy.check_admission(body, settings)
    assert excinfo.value.status_code == 400


def test_check_admission_accepts_valid_llamacpp_params(settings) -> None:
    body = json.dumps(
        {"model": "m", "grammar": "root ::= \"a\"", "cache_prompt": True, "n_probs": settings.max_n_probs}
    ).encode()
    policy.check_admission(body, settings)


def test_filter_models_payload() -> None:
    raw = json.dumps(
        {"object": "list", "data": [{"id": "a"}, {"id": "b"}, {"id": "c"}]}
    ).encode()
    filtered = policy.filter_models_payload(raw, frozenset({"a", "c"}))
    assert filtered is not None
    assert [item["id"] for item in json.loads(filtered)["data"]] == ["a", "c"]


def test_filter_models_payload_passthrough_for_unknown_shape() -> None:
    assert policy.filter_models_payload(b"not json", frozenset({"a"})) is None
    assert policy.filter_models_payload(b'{"data": "x"}', frozenset({"a"})) is None
    assert policy.filter_models_payload(b'{"data": []}', frozenset()) is None
