import asyncio
import builtins
import json
import os
import sys
import threading
import time
import uuid
from pathlib import Path
from typing import Any, Dict, Optional

import pytest
import requests

# Ensure imports resolve to the repo's `src/` + `lib/` code.
PROJECT_ROOT = Path(__file__).resolve().parents[1]
SRC_PATH = PROJECT_ROOT / "src"
LIB_PATH = PROJECT_ROOT / "lib"

for p in (str(SRC_PATH), str(LIB_PATH), str(PROJECT_ROOT)):
    if p in sys.path:
        sys.path.remove(p)
    sys.path.insert(0, p)

from microdot import Microdot, send_file  # noqa: E402
from microdot.session import Session  # noqa: E402
from microdot.websocket import with_websocket  # noqa: E402


class DummySender:
    def __init__(self):
        self.sent: list[tuple[str, Optional[str]]] = []

    async def send(self, data: Any, addr: Optional[str] = None):
        if isinstance(data, (bytes, bytearray)):
            data = bytes(data).decode(errors="ignore")
        self.sent.append((data, addr))
        return True


def _json(resp: requests.Response) -> Dict[str, Any]:
    # Many endpoints already set Content-Type; but be tolerant for now.
    return resp.json()  # pragma: no cover


def _find_id_by_field(list_resp_json: Dict[str, Any], field: str, value: str) -> str:
    for obj_id, data in list_resp_json.items():
        if isinstance(data, dict) and data.get(field) == value:
            return str(obj_id)
    raise AssertionError(f"Could not find id for {field}={value!r}")


def _start_microdot_server(app: Microdot, host: str, port: int):
    """
    Start Microdot server on a background thread.
    Returns (thread, chosen_port).
    """

    def runner():
        loop = asyncio.new_event_loop()
        asyncio.set_event_loop(loop)
        try:
            loop.run_until_complete(app.start_server(host=host, port=port))
        finally:
            try:
                loop.close()
            except Exception:
                pass

    thread = threading.Thread(target=runner, daemon=True)
    thread.start()

    # Poll until the socket is bound and app.server is available.
    chosen_port = None
    deadline = time.time() + 5.0
    while time.time() < deadline:
        server = getattr(app, "server", None)
        if server and getattr(server, "sockets", None):
            sockets = server.sockets or []
            if sockets:
                chosen_port = sockets[0].getsockname()[1]
                break
        time.sleep(0.05)

    if chosen_port is None:
        raise RuntimeError("Microdot server failed to start in time")

    return thread, chosen_port


