"""The parser version moves whenever the kept payload's shape moves (K12.1).

Cost: the 18 Sep trial collection stopped storing the tile's markup, so the kept fragment no
longer carried the key the older records did, and every new record was still stamped
`avolta/2026-09-05`. Nothing could tell from a record which shape of code wrote it. The version
is what `listed_fields` dispatches on and what a backfill trusts, so the shape and the version
must move together.

This pins, per collector module, a fingerprint of the source text of the expression that builds
`raw=` (the fragment) and of `facts_only`'s key filter. Change either and this fails until the
module's `PARSER_VERSION` is bumped, which re-pins the fingerprint below. Static: no fixture, no
network, no import of the collectors.
"""
from __future__ import annotations

import ast
import hashlib
import pathlib
import re

import pytest

COLLECTORS = pathlib.Path(__file__).resolve().parents[1] / "app" / "services" / "collectors"

#: module -> (PARSER_VERSION, fingerprint of the fragment-building source at that version).
PINNED = {
    "ari": ("ari/2026-09-05", "45a194a218cb"),
    "avolta": ("avolta/2026-09-18", "4c48c9baf8df"),
    "changi": ("changi/2026-09-05", "e11e6342a589"),
    "dubai": ("dubai/2026-09-04", "b250e14dfcaa"),
    "extime": ("extime/2026-09-05", "c9e0d3b52edf"),
    "heinemann": ("heinemann/2026-09-04", "b250e14dfcaa"),
    "heinemann_platform": ("heinemann-platform/2026-09-04", "b250e14dfcaa"),
    "shilla": ("shilla/2026-09-05", "8528579da7fe"),
    "shopify": ("shopify/2026-09-17", "cd4de09fc78c"),
}


def _version(source: str) -> str:
    match = re.search(r'^PARSER_VERSION\s*=\s*"([^"]+)"', source, re.M)
    assert match, "no PARSER_VERSION"
    return match.group(1)


def _raw_sources(source: str) -> list[str]:
    """The source text of every `raw=<expr>` keyword in the module, plus the name any of them
    resolves to when it is a bare name assigned once in the same function (extime's
    `raw=raw_payload`)."""
    tree = ast.parse(source)
    out: list[str] = []
    assigned: dict[str, str] = {}
    for node in ast.walk(tree):
        if isinstance(node, ast.Assign) and len(node.targets) == 1 and isinstance(node.targets[0], ast.Name):
            assigned[node.targets[0].id] = ast.get_source_segment(source, node.value) or ""
    for node in ast.walk(tree):
        if isinstance(node, ast.Call):
            for kw in node.keywords:
                if kw.arg == "raw":
                    text = ast.get_source_segment(source, kw.value) or ""
                    if isinstance(kw.value, ast.Name) and kw.value.id in assigned:
                        text = assigned[kw.value.id]
                    out.append(text)
    return out


def fingerprint(module: str) -> str:
    source = (COLLECTORS / f"{module}.py").read_text(encoding="utf-8")
    base = (COLLECTORS / "base.py").read_text(encoding="utf-8")
    key_re = re.search(r"_EXPRESSION_KEY_RE\s*=\s*re\.compile\((.*?)\n\)", base, re.S)
    parts = _raw_sources(source)
    assert parts, f"{module}: no raw= construction found"
    blob = "\n---\n".join(p.strip() for p in parts) + "\n===\n" + (key_re.group(1) if key_re else "")
    return hashlib.sha256(blob.encode("utf-8")).hexdigest()[:12]


@pytest.mark.parametrize("module", sorted(PINNED))
def test_the_kept_payload_shape_and_the_parser_version_move_together(module):
    version, pinned = PINNED[module]
    source = (COLLECTORS / f"{module}.py").read_text(encoding="utf-8")
    actual_version = _version(source)
    actual = fingerprint(module)
    if actual != pinned:
        assert actual_version != version, (
            f"{module}: the kept payload shape changed (fingerprint {actual}, pinned {pinned}) but PARSER_VERSION is still "
            f"{version}. Bump PARSER_VERSION in collectors/{module}.py, then re-pin PINNED[{module!r}] = ({actual_version!r}, {actual!r}).")
    assert actual_version == version, (
        f"{module}: PARSER_VERSION is {actual_version}; re-pin PINNED[{module!r}] = ({actual_version!r}, {actual!r}).")
