"""HTTP-level tests for the overlay 'Files' tree-fragment + download routes."""
from __future__ import annotations
from datetime import UTC, datetime

import io
import os
from pathlib import Path

import pytest

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


@pytest.fixture
def app(tmp_path, monkeypatch):
    db_url = f"sqlite:///{tmp_path/'files-routes.db'}"
    monkeypatch.setenv("DATABASE_URL", db_url)
    monkeypatch.setenv("LEFT4ME_ROOT", str(tmp_path))
    flask_app = create_app(
        {"TESTING": True, "DATABASE_URL": db_url, "SECRET_KEY": "test"}
    )
    init_db()
    return flask_app


@pytest.fixture
def left4me_root(tmp_path) -> Path:
    return tmp_path


def _client_for(app, user_id: int):
    client = app.test_client()
    with client.session_transaction() as sess:
        sess["user_id"] = user_id
        sess["pw_changed_at"] = datetime.now(UTC).isoformat()
        sess["csrf_token"] = "test-token"
    return client


def _make_user(*, username: str = "alice", admin: bool = False) -> int:
    with session_scope() as s:
        user = User(
            username=username, password_digest=hash_password("x"), admin=admin
        )
        s.add(user)
        s.flush()
        return user.id


def _make_overlay(left4me_root: Path, *, user_id: int | None, name: str) -> int:
    """Create an Overlay row + the matching `LEFT4ME_ROOT/overlays/{id}/`
    directory, mirroring what `overlay_creation.create_overlay_directory`
    would do in production."""
    with session_scope() as s:
        overlay = Overlay(name=name, path="", type="script", user_id=user_id)
        s.add(overlay)
        s.flush()
        overlay.path = str(overlay.id)
        overlay_id = overlay.id
    (left4me_root / "overlays" / str(overlay_id)).mkdir(parents=True)
    return overlay_id


def _make_files_overlay(left4me_root: Path, *, user_id: int | None, name: str) -> int:
    with session_scope() as s:
        overlay = Overlay(name=name, path="", type="files", user_id=user_id)
        s.add(overlay)
        s.flush()
        overlay.path = str(overlay.id)
        overlay_id = overlay.id
    (left4me_root / "overlays" / str(overlay_id)).mkdir(parents=True)
    return overlay_id


def test_non_admin_cannot_mkdir_in_system_overlay(app, left4me_root: Path) -> None:
    """System overlays (user_id IS NULL) are readable by everyone but only
    writable by admins. A regular user attempting to mutate gets 403."""
    user_id = _make_user(username="bob", admin=False)
    overlay_id = _make_files_overlay(left4me_root, user_id=None, name="system")

    client = _client_for(app, user_id)
    response = client.post(
        f"/overlays/{overlay_id}/files/mkdir",
        json={"path": "newdir"},
        headers={"X-CSRF-Token": "test-token"},
    )
    assert response.status_code == 403


def test_non_admin_can_still_read_system_overlay(app, left4me_root: Path) -> None:
    user_id = _make_user(username="bob", admin=False)
    overlay_id = _make_files_overlay(left4me_root, user_id=None, name="system")
    (left4me_root / "overlays" / str(overlay_id) / "config.cfg").write_text("ok")

    client = _client_for(app, user_id)
    response = client.get(f"/overlays/{overlay_id}/files")
    assert response.status_code == 200
    assert b"config.cfg" in response.data


def test_admin_can_mkdir_in_system_overlay(app, left4me_root: Path) -> None:
    admin_id = _make_user(username="admin", admin=True)
    overlay_id = _make_files_overlay(left4me_root, user_id=None, name="system")

    client = _client_for(app, admin_id)
    response = client.post(
        f"/overlays/{overlay_id}/files/mkdir",
        json={"path": "newdir"},
        headers={"X-CSRF-Token": "test-token"},
    )
    assert response.status_code in (200, 201, 204)
    assert (left4me_root / "overlays" / str(overlay_id) / "newdir").is_dir()


def test_owner_can_mkdir_in_own_overlay(app, left4me_root: Path) -> None:
    user_id = _make_user(username="bob", admin=False)
    overlay_id = _make_files_overlay(left4me_root, user_id=user_id, name="mine")

    client = _client_for(app, user_id)
    response = client.post(
        f"/overlays/{overlay_id}/files/mkdir",
        json={"path": "newdir"},
        headers={"X-CSRF-Token": "test-token"},
    )
    assert response.status_code in (200, 201, 204)


