from __future__ import annotations

import importlib
import sys
from types import ModuleType
from typing import Any

from fastapi.testclient import TestClient

from app.minio_provisioning import (
    MinIOProvisioningConfig,
    deprovision_user_file_minio_resources,
    provision_user_file_minio_settings,
)
from app.models import MinIOSettings


class _FakeObject:
    def __init__(self, object_name: str) -> None:
        self.object_name = object_name


class _FakeMinio:
    bucket_exists_value = True
    objects: list[str] = []
    removed_objects: list[str] = []
    made_buckets: list[str] = []

    def __init__(self, **_kwargs: Any) -> None:
        pass

    def bucket_exists(self, bucket: str) -> bool:
        return self.bucket_exists_value

    def make_bucket(self, bucket: str, location: str | None = None) -> None:
        self.made_buckets.append(bucket)

    def list_objects(self, bucket: str, *, prefix: str, recursive: bool) -> list[_FakeObject]:
        return [_FakeObject(name) for name in self.objects if name.startswith(prefix)]

    def remove_objects(self, bucket: str, objects: list[Any]) -> list[Any]:
        self.removed_objects.extend(item.object_name for item in objects)
        return []


class _FakeAdmin:
    calls: list[tuple[str, Any]] = []
    missing = False
    attach_policy_already_applied = False

    def __init__(self, **_kwargs: Any) -> None:
        pass

    def user_add(self, access_key: str, secret_key: str) -> None:
        self.calls.append(("user_add", access_key))

    def policy_add(self, policy_name: str, *, policy: dict[str, Any]) -> None:
        self.calls.append(("policy_add", policy_name))

    def attach_policy(self, **kwargs: Any) -> None:
        self.calls.append(("attach_policy", kwargs))
        if self.attach_policy_already_applied:
            raise RuntimeError(
                "admin request failed; Status: 400, Body: "
                '{"Code":"XMinioAdminPolicyChangeAlreadyApplied",'
                '"Message":"The specified policy change is already in effect."}'
            )

    def detach_policy(self, **kwargs: Any) -> None:
        self.calls.append(("detach_policy", kwargs))
        if self.missing:
            raise RuntimeError("policy not found")

    def user_remove(self, access_key: str) -> None:
        self.calls.append(("user_remove", access_key))
        if self.missing:
            raise RuntimeError("user not found")

    def policy_remove(self, policy_name: str) -> None:
        self.calls.append(("policy_remove", policy_name))
        if self.missing:
            raise RuntimeError("policy not found")


class _FakeStaticProvider:
    def __init__(self, **_kwargs: Any) -> None:
        pass


class _FakeDeleteObject:
    def __init__(self, object_name: str) -> None:
        self.object_name = object_name


def _install_fake_minio(monkeypatch) -> None:
    minio_module = ModuleType("minio")
    minio_module.Minio = _FakeMinio
    minio_module.MinioAdmin = _FakeAdmin

    credentials_module = ModuleType("minio.credentials")
    credentials_module.StaticProvider = _FakeStaticProvider

    deleteobjects_module = ModuleType("minio.deleteobjects")
    deleteobjects_module.DeleteObject = _FakeDeleteObject

    monkeypatch.setitem(sys.modules, "minio", minio_module)
    monkeypatch.setitem(sys.modules, "minio.credentials", credentials_module)
    monkeypatch.setitem(sys.modules, "minio.deleteobjects", deleteobjects_module)
    _FakeMinio.bucket_exists_value = True
    _FakeMinio.objects = []
    _FakeMinio.removed_objects = []
    _FakeMinio.made_buckets = []
    _FakeAdmin.calls = []
    _FakeAdmin.missing = False
    _FakeAdmin.attach_policy_already_applied = False


def _config() -> MinIOProvisioningConfig:
    return MinIOProvisioningConfig(
        enabled=True,
        endpoint="minio.local:9000",
        public_endpoint="minio.local:9000",
        admin_access_key="admin",
        admin_secret_key="admin-secret",
        bucket="beaver-user-files",
        secure=False,
        region=None,
    )


def _settings() -> MinIOSettings:
    return MinIOSettings(
        endpoint="minio.local:9000",
        access_key="beaver-alice",
        secret_key="alice-secret",
        bucket="beaver-user-files",
        namespace="users/alice",
    )


