#!/usr/bin/env python3
"""Original LockedIn Labs lesson: follow a request through real FastAPI.

Python 3.11+. Install the accompanying pinned requirements in a new environment.
Synthetic, read-only, in-process exercise: no listening port, credentials,
network service, model call or production authentication is configured.
"""
from __future__ import annotations

import argparse
from copy import deepcopy
from importlib.metadata import version
import json
import unittest
from typing import Annotated

from fastapi import FastAPI, HTTPException, Path
from fastapi.testclient import TestClient
from pydantic import BaseModel


TICKETS = {104: {"id": 104, "title": "Inspect the training pump", "status": "open"}}
CASES = {
    "found": ("GET", "/tickets/104"),
    "missing": ("GET", "/tickets/105"),
    "invalid": ("GET", "/tickets/pump"),
    "wrong-method": ("POST", "/tickets/104"),
}


class Ticket(BaseModel):
    id: int
    title: str
    status: str


def build_app():
    """Each exercise has its own fixtures and observations; no shared service."""
    app = FastAPI(title="First ticket request · local teaching exercise")
    tickets = deepcopy(TICKETS)
    events = []

    def lookup_ticket(ticket_id: int):
        events.append({"at": "lookup", "ticket_id": ticket_id})
        return tickets.get(ticket_id)

    @app.get("/tickets/{ticket_id}", response_model=Ticket)
    def read_ticket(ticket_id: Annotated[int, Path(gt=0)]):
        events.append({"at": "handler", "ticket_id": ticket_id})
        ticket = lookup_ticket(ticket_id)
        if ticket is None:
            raise HTTPException(status_code=404, detail="Ticket not found")
        return ticket

    return app, events, tickets


def inspect_request(scenario: str):
    method, path = CASES[scenario]
    app, events, tickets = build_app()
    before = deepcopy(tickets)
    # TestClient invokes the actual ASGI application in this process. It does
    # not send a request to a real host, despite the client's synthetic base URL.
    with TestClient(app) as client:
        response = client.request(method, path, headers={"Accept": "application/json"})
    return {
        "id": scenario,
        "request": {"method": method, "path": path, "headers": {"Accept": "application/json"}, "body": None},
        "response": {
            "status": response.status_code,
            "headers": {key: value for key, value in response.headers.items() if key in ("content-type", "allow")},
            "body": response.json(),
        },
        "events": events,
        "records_unchanged": tickets == before,
    }


class FirstRequestTests(unittest.TestCase):
    def test_found_parses_path_calls_handler_and_returns_contract(self):
        report = inspect_request("found")
        self.assertEqual(report["response"]["status"], 200)
        self.assertEqual(report["response"]["body"], TICKETS[104])
        self.assertEqual(report["response"]["headers"]["content-type"], "application/json")
        self.assertEqual(report["events"], [{"at": "handler", "ticket_id": 104}, {"at": "lookup", "ticket_id": 104}])
        self.assertIsInstance(report["events"][0]["ticket_id"], int)

    def test_missing_is_application_outcome_after_lookup(self):
        report = inspect_request("missing")
        self.assertEqual(report["response"]["status"], 404)
        self.assertEqual(report["response"]["body"], {"detail": "Ticket not found"})
        self.assertEqual([event["at"] for event in report["events"]], ["handler", "lookup"])

    def test_invalid_never_enters_handler(self):
        report = inspect_request("invalid")
        self.assertEqual(report["response"]["status"], 422)
        self.assertEqual(report["events"], [])
        error = report["response"]["body"]["detail"][0]
        self.assertEqual(error["loc"], ["path", "ticket_id"])
        self.assertEqual(error["input"], "pump")
        self.assertEqual(error["type"], "int_parsing")

    def test_wrong_method_has_allow_header_without_business_work(self):
        report = inspect_request("wrong-method")
        self.assertEqual(report["response"]["status"], 405)
        self.assertIn("GET", report["response"]["headers"]["allow"])
        self.assertEqual(report["events"], [])

    def test_numeric_constraint_and_openapi_agree(self):
        app, events, _ = build_app()
        with TestClient(app) as client:
            self.assertEqual(client.get("/tickets/0").status_code, 422)
            schema = client.get("/openapi.json").json()
        parameter = schema["paths"]["/tickets/{ticket_id}"]["get"]["parameters"][0]
        self.assertEqual(parameter["in"], "path")
        self.assertEqual(parameter["schema"]["type"], "integer")
        self.assertEqual(parameter["schema"]["exclusiveMinimum"], 0)
        self.assertEqual(events, [])

    def test_all_cases_leave_records_unchanged(self):
        for scenario in CASES:
            with self.subTest(scenario=scenario):
                self.assertTrue(inspect_request(scenario)["records_unchanged"])


def main():
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument("command", choices=["test", "demo", "inspect", "contract"])
    parser.add_argument("--scenario", choices=list(CASES), default="found")
    args = parser.parse_args()
    if args.command == "test":
        suite = unittest.defaultTestLoader.loadTestsFromTestCase(FirstRequestTests)
        raise SystemExit(0 if unittest.TextTestRunner(verbosity=2).run(suite).wasSuccessful() else 1)
    if args.command == "inspect":
        result = inspect_request(args.scenario)
    elif args.command == "contract":
        app, _, _ = build_app()
        with TestClient(app) as client:
            result = client.get("/openapi.json").json()
    else:
        result = {
            "runtime": {name: version(name) for name in ("fastapi", "starlette", "pydantic", "httpx2")},
            "transport": "in-process ASGI TestClient",
            "cases": [inspect_request(scenario) for scenario in CASES],
        }
    print(json.dumps(result, indent=2))


if __name__ == "__main__":
    main()
