from __future__ import annotations

from io import BytesIO

import pytest

from beaver.services.user_files import (
    LocalUserFileStorage,
    MinIOStorageConfig,
    MinIOUserFileStorage,
    UserFileNotFoundError,
    UserFilePathError,
    UserFileSizeError,
    UserFileService,
    normalize_user_path,
)


def test_normalize_user_path_accepts_fixed_roots() -> None:
    assert normalize_user_path("uploads/readme.txt", allow_root=False) == "uploads/readme.txt"
    assert normalize_user_path("outputs/report.md", allow_root=False) == "outputs/report.md"
    assert normalize_user_path("tasks/task-123/draft.md", allow_root=False) == "tasks/task-123/draft.md"
    assert normalize_user_path("", allow_root=True) == ""


@pytest.mark.parametrize(
    "path",
    [
        "../secret.txt",
        "/uploads/input.txt",
        "/outputs/result.txt",
        "/shared/profile.json",
        "/tasks/task-123/draft.md",
        "uploads/../state/config.json",
        "memory/private.txt",
        "uploads/.internal",
        "",
    ],
)
def test_normalize_user_path_rejects_invalid_paths(path: str) -> None:
    with pytest.raises(UserFilePathError):
        normalize_user_path(path, allow_root=False)


@pytest.mark.asyncio
async def test_user_file_service_root_and_round_trip(tmp_path) -> None:
    service = UserFileService(LocalUserFileStorage(tmp_path / "user-files"))

    root = await service.browse("")
    uploaded = await service.upload(
        "uploads",
        "hello.txt",
        b"hello user files",
        content_type="text/plain",
    )
    uploads = await service.browse("uploads")
    preview = await service.preview("uploads/hello.txt")
    downloaded = await service.download("uploads/hello.txt")
    deleted = await service.delete("uploads/hello.txt")

    assert [item["name"] for item in root["items"]] == ["uploads", "outputs", "shared", "tasks"]
    assert uploaded["path"] == "uploads/hello.txt"
    assert uploaded["content_type"] == "text/plain"
    assert [item["name"] for item in uploads["items"]] == ["hello.txt"]
    assert preview["content"] == "hello user files"
    assert downloaded.content == b"hello user files"
    assert deleted is True


@pytest.mark.asyncio
async def test_user_file_service_stream_upload_and_size_limit(tmp_path) -> None:
    service = UserFileService(LocalUserFileStorage(tmp_path / "user-files"))

    uploaded = await service.upload_stream(
        "uploads",
        "streamed.txt",
        BytesIO(b"streamed user file"),
        content_type="text/plain",
        max_bytes=1024,
        part_size=4,
    )
    preview = await service.preview("uploads/streamed.txt")

    assert uploaded["path"] == "uploads/streamed.txt"
    assert uploaded["size"] == len(b"streamed user file")
    assert preview["content"] == "streamed user file"

    with pytest.raises(UserFileSizeError):
        await service.upload_stream(
            "uploads",
            "too-large.txt",
            BytesIO(b"abcdef"),
            content_type="text/plain",
            max_bytes=5,
            part_size=2,
        )
    with pytest.raises(UserFileNotFoundError):
        await service.preview("uploads/too-large.txt")


@pytest.mark.asyncio
async def test_user_file_service_rejects_root_delete_and_traversal(tmp_path) -> None:
    service = UserFileService(LocalUserFileStorage(tmp_path / "user-files"))

    with pytest.raises(UserFilePathError):
        await service.delete("uploads")

    with pytest.raises(UserFilePathError):
        await service.upload("../workspace", "hello.txt", b"x", content_type="text/plain")


@pytest.mark.asyncio
async def test_user_file_service_creates_nested_directories(tmp_path) -> None:
    service = UserFileService(LocalUserFileStorage(tmp_path / "user-files"))

    created = await service.mkdir("tasks/task-123/references")
    tasks = await service.browse("tasks/task-123")

    assert created["path"] == "tasks/task-123/references"
    assert created["type"] == "directory"
    assert [item["name"] for item in tasks["items"]] == ["references"]


def test_minio_storage_maps_virtual_paths_under_namespace() -> None:
    storage = object.__new__(MinIOUserFileStorage)
    storage.config = MinIOStorageConfig(
        endpoint="minio.local:9000",
        access_key="alice-access",
        secret_key="alice-secret",
        bucket="beaver-user-files",
        namespace="users/alice",
    )

    assert storage._object_name("uploads/report.pdf") == "users/alice/uploads/report.pdf"
    assert storage._object_name("tasks/task-123/result.json") == "users/alice/tasks/task-123/result.json"
    assert storage._user_path("users/alice/outputs/summary.md") == "outputs/summary.md"


def test_minio_storage_rejects_paths_that_escape_namespace() -> None:
    storage = object.__new__(MinIOUserFileStorage)
    storage.config = MinIOStorageConfig(
        endpoint="minio.local:9000",
        access_key="alice-access",
        secret_key="alice-secret",
        bucket="beaver-user-files",
        namespace="users/alice",
    )

    with pytest.raises(UserFilePathError):
        storage._object_name("uploads/../state/config.json")

    with pytest.raises(UserFilePathError):
        storage._user_path("users/bob/uploads/secret.txt")
