"""Run: python query_contract.py. Python 3.10+, standard library only.

An application-specific validator, not a JSON Schema implementation.
"""
import json
import re
from datetime import datetime, timedelta, timezone
import unittest


# BEGIN decode
def unique_object(pairs):
    result = {}
    for key, value in pairs:
        if key in result:
            raise ValueError("duplicate field: " + key)
        result[key] = value
    return result


def reject_constant(value):
    raise ValueError("non-JSON constant: " + value)


def decode_query(raw):
    args = json.loads(raw, object_pairs_hook=unique_object,
                      parse_constant=reject_constant)
    required = {"service", "start", "end", "limit"}
    if type(args) is not dict or set(args) != required:
        raise ValueError("expected exactly service, start, end, limit")
    for field in ("service", "start", "end"):
        if type(args[field]) is not str:
            raise ValueError(field + " must be a string")
    if not args["service"]:
        raise ValueError("service must not be empty")
    if type(args["limit"]) is not int or not 1 <= args["limit"] <= 200:
        raise ValueError("limit must be an integer from 1 to 200")
    return args
# END decode


# BEGIN validate
def parse_utc(value):
    pattern = r"[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}Z"
    if re.fullmatch(pattern, value) is None:
        raise ValueError("expected UTC time with second precision")
    return datetime.strptime(value, "%Y-%m-%dT%H:%M:%SZ").replace(tzinfo=timezone.utc)


def validate_query(args, allowed_services, max_span=timedelta(hours=1)):
    if args["service"] not in allowed_services:
        raise PermissionError("service access denied")
    start, end = parse_utc(args["start"]), parse_utc(args["end"])
    if not start < end:
        raise ValueError("start must precede end")
    if end - start > max_span:
        raise ValueError("query span exceeds limit")
    return {**args, "start": start, "end": end}
# END validate


GOOD = {"service": "inventory", "start": "2026-10-28T08:00:00Z",
        "end": "2026-10-28T09:00:00Z", "limit": 20}


class QueryTests(unittest.TestCase):
    def check_args(self, args):
        return validate_query(decode_query(json.dumps(args)), {"inventory"})

    def test_valid_request_normalizes_time(self):
        result = self.check_args(GOOD)
        self.assertEqual(result["end"] - result["start"], timedelta(hours=1))

    def test_invalid_structure_and_types(self):
        for args in ({k: v for k, v in GOOD.items() if k != "limit"},
                     {**GOOD, "debug": True}, {**GOOD, "limit": True},
                     {**GOOD, "limit": 201}, {**GOOD, "limit": 20.0},
                     {**GOOD, "service": ""}, []):
            with self.subTest(args=args), self.assertRaises(ValueError):
                self.check_args(args)

    def test_duplicate_field_and_non_json_constant(self):
        for raw in ('{"service":"inventory","service":"admin"}', '{"limit":NaN}'):
            with self.subTest(raw=raw), self.assertRaises(ValueError):
                decode_query(raw)

    def test_invalid_time_and_span(self):
        for changes in ({"end": GOOD["start"]}, {"end": "2026-10-28T07:00:00Z"},
                        {"end": "2026-10-28T09:00:01Z"}, {"start": "2026-02-30T08:00:00Z"},
                        {"start": "2026-10-28T08:00:00"}):
            with self.subTest(changes=changes), self.assertRaises(ValueError):
                self.check_args({**GOOD, **changes})

    def test_valid_shape_does_not_grant_access(self):
        with self.assertRaises(PermissionError):
            self.check_args({**GOOD, "service": "admin"})


if __name__ == "__main__":
    unittest.main(verbosity=2)