def test_files_fragment_lists_root_directory(app, left4me_root: Path) -> None:
    user_id = _make_user()
    overlay_id = _make_overlay(left4me_root, user_id=user_id, name="my")
    overlay_dir = left4me_root / "overlays" / str(overlay_id)
    (overlay_dir / "left4dead2").mkdir()
    (overlay_dir / "readme.txt").write_text("hi")

    client = _client_for(app, user_id)
    response = client.get(f"/overlays/{overlay_id}/files")

    assert response.status_code == 200
    text = response.get_data(as_text=True)
    assert "left4dead2" in text
    assert "readme.txt" in text


def test_files_fragment_lists_subdirectory(app, left4me_root: Path) -> None:
    user_id = _make_user()
    overlay_id = _make_overlay(left4me_root, user_id=user_id, name="my")
    overlay_dir = left4me_root / "overlays" / str(overlay_id)
    addons = overlay_dir / "left4dead2" / "addons"
    addons.mkdir(parents=True)
    (addons / "deathcraft.vpk").write_text("vpk")

    client = _client_for(app, user_id)
    response = client.get(
        f"/overlays/{overlay_id}/files?path=left4dead2/addons"
    )

    assert response.status_code == 200
    text = response.get_data(as_text=True)
    assert "deathcraft.vpk" in text
    # Fragment, no full document.
    assert "<html" not in text


def test_files_fragment_returns_400_on_dotdot(app, left4me_root: Path) -> None:
    user_id = _make_user()
    overlay_id = _make_overlay(left4me_root, user_id=user_id, name="my")

    client = _client_for(app, user_id)
    response = client.get(f"/overlays/{overlay_id}/files?path=../../etc")

    assert response.status_code == 400


def test_files_fragment_returns_400_on_absolute_path(app, left4me_root: Path) -> None:
    user_id = _make_user()
    overlay_id = _make_overlay(left4me_root, user_id=user_id, name="my")

    client = _client_for(app, user_id)
    response = client.get(f"/overlays/{overlay_id}/files?path=/etc/passwd")

    assert response.status_code == 400


def test_files_fragment_returns_404_for_unknown_overlay(app) -> None:
    user_id = _make_user()
    client = _client_for(app, user_id)
    response = client.get("/overlays/9999/files")

    assert response.status_code == 404


def test_files_fragment_returns_404_for_missing_subdir(app, left4me_root: Path) -> None:
    user_id = _make_user()
    overlay_id = _make_overlay(left4me_root, user_id=user_id, name="my")

    client = _client_for(app, user_id)
    response = client.get(f"/overlays/{overlay_id}/files?path=ghost")

    assert response.status_code == 404


def test_files_fragment_returns_403_for_other_users_overlay(
    app, left4me_root: Path
) -> None:
    owner_id = _make_user(username="owner")
    other_id = _make_user(username="other")
    overlay_id = _make_overlay(left4me_root, user_id=owner_id, name="private")

    client = _client_for(app, other_id)
    response = client.get(f"/overlays/{overlay_id}/files")

    assert response.status_code == 403


def test_admin_can_view_files_for_other_users_overlay(
    app, left4me_root: Path
) -> None:
    owner_id = _make_user(username="owner")
    admin_id = _make_user(username="admin", admin=True)
    overlay_id = _make_overlay(left4me_root, user_id=owner_id, name="private")
    overlay_dir = left4me_root / "overlays" / str(overlay_id)
    (overlay_dir / "secret.cfg").write_text("k=v")

    client = _client_for(app, admin_id)
    response = client.get(f"/overlays/{overlay_id}/files")

    assert response.status_code == 200
    assert "secret.cfg" in response.get_data(as_text=True)


def test_download_streams_regular_file(app, left4me_root: Path) -> None:
    user_id = _make_user()
    overlay_id = _make_overlay(left4me_root, user_id=user_id, name="my")
    overlay_dir = left4me_root / "overlays" / str(overlay_id)
    cfg = overlay_dir / "cfg" / "server.cfg"
    cfg.parent.mkdir()
    cfg.write_bytes(b"hostname test")

    client = _client_for(app, user_id)
    response = client.get(
        f"/overlays/{overlay_id}/files/download?path=cfg/server.cfg"
    )

    assert response.status_code == 200
    assert response.headers["Content-Disposition"].startswith("attachment")
    assert "filename=server.cfg" in response.headers["Content-Disposition"]
    assert response.get_data() == b"hostname test"


def test_download_follows_workshop_cache_symlink(
    app, left4me_root: Path
) -> None:
    user_id = _make_user()
    overlay_id = _make_overlay(left4me_root, user_id=user_id, name="my")
    overlay_dir = left4me_root / "overlays" / str(overlay_id)

    cache_file = left4me_root / "workshop_cache" / "12345.vpk"
    cache_file.parent.mkdir(parents=True)
    cache_file.write_bytes(b"vpk-content")

    addons = overlay_dir / "left4dead2" / "addons"
    addons.mkdir(parents=True)
    (addons / "deathcraft.vpk").symlink_to(cache_file)

    client = _client_for(app, user_id)
    response = client.get(
        f"/overlays/{overlay_id}/files/download?path=left4dead2/addons/deathcraft.vpk"
    )

    assert response.status_code == 200
    assert response.get_data() == b"vpk-content"


