"""
verify_spec_examples.py — spec-example verification for moml2.py
==================================================================

This is deliberately NOT a comprehensive adversarial test suite in the
style of MOML V1's 62-test harness. That style of test — many small
cases, some inherited from cross-language ports — is exactly the shape
that went wrong earlier this session: a test can encode an assumption
borrowed from somewhere else, look thorough, and still be checking the
wrong contract.

Every case below is instead pulled directly from either:
  (a) the pasted "MOML 2.0 Language Specification" document's own
      worked examples, or
  (b) the specific real-world traps this session traced through MOML
      V1 (the 085041 leading-zero loss, the #h vanishing, the raw
      backslash path corruption, the Smith/John comma name) — used
      here to confirm V2 does NOT reproduce them, not to re-verify V1.

Run: python3 verify_spec_examples.py
"""

import moml2
from moml2 import loads as _loads_raw, dumps, get, get_active, MOMLError, MOMLDict, MOMLList

passed = 0
failed = 0


def loads(text):
    """Prepend the mandatory header so fixtures below can stay focused on
    the rule being tested rather than repeating '# MOML 2.0\\n' everywhere."""
    if not text.startswith(moml2.HEADER):
        text = moml2.HEADER + "\n" + text
    return _loads_raw(text)


def check(label, actual, expected):
    global passed, failed
    ok = actual == expected
    print(f"  {'PASS' if ok else 'FAIL'}  {label}")
    if not ok:
        print(f"        expected: {expected!r}")
        print(f"        actual:   {actual!r}")
        failed += 1
    else:
        passed += 1


def check_raises(label, fn):
    global passed, failed
    try:
        fn()
        print(f"  FAIL  {label}  (expected MOMLError, none raised)")
        failed += 1
    except MOMLError as e:
        print(f"  PASS  {label}  ({e})")
        passed += 1


def section(name):
    print(f"\n── {name} " + "─" * max(0, 60 - len(name)))


# ══════════════════════════════════════════════════════════════════════════
section("Rule 1 — everything is a string")
# ══════════════════════════════════════════════════════════════════════════

c = loads('timeout = 45\nenabled = true\nname = Mike\n')
check("timeout is str '45', not int", c["timeout"], "45")
check("enabled is str 'true', not bool", c["enabled"], "true")
check("name is str 'Mike'", c["name"], "Mike")
check("timeout type is str", type(c["timeout"]), str)
check("enabled type is str", type(c["enabled"]), str)


# ══════════════════════════════════════════════════════════════════════════
section("Rule 2 — bare unless quoting required; quotes ≠ type")
# ══════════════════════════════════════════════════════════════════════════

c = loads('name = Mike\ncity = Houston\ntimeout = 45\nprovider = openrouter\n')
check("name bare", c["name"], "Mike")
check("city bare", c["city"], "Houston")

c = loads('name = "Smith, John"\n')
check('quoted "Smith, John" is ONE scalar, not split', c["name"], "Smith, John")
check("scalar type is str, not list", type(c["name"]), str)

c1 = loads("name = Mike\n")
c2 = loads('name = "Mike"\n')
check("bare Mike == quoted \"Mike\" (quotes don't declare type)", c1["name"], c2["name"])


# ══════════════════════════════════════════════════════════════════════════
section("Rule 3 — backslash literal outside quotes")
# ══════════════════════════════════════════════════════════════════════════

# The exact trap: MOML V1 corrupted this path because \U and \D aren't
# recognised escapes there either, but \n and \t inside a longer path
# WOULD have been silently eaten. V2 must not touch it at all.
c = loads(r"path = C:\Users\Karim\Documents")
check("Windows path untouched, no escaping applied",
      c["path"], r"C:\Users\Karim\Documents")

c = loads(r"file = D:\Invoices\2026\September.xlsx")
check("second path example untouched", c["file"], r"D:\Invoices\2026\September.xlsx")

c = loads(r"pattern = \d+\.\d+")
check("regex-like bare value untouched", c["pattern"], r"\d+\.\d+")

c = loads(r'message = "Line one\nLine two"')
check("quoted \\n becomes real newline", c["message"], "Line one\nLine two")