@pytest.fixture(scope="function")
def server(monkeypatch, tmp_path_factory):
    """
    Start the Microdot app in-process and return a test client.
    """

    tmp_root = tmp_path_factory.mktemp("endpoint-tests")
    tmp_db_dir = tmp_root / "db"
    tmp_settings_file = tmp_root / "settings.json"

    # Be defensive: pytest runners can sometimes alter sys.path ordering.
    for p in (str(SRC_PATH), str(LIB_PATH), str(PROJECT_ROOT)):
        if p in sys.path:
            sys.path.remove(p)
        sys.path.insert(0, p)

    # Patch Settings so endpoint tests never touch real `settings.json`.
    import settings as settings_mod  # noqa: E402

    settings_mod.Settings.SETTINGS_FILE = str(tmp_settings_file)

    # Patch the Model db directory so endpoint CRUD is isolated.
    import models.model as model_mod  # noqa: E402

    monkeypatch.setattr(model_mod, "_db_dir", lambda: str(tmp_db_dir))

    # Reset model singletons (controllers instantiate model classes at import time).
    # Import the classes first so we can delete their `_instance` attribute if present.
    import models.preset as models_preset  # noqa: E402
    import models.profile as models_profile  # noqa: E402
    import models.group as models_group  # noqa: E402
    import models.zone as models_zone  # noqa: E402
    import models.pallet as models_pallet  # noqa: E402
    import models.scene as models_scene  # noqa: E402
    import models.pattern as models_pattern  # noqa: E402
    import models.squence as models_sequence  # noqa: E402
    import models.device as models_device  # noqa: E402

    for cls in (
        models_preset.Preset,
        models_profile.Profile,
        models_group.Group,
        models_zone.Zone,
        models_pallet.Palette,
        models_scene.Scene,
        models_pattern.Pattern,
        models_sequence.Sequence,
        models_device.Device,
    ):
        if hasattr(cls, "_instance"):
            delattr(cls, "_instance")

    # Patch open() so pattern definitions work after we `chdir` into src/.
    orig_open = builtins.open

    def patched_open(file, *args, **kwargs):
        if isinstance(file, str):
            # Pattern controller loads definitions from a relative db/ path.
            if file in {"db/pattern.json", "pattern.json", "/db/pattern.json"}:
                file = str(PROJECT_ROOT / "db" / "pattern.json")
        return orig_open(file, *args, **kwargs)

    monkeypatch.setattr(builtins, "open", patched_open)

    old_cwd = os.getcwd()
    os.chdir(str(SRC_PATH))

    dummy_sender = DummySender()

    try:
        # Ensure controllers are imported fresh after our patching.
        for mod_name in (
            "controllers.preset",
            "controllers.profile",
            "controllers.group",
            "controllers.sequence",
            "controllers.zone",
            "controllers.palette",
            "controllers.scene",
            "controllers.pattern",
            "controllers.settings",
            "controllers.device",
        ):
            sys.modules.pop(mod_name, None)

        # Import controllers after patching db/settings/model singletons.
        import controllers.preset as preset_ctl  # noqa: E402
        import controllers.profile as profile_ctl  # noqa: E402
        import controllers.group as group_ctl  # noqa: E402
        import controllers.sequence as sequence_ctl  # noqa: E402
        import controllers.zone as zone_ctl  # noqa: E402
        import controllers.palette as palette_ctl  # noqa: E402
        import controllers.scene as scene_ctl  # noqa: E402
        import controllers.pattern as pattern_ctl  # noqa: E402
        import controllers.settings as settings_ctl  # noqa: E402
        import controllers.device as device_ctl  # noqa: E402

        # Configure transport sender used by /presets/send.
        from models.transport import set_sender  # noqa: E402

        set_sender(dummy_sender)

        app = Microdot()

        # Session secret key comes from settings (patched to tmp).
        settings = settings_mod.Settings()
        secret_key = settings.get(
            "session_secret_key",
            "led-controller-secret-key-change-in-production",
        )
        Session(app, secret_key=secret_key)

        # Mount model controllers under their public prefixes.
        app.mount(preset_ctl.controller, "/presets")
        app.mount(profile_ctl.controller, "/profiles")
        app.mount(group_ctl.controller, "/groups")
        app.mount(sequence_ctl.controller, "/sequences")
        app.mount(zone_ctl.controller, "/zones")
        app.mount(palette_ctl.controller, "/palettes")
        app.mount(scene_ctl.controller, "/scenes")
        app.mount(pattern_ctl.controller, "/patterns")
        app.mount(settings_ctl.controller, "/settings")
        app.mount(device_ctl.controller, "/devices")

        @app.route("/")
        def index(request):
            return send_file("templates/index.html")

        @app.route("/settings")
        def settings_page(request):
            return send_file("templates/settings.html")

        @app.route("/favicon.ico")
        def favicon(request):
            return "", 204

        @app.route("/static/<path:path>")
        def static_handler(request, path):
            if ".." in path:
                return "Not found", 404
            return send_file("static/" + path)

        @app.route("/ws")
        @with_websocket
        async def ws(request, ws):
            # Minimal websocket handler: forward raw JSON/text payloads to dummy sender.
            while True:
                data = await ws.receive()
                if not data:
                    break
                try:
                    parsed = json.loads(data)
                    addr = parsed.pop("to", None)
                    payload = json.dumps(parsed) if parsed else data
                    await dummy_sender.send(payload, addr=addr)
                except Exception:
                    await dummy_sender.send(data)

        thread, chosen_port = _start_microdot_server(app, host="127.0.0.1", port=0)
        base_url = f"http://127.0.0.1:{chosen_port}"

        client = requests.Session()
        client.headers.update(
            {
                "User-Agent": "pytest/requests",
                "Accept": "application/json",
            }
        )

        yield {
            "base_url": base_url,
            "client": client,
            "sender": dummy_sender,
            "thread": thread,
            "app": app,
        }
    finally:
        # Stop server cleanly.
        try:
            app = locals().get("app")
            if app is not None:
                app.shutdown()
        except Exception:
            pass

        # Give it a moment to close sockets.
        time.sleep(0.1)
        try:
            thread = locals().get("thread")
            if thread is not None:
                thread.join(timeout=5)
        except Exception:
            pass

        os.chdir(old_cwd)