def test_download_rejects_symlink_outside_left4me_root(
    app, left4me_root: Path, tmp_path_factory
) -> None:
    user_id = _make_user()
    overlay_id = _make_overlay(left4me_root, user_id=user_id, name="my")
    overlay_dir = left4me_root / "overlays" / str(overlay_id)

    outside = tmp_path_factory.mktemp("outside") / "secret.txt"
    outside.write_text("nope")
    (overlay_dir / "evil").symlink_to(outside)

    client = _client_for(app, user_id)
    response = client.get(f"/overlays/{overlay_id}/files/download?path=evil")

    assert response.status_code == 400


def test_download_rejects_directory_target(app, left4me_root: Path) -> None:
    user_id = _make_user()
    overlay_id = _make_overlay(left4me_root, user_id=user_id, name="my")
    overlay_dir = left4me_root / "overlays" / str(overlay_id)
    (overlay_dir / "left4dead2").mkdir()

    client = _client_for(app, user_id)
    response = client.get(
        f"/overlays/{overlay_id}/files/download?path=left4dead2"
    )

    assert response.status_code == 400


def test_download_returns_404_for_missing_file(app, left4me_root: Path) -> None:
    user_id = _make_user()
    overlay_id = _make_overlay(left4me_root, user_id=user_id, name="my")

    client = _client_for(app, user_id)
    response = client.get(
        f"/overlays/{overlay_id}/files/download?path=ghost.txt"
    )

    assert response.status_code == 404


def test_download_returns_403_for_other_users_overlay(
    app, left4me_root: Path
) -> None:
    owner_id = _make_user(username="owner")
    other_id = _make_user(username="other")
    overlay_id = _make_overlay(left4me_root, user_id=owner_id, name="private")
    overlay_dir = left4me_root / "overlays" / str(overlay_id)
    (overlay_dir / "secret.cfg").write_text("nope")

    client = _client_for(app, other_id)
    response = client.get(
        f"/overlays/{overlay_id}/files/download?path=secret.cfg"
    )

    assert response.status_code == 403


def test_files_fragment_truncates_at_cap(app, left4me_root: Path, monkeypatch) -> None:
    """The cap default is 500 — exercise it via the public route by lowering
    it for this test through the helper module."""
    from l4d2web.services import overlay_files as overlay_files_module

    monkeypatch.setattr(overlay_files_module, "DEFAULT_MAX_ENTRIES", 5)

    user_id = _make_user()
    overlay_id = _make_overlay(left4me_root, user_id=user_id, name="my")
    overlay_dir = left4me_root / "overlays" / str(overlay_id)
    for i in range(8):
        (overlay_dir / f"f{i}.txt").write_text("x")

    client = _client_for(app, user_id)
    response = client.get(f"/overlays/{overlay_id}/files")
    text = response.get_data(as_text=True)

    assert response.status_code == 200
    assert "+ 3 more" in text


def test_overlay_detail_renders_files_section_with_tree(
    app, left4me_root: Path
) -> None:
    user_id = _make_user()
    overlay_id = _make_overlay(left4me_root, user_id=user_id, name="my")
    overlay_dir = left4me_root / "overlays" / str(overlay_id)
    (overlay_dir / "left4dead2").mkdir()
    (overlay_dir / "readme.txt").write_text("hi")

    client = _client_for(app, user_id)
    response = client.get(f"/overlays/{overlay_id}")
    text = response.get_data(as_text=True)

    assert response.status_code == 200
    assert "Files" in text
    assert "left4dead2" in text
    assert "readme.txt" in text


def test_overlay_detail_shows_empty_state_when_overlay_dir_missing(
    app, left4me_root: Path
) -> None:
    user_id = _make_user()
    overlay_id = _make_overlay(left4me_root, user_id=user_id, name="my")
    # Wipe the directory created by _make_overlay so the on-disk dir is gone.
    overlay_dir = left4me_root / "overlays" / str(overlay_id)
    overlay_dir.rmdir()

    client = _client_for(app, user_id)
    response = client.get(f"/overlays/{overlay_id}")
    text = response.get_data(as_text=True)

    assert response.status_code == 200
    assert "No files yet" in text


