import threading
import time
from contextlib import contextmanager, nullcontext
from dataclasses import dataclass
from random import randint

import httpx
import uvicorn


class ServerThread(threading.Thread):
    # Generated by Claude

    def __init__(self, app, host, port, wrap=nullcontext):
        super().__init__(daemon=True)
        self.app = app
        self.host = host
        self.port = port
        self.server = None
        self.wrap = wrap

    def run(self):
        """Run the uvicorn server."""
        with self.wrap():
            config = uvicorn.Config(
                self.app,
                host=self.host,
                port=self.port,
                log_level="error",  # Suppress logs during tests
                access_log=False,
            )
            self.server = uvicorn.Server(config)
            self.server.run()

    def stop(self):
        """Stop the server."""
        if self.server:
            self.server.should_exit = True


@contextmanager
def create_endpoint(app, host, port, wrap=nullcontext):
    # Mostly generated by Claude

    # Start server in background thread
    server_thread = ServerThread(app=app, host=host, port=port, wrap=wrap)
    server_thread.start()

    # Wait for server to be ready
    base_url = f"http://{host}:{port}"
    max_retries = 50
    retry_delay = 0.1

    for _ in range(max_retries):
        try:
            import httpx

            with httpx.Client() as client:
                response = client.get(f"{base_url}/health", timeout=1.0)
                if response.status_code in (200, 404):
                    break
        except (httpx.ConnectError, httpx.TimeoutException):
            time.sleep(retry_delay)
    else:
        raise RuntimeError(f"Server failed to start on {base_url}")

    yield base_url

    # Cleanup: stop the server thread
    server_thread.stop()


class BaseServer:
    def __init__(self, app, host, port, wrap=nullcontext):
        self.app = app
        self.host = host
        self.port = port or (30000 + randint(1, 9999))
        self.wrap = wrap
        self.base_url = None

    def __enter__(self):
        # Start server in background thread
        server_thread = ServerThread(app=self.app, host=self.host, port=self.port, wrap=self.wrap)
        server_thread.start()

        # Wait for server to be ready
        base_url = f"http://{self.host}:{self.port}"
        max_retries = 50
        retry_delay = 0.1

        for _ in range(max_retries):
            try:
                import httpx

                with httpx.Client() as client:
                    response = client.get(f"{base_url}/health", timeout=1.0)
                    if response.status_code in (200, 404):
                        break
            except (httpx.ConnectError, httpx.TimeoutException):
                time.sleep(retry_delay)
        else:
            raise RuntimeError(f"Server failed to start on {base_url}")

        self._thread = server_thread
        self.base_url = base_url
        return self

    def __exit__(self, exc_type, exc_val, exc_tb):
        if self._thread:
            self._thread.stop()


class OAuthMock(BaseServer):
    def __init__(self, host="127.0.0.1", port=29313):
        from .oauth_mock import app

        super().__init__(app=app, host=host, port=port)

    def set_email(self, email: str):
        response = httpx.post(f"{self.base_url}/set_email", data={"email": email})
        response.raise_for_status()
        return response.json()


class AppTester(BaseServer):
    def __init__(self, app, oauth_mock: OAuthMock, host="127.0.0.1", port=None, wrap=nullcontext):
        super().__init__(app=app, host=host, port=port, wrap=wrap)
        self.oauth_mock = oauth_mock

    def set_email(self, email):
        return self.oauth_mock.set_email(email)

    def client(self, email=None, prefix=""):
        if email is None:
            token = None
        else:
            self.set_email(email)
            response = httpx.get(f"{self.base_url}{prefix}/token", follow_redirects=True)
            assert response.status_code == 200
            token = response.json()["refresh_token"]
        return TokenInteractor(self.base_url, email, token)

    def __str__(self):
        return self.base_url


@dataclass
class TokenInteractor:
    root: str
    email: str
    token: str | None

    def __post_init__(self):
        if self.token is None:
            self.headers = {}
        else:
            self.headers = {"Authorization": f"Bearer {self.token}"}

    def expect(self, response, expect=None):
        expect = 200 if expect is None else expect
        if response.status_code != expect:
            raise AssertionError(
                f"Expected status {expect}, got {response.status_code}: {response.text}"
            )
        return response

    def get(self, endpoint, expect=None, **data):
        response = httpx.get(f"{self.root}{endpoint}", headers=self.headers, params=data)
        return self.expect(response, expect)

    def post(self, endpoint, expect=None, **data):
        response = httpx.post(f"{self.root}{endpoint}", headers=self.headers, json=data)
        return self.expect(response, expect)

    def delete(self, endpoint, expect=None, **data):
        response = httpx.delete(f"{self.root}{endpoint}", headers=self.headers, json=data)
        return self.expect(response, expect)
