"""Pytest fixtures for end-to-end browser tests.

Boots the Flask app in a background thread on an ephemeral port and
yields the base URL. The app uses a temp SQLite DB so e2e runs don't
contaminate the dev database.
"""

import socket
import threading

import pytest
from werkzeug.serving import make_server

from l4d2web.app import create_app
from l4d2web.auth import hash_password
from l4d2web.db import init_db, session_scope
from l4d2web.models import Blueprint, Overlay, Server, User


def _free_port() -> int:
    s = socket.socket()
    s.bind(("127.0.0.1", 0))
    port = s.getsockname()[1]
    s.close()
    return port


def _boot_app(tmp_path, monkeypatch):
    """Set the env vars + create_app + init_db combo shared by every e2e
    fixture. Returns the Flask app.

    Sets DATABASE_URL → temp sqlite; SESSION_COOKIE_SECURE=0 so the
    browser keeps the session cookie over http://127.0.0.1 (app.py:57
    would otherwise mark it Secure). Caller-specific env vars (e.g.
    LEFT4ME_ROOT for files-overlay tests) must be set BEFORE calling
    this, since create_app reads them during app construction.
    """
    db_path = tmp_path / "e2e.db"
    db_url = f"sqlite:///{db_path}"
    monkeypatch.setenv("DATABASE_URL", db_url)
    monkeypatch.setenv("SESSION_COOKIE_SECURE", "0")
    app = create_app({"TESTING": False, "DATABASE_URL": db_url, "SECRET_KEY": "e2e"})
    # create_app() already calls init_db() inside an app context, which
    # binds tables to the in-app engine. The seed work below uses
    # session_scope() OUTSIDE any app context, which reads DATABASE_URL
    # from the environment and binds its own engine. This second init_db()
    # call creates the tables on that env-derived engine so the seed
    # inserts have somewhere to land.
    init_db()
    return app


def _serve(app):
    """Start the app on a background thread, return (base_url, shutdown).

    Caller is responsible for calling shutdown() in a finally block.
    """
    port = _free_port()
    server = make_server("127.0.0.1", port, app)
    thread = threading.Thread(target=server.serve_forever, daemon=True)
    thread.start()

    def shutdown():
        server.shutdown()
        thread.join(timeout=2)

    return f"http://127.0.0.1:{port}", shutdown


def login(page, base_url: str, username: str = "alice", password: str = "secret") -> None:
    """Form-POST login helper. Reused by every e2e test."""
    page.goto(f"{base_url}/login")
    page.fill('input[name="username"]', username)
    page.fill('input[name="password"]', password)
    page.click('button[type="submit"]')


@pytest.fixture(scope="function")
def live_server(tmp_path, monkeypatch):
    app = _boot_app(tmp_path, monkeypatch)

    with session_scope() as session:
        user = User(
            username="alice",
            password_digest=hash_password("secret"),
            admin=False,
        )
        session.add(user)
        session.flush()
        bp = Blueprint(
            user_id=user.id, name="bp", arguments="[]", config="[]"
        )
        session.add(bp)
        session.flush()
        blueprint_id = bp.id
        user_id = user.id

    base_url, shutdown = _serve(app)
    try:
        yield {
            "base_url": base_url,
            "user_id": user_id,
            "blueprint_id": blueprint_id,
        }
    finally:
        shutdown()


@pytest.fixture(scope="function")
def files_overlay_server(tmp_path, monkeypatch):
    """live_server + a files-type Overlay owned by alice, seeded with:

      <overlay_root>/server.cfg          (text, editable)
      <overlay_root>/icon.png            (binary, replaceable)
      <overlay_root>/cfg/admins.txt      (nested-folder fixture)

    LEFT4ME_ROOT is monkey-patched to tmp_path BEFORE create_app() so
    overlay path resolution (l4d2host.paths.overlay_path) lands under
    the temp directory instead of /var/lib/left4me — without this every
    files-overlay route 404s on macOS (see AGENTS.md "symptom-to-cause").
    """
    monkeypatch.setenv("LEFT4ME_ROOT", str(tmp_path))
    app = _boot_app(tmp_path, monkeypatch)

    with session_scope() as session:
        user = User(
            username="alice",
            password_digest=hash_password("secret"),
            admin=False,
        )
        session.add(user)
        session.flush()
        # Same insert-then-update-path pattern dev-server.py uses: we
        # need overlay.id to write into overlay.path, but the column is
        # NOT NULL so it gets a placeholder until we know the id.
        overlay = Overlay(
            name="cfgs",
            path="_pending",
            type="files",
            user_id=user.id,
        )
        session.add(overlay)
        session.flush()
        overlay.path = str(overlay.id)
        user_id = user.id
        overlay_id = overlay.id

    overlay_root = tmp_path / "overlays" / str(overlay_id)
    overlay_root.mkdir(parents=True)
    (overlay_root / "server.cfg").write_text('hostname "left4me"\n')
    # 8-byte PNG signature + 60 null bytes — large enough for the
    # editor's binary-mode detection (which checks the magic header).
    (overlay_root / "icon.png").write_bytes(b"\x89PNG\r\n\x1a\n" + b"\x00" * 60)
    (overlay_root / "cfg").mkdir()
    (overlay_root / "cfg" / "admins.txt").write_text("STEAM_1:0:1\n")

    base_url, shutdown = _serve(app)
    try:
        yield {
            "base_url": base_url,
            "user_id": user_id,
            "overlay_id": overlay_id,
            "overlay_root": overlay_root,
        }
    finally:
        shutdown()


@pytest.fixture(scope="function")
def server_with_files(tmp_path, monkeypatch):
    """live_server + a Server owned by alice with a populated runtime
    merged directory. Used by the server-detail e2e tests that exercise
    file rows + download.

    The /servers/<id> page lists files from
    LEFT4ME_ROOT/runtime/<server_id>/merged/ (the kernel-overlayfs view
    of a running server). On a dev/test box no overlayfs is mounted, so
    the fixture pre-creates that directory and seeds one plain file —
    enough for the file-tree partial to render rows.

    Yields {base_url, user_id, blueprint_id, server_id, merged_root}.
    """
    monkeypatch.setenv("LEFT4ME_ROOT", str(tmp_path))
    app = _boot_app(tmp_path, monkeypatch)

    with session_scope() as session:
        user = User(
            username="alice",
            password_digest=hash_password("secret"),
            admin=False,
        )
        session.add(user)
        session.flush()
        bp = Blueprint(
            user_id=user.id, name="bp", arguments="[]", config="[]"
        )
        session.add(bp)
        session.flush()
        # Server.port has a global UNIQUE constraint, but this is a
        # fresh per-test SQLite DB so any value works — 27015 is the
        # L4D2 default, semantically obvious.
        server = Server(
            user_id=user.id,
            blueprint_id=bp.id,
            name="srv",
            port=27015,
        )
        session.add(server)
        session.flush()
        user_id = user.id
        blueprint_id = bp.id
        server_id = server.id

    merged_root = tmp_path / "runtime" / str(server_id) / "merged"
    merged_root.mkdir(parents=True)
    (merged_root / "server.cfg").write_text('hostname "from-merged-runtime"\n')

    base_url, shutdown = _serve(app)
    try:
        yield {
            "base_url": base_url,
            "user_id": user_id,
            "blueprint_id": blueprint_id,
            "server_id": server_id,
            "merged_root": merged_root,
        }
    finally:
        shutdown()