c = loads(r'quote = "He said \"Hello\""')
check('quoted \\" becomes literal "', c["quote"], 'He said "Hello"')

c = loads(r'path = "C:\\Data\\Folder"')
check("quoted \\\\ becomes single backslash", c["path"], r"C:\Data\Folder")


# ══════════════════════════════════════════════════════════════════════════
section("Rule 4 — # is a comment only at line start")
# ══════════════════════════════════════════════════════════════════════════

c = loads("# Main application settings\ntimeout = 45\n    # Window state\nwidth = 500\n")
check("full-line comments stripped", (c["timeout"], c["width"]), ("45", "500"))

# The exact trap: MOML V1 cut every line at its FIRST '#', so this
# became an empty string. V2 must return it untouched.
c = loads("hotkey = #h\n")
check("hotkey = #h survives, no quoting/escaping needed", c["hotkey"], "#h")

c = loads("color = #085041\n")
check("color = #085041 survives", c["color"], "#085041")

c = loads("tag = invoice#2026\n")
check("# mid-value is ordinary data", c["tag"], "invoice#2026")

c = loads("timeout = 45 # seconds\n")
check('inline "comment" is stored as literal value text (Rule 4 says so explicitly)',
      c["timeout"], "45 # seconds")


# ══════════════════════════════════════════════════════════════════════════
section("Rule 5 — { } blocks: pairs or bare items, never mixed")
# ══════════════════════════════════════════════════════════════════════════

c = loads("gui {\n    x = 100\n    y = 100\n    width = 360\n    height = 500\n}\n")
check("pair block parses", (c["gui"]["x"], c["gui"]["width"]), ("100", "360"))

c = loads("models {\n    llama\n    kimi\n    nemotron\n}\n")
check("bare-item block type is MOMLList", isinstance(c["models"], MOMLList), True)
check("bare-item block contents", list(c["models"]), ["llama", "kimi", "nemotron"])

c = loads(
    "llm {\n"
    "    provider {\n"
    "        groq {\n"
    "            base_url = https://api.groq.com/openai/v1\n"
    "        }\n"
    "    }\n"
    "}\n"
)
check("three-level nesting", c["llm"]["provider"]["groq"]["base_url"],
      "https://api.groq.com/openai/v1")

check_raises(
    "mixed block (bare item after pair) rejected — spec's own invalid example",
    lambda: loads("models {\n    llama\n    default = kimi\n}\n"),
)


# ══════════════════════════════════════════════════════════════════════════
section("Rule 6 — commas make inline lists; * only structural in blocks")
# ══════════════════════════════════════════════════════════════════════════

c = loads("colors = red, green, blue\n")
check("comma list parses to plain list", c["colors"], ["red", "green", "blue"])
check("inline list has no .active (plain list, not MOMLList)",
      isinstance(c["colors"], MOMLList), False)

c1 = loads("colors = red, green, blue\n")
c2 = loads("colors = red,green,blue\n")
check("whitespace around commas is insignificant", c1["colors"], c2["colors"])

c = loads('name = "Smith, John"\n')
check("comma INSIDE quotes stays one scalar", c["name"], "Smith, John")

c = loads('names = Mike, "Smith, John", Karim\n')
check("mixed bare + quoted-with-comma in one list",
      c["names"], ["Mike", "Smith, John", "Karim"])

c = loads("values = Mike, 45, true\n")
check("every list element is a string, no per-element inference",
      c["values"], ["Mike", "45", "true"])

# The exact spec statement: "* has no structural meaning inside an
# inline comma list. providers = groq, *openrouter, together is simply
# ["groq", "*openrouter", "together"]. The * is part of the string."
c = loads("providers = groq, *openrouter, together\n")
check('"*" inside an inline list is literal text, not an active marker',
      c["providers"], ["groq", "*openrouter", "together"])

# Active marker IS structural inside a block.
c = loads("models {\n    llama\n   *kimi\n    nemotron\n}\n")
check("* on a bare item sets the list's .active", c["models"].active, "kimi")

c = loads(
    "provider {\n"
    "    groq {\n"
    "    }\n"
    "   *openrouter {\n"
    "    }\n"
    "    together {\n"
    "    }\n"
    "}\n"
)
check("* on a named block sets the parent dict's .active",
      c["provider"].active, "openrouter")
