from __future__ import annotations

import http.client
import json
import threading
import unittest
from contextlib import contextmanager
from http.server import ThreadingHTTPServer
from typing import Any, Iterator, Mapping

from arr_web.contracts import MAX_UPLOAD_BYTES, Response, success
from arr_web.server import handler_for


class RecordingApplication:
    def __init__(self) -> None:
        self.calls: list[dict[str, Any]] = []

    def handle(
        self,
        method: str,
        path: str,
        headers: Mapping[str, str],
        body: bytes,
        *,
        client_id: str,
    ) -> Response:
        self.calls.append(
            {
                "method": method,
                "path": path,
                "headers": dict(headers),
                "body": body,
                "client_id": client_id,
            }
        )
        return Response.json(200, success({"method": method}))


@contextmanager
def running_server(
    application: RecordingApplication,
) -> Iterator[tuple[str, int]]:
    server = ThreadingHTTPServer(("127.0.0.1", 0), handler_for(application))
    thread = threading.Thread(target=server.serve_forever, daemon=True)
    thread.start()
    try:
        host, port = server.server_address
        yield str(host), int(port)
    finally:
        server.shutdown()
        server.server_close()
        thread.join(timeout=5)


class PortalHttpAdapterTests(unittest.TestCase):
    def test_patch_and_delete_forward_json_bodies_to_application(self) -> None:
        application = RecordingApplication()
        with running_server(application) as (host, port):
            connection = http.client.HTTPConnection(host, port, timeout=5)
            try:
                patch_body = json.dumps(
                    {"draft_id": "bookingdraft-test", "room_type": "U-TWN", "quantity": 2}
                ).encode("utf-8")
                connection.request(
                    "PATCH",
                    "/api/company-reports/source/draft/items/7",
                    body=patch_body,
                    headers={"Content-Type": "application/json"},
                )
                patch_response = connection.getresponse()
                self.assertEqual(patch_response.status, 200)
                self.assertEqual(patch_response.getheader("X-Content-Type-Options"), "nosniff")
                patch_response.read()

                delete_body = b'{"draft_id":"bookingdraft-test"}'
                connection.request(
                    "DELETE",
                    "/api/company-reports/source/draft/items/7",
                    body=delete_body,
                    headers={"Content-Type": "application/json"},
                )
                delete_response = connection.getresponse()
                self.assertEqual(delete_response.status, 200)
                delete_response.read()

                batch_delete_body = b'{"draft_id":"bookingdraft-test","item_ids":[7,8]}'
                connection.request(
                    "DELETE",
                    "/api/company-reports/source/draft/items",
                    body=batch_delete_body,
                    headers={"Content-Type": "application/json"},
                )
                batch_delete_response = connection.getresponse()
                self.assertEqual(batch_delete_response.status, 200)
                batch_delete_response.read()
            finally:
                connection.close()

        self.assertEqual(
            [(call["method"], call["body"]) for call in application.calls],
            [
                ("PATCH", patch_body),
                ("DELETE", delete_body),
                ("DELETE", batch_delete_body),
            ],
        )
        self.assertEqual(
            [call["path"] for call in application.calls],
            [
                "/api/company-reports/source/draft/items/7",
                "/api/company-reports/source/draft/items/7",
                "/api/company-reports/source/draft/items",
            ],
        )

    def test_mutation_methods_reject_missing_or_oversized_content_length(self) -> None:
        application = RecordingApplication()
        with running_server(application) as (host, port):
            missing = http.client.HTTPConnection(host, port, timeout=5)
            try:
                missing.putrequest("PATCH", "/api/company-reports/source/draft/items/7")
                missing.endheaders()
                missing_response = missing.getresponse()
                self.assertEqual(missing_response.status, 411)
                self.assertEqual(
                    json.loads(missing_response.read())["error"]["code"],
                    "CONTENT_LENGTH_REQUIRED",
                )
            finally:
                missing.close()

            oversized = http.client.HTTPConnection(host, port, timeout=5)
            try:
                oversized.putrequest("DELETE", "/api/company-reports/source/draft")
                oversized.putheader("Content-Length", str(MAX_UPLOAD_BYTES + 1))
                oversized.endheaders()
                oversized_response = oversized.getresponse()
                self.assertEqual(oversized_response.status, 413)
                self.assertEqual(
                    json.loads(oversized_response.read())["error"]["code"],
                    "REQUEST_TOO_LARGE",
                )
            finally:
                oversized.close()

        self.assertEqual(application.calls, [])


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