def _client(tmp_path, monkeypatch) -> TestClient:
    monkeypatch.setenv("AUTHZ_DATA_DIR", str(tmp_path))
    monkeypatch.setenv("AUTHZ_PRIVATE_KEY_PATH", str(tmp_path / "signing_key.pem"))
    monkeypatch.setenv("AUTHZ_INTERNAL_TOKEN", "test-internal-token")
    monkeypatch.setenv("USER_FILES_MINIO_ENDPOINT", "minio.local:9000")
    monkeypatch.setenv("USER_FILES_MINIO_ADMIN_ACCESS_KEY", "admin")
    monkeypatch.setenv("USER_FILES_MINIO_ADMIN_SECRET_KEY", "admin-secret")
    monkeypatch.setenv("USER_FILES_MINIO_BUCKET", "beaver-user-files")
    import app.main as main

    main = importlib.reload(main)
    return TestClient(main.app)


def _register_backend(client: TestClient) -> None:
    response = client.post(
        "/backends/register",
        json={"backend_id": "alice", "name": "Alice", "base_url": "http://alice.local"},
    )
    assert response.status_code == 200


def test_deprovision_removes_namespace_resources_without_secrets(monkeypatch) -> None:
    _install_fake_minio(monkeypatch)
    _FakeMinio.objects = [
        "users/alice/uploads/a.txt",
        "users/alice/outputs/b.txt",
        "users/bob/uploads/c.txt",
    ]

    result = deprovision_user_file_minio_resources(
        backend_id="alice",
        existing=_settings(),
        config=_config(),
    )

    assert result["ok"] is True
    assert result["objects"] == {"status": "removed", "deleted": 2}
    assert _FakeMinio.removed_objects == ["users/alice/uploads/a.txt", "users/alice/outputs/b.txt"]
    assert ("user_remove", "beaver-alice") in _FakeAdmin.calls
    assert ("policy_remove", "beaver-user-files-alice") in _FakeAdmin.calls
    assert "secret" not in str(result).lower()


def test_provision_treats_already_attached_policy_as_idempotent(monkeypatch) -> None:
    _install_fake_minio(monkeypatch)
    _FakeAdmin.attach_policy_already_applied = True

    settings = provision_user_file_minio_settings(
        backend_id="alice",
        existing=None,
        config=_config(),
    )

    assert settings is not None
    assert settings.endpoint == "minio.local:9000"
    assert settings.access_key == "beaver-alice"
    assert settings.bucket == "beaver-user-files"
    assert settings.namespace == "users/alice"
    assert settings.secret_key
    assert ("attach_policy", {"policies": ["beaver-user-files-alice"], "user": "beaver-alice"}) in _FakeAdmin.calls


def test_deprovision_is_idempotent_when_resources_are_absent(monkeypatch) -> None:
    _install_fake_minio(monkeypatch)
    _FakeMinio.bucket_exists_value = False
    _FakeAdmin.missing = True

    result = deprovision_user_file_minio_resources(
        backend_id="alice",
        existing=_settings(),
        config=_config(),
    )

    assert result["ok"] is True
    assert result["bucket"] == "absent"
    assert result["objects"] == {"status": "absent", "deleted": 0}
    assert result["user"] == {"status": "absent"}
    assert result["policy"] == {"status": "absent"}


def test_internal_user_file_deprovision_requires_internal_token(tmp_path, monkeypatch) -> None:
    with _client(tmp_path, monkeypatch) as client:
        unauthorized = client.delete("/internal/backends/alice/user-files")

    assert unauthorized.status_code == 401


def test_internal_user_file_deprovision_deletes_settings_without_returning_secret(tmp_path, monkeypatch) -> None:
    _install_fake_minio(monkeypatch)
    with _client(tmp_path, monkeypatch) as client:
        _register_backend(client)
        client.post(
            "/backends/alice/settings/minio",
            json={
                "endpoint": "minio.local:9000",
                "access_key": "beaver-alice",
                "secret_key": "alice-secret",
                "bucket": "beaver-user-files",
                "namespace": "users/alice",
            },
        )
        deleted = client.delete(
            "/internal/backends/alice/user-files",
            headers={"Authorization": "Bearer test-internal-token"},
        )
        after_delete = client.get("/backends/alice/settings/minio")

    assert deleted.status_code == 200
    payload = deleted.json()
    assert payload["ok"] is True
    assert payload["settings"] == {"status": "removed"}
    assert "secret" not in str(payload).lower()
    assert after_delete.json() == {"configured": False}