k, v = get_active(c["provider"])
check("get_active() returns the same key", k, "openrouter")


# ══════════════════════════════════════════════════════════════════════════
section("Rule 7 — empty RHS is empty string; one-element list convention")
# ══════════════════════════════════════════════════════════════════════════

c = loads("api_key =\n")
check("bare empty RHS -> ''", c["api_key"], "")
check("type is str, not None", type(c["api_key"]), str)

c = loads('api_key = ""\n')
check('explicit "" gives the same result as bare empty', c["api_key"], "")

c = loads('models = llama4, ""\n')
check('trailing explicit "" makes a real two-element list, NOT auto-collapsed',
      c["models"], ["llama4", ""])
check("list, not a scalar", isinstance(c["models"], list), True)


# ══════════════════════════════════════════════════════════════════════════
section("Assignment syntax — whitespace around '=' is insignificant")
# ══════════════════════════════════════════════════════════════════════════

for src in ["x=45", "x =45", "x= 45", "x = 45"]:
    c = loads(src)
    check(f"{src!r} -> '45'", c["x"], "45")

c = loads("equation = x = y + z\n")
check("first '=' is the delimiter; rest of line is free to contain '='",
      c["equation"], "x = y + z")

c = loads("url = https://example.com/?a=1&b=2\n")
check("URL with query-string '=' signs parses whole",
      c["url"], "https://example.com/?a=1&b=2")


# ══════════════════════════════════════════════════════════════════════════
section("Round-trip — the property V1 could not guarantee")
# ══════════════════════════════════════════════════════════════════════════

def roundtrip(src_dict_thunk, label):
    c1 = src_dict_thunk()
    d = dumps(c1)
    c2 = loads(d)
    ok = (d == dumps(c2))
    print(f"  {'PASS' if ok else 'FAIL'}  {label}")
    global passed, failed
    if ok:
        passed += 1
    else:
        failed += 1
        print(f"        first dump:  {d!r}")
        print(f"        second dump: {dumps(c2)!r}")
    return c2

# The specific bug this whole design exists to close: an all-digit hex
# colour losing its leading zero. V1: 085041 -> Integer 85041 -> "#85041".
c = loads("theme_color = 085041\n")
check("085041 stays a string, leading zero intact", c["theme_color"], "085041")

c2 = roundtrip(
    lambda: loads("theme_color = 085041\n"), "085041 round-trips byte-stable"
)
check("085041 still correct after round trip", c2["theme_color"], "085041")

c3 = roundtrip(
    lambda: loads(
        "gui {\n    width = 360\n    pinned = true\n    theme_color = 085041\n}\n"
        r"log_path = C:\Users\Karim\Documents" "\n"
        "dictate_key = #h\n"
        'contact = "Smith, John"\n'
    ),
    "the full session example file round-trips byte-stable",
)
check("theme_color survived the round trip", c3["gui"]["theme_color"], "085041")
check("log_path survived the round trip", c3["log_path"], r"C:\Users\Karim\Documents")
check("dictate_key survived the round trip", c3["dictate_key"], "#h")
check("contact survived the round trip", c3["contact"], "Smith, John")


# ══════════════════════════════════════════════════════════════════════════
section("Malformed input — decisions documented in moml2.py's module docstring")
# ══════════════════════════════════════════════════════════════════════════

check_raises("unterminated quoted string", lambda: loads('name = "unterminated\n'))
check_raises("missing closing brace", lambda: loads("gui {\n    x = 1\n"))
check_raises("partial quoting (trailing junk after close quote)",
             lambda: loads('x = "abc"def\n'))
check_raises("bare empty list element (a,,b)", lambda: loads("x = a,,b\n"))
check_raises("bare trailing comma (a,)", lambda: loads("x = a,\n"))
check_raises("duplicate * in a dict", lambda: loads("*x = 1\n*y = 2\n"))
check_raises("duplicate * in a list block",
             lambda: loads("items {\n    *a\n    *b\n}\n"))
check_raises("unexpected closing brace at top level", lambda: loads("}\n"))


# ══════════════════════════════════════════════════════════════════════════
section("get() and get_active() convenience accessors")
# ══════════════════════════════════════════════════════════════════════════

