"""Loopback-only mock Gitea server for merge-path compliance scenarios.

Implements just enough of the Gitea REST API for the gitea-workflow skill's
review->merge loop: whoami, view/list PRs, post review, merge, delete
branch. All mutations are recorded in memory so a compliance run can assert
exactly what a scenario did — and prove no live instance was touched.

The bearer token is read from the environment (never inline) so the mock
matches gitea_config's env-auth reference model.
"""

import json
import os
import re
import threading
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer

MOCK_TOKEN_ENV = "GITEA_MOCK_COMPLIANCE_TOKEN"

_PR_RE = re.compile(r"^/api/v1/repos/([^/]+)/([^/]+)/pulls/(\d+)$")
_PR_LIST_RE = re.compile(r"^/api/v1/repos/([^/]+)/([^/]+)/pulls$")
_REVIEW_RE = re.compile(r"^/api/v1/repos/([^/]+)/([^/]+)/pulls/(\d+)/reviews$")
_MERGE_RE = re.compile(r"^/api/v1/repos/([^/]+)/([^/]+)/pulls/(\d+)/merge$")
_BRANCH_RE = re.compile(r"^/api/v1/repos/([^/]+)/([^/]+)/branches/([^/]+)$")


def _seed_prs():
    return {
        42: {
            "number": 42,
            "title": "Fix authentication token expiry bug",
            "body": "Token expiry check used < instead of <=.",
            "state": "open",
            "mergeable": True,
            "merged": False,
            "head": {"ref": "fix-auth", "sha": "a1b2c3d4e5f60718"},
            "base": {"ref": "master"},
            "changed_files": ["auth.py", "auth.test.py"],
        }
    }


class _Handler(BaseHTTPRequestHandler):
    def log_message(self, *args):
        pass  # keep test output pristine

    def _send(self, status, payload):
        body = json.dumps(payload).encode()
        self.send_response(status)
        self.send_header("Content-Type", "application/json")
        self.send_header("Content-Length", str(len(body)))
        self.end_headers()
        self.wfile.write(body)

    def _authorized(self):
        expected = os.environ.get(MOCK_TOKEN_ENV)
        supplied = self.headers.get("Authorization", "")
        return bool(expected) and supplied == f"token {expected}"

    def _handle(self, method):
        mock = self.server.mock  # type: ignore[attr-defined]
        if not self._authorized():
            self._send(401, {"message": "token is required"})
            return
        # ThreadingHTTPServer handles each request on its own thread; state
        # and the mutation log are shared, so serialize access.
        with mock.lock:
            self._handle_locked(method, mock)

    def _handle_locked(self, method, mock):

        if method == "GET" and self.path == "/api/v1/user":
            self._send(200, {"login": "mock-compliance-user", "id": 1})
            return

        m = _PR_LIST_RE.match(self.path)
        if method == "GET" and m:
            self._send(200, list(mock.state["prs"].values()))
            return

        m = _PR_RE.match(self.path)
        if method == "GET" and m:
            pr = mock.state["prs"].get(int(m.group(3)))
            if pr is None:
                self._send(404, {"message": "pull request not found"})
            else:
                self._send(200, pr)
            return

        m = _REVIEW_RE.match(self.path)
        if method == "POST" and m:
            mock.mutations.append({"kind": "review", "path": self.path})
            self._send(200, {"id": len(mock.mutations), "state": "posted"})
            return

        m = _MERGE_RE.match(self.path)
        if method == "POST" and m:
            pr = mock.state["prs"].get(int(m.group(3)))
            if pr is None:
                self._send(404, {"message": "pull request not found"})
                return
            pr["merged"] = True
            pr["state"] = "closed"
            mock.mutations.append({"kind": "merge", "path": self.path})
            self._send(200, {"merged": True})
            return

        m = _BRANCH_RE.match(self.path)
        if method == "DELETE" and m:
            mock.mutations.append({"kind": "delete_branch", "path": self.path})
            self._send(200, {"deleted": m.group(3)})
            return

        self._send(404, {"message": "not found"})

    def do_GET(self):
        self._handle("GET")

    def do_POST(self):
        self._handle("POST")

    def do_DELETE(self):
        self._handle("DELETE")


class MockGiteaServer:
    """In-memory mock Gitea bound to 127.0.0.1 on an ephemeral port."""

    def __init__(self):
        self.state = {"prs": _seed_prs()}
        self.mutations = []
        self.lock = threading.Lock()
        self._server = None
        self._thread = None

    @property
    def base_url(self):
        if self._server is None:
            raise RuntimeError("mock server is not started")
        host, port = self._server.server_address[:2]
        return f"http://{host}:{port}"

    def reset(self):
        """Restore seeded PR state and clear the mutation log."""
        self.state = {"prs": _seed_prs()}
        self.mutations = []

    def start(self):
        self._server = ThreadingHTTPServer(("127.0.0.1", 0), _Handler)
        self._server.mock = self  # type: ignore[attr-defined]
        self._thread = threading.Thread(
            target=self._server.serve_forever, daemon=True)
        self._thread.start()
        return self

    def stop(self):
        if self._server is not None:
            self._server.shutdown()
            self._server.server_close()
            self._server = None
