#!/usr/bin/env python3
"""Rebuild the whole shipped map through the public registration API, then diff
what the translator can observe against the map beside it.

    python/bin/sqlreplay

The counterpart of js/bin/sqlreplay.mjs and php/bin/sqlreplay; see the first for
why this check exists. sql/MAP.md §4.5¼ is the property it asserts.
"""

from __future__ import annotations

import json
import os
import sys

_HERE = os.path.dirname(os.path.abspath(__file__))
# map_replay.py is generated beside this script and is harness data, not part of
# the package, so its directory is always added. Same shape as sqlt.
sys.path.insert(0, _HERE)
try:
    import sel as _probe                                            # noqa: F401
except ImportError:
    sys.path.insert(1, os.path.join(_HERE, '..'))

from map_replay import RAW                                          # noqa: E402
from sel.sql import map as smap                                     # noqa: E402
from sel.sql._map import DIALECTS                                   # noqa: E402
from sel import compile as sel_compile                              # noqa: E402
from sel.sql import Binding, Sql                                    # noqa: E402

SUF = '~replay'


def main() -> int:
    calls = 0
    for doc in RAW:
        parent = doc.get('extends')
        smap.define_dialect(doc['dialect'] + SUF, {
            'extends': None if parent is None else parent + SUF,
            'version': doc.get('version'),
            'target': doc.get('target', True),
            'lexical': doc.get('lexical', {}),
        })
        calls += 1
        for section in ('ops', 'funcs', 'skel'):
            for key, entry in (doc.get(section) or {}).items():
                smap.define(doc['dialect'] + SUF, section, key, entry)
                calls += 1

    def same(a, b):
        return json.dumps(a, sort_keys=True) == json.dumps(b, sort_keys=True)

    problems, compared = [], 0
    for name, flat in DIALECTS.items():
        if not isinstance(flat, dict) or 'dialect' not in flat:
            continue
        compared += 1
        if smap.version(name) != smap.version(name + SUF):
            problems.append(f'{name}: version')
        for key in (flat.get('lexical') or {}):
            compared += 1
            if not same(smap.lexical(name, key), smap.lexical(name + SUF, key)):
                problems.append(f'{name}.lexical.{key}')
        for section in ('ops', 'funcs', 'skel'):
            for key in (flat.get(section) or {}):
                compared += 1
                if not same(smap.entry(name, section, key),
                            smap.entry(name + SUF, section, key)):
                    problems.append(f'{name}.{section}.{key}')

    # Rule 10 of sql/MAP.md, at run time. Registering a derived dialect is the
    # documented way to adapt the map to a server -- the first external user of this
    # layer did it on their first day -- and numericGuard is the one lexical key where
    # a wrong override fails SILENTLY: every other one blows up at template expansion,
    # and this one emits SQL that answers where SEL refuses. The generator has
    # enforced the rule since the guard existed; nothing enforced it for a dialect the
    # generator never sees.
    #
    # End to end rather than by calling the check directly, because the check being
    # right is worth nothing if the emit path does not reach it. The bad guard accepts
    # integers only, so it lets through the '2.5' that ISNUM's pattern catches: a
    # guard that asks a narrower question passes what it should have stopped.
    guard_name = 'mariadb' + SUF + '~guard'
    smap.define_dialect(guard_name, {
        'extends': 'mariadb' + SUF,
        'lexical': {'numericGuard':
                    "CASE WHEN ({0} REGEXP '\\\\A-?[0-9]+\\\\z') THEN "
                    'CAST({0} AS DECIMAL(65,10)) ELSE NULL END'},
    })
    refused = False
    try:
        Sql.translate(sel_compile('T == 25'), guard_name,
                      {'T': Binding.column('t', None, 'TEXT')})
    except ValueError:
        refused = True
    except Exception as e:                     # noqa: BLE001
        problems.append('numericGuard disagreeing with ISNUM raised '
                        f'{type(e).__name__} rather than a registration error')
    if not refused:
        problems.append('a numericGuard that disagrees with its funcs.ISNUM was '
                        'accepted; sql/MAP.md rule 10 holds at generation time and '
                        'not at run time')

    # And the memo must not outlive what it vouched for. define() lets the last
    # writer win, so an ISNUM registered AFTER a dialect's guard was checked would
    # never be compared against it. This dialect inherits a good guard, is translated
    # once so the check runs and passes, and then has its ISNUM replaced by one the
    # inherited guard does not carry.
    memo_name = 'mariadb' + SUF + '~memo'
    smap.define_dialect(memo_name, {'extends': 'mariadb' + SUF})
    program = sel_compile('T == 25')
    text_col = {'T': Binding.column('t', None, 'TEXT')}
    Sql.translate(program, memo_name, text_col)
    smap.define(memo_name, 'funcs', 'ISNUM',
                {'tpl': "({0} REGEXP '^[0-9]+$')", 'ret': 'BOOL'})
    stale = False
    try:
        Sql.translate(program, memo_name, text_col)
    except ValueError:
        stale = True
    except Exception as e:                     # noqa: BLE001
        problems.append(f'a redefined ISNUM raised {type(e).__name__} rather than '
                        'a registration error')
    if not stale:
        problems.append('an ISNUM redefined after the guard was checked was not '
                        'noticed; the memo outlived the pairing it vouched for')

    for p in problems:
        print(f'  DIFFERS {p}')
    print(f'{calls} registration calls rebuilt the map, {compared} lookups '
          f'compared, {len(problems)} differences'
          ' (and a disagreeing numericGuard is refused)')
    return 0 if not problems else 1


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