"""Unit tests for ``recoil.api.adapters._ids`` validators.

Debug R3 widened ``HIERARCHY_ID_RE`` to allow ``-`` so the production memory
ID shape ``L-YYYY-MM-DD`` (synthesized fallback in
``recoil/api/adapters/memory.py:96``) round-trips through
``/api/memory/{entry_id}/toggle`` instead of 400-ing.

These tests pin both the new accept-list AND the path-traversal reject-list
so the regex can't be re-narrowed (or accidentally widened) without flagging.
"""
from __future__ import annotations

import pytest

from recoil.api.adapters._ids import (
    HIERARCHY_ID_RE,
    PROJECT_ID_RE,
    validate_hierarchy_id,
    validate_project_id,
)


# ── HIERARCHY_ID_RE accept-list ─────────────────────────────────────────────


@pytest.mark.parametrize(
    "value",
    [
        # Beat / take / scene / episode IDs (existing shapes).
        "EP001_SH02",
        "EP001_SH05A",
        "T_alpha",
        "ep001_sc02",
        "scene_3",
        "T0",
        "a",
        "Z",
        "0",
        "_",
        # Debug R3 — production memory ID shape MUST be accepted.
        "L-2026-03-25",
        "L-2026-12-31",
        "AP-001",
        # Mixed underscore + dash.
        "EP001-SH02_v2",
    ],
)
def test_hierarchy_id_re_accepts_valid_shapes(value: str) -> None:
    assert HIERARCHY_ID_RE.match(value) is not None
    # validate_hierarchy_id should also accept and round-trip.
    assert validate_hierarchy_id("entry_id", value) == value


def test_validate_hierarchy_id_accepts_production_memory_id() -> None:
    """Pin the Debug R3 fix: live LEARNINGS.md fallback IDs round-trip."""
    assert validate_hierarchy_id("entry_id", "L-2026-03-25") == "L-2026-03-25"


# ── HIERARCHY_ID_RE reject-list (path-traversal protection) ─────────────────


@pytest.mark.parametrize(
    "value",
    [
        # Empty (length min=1).
        "",
        # Path-traversal segment.
        "..",
        # Forward-slash (decoded ``/`` or ``%2f``).
        "/",
        "../evil",
        "EP001/SH02",
        # Backslash (Windows path separator) — also not in charset.
        "EP001\\SH02",
        # Spaces / special chars.
        "has space",
        "EP001 SH02",
        "EP001@SH02",
        "EP001.SH02",
        # Decoded percent-encodings of dangerous chars.
        "%2f",
        "%2F",
        "%2e%2e",
        # Length cap (65 chars).
        "a" * 65,
        # Non-ASCII.
        "EP001\u202eSH02",
        # Non-string types.
    ],
)
def test_hierarchy_id_re_rejects_traversal_and_malformed(value: str) -> None:
    assert HIERARCHY_ID_RE.match(value) is None
    with pytest.raises(ValueError):
        validate_hierarchy_id("entry_id", value)


@pytest.mark.parametrize("value", [None, 42, 3.14, ["EP001"], {"x": 1}])
def test_validate_hierarchy_id_rejects_non_strings(value: object) -> None:
    with pytest.raises(ValueError):
        validate_hierarchy_id("entry_id", value)  # type: ignore[arg-type]


# ── PROJECT_ID_RE remains unchanged ─────────────────────────────────────────


@pytest.mark.parametrize(
    "value",
    ["tartarus", "the-afterimage", "leviathan_v2", "a", "p1"],
)
def test_project_id_re_accepts_valid(value: str) -> None:
    assert PROJECT_ID_RE.match(value) is not None
    assert validate_project_id(value) == value


@pytest.mark.parametrize(
    "value",
    [
        "",
        "Tartarus",  # uppercase rejected
        "../evil",
        "../../etc/passwd",
        "tartarus/",
        "tartarus.",
        "_leading-underscore",
        "-leading-dash",
        "a" * 65,
    ],
)
def test_project_id_re_rejects_invalid(value: str) -> None:
    assert PROJECT_ID_RE.match(value) is None
    with pytest.raises(ValueError):
        validate_project_id(value)
