# (c) cavaliba.com - tests / helper - field template rendering

"""
Shared fixture + render indirection for the field-template golden-markup tests
(test_field_render_*.py).

The golden tests render ONE datapoint through the field-template layer and
compare the normalized markup against a stored string. They must stay green
across the _<dataformat>.html unification (see
project/claude_doc/2026-10-03-FIELD-TEMPLATE-UNIFICATION-DONE.md), so the knowledge
of *which* template implements a display_mode lives here, in
template_for_mode(), and nowhere else:

  * phase 0        -> fields/_datapoint_{list,detail,print}.html for the
                      display modes, fields/_<dataformat>.html for edit
  * phase 1        -> fields/_field.html for edit, dispatchers for the rest
  * phase 3 (now)  -> fields/_field.html for every call site; the mode travels
                      in the context, and _field.html resolves the per-type
                      template from item.dataformat

Updating template_for_mode() (and nothing else) is what let the same golden
strings prove the refactor changed no markup.
"""

import re

from django.template.loader import render_to_string

from app_data.data import Instance
from app_data.models import DataFile, DataInstance

from .helper_data import add_instance, add_schema, add_test_sites

# tests/helper.py re-exports this module with "import *", so pin what goes out:
# without it, re / render_to_string / Instance / DataFile / DataInstance would
# land in the tests.helper namespace and could shadow another helper's names.
__all__ = [
    "DISPLAY_MODE_FOR_CALL_SITE",
    "FIELDRENDER_CLASSNAME",
    "FIELDRENDER_DATAVIEW",
    "FIELDRENDER_ENUM",
    "FIELDRENDER_FIELDS",
    "FIELDRENDER_FILEID",
    "FIELDRENDER_KEYNAME",
    "FIELDRENDER_VALUES",
    "LONG_TEXT",
    "add_fieldrender_fixture",
    "add_fieldrender_instance",
    "add_fieldrender_schema",
    "display_mode_for_call_site",
    "normalize_html",
    "render_datapoint",
    "render_field",
    "site_id",
    "template_for_mode",
]

_HTML_COMMENT = re.compile(r"<!--.*?-->", re.DOTALL)

FIELDRENDER_CLASSNAME = "fieldrender"
FIELDRENDER_KEYNAME = "obj01"
FIELDRENDER_ENUM = "fieldrender_enum"
FIELDRENDER_DATAVIEW = "fieldrender_all"
FIELDRENDER_FILEID = "11111111-2222-3333-4444-555555555555"

# >64 chars on purpose: pins the list-mode truncatechars:64 behaviour
LONG_TEXT = "Lorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod tempor"

# every dataformat in FIELD_CONSTRUCTOR_TABLE, plus the dataformat_ext and
# is_multi variants that select a different markup branch today
FIELDRENDER_FIELDS = {
    "f_string": {"dataformat": "string", "displayname": "F String", "order": 10},
    "f_string_safe": {
        "dataformat": "string",
        "dataformat_ext": "safe",
        "displayname": "F String Safe",
        "order": 11,
    },
    "f_string_url": {
        "dataformat": "string",
        "dataformat_ext": "url",
        "displayname": "F String Url",
        "order": 12,
    },
    "f_string_multi": {
        "dataformat": "string",
        "displayname": "F String Multi",
        "cardinal_max": 0,
        "order": 13,
    },
    "f_int": {"dataformat": "int", "displayname": "F Int", "order": 20},
    "f_float": {"dataformat": "float", "displayname": "F Float", "order": 21},
    "f_boolean": {"dataformat": "boolean", "displayname": "F Boolean", "order": 22},
    "f_date": {"dataformat": "date", "displayname": "F Date", "order": 30},
    "f_datetime": {"dataformat": "datetime", "displayname": "F Datetime", "order": 31},
    "f_time": {"dataformat": "time", "displayname": "F Time", "order": 32},
    "f_ipv4": {"dataformat": "ipv4", "displayname": "F Ipv4", "order": 40},
    "f_ipv4_subnet": {
        "dataformat": "ipv4",
        "dataformat_ext": "subnet",
        "displayname": "F Ipv4 Subnet",
        "order": 41,
    },
    "f_text": {"dataformat": "text", "displayname": "F Text", "order": 50},
    "f_text_html": {
        "dataformat": "text",
        "dataformat_ext": "html",
        "displayname": "F Text Html",
        "order": 51,
    },
    "f_text_yaml": {
        "dataformat": "text",
        "dataformat_ext": "yaml",
        "displayname": "F Text Yaml",
        "order": 52,
    },
    "f_schema": {
        "dataformat": "schema",
        "dataformat_ext": "site",
        "displayname": "F Schema",
        "order": 60,
    },
    "f_schema_multi": {
        "dataformat": "schema",
        "dataformat_ext": "site",
        "displayname": "F Schema Multi",
        "cardinal_max": 0,
        "order": 61,
    },
    "f_enumerate": {
        "dataformat": "enumerate",
        "dataformat_ext": FIELDRENDER_ENUM,
        "displayname": "F Enumerate",
        "order": 70,
    },
    "f_permission": {
        "dataformat": "permission",
        "displayname": "F Permission",
        "order": 80,
    },
    "f_password": {"dataformat": "password", "displayname": "F Password", "order": 90},
    "f_file": {"dataformat": "file", "displayname": "F File", "order": 100},
    # external fieldnames must start with "_"; resolves site.description
    # through the f_schema reference and injects it as "f_ext"
    "_f_ext": {
        "dataformat": "external",
        "dataformat_ext": "f_schema description",
        "displayname": "F External",
        "order": 110,
    },
}