def test_overlay_detail_shows_empty_state_when_overlay_dir_is_empty(
    app, left4me_root: Path
) -> None:
    """A built overlay whose directory has been wiped (or seeded but never
    built) should also fall back to the empty-state message — not render an
    invisible empty <ul>."""
    user_id = _make_user()
    overlay_id = _make_overlay(left4me_root, user_id=user_id, name="my")
    # _make_overlay leaves the directory in place but empty.

    client = _client_for(app, user_id)
    response = client.get(f"/overlays/{overlay_id}")
    text = response.get_data(as_text=True)

    assert response.status_code == 200
    assert "No files yet" in text


def test_overlay_detail_files_section_present_for_workshop_overlays(
    app, left4me_root: Path
) -> None:
    user_id = _make_user()
    # Create a workshop overlay manually since _make_overlay defaults to script.
    with session_scope() as s:
        overlay = Overlay(name="ws", path="", type="workshop", user_id=user_id)
        s.add(overlay)
        s.flush()
        overlay.path = str(overlay.id)
        overlay_id = overlay.id
    (left4me_root / "overlays" / str(overlay_id)).mkdir(parents=True)

    client = _client_for(app, user_id)
    response = client.get(f"/overlays/{overlay_id}")
    text = response.get_data(as_text=True)

    assert response.status_code == 200
    # Section heading present even when the overlay dir is empty.
    assert "Files" in text


def test_files_fragment_renders_broken_symlink_without_download_link(
    app, left4me_root: Path
) -> None:
    user_id = _make_user()
    overlay_id = _make_overlay(left4me_root, user_id=user_id, name="my")
    overlay_dir = left4me_root / "overlays" / str(overlay_id)
    (overlay_dir / "missing.vpk").symlink_to(overlay_dir / "does-not-exist.vpk")

    client = _client_for(app, user_id)
    response = client.get(f"/overlays/{overlay_id}/files")
    text = response.get_data(as_text=True)

    assert response.status_code == 200
    assert "missing.vpk" in text
    assert "broken" in text
    # No download link for broken symlinks.
    assert (
        f'href="/overlays/{overlay_id}/files/download?path=missing.vpk"' not in text
    )


def _make_server(left4me_root: Path, *, user_id: int, port: int = 27015) -> int:
    with session_scope() as s:
        bp = Blueprint(user_id=user_id, name="bp", arguments="[]", config="[]")
        s.add(bp)
        s.flush()
        server = Server(user_id=user_id, blueprint_id=bp.id, name="alpha", port=port)
        s.add(server)
        s.flush()
        return server.id


def test_server_files_fragment_lists_root(app, left4me_root: Path) -> None:
    user_id = _make_user()
    server_id = _make_server(left4me_root, user_id=user_id)
    merged = left4me_root / "runtime" / str(server_id) / "merged"
    (merged / "left4dead2").mkdir(parents=True)
    (merged / "srcds_run").write_text("#!/bin/sh\n")

    client = _client_for(app, user_id)
    response = client.get(f"/servers/{server_id}/files")

    assert response.status_code == 200
    text = response.get_data(as_text=True)
    assert "left4dead2" in text
    assert "srcds_run" in text
    # Download links are now rendered (mirrors overlay tree).
    assert f"/servers/{server_id}/files/download?path=srcds_run" in text


def test_server_download_streams_regular_file(app, left4me_root: Path) -> None:
    user_id = _make_user()
    server_id = _make_server(left4me_root, user_id=user_id, port=27050)
    merged = left4me_root / "runtime" / str(server_id) / "merged"
    cfg = merged / "left4dead2" / "cfg" / "server.cfg"
    cfg.parent.mkdir(parents=True)
    cfg.write_bytes(b"hostname zonemod")

    client = _client_for(app, user_id)
    response = client.get(
        f"/servers/{server_id}/files/download?path=left4dead2/cfg/server.cfg"
    )

    assert response.status_code == 200
    assert response.headers["Content-Disposition"].startswith("attachment")
    assert "filename=server.cfg" in response.headers["Content-Disposition"]
    assert response.get_data() == b"hostname zonemod"


def test_server_download_rejects_symlink_outside_left4me_root(
    app, left4me_root: Path, tmp_path_factory
) -> None:
    user_id = _make_user()
    server_id = _make_server(left4me_root, user_id=user_id, port=27060)
    merged = left4me_root / "runtime" / str(server_id) / "merged"
    merged.mkdir(parents=True)

    outside = tmp_path_factory.mktemp("outside-server") / "secret.txt"
    outside.write_text("nope")
    (merged / "evil").symlink_to(outside)

    client = _client_for(app, user_id)
    response = client.get(f"/servers/{server_id}/files/download?path=evil")

    assert response.status_code == 400


def test_server_files_fragment_returns_404_when_merged_missing(app, left4me_root: Path) -> None:
    user_id = _make_user()
    server_id = _make_server(left4me_root, user_id=user_id, port=27020)

    client = _client_for(app, user_id)
    response = client.get(f"/servers/{server_id}/files")

    assert response.status_code == 404