c = loads("gui {\n    width = 360\n}\n")
check("get() finds a nested value", get(c, "gui", "width"), "360")
check("get() returns default for a missing key", get(c, "gui", "height", default="0"), "0")
check("get() returns default for a missing top-level key",
      get(c, "nonexistent", default="fallback"), "fallback")


# ══════════════════════════════════════════════════════════════════════════
section("Header and .m2 enforcement — added after external review")
# ══════════════════════════════════════════════════════════════════════════

check_raises("loads() rejects a string with no header",
             lambda: moml2.loads("settings {\n    x = 1\n}\n"))
check_raises("loads() rejects the wrong header text",
             lambda: moml2.loads("# MOML 1.0\nsettings {\n    x = 1\n}\n"))

_ok = loads("settings {\n    x = 1\n}\n")
check("loads() accepts the correct header via the test helper", _ok["settings"]["x"], "1")

_d = dumps(loads("x = 1\n"))
check("dumps() emits the header as the first line",
      _d.splitlines()[0], moml2.HEADER)

import tempfile, os
with tempfile.TemporaryDirectory() as tmp:
    bad_path = os.path.join(tmp, "config.moml")   # V1 extension, not .m2
    good_path = os.path.join(tmp, "config.m2")

    with open(bad_path, "w", encoding="utf-8") as f:
        f.write(dumps(loads("x = 1\n")))

    check_raises("load() rejects a .moml path", lambda: moml2.load(bad_path))
    check_raises("dump() rejects a .moml path",
                 lambda: moml2.dump(loads("x = 1\n"), bad_path))

    moml2.dump(loads("x = 1\n"), good_path)
    reloaded = moml2.load(good_path)
    check("load()/dump() round-trip through a real .m2 file", reloaded["x"], "1")


# ══════════════════════════════════════════════════════════════════════════
section("One-element list write-drift fix — added after external review")
# ══════════════════════════════════════════════════════════════════════════

d = MOMLDict()
d["dictate_keys"] = ["#h"]
out = dumps(d)
check('one-element list writes padded, as "#h, \\"\\""',
      '#h, ""' in out, True)

reread = loads(out)
check("padded one-element list reads back as a real two-element list",
      reread["dictate_keys"], ["#h", ""])
check("still a list, not collapsed to a scalar",
      isinstance(reread["dictate_keys"], list), True)

d2 = MOMLDict()
d2["tags"] = ["mkr", "urgent"]
out2 = dumps(d2)
reread2 = loads(out2)
check("two-element list round-trips unpadded",
      reread2["tags"], ["mkr", "urgent"])

d3 = MOMLDict()
d3["empty_list"] = []
out3 = dumps(d3)
reread3 = loads(out3)
check("empty list writes bare and reads back as empty string (documented gap)",
      reread3["empty_list"], "")


# ══════════════════════════════════════════════════════════════════════════
section("Bare-item structural-collision fix — added after external review")
# ══════════════════════════════════════════════════════════════════════════
#
# Each of these is a Python string that, if a writer emitted it bare as
# a whole physical line inside a { } bare-item block, would be misread
# as something other than the item's own value on the next load. Every
# one must survive a full write -> read round trip unchanged.

for val, note in [
    ("#tag",     "would be swallowed as a full-line comment"),
    ("*star",    "would be misread as an active-marker prefix"),
    ("x = y",    "contains unquoted '=' — would be misread as a pair"),
    ("}",        "would close the block early"),
    ("nested {", "ends in unquoted '{' — misread as opening a nested block"),
    ("plain",    "control case, no collision risk"),
]:
    lst = MOMLList([val])
    d = MOMLDict({"items": lst})
    out = dumps(d)
    back = loads(out)
    ok = list(back["items"]) == [val]
    print(f"  {'PASS' if ok else 'FAIL'}  {val!r:12} ({note})")
    if ok:
        passed += 1
    else:
        failed += 1
        print(f"        written as: {out!r}")
        print(f"        reread as:  {back.get('items')!r}")