def test_main_routes(server):
    c: requests.Session = server["client"]
    base_url: str = server["base_url"]

    resp = c.get(f"{base_url}/")
    assert resp.status_code == 200
    assert "LED Controller" in resp.text

    resp = c.get(f"{base_url}/favicon.ico")
    assert resp.status_code == 204

    resp = c.get(f"{base_url}/static/style.css")
    assert resp.status_code == 200

    resp = c.get(f"{base_url}/settings/page")
    assert resp.status_code == 200
    assert "LED Controller" in resp.text

    resp = c.get(f"{base_url}/ws")
    # WebSocket endpoints should reject non-upgraded HTTP requests.
    assert resp.status_code != 200
    assert resp.status_code in {400, 401, 403, 404, 405, 426}


def test_settings_controller(server):
    c: requests.Session = server["client"]
    base_url: str = server["base_url"]

    resp = c.get(f"{base_url}/settings")
    assert resp.status_code == 200
    data = resp.json()
    assert isinstance(data, dict)
    assert "wifi_channel" in data

    resp = c.get(f"{base_url}/settings/wifi/ap")
    assert resp.status_code == 200
    ap = resp.json()
    assert "saved_ssid" in ap
    assert "active" in ap

    unique_ssid = f"pytest-ssid-{uuid.uuid4().hex[:8]}"
    resp = c.post(
        f"{base_url}/settings/wifi/ap",
        json={"ssid": unique_ssid, "password": "secret", "channel": 1},
    )
    assert resp.status_code == 200
    msg = resp.json()
    assert msg["ssid"] == unique_ssid
    assert msg["channel"] == 1

    resp = c.post(
        f"{base_url}/settings/wifi/ap",
        json={"ssid": "bad-ssid", "password": "secret", "channel": 12},
    )
    assert resp.status_code == 400

    resp = c.put(f"{base_url}/settings/settings", json={"wifi_channel": 11})
    assert resp.status_code == 200

    resp = c.put(f"{base_url}/settings/settings", json={"wifi_channel": 12})
    assert resp.status_code == 400

    resp = c.put(f"{base_url}/settings/settings", json={"global_brightness": 42})
    assert resp.status_code == 200
    resp = c.get(f"{base_url}/settings")
    assert resp.status_code == 200
    assert resp.json().get("global_brightness") == 42

    resp = c.put(f"{base_url}/settings/settings", json={"global_brightness": 300})
    assert resp.status_code == 400