def test_server_files_fragment_returns_404_for_unknown_server(app) -> None:
    user_id = _make_user()
    client = _client_for(app, user_id)
    response = client.get("/servers/9999/files")

    assert response.status_code == 404


def test_server_files_fragment_returns_400_on_dotdot(app, left4me_root: Path) -> None:
    user_id = _make_user()
    server_id = _make_server(left4me_root, user_id=user_id, port=27030)
    (left4me_root / "runtime" / str(server_id) / "merged").mkdir(parents=True)

    client = _client_for(app, user_id)
    response = client.get(f"/servers/{server_id}/files?path=../../etc")

    assert response.status_code == 400


def test_server_detail_renders_files_section(app, left4me_root: Path) -> None:
    user_id = _make_user()
    server_id = _make_server(left4me_root, user_id=user_id, port=27040)
    merged = left4me_root / "runtime" / str(server_id) / "merged"
    (merged / "left4dead2").mkdir(parents=True)

    client = _client_for(app, user_id)
    response = client.get(f"/servers/{server_id}")
    text = response.get_data(as_text=True)

    assert response.status_code == 200
    assert ">Files<" in text
    assert "left4dead2" in text


# ============================================================================
# Files-overlay mutating endpoints
# ============================================================================


def _make_files_overlay(left4me_root: Path, *, user_id: int | None, name: str) -> int:
    with session_scope() as s:
        overlay = Overlay(name=name, path="", type="files", user_id=user_id, last_build_status="ok")
        s.add(overlay)
        s.flush()
        overlay.path = str(overlay.id)
        overlay_id = overlay.id
    (left4me_root / "overlays" / str(overlay_id)).mkdir(parents=True)
    return overlay_id


def _csrf_headers():
    return {"X-CSRF-Token": "test-token"}


# ---- /save ---------------------------------------------------------------


def test_save_creates_new_file(app, left4me_root: Path) -> None:
    user_id = _make_user()
    overlay_id = _make_files_overlay(left4me_root, user_id=user_id, name="files")
    overlay_dir = left4me_root / "overlays" / str(overlay_id)

    client = _client_for(app, user_id)
    r = client.post(
        f"/overlays/{overlay_id}/files/save",
        json={"path": "left4dead2/cfg/admins.txt", "content": "STEAM_1:0:1\n"},
        headers=_csrf_headers(),
    )

    assert r.status_code == 200
    assert (overlay_dir / "left4dead2" / "cfg" / "admins.txt").read_text() == "STEAM_1:0:1\n"


def test_save_overwrites_existing_file(app, left4me_root: Path) -> None:
    user_id = _make_user()
    overlay_id = _make_files_overlay(left4me_root, user_id=user_id, name="files")
    overlay_dir = left4me_root / "overlays" / str(overlay_id)
    (overlay_dir / "motd.txt").write_text("old")

    client = _client_for(app, user_id)
    r = client.post(
        f"/overlays/{overlay_id}/files/save",
        json={"path": "motd.txt", "content": "new"},
        headers=_csrf_headers(),
    )

    assert r.status_code == 200
    assert (overlay_dir / "motd.txt").read_text() == "new"


def test_save_with_new_path_renames_atomically(app, left4me_root: Path) -> None:
    user_id = _make_user()
    overlay_id = _make_files_overlay(left4me_root, user_id=user_id, name="files")
    overlay_dir = left4me_root / "overlays" / str(overlay_id)
    (overlay_dir / "motd.txt").write_text("welcome")

    client = _client_for(app, user_id)
    r = client.post(
        f"/overlays/{overlay_id}/files/save",
        json={"path": "motd.txt", "new_path": "motd_de.txt", "content": "willkommen"},
        headers=_csrf_headers(),
    )

    assert r.status_code == 200
    assert not (overlay_dir / "motd.txt").exists()
    assert (overlay_dir / "motd_de.txt").read_text() == "willkommen"


def test_save_rejects_oversized_content(app, left4me_root: Path) -> None:
    user_id = _make_user()
    overlay_id = _make_files_overlay(left4me_root, user_id=user_id, name="files")

    big = "x" * (1024 * 1024 + 1)
    client = _client_for(app, user_id)
    r = client.post(
        f"/overlays/{overlay_id}/files/save",
        json={"path": "huge.txt", "content": big},
        headers=_csrf_headers(),
    )

    assert r.status_code == 413


def test_save_rejects_dotdot_path(app, left4me_root: Path) -> None:
    user_id = _make_user()
    overlay_id = _make_files_overlay(left4me_root, user_id=user_id, name="files")

    client = _client_for(app, user_id)
    r = client.post(
        f"/overlays/{overlay_id}/files/save",
        json={"path": "../escape.txt", "content": "x"},
        headers=_csrf_headers(),
    )

    assert r.status_code == 422


