# SPDX-License-Identifier: Apache-2.0
"""Run the protocol's conformance vectors against this implementation (SPEC §12).

    python3 -m resolution_protocol.conformance        # prints a summary, exit 1 on failure
"""
from __future__ import annotations

import json
import sys
from pathlib import Path

from . import (Intent, Profile, Session, apply_preference, load_locale, merge_lowerings, parse, parse_duration,
               readable_level, resolve_locales, unreadable_after)

ROOT = Path(__file__).resolve().parents[2]
DAY = 86400.0


def _n(x):
    return int(x) if isinstance(x, float) and x.is_integer() else x


def _intent_dict(i) -> dict:
    d = {"kind": i.kind, "scope": i.scope}
    if i.level is not None:
        d["level"] = i.level
    if i.seconds is not None:
        d["seconds"] = _n(i.seconds)
    if i.addressed:
        d["addressed"] = True
    return d


def _locales(tags):
    available = {p.stem: load_locale(p) for p in (ROOT / "locales").glob("*.json")}
    return resolve_locales(tags, available)


def phrases(path: Path):
    v = json.loads(path.read_text())
    locs = _locales([v["locale"]])
    return [(f"{v['locale']}: {c['say']}", got == c["intents"], f"got {got}")
            for c in v["cases"] for got in [[_intent_dict(i) for i in parse(c["say"], locs, v.get("invocation", []), v.get("extensions", []))]]]


def sessions(path: Path):
    v = json.loads(path.read_text())
    out = []
    for c in v["cases"]:
        prof = Profile.from_dict({**v["profile"], **c.get("profile", {})})
        locs = _locales(prof.locales)
        s, outcomes, ok, why, n0 = Session(prof), [], True, "", 0
        actions = ("heard", "stamp", "say", "intent", "merge", "preference", "tick", "end")
        for st in c["steps"]:
            t = st.get("t", 0)
            if any(k in st for k in actions):
                n0 = len(s.events)                     # expect_events checks what the last acting step emitted
            if "heard" in st:
                s.heard(st["heard"], t)
            if "stamp" in st:
                s.stamp(st["stamp"], st["from"], st["to"])
            if "say" in st:
                s.heard(st["by"], t)
                outcomes = [s.apply(i, st["by"], st.get("confidence", 0.95), t) for i in parse(st["say"], locs, prof.invocation, prof.extensions)]
            if "intent" in st:
                i = st["intent"]
                outcomes = [s.apply(Intent(i["kind"], i.get("scope", "room"), i.get("level"), i.get("seconds"), i.get("addressed", False), i.get("source", "ui")),
                                    st.get("by"), st.get("confidence", 1.0), t)]
            if "merge" in st:
                merge_lowerings(s, st["merge"], t)
            if "preference" in st:
                apply_preference(s, st["by"], st["preference"], t, st.get("authenticated", False))
            if st.get("tick"):
                s.tick(t)
            if st.get("end"):
                s.end(t)
            emitted = [e["type"] for e in s.events[n0:]]
            checks = []
            if "expect" in st:
                checks.append(({k: s.applied(k) for k in st["expect"]}, st["expect"]))
            if "expect_default" in st:
                checks.append((s.applied(None), st["expect_default"]))
            if "expect_outcomes" in st:
                checks.append((outcomes, st["expect_outcomes"]))
            if "expect_voices" in st:
                checks.append((list(s.voices), st["expect_voices"]))
            if "expect_record" in st:
                checks.append(({k: [x.level for x in s.voices[k].record] for k in st["expect_record"]}, st["expect_record"]))
            if "expect_usable" in st:
                labels = sorted({lab for lab, vo in s.voices.items() for seg in s.usable(st["expect_usable"]["level"]) if seg.voice == vo.id})
                checks.append((labels, st["expect_usable"]["labels"]))
            if "expect_events" in st:
                checks.append((emitted, st["expect_events"]))
            if "expect_retention" in st:
                checks.append(({k: _n(s.retention_for(None if k == "*" else k)) for k in st["expect_retention"]}, st["expect_retention"]))
            if "expect_log_omits" in st:
                vid = s.voices[st["expect_log_omits"]].id
                checks.append((any(e.get("voice") == vid or vid in (e.get("levels") or {}) for e in s.events), False))
            for got, want in checks:
                if got != want and ok:
                    ok, why = False, f"got {got}, want {want}"
        out.append((f"session: {c['name']}", ok, why))
    return out


def profiles(path: Path):
    v = json.loads(path.read_text())
    out = []
    for c in v["cases"]:
        d = {k: x for k, x in {**v["base"], **c["patch"]}.items() if x is not None}
        try:
            Profile.from_dict(d)
            ok = True
        except (ValueError, TypeError):
            ok = False
        out.append((f"profile: {c['name']}", ok == c["valid"], f"accepted={ok}"))
    return out


def lints():
    from .grammar import lint
    return [(f"lint: {p.stem}", not lint(load_locale(p)), "; ".join(lint(load_locale(p)))) for p in sorted((ROOT / "locales").glob("*.json"))]


def retention(path: Path):
    v = json.loads(path.read_text())
    sch = tuple((parse_duration(x["until"]), x["max_level"]) for x in v["schedule"])
    lim = lambda d: None if d is None else d * DAY
    out = [(f"retention: age {c['age_days']} d, limit {c['limit_days']}", readable_level(c["age_days"] * DAY, sch, lim(c["limit_days"])) == c["readable_level"], "")
           for c in v["cases"]]
    out += [(f"retention: level {c['level']} unreadable after", abs(unreadable_after(c["level"], sch, lim(c["limit_days"])) / DAY - c["days"]) < 1e-9, "")
            for c in v["unreadable_after"]]
    return out


def run_all():
    vec = ROOT / "vectors"
    res = []
    for p in sorted(vec.glob("phrases.*.json")):
        res += phrases(p)
    return res + lints() + sessions(vec / "sessions.json") + retention(vec / "retention.json") + profiles(vec / "profiles.json")


if __name__ == "__main__":
    r = run_all()
    for name, ok, why in r:
        if not ok:
            print("FAIL", name, "—", why)
    print(f"{sum(ok for _, ok, _ in r)}/{len(r)} conformance vectors pass (python)")
    sys.exit(0 if all(ok for _, ok, _ in r) else 1)
