"""Per-field validation rules + messages (Form Designer's "Validation"
section): rule config is checked when a Form is saved, and every rule is
enforced again server-side on submission (app/core/form_validation.py) —
the browser's own check (frontend lib/formValidation.ts) can be skipped by a
crafted request, this can't."""

from datetime import date

from app.core.form_validation import resolve_date_limit, validate_field_values


def _fields():
    return [
        {
            "name": "code",
            "type": "text",
            "label": "Vendor Code",
            "validation_rules": [
                {"type": "min_length", "value": 3},
                {"type": "pattern", "value": "[A-Z]{3}-\\d{2}", "message": "{label} must look like ABC-12"},
            ],
        },
        {"name": "email", "type": "email", "label": "Email", "validation_rules": [{"type": "email"}]},
        {"name": "confirm_email", "type": "email", "label": "Confirm Email", "validation_rules": [{"type": "equals_field", "value": "email"}]},
        {"name": "qty", "type": "number", "label": "Quantity", "validation_rules": [{"type": "min_value", "value": 1}, {"type": "max_value", "value": 10}]},
        {"name": "tags", "type": "multiselect", "label": "Tags", "options": ["a", "b", "c"], "validation_rules": [{"type": "max_selections", "value": 2}]},
        {"name": "start", "type": "date", "label": "Start", "validation_rules": [{"type": "min_date", "value": "2026-01-01"}]},
    ]


def test_rules_pass_fail_and_messages():
    fields = _fields()
    good = {"code": "ABC-12", "email": "a@b.co", "confirm_email": "a@b.co", "qty": 5, "tags": ["a"], "start": "2026-03-01"}
    assert validate_field_values(fields, good) == {}

    bad = {"code": "AB", "email": "nope", "confirm_email": "x@y.co", "qty": 11, "tags": ["a", "b", "c"], "start": "2025-12-31"}
    assert validate_field_values(fields, bad) == {
        "code": "Must be at least 3 characters.",  # first failing rule wins
        "email": "Enter a valid email address.",
        "confirm_email": "Must match Email.",
        "qty": "Must be at most 10.",
        "tags": "Select at most 2.",
        "start": "Must be on or after 2026-01-01.",
    }
    # Custom message with {label} substitution; pattern is a whole-value match.
    assert validate_field_values(fields, {"code": "ABC-123"}) == {"code": "Vendor Code must look like ABC-12"}


def test_empty_and_hidden_fields_are_not_rule_checked():
    fields = _fields() + [
        {
            "name": "reason",
            "type": "text",
            "label": "Reason",
            "visibility_rules": [{"driver_field": "qty", "operator": "gt", "value": 5}],
            "validation_rules": [{"type": "min_length", "value": 10}],
        }
    ]
    # Empty values: required's job, not the rules'.
    assert validate_field_values(fields, {}) == {}
    # "reason" is hidden (qty <= 5), so its too-short value is ignored.
    assert validate_field_values(fields, {"qty": 3, "reason": "short"}) == {}
    assert validate_field_values(fields, {"qty": 7, "reason": "short"}) == {"reason": "Must be at least 10 characters."}


def test_relative_date_limits():
    today = date(2026, 9, 24)
    assert resolve_date_limit("today", today) == "2026-09-24"
    assert resolve_date_limit("today+7", today) == "2026-10-01"
    assert resolve_date_limit("today - 30", today) == "2026-08-25"
    assert resolve_date_limit("2026-02-03") == "2026-02-03"
    assert resolve_date_limit("next week") is None


async def test_invalid_rule_config_is_rejected_on_save(client, auth_headers):
    def form(rule):
        return {"name": "Bad rules", "field_definitions": [{"name": "a", "type": "text", "label": "A", "validation_rules": [rule]}]}

    for rule in (
        {"type": "pattern", "value": "[unclosed"},
        {"type": "min_length"},
        {"type": "max_length", "value": "lots"},
        {"type": "min_date", "value": "someday"},
        {"type": "equals_field", "value": "nope"},
    ):
        resp = await client.post("/forms", headers=auth_headers, json=form(rule))
        assert resp.status_code == 422, (rule, resp.text)


async def test_submission_enforces_rules_server_side(client, auth_headers):
    created = await client.post("/forms", headers=auth_headers, json={"name": "Validated", "field_definitions": _fields()})
    assert created.status_code == 201, created.text
    form = created.json()
    # Round-trips the rules and messages as authored.
    assert form["field_definitions"][0]["validation_rules"][1]["message"] == "{label} must look like ABC-12"

    body = {"form_id": form["form_id"], "form_version": form["version"]}
    bad = await client.post("/form-submissions", headers=auth_headers, json={**body, "field_values": {"code": "zz", "qty": 0}})
    assert bad.status_code == 422
    messages = {d["loc"][-1]: d["msg"] for d in bad.json()["detail"]}
    assert messages == {"code": "Vendor Code: Must be at least 3 characters.", "qty": "Quantity: Must be at least 1."}

    ok = await client.post("/form-submissions", headers=auth_headers, json={**body, "field_values": {"code": "ABC-12", "qty": 2}})
    assert ok.status_code == 201, ok.text

    # Editing a submission is held to the same rules.
    edit = await client.patch(f"/form-submissions/{ok.json()['id']}", headers=auth_headers, json={"field_values": {"code": "ABC-12", "qty": 99}})
    assert edit.status_code == 422
