"""SLICE-2 unit tests — collection field schema + central validation.

Pure functions only (no DB / no Gitea). Per
docs/design/2026-06-06-configurable-collection-metadata.md §7.2 (SLICE-2):
`metadata_schema.parse_fields` (lenient schema parsing) and
`metadata_schema.validate` (advisory at read / enforcement point at write).
Honors INV-3 (never hard-fails), INV-5 (no-fields unchanged), INV-7 (undeclared
keys ride along).
"""
from __future__ import annotations

from app import metadata_schema as ms


# ---- parse_fields: normalization + leniency ----

def test_parse_fields_each_type():
    raw = {
        "priority": {"type": "enum", "values": ["P0", "P1", "P2"], "label": "Priority"},
        "tags": {"type": "tags"},
        "owner": {"type": "text"},
    }
    fields = ms.parse_fields(raw)
    assert list(fields) == ["priority", "tags", "owner"]  # order preserved
    assert fields["priority"] == {
        "type": "enum",
        "values": ["P0", "P1", "P2"],
        "label": "Priority",
    }
    assert fields["tags"] == {"type": "tags"}
    assert fields["owner"] == {"type": "text"}


def test_parse_fields_controlled_tags_keeps_values():
    fields = ms.parse_fields({"area": {"type": "tags", "values": ["a", "b"]}})
    assert fields["area"] == {"type": "tags", "values": ["a", "b"]}


def test_parse_fields_missing_block_is_empty():
    assert ms.parse_fields(None) == {}
    assert ms.parse_fields({}) == {}


def test_parse_fields_non_mapping_block_skipped():
    assert ms.parse_fields(["not", "a", "mapping"]) == {}
    assert ms.parse_fields("nope") == {}


def test_parse_fields_enum_without_values_skipped():
    # enum requires a non-empty values list — skipped, not fatal.
    assert ms.parse_fields({"p": {"type": "enum"}}) == {}
    assert ms.parse_fields({"p": {"type": "enum", "values": []}}) == {}


def test_parse_fields_unknown_type_skipped():
    fields = ms.parse_fields(
        {"good": {"type": "text"}, "bad": {"type": "ref"}, "huh": {"type": "frob"}}
    )
    assert list(fields) == ["good"]


def test_parse_fields_non_mapping_def_skipped():
    fields = ms.parse_fields({"good": {"type": "text"}, "bad": "scalar"})
    assert list(fields) == ["good"]


def test_parse_fields_values_coerced_to_str_list():
    fields = ms.parse_fields({"p": {"type": "enum", "values": [0, 1, 2]}})
    assert fields["p"]["values"] == ["0", "1", "2"]


# ---- validate: advisory problem reporting ----

SCHEMA = ms.parse_fields(
    {
        "priority": {"type": "enum", "values": ["P0", "P1", "P2"]},
        "tags": {"type": "tags"},
        "area": {"type": "tags", "values": ["checkout", "cart"]},
        "owner": {"type": "text"},
    }
)


def test_validate_happy():
    values = {
        "priority": "P0",
        "tags": ["anything", "free"],
        "area": ["checkout"],
        "owner": "ben",
    }
    assert ms.validate(values, SCHEMA) == []


def test_validate_absent_fields_ok():
    # A declared field that the entry omits is fine (no required fields in v1).
    assert ms.validate({}, SCHEMA) == []


def test_validate_enum_bad_value():
    problems = ms.validate({"priority": "P9"}, SCHEMA)
    assert [p.field for p in problems] == ["priority"]
    assert problems[0].code == "not-in-values"


def test_validate_enum_wrong_type():
    problems = ms.validate({"priority": ["P0"]}, SCHEMA)
    assert problems[0].field == "priority"
    assert problems[0].code == "wrong-type"


def test_validate_tags_free_form_ok():
    assert ms.validate({"tags": ["x", "y", "z"]}, SCHEMA) == []


def test_validate_tags_wrong_type():
    problems = ms.validate({"tags": "notalist"}, SCHEMA)
    assert problems[0].field == "tags"
    assert problems[0].code == "wrong-type"


def test_validate_controlled_tags_bad_member():
    problems = ms.validate({"area": ["checkout", "nope"]}, SCHEMA)
    assert problems[0].field == "area"
    assert problems[0].code == "not-in-values"


def test_validate_text_wrong_type():
    problems = ms.validate({"owner": ["a", "b"]}, SCHEMA)
    assert problems[0].field == "owner"
    assert problems[0].code == "wrong-type"


def test_validate_undeclared_keys_ignored():
    # INV-7: keys outside the schema ride along untouched, never flagged.
    assert ms.validate({"random": "value", "slug": "x", "title": "y"}, SCHEMA) == []


def test_validate_empty_schema_no_problems():
    # INV-5: a collection with no fields validates everything as clean.
    assert ms.validate({"priority": "anything", "x": 1}, {}) == []


def test_problem_as_dict():
    p = ms.Problem(field="priority", code="not-in-values", message="bad")
    assert p.as_dict() == {
        "field": "priority",
        "code": "not-in-values",
        "message": "bad",
    }