def test_save_404_for_non_files_overlay(app, left4me_root: Path) -> None:
    user_id = _make_user()
    overlay_id = _make_overlay(left4me_root, user_id=user_id, name="ws")

    client = _client_for(app, user_id)
    r = client.post(
        f"/overlays/{overlay_id}/files/save",
        json={"path": "motd.txt", "content": "x"},
        headers=_csrf_headers(),
    )

    assert r.status_code == 404


# ---- /upload -------------------------------------------------------------


def test_upload_writes_file_at_target_path(app, left4me_root: Path) -> None:
    user_id = _make_user()
    overlay_id = _make_files_overlay(left4me_root, user_id=user_id, name="files")
    overlay_dir = left4me_root / "overlays" / str(overlay_id)

    client = _client_for(app, user_id)
    r = client.post(
        f"/overlays/{overlay_id}/files/upload",
        data={"target_path": "addons", "file": (io.BytesIO(b"vpk-bytes"), "map.vpk")},
        headers=_csrf_headers(),
        content_type="multipart/form-data",
    )

    assert r.status_code == 200
    assert (overlay_dir / "addons" / "map.vpk").read_bytes() == b"vpk-bytes"


def test_upload_with_relative_path_creates_intermediates(app, left4me_root: Path) -> None:
    user_id = _make_user()
    overlay_id = _make_files_overlay(left4me_root, user_id=user_id, name="files")
    overlay_dir = left4me_root / "overlays" / str(overlay_id)

    client = _client_for(app, user_id)
    r = client.post(
        f"/overlays/{overlay_id}/files/upload",
        data={
            "target_path": "addons",
            "relative_path": "sourcemod/configs/admins.cfg",
            "file": (io.BytesIO(b"#admins"), "admins.cfg"),
        },
        headers=_csrf_headers(),
        content_type="multipart/form-data",
    )

    assert r.status_code == 200
    assert (
        overlay_dir / "addons" / "sourcemod" / "configs" / "admins.cfg"
    ).read_bytes() == b"#admins"


def test_upload_409_on_existing_without_overwrite(app, left4me_root: Path) -> None:
    user_id = _make_user()
    overlay_id = _make_files_overlay(left4me_root, user_id=user_id, name="files")
    overlay_dir = left4me_root / "overlays" / str(overlay_id)
    (overlay_dir / "motd.txt").write_bytes(b"existing")

    client = _client_for(app, user_id)
    r = client.post(
        f"/overlays/{overlay_id}/files/upload",
        data={"target_path": "", "file": (io.BytesIO(b"new"), "motd.txt")},
        headers=_csrf_headers(),
        content_type="multipart/form-data",
    )

    assert r.status_code == 409
    assert (overlay_dir / "motd.txt").read_bytes() == b"existing"


def test_upload_overwrite_replaces_existing(app, left4me_root: Path) -> None:
    user_id = _make_user()
    overlay_id = _make_files_overlay(left4me_root, user_id=user_id, name="files")
    overlay_dir = left4me_root / "overlays" / str(overlay_id)
    (overlay_dir / "motd.txt").write_bytes(b"existing")

    client = _client_for(app, user_id)
    r = client.post(
        f"/overlays/{overlay_id}/files/upload",
        data={"target_path": "", "overwrite": "1", "file": (io.BytesIO(b"new"), "motd.txt")},
        headers=_csrf_headers(),
        content_type="multipart/form-data",
    )

    assert r.status_code == 200
    assert (overlay_dir / "motd.txt").read_bytes() == b"new"


# ---- /move ---------------------------------------------------------------


def test_move_renames_file(app, left4me_root: Path) -> None:
    user_id = _make_user()
    overlay_id = _make_files_overlay(left4me_root, user_id=user_id, name="files")
    overlay_dir = left4me_root / "overlays" / str(overlay_id)
    (overlay_dir / "motd.txt").write_text("hi")
    (overlay_dir / "addons").mkdir()

    client = _client_for(app, user_id)
    r = client.post(
        f"/overlays/{overlay_id}/files/move",
        json={"src": "motd.txt", "dst": "addons/motd.txt"},
        headers=_csrf_headers(),
    )

    assert r.status_code == 200
    assert not (overlay_dir / "motd.txt").exists()
    assert (overlay_dir / "addons" / "motd.txt").read_text() == "hi"