FIELDRENDER_VALUES = {
    "f_string": "plain string value",
    "f_string_safe": "<b>bold</b>",
    "f_string_url": "https://example.test/doc",
    "f_string_multi": ["multi one", "multi two"],
    "f_int": 42,
    "f_float": 3.5,
    "f_boolean": True,
    "f_date": "2026-01-15",
    "f_datetime": "2026-01-15 10:20:30",
    "f_time": "10:20:30",
    "f_ipv4": "10.1.2.3",
    "f_ipv4_subnet": "10.1.2.0/24",
    "f_text": LONG_TEXT,
    "f_text_html": "<p>html body</p>",
    "f_text_yaml": "a: 1\nb: 2\n",
    "f_schema": "site01",
    "f_schema_multi": ["site01", "site02"],
    "f_enumerate": "Major",
    "f_permission": "p_data_admin",
    "f_password": "Secr3t-Passw0rd!",
    "f_file": FIELDRENDER_FILEID,
}

ENUM_CONTENT = """
- value: Minor
  widget: yellow_circle
- value: Major
  widget: red_circle
- value: Plain
"""

# one column per field, so instance_list renders every dataformat
DATAVIEW_CONTENT = "columns:\n  - keyname\n" + "".join(
    f"  - {name}\n" for name in FIELDRENDER_FIELDS if not name.startswith("_")
)


def add_fieldrender_schema():
    """Create the "fieldrender" schema (one field per dataformat + variants), its
    _enumerate, the referenced site instances, a DataFile and a _dataview listing
    every field as a column. Returns the Schema object."""

    add_test_sites(count=2)

    add_instance(
        classname="_enumerate",
        keyname=FIELDRENDER_ENUM,
        fields={"content": ENUM_CONTENT},
    )

    DataFile.objects.get_or_create(
        fileid=FIELDRENDER_FILEID,
        defaults={
            "filename": "report.pdf",
            "displayname": "Report",
            "size": 2048,
        },
    )

    add_instance(
        classname="_dataview",
        keyname=FIELDRENDER_DATAVIEW,
        fields={"target_class": FIELDRENDER_CLASSNAME, "content": DATAVIEW_CONTENT},
    )

    return add_schema(classname=FIELDRENDER_CLASSNAME, field_definition=FIELDRENDER_FIELDS)


def add_fieldrender_instance(keyname=FIELDRENDER_KEYNAME):
    """Create one "fieldrender" instance with every field populated. Returns the
    Instance (expanded, so injected/external fields are resolved)."""

    add_instance(
        classname=FIELDRENDER_CLASSNAME,
        keyname=keyname,
        fields=dict(FIELDRENDER_VALUES),
    )
    return Instance.from_keyname(classname=FIELDRENDER_CLASSNAME, keyname=keyname, expand=True)


def add_fieldrender_fixture():
    """add_fieldrender_schema() + add_fieldrender_instance(); returns the Instance."""
    add_fieldrender_schema()
    return add_fieldrender_instance()


# call site -> the display_mode value that call site passes. "new" is a call
# site, not a mode: instance_new.html renders with "edit". Keeping the two
# concepts apart is what let the phase-2 tests catch a template branching on a
# mode nobody passes. (A "tech" call site existed until the detail tech view was
# removed in 5.3.0-RC; it passed "detail".)
DISPLAY_MODE_FOR_CALL_SITE = {
    "list": "list",
    "detail": "detail",
    "print": "print",
    "edit": "edit",
    "new": "edit",
}


def display_mode_for_call_site(call_site):
    """Return the display_mode the given call site passes to the field layer."""

    try:
        return DISPLAY_MODE_FOR_CALL_SITE[call_site]
    except KeyError:
        raise ValueError(f"unknown call site: {call_site}") from None


def template_for_mode(call_site):
    """Return the template path the given call site includes.

    THE single place the golden tests know about template layout - see this
    module's docstring. Now one path for every call site: the unification is
    complete, so the mode travels in the context and the per-type template is
    resolved by _field.html from item.dataformat at render time."""

    display_mode_for_call_site(call_site)  # rejects an unknown call site
    return "app_data/fields/_field.html"


def render_datapoint(datapoint, call_site):
    """Render one datapoint dict the way the given call site does, and return the
    normalized markup (see normalize_html)."""

    template = template_for_mode(call_site)
    context = {"item": datapoint, "display_mode": display_mode_for_call_site(call_site)}
    return normalize_html(render_to_string(template, context))


def render_field(instance, fieldname, call_site):
    """Build the datapoint for instance.fields[fieldname] in the shape the given
    call site consumes (ui_edit for edit/new, ui_detail otherwise) and render it
    as that call site does. Returns normalized markup."""

    field = instance.fields[fieldname]
    if display_mode_for_call_site(call_site) == "edit":
        datapoint = field.get_datapoint_ui_edit()
    else:
        datapoint = field.get_datapoint_ui_detail()
    return render_datapoint(datapoint, call_site)


def normalize_html(text):
    """Drop HTML comments, collapse every run of whitespace to one space and strip
    it around tag boundaries, so the churn the refactor inevitably produces
    (indentation from nesting markup inside a display_mode branch, and the
    per-file "<!-- FILE format -->" banners that go away with the merge) doesn't
    fail a golden comparison while real markup changes still do.

    Caveat: this also collapses newlines *inside* <pre>, so a golden comparison
    can't see them - a test that cares asserts on the unnormalized render
    instead (see test_field_render_text)."""

    out = _HTML_COMMENT.sub("", text)
    out = " ".join(out.split())
    out = out.replace("> <", "><")
    return out.strip()


def site_id(keyname):
    """Return the DataInstance id of a fixture site instance. Golden strings can't
    hardcode it - it depends on how many instances the builtin bootstrap created."""

    return DataInstance.objects.get(classname="site", keyname=keyname).id