def test_profiles_presets_zones_endpoints(server, monkeypatch):
    c: requests.Session = server["client"]
    base_url: str = server["base_url"]
    sender: DummySender = server["sender"]

    import controllers.device as device_ctl

    monkeypatch.setattr(device_ctl, "IDENTIFY_OFF_DELAY_S", 0.05)

    unique_profile_name = f"pytest-profile-{uuid.uuid4().hex[:8]}"

    resp = c.post(f"{base_url}/profiles", json={"name": unique_profile_name})
    assert resp.status_code == 201
    created = resp.json()
    assert isinstance(created, dict)
    profile_id = next(iter(created.keys()))

    resp = c.post(f"{base_url}/profiles/{profile_id}/apply")
    assert resp.status_code == 200

    resp = c.get(f"{base_url}/profiles/current")
    assert resp.status_code == 200
    current = resp.json()
    assert str(current["id"]) == str(profile_id)

    # Presets CRUD (scoped to current profile session).
    resp = c.get(f"{base_url}/presets")
    assert resp.status_code == 200
    presets = resp.json()
    assert isinstance(presets, dict)
    assert presets  # seeded presets should exist

    first_preset_id = next(iter(presets.keys()))
    resp = c.get(f"{base_url}/presets/{first_preset_id}")
    assert resp.status_code == 200
    assert resp.json()  # dict

    unique_preset_name = f"pytest-preset-{uuid.uuid4().hex[:8]}"
    resp = c.post(
        f"{base_url}/presets",
        json={
            "name": unique_preset_name,
            "pattern": "on",
            "colors": ["#ff0000"],
            "brightness": 123,
            "delay": 100,
        },
    )
    assert resp.status_code == 201
    created_preset = resp.json()
    new_preset_id = next(iter(created_preset.keys()))
    assert created_preset[new_preset_id]["profile_id"] == str(profile_id)

    resp = c.put(
        f"{base_url}/presets/{new_preset_id}",
        json={"brightness": 77},
    )
    assert resp.status_code == 200
    assert resp.json()["brightness"] == 77

    sender.sent.clear()
    resp = c.post(
        f"{base_url}/presets/send",
        json={"preset_ids": [new_preset_id], "save": False},
    )
    assert resp.status_code == 200
    sent_result = resp.json()
    assert sent_result["presets_sent"] >= 1
    assert len(sender.sent) >= 1

    resp = c.delete(f"{base_url}/presets/{new_preset_id}")
    assert resp.status_code == 200
    resp = c.get(f"{base_url}/presets/{new_preset_id}")
    assert resp.status_code == 404

    # Zones CRUD (scoped to current profile session).
    unique_zone_name = f"pytest-zone-{uuid.uuid4().hex[:8]}"
    resp = c.post(
        f"{base_url}/zones",
        json={"name": unique_zone_name, "names": ["1", "2"]},
    )
    assert resp.status_code == 201
    created_zones = resp.json()
    zone_id = next(iter(created_zones.keys()))

    resp = c.get(f"{base_url}/zones/{zone_id}")
    assert resp.status_code == 200
    assert resp.json()["name"] == unique_zone_name

    resp = c.post(f"{base_url}/zones/{zone_id}/set-current")
    assert resp.status_code == 200

    resp = c.get(f"{base_url}/zones/current")
    assert resp.status_code == 200
    assert resp.json()["zone_id"] == str(zone_id)

    resp = c.put(
        f"{base_url}/zones/{zone_id}",
        json={"name": f"{unique_zone_name}-updated", "names": ["3"]},
    )
    assert resp.status_code == 200
    assert resp.json()["names"] == ["3"]

    resp = c.post(f"{base_url}/zones/{zone_id}/clone", json={"name": "pytest-zone-clone"})
    assert resp.status_code == 201
    clone_payload = resp.json()
    clone_id = next(iter(clone_payload.keys()))

    resp = c.get(f"{base_url}/zones/{clone_id}")
    assert resp.status_code == 200

    resp = c.delete(f"{base_url}/zones/{clone_id}")
    assert resp.status_code == 200

    resp = c.delete(f"{base_url}/zones/{zone_id}")
    assert resp.status_code == 200

    # Profile clone + update endpoints.
    clone_name = f"pytest-profile-clone-{uuid.uuid4().hex[:8]}"
    resp = c.post(f"{base_url}/profiles/{profile_id}/clone", json={"name": clone_name})
    assert resp.status_code == 201
    cloned = resp.json()
    clone_profile_id = next(iter(cloned.keys()))

    resp = c.post(f"{base_url}/profiles/{clone_profile_id}/apply")
    assert resp.status_code == 200

    resp = c.put(
        f"{base_url}/profiles/current",
        json={"name": f"{clone_name}-updated"},
    )
    assert resp.status_code == 200

    resp = c.put(
        f"{base_url}/profiles/{clone_profile_id}",
        json={"name": f"{clone_name}-updated-2"},
    )
    assert resp.status_code == 200

    resp = c.delete(f"{base_url}/profiles/{clone_profile_id}")
    assert resp.status_code == 200

    resp = c.delete(f"{base_url}/profiles/{profile_id}")
    assert resp.status_code == 200