def test_move_409_on_existing_dst(app, left4me_root: Path) -> None:
    user_id = _make_user()
    overlay_id = _make_files_overlay(left4me_root, user_id=user_id, name="files")
    overlay_dir = left4me_root / "overlays" / str(overlay_id)
    (overlay_dir / "motd.txt").write_text("a")
    (overlay_dir / "other.txt").write_text("b")

    client = _client_for(app, user_id)
    r = client.post(
        f"/overlays/{overlay_id}/files/move",
        json={"src": "motd.txt", "dst": "other.txt"},
        headers=_csrf_headers(),
    )

    assert r.status_code == 409
    assert (overlay_dir / "motd.txt").read_text() == "a"
    assert (overlay_dir / "other.txt").read_text() == "b"


def test_move_rejects_cycle(app, left4me_root: Path) -> None:
    user_id = _make_user()
    overlay_id = _make_files_overlay(left4me_root, user_id=user_id, name="files")
    overlay_dir = left4me_root / "overlays" / str(overlay_id)
    (overlay_dir / "addons" / "child").mkdir(parents=True)

    client = _client_for(app, user_id)
    r = client.post(
        f"/overlays/{overlay_id}/files/move",
        json={"src": "addons", "dst": "addons/child/addons"},
        headers=_csrf_headers(),
    )

    assert r.status_code == 422


# ---- /mkdir --------------------------------------------------------------


def test_mkdir_creates_directory(app, left4me_root: Path) -> None:
    user_id = _make_user()
    overlay_id = _make_files_overlay(left4me_root, user_id=user_id, name="files")
    overlay_dir = left4me_root / "overlays" / str(overlay_id)

    client = _client_for(app, user_id)
    r = client.post(
        f"/overlays/{overlay_id}/files/mkdir",
        json={"path": "addons/sourcemod/configs"},
        headers=_csrf_headers(),
    )

    assert r.status_code == 200
    assert (overlay_dir / "addons" / "sourcemod" / "configs").is_dir()


def test_mkdir_idempotent_on_existing_dir(app, left4me_root: Path) -> None:
    user_id = _make_user()
    overlay_id = _make_files_overlay(left4me_root, user_id=user_id, name="files")
    overlay_dir = left4me_root / "overlays" / str(overlay_id)
    (overlay_dir / "addons").mkdir()

    client = _client_for(app, user_id)
    r = client.post(
        f"/overlays/{overlay_id}/files/mkdir",
        json={"path": "addons"},
        headers=_csrf_headers(),
    )

    assert r.status_code == 200
    assert (overlay_dir / "addons").is_dir()


def test_mkdir_409_when_path_is_file(app, left4me_root: Path) -> None:
    user_id = _make_user()
    overlay_id = _make_files_overlay(left4me_root, user_id=user_id, name="files")
    overlay_dir = left4me_root / "overlays" / str(overlay_id)
    (overlay_dir / "blocker").write_text("x")

    client = _client_for(app, user_id)
    r = client.post(
        f"/overlays/{overlay_id}/files/mkdir",
        json={"path": "blocker"},
        headers=_csrf_headers(),
    )

    assert r.status_code == 409


# ---- /delete -------------------------------------------------------------


def test_delete_removes_file(app, left4me_root: Path) -> None:
    user_id = _make_user()
    overlay_id = _make_files_overlay(left4me_root, user_id=user_id, name="files")
    overlay_dir = left4me_root / "overlays" / str(overlay_id)
    target = overlay_dir / "motd.txt"
    target.write_text("hi")

    client = _client_for(app, user_id)
    r = client.post(
        f"/overlays/{overlay_id}/files/delete",
        data={"path": "motd.txt", "csrf_token": "test-token"},
    )

    assert r.status_code == 200
    assert not target.exists()


def test_delete_removes_empty_dir(app, left4me_root: Path) -> None:
    user_id = _make_user()
    overlay_id = _make_files_overlay(left4me_root, user_id=user_id, name="files")
    overlay_dir = left4me_root / "overlays" / str(overlay_id)
    target = overlay_dir / "empty-dir"
    target.mkdir()

    client = _client_for(app, user_id)
    r = client.post(
        f"/overlays/{overlay_id}/files/delete",
        data={"path": "empty-dir", "csrf_token": "test-token"},
    )

    assert r.status_code == 200
    assert not target.exists()


def test_delete_409_on_non_empty_dir(app, left4me_root: Path) -> None:
    user_id = _make_user()
    overlay_id = _make_files_overlay(left4me_root, user_id=user_id, name="files")
    overlay_dir = left4me_root / "overlays" / str(overlay_id)
    (overlay_dir / "addons" / "file.txt").parent.mkdir()
    (overlay_dir / "addons" / "file.txt").write_text("x")

    client = _client_for(app, user_id)
    r = client.post(
        f"/overlays/{overlay_id}/files/delete",
        data={"path": "addons", "csrf_token": "test-token"},
    )

    assert r.status_code == 409
    assert (overlay_dir / "addons" / "file.txt").exists()