for val in ["#tag", "*star", "x = y", "}", "nested {"]:
    lst = MOMLList([val])
    lst.active = val
    d = MOMLDict({"items": lst})
    out = dumps(d)
    back = loads(out)
    ok = (list(back["items"]) == [val]) and (back["items"].active == val)
    print(f"  {'PASS' if ok else 'FAIL'}  {val!r:12} marked active — value and active-ness both survive")
    if ok:
        passed += 1
    else:
        failed += 1
        print(f"        written as: {out!r}")
        print(f"        reread items={list(back['items'])!r} active={back['items'].active!r}")


# ══════════════════════════════════════════════════════════════════════════
section("Assignment-collision fix — added after second external review")
# ══════════════════════════════════════════════════════════════════════════
#
# The exact case reported: a bare-item block holding a value that
# itself contains "=" round-trips correctly only when BOTH the writer
# quotes it AND the parser's key/value split is quote-aware. Testing
# both halves separately, then together.

d = MOMLDict({"items": MOMLList(["x = y"])})
out = dumps(d)
check('writer quotes a bare-item value containing "="',
      '"x = y"' in out, True)

back = loads(out)
check('full round trip: ["x = y"] survives as a one-element list',
      list(back["items"]), ["x = y"])
check("not misread as a nested dict (the original bug)",
      isinstance(back["items"], MOMLDict), False)

# Confirms the SECOND half of the fix specifically: even correctly
# quoted, a naive (non-quote-aware) key/value split would still find
# the "=" INSIDE the quotes and break the string in half. This directly
# exercises _find_unquoted_equals via a value that would fail if
# _split_pair ever regressed back to a plain str.find("=").
d2 = MOMLDict({"items": MOMLList(['"already quoted" = trap'])})
out2 = dumps(d2)
back2 = loads(out2)
check('a value that already CONTAINS quote characters and "=" still '
      "round-trips (exercises the quote-aware equals scan, not just the "
      "writer's quoting decision)",
      list(back2["items"]), ['"already quoted" = trap'])

# Regression check: the standard, extremely common shapes that a
# quote-aware equals-finder must NOT change behaviour for.
c = loads("equation = x = y + z\n")
check("equation = x = y + z still parses correctly (no regression)",
      c["equation"], "x = y + z")

c = loads("url = https://example.com/?a=1&b=2\n")
check("URL with query-string '=' signs still parses whole (no regression)",
      c["url"], "https://example.com/?a=1&b=2")

c = loads('key = "a = b"\n')
check('a quoted VALUE (not a bare item) containing "=" still parses correctly',
      c["key"], "a = b")


# ══════════════════════════════════════════════════════════════════════════
section("Duplicate-value active-marker fix — found via randomized round-trip batches")
# ══════════════════════════════════════════════════════════════════════════

c = loads("items {\n    a\n   *a\n}\n")
check("parses: two identical items, one starred, no error", list(c["items"]), ["a", "a"])
check("active value recorded correctly", c["items"].active, "a")

out = dumps(c)
check("writer stars only ONE line, not two",
      out.count("\n   *a\n") + out.count("\n    *a\n"), 1)

back = loads(out)
check("self-round-trip now succeeds (was: reread raised duplicate-active error)",
      list(back["items"]), ["a", "a"])
check("active preserved through the round trip", back["items"].active, "a")

c2 = loads("items {\n    a\n   *a\n    a\n}\n")
out2 = dumps(c2)
back2 = loads(out2)
check("three identical items, one originally active, round-trips",
      list(back2["items"]), ["a", "a", "a"])
check("active survives (value-level active can't distinguish position anyway)",
      back2["items"].active, "a")


# ══════════════════════════════════════════════════════════════════════════
section("Non-string scalar hardening — Rule 1 enforcement at the writer")
# ══════════════════════════════════════════════════════════════════════════

check_raises("int scalar value raises MOMLError, not a bare TypeError",
             lambda: dumps(MOMLDict({"timeout": 45})))
check_raises("bool scalar value raises MOMLError",
             lambda: dumps(MOMLDict({"enabled": True})))
check_raises("None scalar value raises MOMLError",
             lambda: dumps(MOMLDict({"x": None})))
check_raises("non-str element inside an inline list raises MOMLError",
             lambda: dumps(MOMLDict({"nums": [1, 2, 3]})))