def test_groups_sequences_scenes_palettes_patterns_endpoints(server):
    c: requests.Session = server["client"]
    base_url: str = server["base_url"]
    sender: DummySender = server["sender"]

    # Groups.
    unique_group_name = f"pytest-group-{uuid.uuid4().hex[:8]}"
    resp = c.post(f"{base_url}/groups", json={"name": unique_group_name})
    assert resp.status_code == 201
    groups_list = c.get(f"{base_url}/groups").json()
    group_id = _find_id_by_field(groups_list, "name", unique_group_name)

    resp = c.get(f"{base_url}/groups/{group_id}")
    assert resp.status_code == 200
    assert resp.json()["name"] == unique_group_name

    resp = c.put(f"{base_url}/groups/{group_id}", json={"brightness": 10})
    assert resp.status_code == 200
    assert resp.json()["brightness"] == 10

    resp = c.delete(f"{base_url}/groups/{group_id}")
    assert resp.status_code == 200

    # Sequences.
    unique_seq_group_name = f"pytest-seq-group-{uuid.uuid4().hex[:8]}"
    resp = c.post(
        f"{base_url}/sequences",
        json={"group_name": unique_seq_group_name, "presets": []},
    )
    assert resp.status_code == 201
    sequences_list = c.get(f"{base_url}/sequences").json()
    seq_id = _find_id_by_field(sequences_list, "group_name", unique_seq_group_name)

    resp = c.get(f"{base_url}/sequences/{seq_id}")
    assert resp.status_code == 200

    resp = c.put(f"{base_url}/sequences/{seq_id}", json={"sequence_duration": 1234})
    assert resp.status_code == 200
    assert resp.json()["sequence_duration"] == 1234

    resp = c.delete(f"{base_url}/sequences/{seq_id}")
    assert resp.status_code == 200

    # Scenes.
    unique_scene_name = f"pytest-scene-{uuid.uuid4().hex[:8]}"
    resp = c.post(f"{base_url}/scenes", json={"name": unique_scene_name})
    assert resp.status_code == 201
    scenes_list = c.get(f"{base_url}/scenes").json()
    scene_id = _find_id_by_field(scenes_list, "name", unique_scene_name)

    resp = c.get(f"{base_url}/scenes/{scene_id}")
    assert resp.status_code == 200

    resp = c.put(f"{base_url}/scenes/{scene_id}", json={"name": unique_scene_name + "-updated"})
    assert resp.status_code == 200
    assert resp.json()["name"].endswith("-updated")

    resp = c.delete(f"{base_url}/scenes/{scene_id}")
    assert resp.status_code == 200

    # Palettes.
    colors = ["#112233", "#445566"]
    resp = c.post(f"{base_url}/palettes", json={"colors": colors})
    assert resp.status_code == 201
    palette_payload = resp.json()
    palette_id = str(palette_payload["id"])

    resp = c.get(f"{base_url}/palettes/{palette_id}")
    assert resp.status_code == 200
    assert resp.json()["id"] == palette_id

    resp = c.put(f"{base_url}/palettes/{palette_id}", json={"colors": ["#000000"]})
    assert resp.status_code == 200
    assert resp.json()["colors"] == ["#000000"]

    resp = c.delete(f"{base_url}/palettes/{palette_id}")
    assert resp.status_code == 200

    # Devices (LED driver registry).
    resp = c.get(f"{base_url}/devices")
    assert resp.status_code == 200
    assert resp.json() == {}

    resp = c.post(f"{base_url}/devices", json={})
    assert resp.status_code == 400

    resp = c.post(
        f"{base_url}/devices",
        json={"name": "pytest-dev", "address": "aa:bb:cc:dd:ee:ff"},
    )
    assert resp.status_code == 201
    dev_map = resp.json()
    dev_id = next(iter(dev_map.keys()))
    assert dev_id == "aabbccddeeff"
    assert dev_map[dev_id]["name"] == "pytest-dev"
    assert dev_map[dev_id]["id"] == dev_id
    assert dev_map[dev_id]["type"] == "led"
    assert dev_map[dev_id]["transport"] == "espnow"
    assert dev_map[dev_id]["address"] == "aabbccddeeff"

    resp = c.get(f"{base_url}/devices/{dev_id}")
    assert resp.status_code == 200
    assert resp.json()["name"] == "pytest-dev"
    assert resp.json()["type"] == "led"
    assert resp.json().get("connected") is None

    resp = c.get(f"{base_url}/devices")
    assert resp.status_code == 200
    assert resp.json()[dev_id].get("connected") is None

    sender.sent.clear()
    resp = c.post(f"{base_url}/devices/{dev_id}/identify")
    assert resp.status_code == 200
    assert resp.json().get("message")
    assert len(sender.sent) >= 1
    first = json.loads(sender.sent[0][0])
    assert "presets" in first and "select" in first
    assert first["presets"]["__identify"]["p"] == "blink"
    assert first["presets"]["__identify"]["d"] == 50
    assert first["select"]["pytest-dev"] == ["__identify"]
    deadline = time.monotonic() + 2.0
    while len(sender.sent) < 2 and time.monotonic() < deadline:
        time.sleep(0.02)
    assert len(sender.sent) >= 2
    second = json.loads(sender.sent[1][0])
    assert second.get("select") == {"pytest-dev": ["off"]}

    resp = c.post(
        f"{base_url}/devices",
        json={
            "name": "pytest-wifi",
            "type": "led",
            "transport": "wifi",
            "address": "192.168.50.10",
            "mac": "102030405060",
        },
    )
    assert resp.status_code == 201
    wid = "102030405060"
    assert wid in resp.json()
    assert resp.json()[wid]["transport"] == "wifi"
    assert resp.json()[wid]["address"] == "192.168.50.10"

    resp = c.get(f"{base_url}/devices/{wid}")
    assert resp.status_code == 200
    assert resp.json().get("connected") is False

    resp = c.post(
        f"{base_url}/devices",
        json={
            "name": "pytest-wifi",
            "transport": "wifi",
            "address": "192.168.50.11",
            "mac": "102030405061",
        },
    )
    assert resp.status_code == 201
    wid2 = "102030405061"
    assert wid2 in resp.json()
    assert resp.json()[wid2]["name"] == "pytest-wifi"

    resp = c.post(
        f"{base_url}/devices",
        json={
            "name": "pytest-wifi-dupmac",
            "transport": "wifi",
            "address": "192.168.50.99",
            "mac": "102030405060",
        },
    )
    assert resp.status_code == 409

    resp = c.post(
        f"{base_url}/devices",
        json={"name": "no-mac-wifi", "transport": "wifi", "address": "192.168.50.12"},
    )
    assert resp.status_code == 400

    resp = c.post(
        f"{base_url}/devices",
        json={"name": "bad-tr", "transport": "serial"},
    )
    assert resp.status_code == 400

    resp = c.put(f"{base_url}/devices/{dev_id}", json={"name": "   "})
    assert resp.status_code == 400

    resp = c.put(f"{base_url}/devices/{dev_id}", json={"name": "renamed"})
    assert resp.status_code == 200
    assert resp.json()["name"] == "renamed"

    resp = c.put(f"{base_url}/devices/{wid}", json={"name": "renamed"})
    assert resp.status_code == 200
    assert resp.json()["name"] == "renamed"

    resp = c.delete(f"{base_url}/devices/{wid2}")
    assert resp.status_code == 200

    resp = c.delete(f"{base_url}/devices/{wid}")
    assert resp.status_code == 200

    resp = c.delete(f"{base_url}/devices/{dev_id}")
    assert resp.status_code == 200

    # Patterns.
    resp = c.get(f"{base_url}/patterns/definitions")
    assert resp.status_code == 200
    definitions = resp.json()
    assert isinstance(definitions, dict)

    pattern_id = f"pytest_pattern_{uuid.uuid4().hex[:8]}"
    resp = c.post(
        f"{base_url}/patterns",
        json={"name": pattern_id, "data": {"foo": "bar"}},
    )
    assert resp.status_code == 201
    assert resp.json()["foo"] == "bar"

    resp = c.get(f"{base_url}/patterns")
    assert resp.status_code == 200
    patterns_list = resp.json()
    assert isinstance(patterns_list, dict)
    # Runtime list merges repo ``db/pattern.json`` + driver ``.py`` names; test DB
    # entries are still exposed on GET /patterns/<id> after POST.
    assert "blink" in patterns_list or len(patterns_list) >= 1

    resp = c.get(f"{base_url}/patterns/{pattern_id}")
    assert resp.status_code == 200
    assert resp.json()["foo"] == "bar"

    resp = c.put(f"{base_url}/patterns/{pattern_id}", json={"baz": 1})
    assert resp.status_code == 200
    assert resp.json()["baz"] == 1

    resp = c.delete(f"{base_url}/patterns/{pattern_id}")
    assert resp.status_code == 200

    # on/off are firmware-only in presets.py — no OTA file; API rejects serve/send/upload/driver.
    resp = c.get(f"{base_url}/patterns/ota/file/on.py")
    assert resp.status_code == 400
    assert "error" in resp.json()

    resp = c.post(f"{base_url}/patterns/off/send", json={})
    assert resp.status_code == 400
    assert "error" in resp.json()

    resp = c.post(
        f"{base_url}/patterns/upload",
        json={"name": "on.py", "code": "class On:\n    def run(self, p):\n        yield\n"},
    )
    assert resp.status_code == 400

    resp = c.post(
        f"{base_url}/patterns/driver",
        json={
            "name": "off",
            "code": "class Off:\n    def run(self, p):\n        yield\n",
        },
    )
    assert resp.status_code == 400

