#!/usr/bin/env python3
"""Run reviewed ShellOrbit fixtures against an explicitly chosen test target."""
import argparse
import datetime
import json
import math
import re
import sys
import urllib.error
import urllib.parse
import urllib.request

class NoRedirect(urllib.request.HTTPRedirectHandler):
    def redirect_request(self, req, fp, code, msg, headers, newurl):
        return None

def suite(path):
    with open(path, encoding="utf-8") as source:
        raw = source.read(1024 * 1024 + 1)
    if len(raw.encode()) > 1024 * 1024:
        raise ValueError("suite must be at most 1 MB")
    fixtures = json.loads(raw)["fixtures"]
    if not isinstance(fixtures, list) or not 1 <= len(fixtures) <= 100:
        raise ValueError("suite must contain 1 to 100 fixtures")
    for f in fixtures:
        if not re.fullmatch(r"[A-Za-z0-9_-]{1,100}", f["id"]):
            raise ValueError("fixture IDs must be safe identifiers")
        if not isinstance(f["name"], str) or not 1 <= len(f["name"]) <= 80:
            raise ValueError("fixture names must be 1 to 80 characters")
        if not isinstance(f["expected_status"], int) or not 100 <= f["expected_status"] <= 599:
            raise ValueError("expected status must be 100 to 599")
        if len(json.dumps(f["body"]).encode()) > 65536:
            raise ValueError("fixture payload must be at most 64 KB")
        fields = f.get("required_fields") or []
        if not isinstance(fields, list) or len(fields) > 30 or any(not isinstance(p, str) or not 1 <= len(p) <= 200 for p in fields):
            raise ValueError("choose at most 30 response field paths")
        if f.get("expires_at"):
            expiry = datetime.datetime.fromisoformat(f["expires_at"].replace("Z", "+00:00"))
            if expiry.tzinfo is None or expiry <= datetime.datetime.now(datetime.timezone.utc):
                raise ValueError("suite contains an expired fixture")
    return fixtures

def main():
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument("suite")
    parser.add_argument("--target", required=True)
    parser.add_argument("--confirm-test-target", action="store_true", required=True)
    parser.add_argument("--repeat", type=int, choices=range(1, 11), default=1)
    parser.add_argument("--reverse", action="store_true", help="Exercise out-of-order events")
    parser.add_argument("--timeout", type=float, default=15)
    args = parser.parse_args()
    parsed = urllib.parse.urlparse(args.target)
    if not parsed.hostname or (parsed.scheme != "https" and not (parsed.scheme == "http" and parsed.hostname in ("localhost", "127.0.0.1", "::1"))):
        parser.error("use HTTPS or an HTTP localhost test target")
    if parsed.username or parsed.password or parsed.query or parsed.fragment:
        parser.error("do not put credentials, query parameters, or fragments in the target URL")
    if not math.isfinite(args.timeout) or not 0 < args.timeout <= 60:
        parser.error("timeout must be positive and at most 60 seconds")
    try:
        fixtures = suite(args.suite)
    except (OSError, ValueError, KeyError, TypeError) as error:
        parser.error(f"invalid suite: {error}")
    if args.reverse:
        fixtures.reverse()
    opener = urllib.request.build_opener(NoRedirect())
    failed = 0
    for _ in range(args.repeat):
        for fixture in fixtures:
            name = "".join(c for c in fixture["name"] if c.isprintable())
            request = urllib.request.Request(args.target, data=json.dumps(fixture["body"]).encode(), headers={"Content-Type": "application/json", "X-ShellOrbit-Fixture-ID": fixture["id"]}, method="POST")
            try:
                try:
                    response = opener.open(request, timeout=args.timeout)
                except urllib.error.HTTPError as error:
                    response = error
                with response:
                    status = response.code
                    raw = response.read(1024 * 1024 + 1)
                if 300 <= status <= 399:
                    raise ValueError("redirect refused")
                if len(raw) > 1024 * 1024:
                    raise ValueError("response exceeded 1 MB")
                if status != fixture["expected_status"]:
                    raise ValueError(f"expected {fixture['expected_status']}, received {status}")
                for field in fixture.get("required_fields") or []:
                    value = json.loads(raw)
                    for part in field.split("."):
                        value = value[int(part)] if isinstance(value, list) else value[part]
                print(f"PASS {name}")
            except Exception as error:
                failed += 1
                print(f"FAIL {name}: {error}", file=sys.stderr)
    return 1 if failed else 0

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