# ---- /replace -------------------------------------------------------------


def test_replace_overwrites_file(app, left4me_root: Path) -> None:
    user_id = _make_user()
    overlay_id = _make_files_overlay(left4me_root, user_id=user_id, name="files")
    overlay_dir = left4me_root / "overlays" / str(overlay_id)
    (overlay_dir / "map.vpk").write_bytes(b"old-bytes")

    client = _client_for(app, user_id)
    r = client.post(
        f"/overlays/{overlay_id}/files/replace",
        data={
            "path": "map.vpk",
            "csrf_token": "test-token",
            "file": (io.BytesIO(b"new-bytes"), "map.vpk"),
        },
        content_type="multipart/form-data",
    )

    assert r.status_code == 200
    assert (overlay_dir / "map.vpk").read_bytes() == b"new-bytes"


def test_replace_with_new_path_renames(app, left4me_root: Path) -> None:
    user_id = _make_user()
    overlay_id = _make_files_overlay(left4me_root, user_id=user_id, name="files")
    overlay_dir = left4me_root / "overlays" / str(overlay_id)
    (overlay_dir / "map.vpk").write_bytes(b"old")

    client = _client_for(app, user_id)
    r = client.post(
        f"/overlays/{overlay_id}/files/replace",
        data={
            "path": "map.vpk",
            "new_path": "renamed.vpk",
            "csrf_token": "test-token",
            "file": (io.BytesIO(b"new"), "ignored"),
        },
        content_type="multipart/form-data",
    )

    assert r.status_code == 200
    assert not (overlay_dir / "map.vpk").exists()
    assert (overlay_dir / "renamed.vpk").read_bytes() == b"new"


# ---- /download_zip --------------------------------------------------------


def test_download_zip_streams_folder_contents(app, left4me_root: Path) -> None:
    import zipfile

    user_id = _make_user()
    overlay_id = _make_files_overlay(left4me_root, user_id=user_id, name="files")
    overlay_dir = left4me_root / "overlays" / str(overlay_id)
    (overlay_dir / "left4dead2" / "cfg").mkdir(parents=True)
    (overlay_dir / "left4dead2" / "cfg" / "server.cfg").write_text("hostname x")
    (overlay_dir / "left4dead2" / "cfg" / "motd.txt").write_text("welcome")

    client = _client_for(app, user_id)
    r = client.get(
        f"/overlays/{overlay_id}/files/download_zip?path=left4dead2/cfg"
    )

    assert r.status_code == 200
    assert r.headers["Content-Disposition"].startswith("attachment")
    body = io.BytesIO(r.get_data())
    with zipfile.ZipFile(body) as zf:
        names = sorted(zf.namelist())
        assert names == ["motd.txt", "server.cfg"]
        assert zf.read("motd.txt") == b"welcome"


# ---- type-isolation across all new endpoints ------------------------------


def test_mutating_endpoints_404_for_workshop_overlay(app, left4me_root: Path) -> None:
    user_id = _make_user()
    with session_scope() as s:
        overlay = Overlay(name="ws", path="", type="workshop", user_id=user_id)
        s.add(overlay)
        s.flush()
        overlay.path = str(overlay.id)
        overlay_id = overlay.id
    (left4me_root / "overlays" / str(overlay_id)).mkdir(parents=True)

    client = _client_for(app, user_id)
    headers = _csrf_headers()
    paths = [
        ("post", f"/overlays/{overlay_id}/files/save", {"json": {"path": "x.txt", "content": "x"}}),
        ("post", f"/overlays/{overlay_id}/files/replace",
            {"data": {"path": "x.bin", "csrf_token": "test-token", "file": (io.BytesIO(b"y"), "x.bin")},
             "content_type": "multipart/form-data"}),
        ("post", f"/overlays/{overlay_id}/files/upload",
            {"data": {"target_path": "", "csrf_token": "test-token", "file": (io.BytesIO(b"y"), "x.bin")},
             "content_type": "multipart/form-data"}),
        ("post", f"/overlays/{overlay_id}/files/move", {"json": {"src": "a", "dst": "b"}}),
        ("post", f"/overlays/{overlay_id}/files/mkdir", {"json": {"path": "x"}}),
        ("post", f"/overlays/{overlay_id}/files/delete",
            {"data": {"path": "x", "csrf_token": "test-token"}}),
        ("get", f"/overlays/{overlay_id}/files/download_zip?path=", {}),
    ]
    for method, url, kwargs in paths:
        kwargs = dict(kwargs)
        if method == "post" and "json" in kwargs:
            kwargs["headers"] = headers
        r = getattr(client, method)(url, **kwargs)
        assert r.status_code == 404, f"{method.upper()} {url} returned {r.status_code}"