check_raises("non-str item inside a bare-item block raises MOMLError",
             lambda: dumps(MOMLDict({"items": MOMLList([1, 2])})))

try:
    dumps(MOMLDict({"timeout": 45}))
    check("error message mentions the gatekeeper", False, True)
except MOMLError as e:
    check("error message mentions the gatekeeper", "gatekeeper" in str(e).lower(), True)



# ══════════════════════════════════════════════════════════════════════════
section("Key/block-name validation — added after third external review")
# ══════════════════════════════════════════════════════════════════════════
#
# The writer previously trusted dict keys completely. Each of these is
# an ordinary Python dict a gatekeeper could hand to dumps() by
# accident; all four now raise before anything is written.

check_raises('key starting with "#" is rejected (would vanish as a comment)',
             lambda: dumps(MOMLDict({"#hash": "v"})))
check_raises('key starting with "*" is rejected (would gain a fake active marker)',
             lambda: dumps(MOMLDict({"*star": "v"})))
check_raises('key containing "=" is rejected (would split into a different key/value)',
             lambda: dumps(MOMLDict({"x=y": "v"})))
check_raises("key containing whitespace is rejected (writer's own reader would reject it)",
             lambda: dumps(MOMLDict({"my key": "v"})))
check_raises('key containing "{" is rejected', lambda: dumps(MOMLDict({"a{b": "v"})))
check_raises('key containing "}" is rejected', lambda: dumps(MOMLDict({"a}b": "v"})))
check_raises('key containing \'"\' is rejected', lambda: dumps(MOMLDict({'a"b': "v"})))
check_raises("empty-string key is rejected", lambda: dumps(MOMLDict({"": "v"})))
check_raises("non-str key is rejected", lambda: dumps(MOMLDict({1: "v"})))

# The same check must apply to a NAMED BLOCK, not just a scalar key —
# the value being a MOMLDict/MOMLList rather than a plain string
# shouldn't change whether the key itself is validated.
check_raises("named-block key with a leading '#' is rejected",
             lambda: dumps(MOMLDict({"#hash": MOMLDict({"x": "1"})})))
check_raises("named-block key with an embedded '=' is rejected",
             lambda: dumps(MOMLDict({"a=b": MOMLList(["x"])})))

# Ordinary keys must be completely unaffected.
c = loads("gui {\n    width = 360\n}\nsimple_key = ok\n")
out = dumps(c)
back = loads(out)
check("ordinary keys still round-trip with no validation false-positive",
      (back["gui"]["width"], back["simple_key"]), ("360", "ok"))


# ══════════════════════════════════════════════════════════════════════════
section("Active-marker-collision fix — found by randomized fuzz batch")
# ══════════════════════════════════════════════════════════════════════════
#
# The narrowest gap found so far: a bare-item value that is EXACTLY the
# single character "{" is safe unquoted on its own, but becomes
# ambiguous the moment it is ALSO marked active — "*{" matches
# _BLOCK_OPEN because the pattern's optional leading "*" can match
# nothing, leaving \S+ free to swallow the literal "*" as if it were
# part of a block's name. No hand-picked example in three prior review
# rounds found this; a randomized batch generating nested structures
# with random active markers did.

lst = MOMLList(["{"])
lst.active = "{"
out3 = dumps(MOMLDict({"items": lst}))
check('writer quotes the value when marking "{" active',
      '*"{"' in out3, True)

back3 = loads(out3)
check('active "{" round-trips as a one-element list', list(back3["items"]), ["{"])
check('active marker survives too', back3["items"].active, "{")

lst2 = MOMLList(["{"])
out4 = dumps(MOMLDict({"items": lst2}))
back4 = loads(out4)
check('unmarked "{" is ALSO quoted -- by design, not a missed optimization: '
      "the check doesn't know in advance whether a DIFFERENT later mutation "
      "might mark this same item active, so it quotes unconditionally rather "
      "than making the decision depend on state outside this one value",
      '"{"' in out4, True)
check('unmarked "{" still round-trips correctly', list(back4["items"]), ["{"])



# ══════════════════════════════════════════════════════════════════════════
print(f"\n{'='*60}")
print(f"RESULT: {passed} passed, {failed} failed")
print(f"{'='*60}")
if failed:
    raise SystemExit(1)
