#!/usr/bin/env python3
"""Test de woordenlijst tegen een kopie van de vervang-engine.

Waarom hier en niet in Swift: de dataset is het risico, niet de engine. Twee
regels naar hetzelfde doel, een verkeerde sorteer-orde of een variant die een
gewoon Nederlands woord raakt, zijn dataset-fouten die je op alle drie de
platforms tegelijk hebt. De engine hieronder is 1-op-1 gespiegeld op
WordReplacementService (macOS), ProatDictionaryStore (iOS) en DictionaryStore.cs
(Windows): regex-trigger eerst, dan komma-varianten, langste eerst, lookarounds
als woordgrens.

Draaien: python3 dictionary/test_replacements.py
"""
from __future__ import annotations

import json
import re
import sys
from pathlib import Path

SOURCE = Path(__file__).resolve().parent / "proat-dictionary.json"

NON_SPACED = [
    (0x3040, 0x309F), (0x30A0, 0x30FF), (0x4E00, 0x9FFF),
    (0xAC00, 0xD7AF), (0x0E00, 0x0E7F),
]


def regex_pattern(trigger: str) -> str | None:
    if len(trigger) < 3 or not trigger.startswith("/"):
        return None
    if trigger.endswith("/"):
        return trigger[1:-1]
    if trigger.endswith("/i"):
        return trigger[1:-2]
    return None


def uses_word_boundaries(text: str) -> bool:
    return not any(lo <= ord(ch) <= hi for ch in text for lo, hi in NON_SPACED)


def apply_replacements(text: str, rules: list[dict]) -> str:
    result = text
    for rule in sorted(rules, key=lambda r: -len(r["from"])):
        group, target = rule["from"], rule["to"]

        pattern = regex_pattern(group.strip())
        if pattern is not None:
            result = re.sub(pattern, target.replace("\\", "\\\\"), result, flags=re.IGNORECASE)
            continue

        variants = sorted(
            (v.strip() for v in group.split(",") if v.strip()),
            key=len, reverse=True,
        )
        for variant in variants:
            if uses_word_boundaries(variant):
                escaped = re.escape(variant)
                result = re.sub(f"(?<![a-zA-Z0-9]){escaped}(?![a-zA-Z0-9])",
                                target.replace("\\", "\\\\"), result, flags=re.IGNORECASE)
            else:
                result = re.sub(re.escape(variant), target.replace("\\", "\\\\"),
                                result, flags=re.IGNORECASE)
    return result


# (invoer, verwacht) - verwacht is de hele uitvoerregel na alle regels.
CASES: list[tuple[str, str]] = [
    # De missers waar het om begon.
    ("we leveren aan graniet", "we leveren aan Granit"),
    ("die order gaat naar graniet parts", "die order gaat naar Granit Parts"),
    ("Granit Parts is al goed", "Granit Parts is al goed"),
    ("factuur voor roemaat", "factuur voor Roemaat"),
    ("bel Aloys Roemaad even", "bel Aloys Roemaat even"),
    ("dat komt onder vakwerk.ai", "dat komt onder Vakwark.ai"),
    ("de rebrand naar vakwerk", "de rebrand naar Vakwark"),
    ("Noober ai is de site", "Noaber.ai is de site"),   # via de regex-vangregel
    ("noober punt ai", "Noaber.ai"),                    # uitgesproken domein
    ("het gaat om nooberschap", "het gaat om Noaberschap"),
    ("Frikke is de moeder van graniet", "FRICKE is de moeder van Granit"),

    # Samenstellingen mogen niet geraakt worden (woordgrens-gedrag).
    ("een granieten aanrecht", "een granieten aanrecht"),
    ("dat is echt vakwerkhuis", "dat is echt vakwerkhuis"),
    ("de graniethandel in Duitsland", "de graniethandel in Duitsland"),

    # Eén doel, één regel: geen regel die een andere terugdraait.
    ("we gaan naar te kloes", "we gaan naar Te Kloeze"),
    ("Te Kloeze Riool techniek", "Te Kloeze Riooltechniek"),
    ("opdracht van Tekloeze", "opdracht van Te Kloeze"),

    # Bestaande regels moeten blijven werken.
    ("zet het in Vault Warden", "zet het in Vaultwarden"),
    ("dat staat in fault worden", "dat staat in Vaultwarden"),
    ("ik gebruik Cloud Flare voor DNS", "ik gebruik Cloudflare voor DNS"),
    ("check je geheugen", "doe een volledige semantic search in claude-mem. Wees grondig"),

    # Nieuwe tools en plaatsen.
    ("draait Olama al lokaal", "draait Ollama al lokaal"),
    ("de Vokstral transcriptie", "de Voxtral transcriptie"),
    ("via Open Router geregeld", "via OpenRouter geregeld"),
    ("dat ligt in Dinksperlo", "dat ligt in Dinxperlo"),
    ("een klant in Seddam", "een klant in Zeddam"),

    # Leestekens gelden als woordgrens.
    ("levering: graniet, en verder niks", "levering: Granit, en verder niks"),
    ("(vakwerk)", "(Vakwark)"),
]

# Woorden die de lijst nooit mag aanraken. Elk is een normaal Nederlands woord of
# een term die per ongeluk in een variant zou kunnen zitten.
MUST_NOT_CHANGE = [
    "de klus is geklaard", "de klos van de haspel", "we hebben een kop koffie",
    "granieten vloer", "vakwerkbouw uit 1850", "het notariskantoor",
    "een boerderij in de buurt", "de winst voor belasting", "een fabel over Rome",
    "praat er eens over", "dat is een quintet van vijf man",
    "het staat in de balans", "wij gaan naar de kloosterkapel",
]


def main() -> int:
    doc = json.loads(SOURCE.read_text())
    rules = doc["replacements"]
    failures: list[str] = []

    for text, expected in CASES:
        actual = apply_replacements(text, rules)
        if actual != expected:
            failures.append(f"  invoer:    {text!r}\n  verwacht:  {expected!r}\n  gekregen:  {actual!r}")

    for text in MUST_NOT_CHANGE:
        actual = apply_replacements(text, rules)
        if actual != text:
            failures.append(f"  mag niet wijzigen: {text!r}\n  gekregen:          {actual!r}")

    # Twee letterlijke regels naar hetzelfde doel vechten om de sorteer-orde.
    targets: dict[str, str] = {}
    for rule in rules:
        if rule["from"].strip().startswith("/"):
            continue
        key = rule["to"].lower()
        if key in targets:
            failures.append(f"  twee letterlijke regels naar {rule['to']!r}: voeg ze samen")
        targets[key] = rule["to"]

    total = len(CASES) + len(MUST_NOT_CHANGE)
    if failures:
        print(f"{len(failures)} van {total} controles faalden:\n")
        print("\n\n".join(failures))
        return 1

    print(f"{total} controles goed ({len(rules)} vervangregels, "
          f"{len(doc['vocabulary'])} woorden)")
    return 0


if __name__ == "__main__":
    sys.exit(main())
