"""Tests for the provider factory + the in-memory FakeProvider (Pass 2)."""

from __future__ import annotations

import importlib.util

import pytest

from mgf.cloud import available_providers, get_provider
from mgf.cloud.config import Credentials
from mgf.cloud.exceptions import (
    ConfigError,
    CredentialsError,
    ProviderNotFoundError,
    ResourceAlreadyExistsError,
    ResourceNotFoundError,
)
from mgf.cloud.models import OWNER_TAG, SecurityRule, ServerSpec, VolumeSpec, owned_metadata
from mgf.cloud.providers.base import CloudProvider
from mgf.cloud.providers.fake import FakeProvider


def _creds() -> Credentials:
    return Credentials(
        auth_url="https://api.pub1.infomaniak.cloud/identity/v3",
        application_credential_id="id",
        application_credential_secret="secret",
    )


def test_factory_known_and_unknown() -> None:
    assert "fake" in available_providers()
    assert "infomaniak" in available_providers()
    with pytest.raises(ProviderNotFoundError):
        get_provider("bogus")


def test_fake_is_a_cloudprovider() -> None:
    provider = get_provider("fake")
    assert isinstance(provider, FakeProvider)
    assert isinstance(provider, CloudProvider)  # runtime_checkable structural match


def test_real_providers_require_credentials() -> None:
    with pytest.raises(CredentialsError):
        get_provider("infomaniak")
    with pytest.raises(CredentialsError):
        get_provider("aws")


def test_infomaniak_without_extra_raises_configerror() -> None:
    # In a core-only env (no [infomaniak]) the lazy import fails → actionable
    # ConfigError. When openstacksdk IS installed the adapter loads instead, so
    # this assertion only holds for the core-only env (the federation [dev] env).
    if importlib.util.find_spec("openstack") is not None:
        pytest.skip("openstacksdk installed — [infomaniak] adapter loads; see test_infomaniak.py")
    with pytest.raises(ConfigError):
        get_provider("infomaniak", _creds())


def test_fake_catalogue_preseeded() -> None:
    provider = get_provider("fake")
    assert any(f.name == "a2-ram2-disk20" for f in provider.list_flavors())
    assert any(i.name == "Ubuntu 24.04 LTS" for i in provider.list_images())
    assert any(n.name == "ext-net1" for n in provider.list_networks())


def test_fake_keypairs_and_security_groups() -> None:
    provider = FakeProvider()
    kp = provider.create_keypair("k1", "ssh-ed25519 AAAA")
    assert kp.name == "k1"
    assert provider.list_keypairs()
    with pytest.raises(ResourceAlreadyExistsError):
        provider.create_keypair("k1", "x")
    provider.delete_keypair("k1")
    with pytest.raises(ResourceNotFoundError):
        provider.delete_keypair("k1")

    rule = SecurityRule(protocol="tcp", port_min=22, port_max=22)
    group = provider.create_security_group("ci", "ci hosts", (rule,))
    assert group.rules[0].port_min == 22
    assert provider.list_security_groups()
    provider.delete_security_group("ci")
    with pytest.raises(ResourceNotFoundError):
        provider.delete_security_group("ci")


def test_fake_server_lifecycle_with_floating_ip_and_boot_volume() -> None:
    provider = FakeProvider()
    spec = ServerSpec(
        name="woodpecker",
        flavor="a2-ram2-disk20",
        image="Ubuntu 24.04 LTS",
        network="ext-net1",
        key_name="k1",
        security_groups=("ci",),
        user_data="#cloud-config\n",
        volume_size_gb=20,
        assign_floating_ip=True,
        metadata=owned_metadata("federation"),
    )
    server = provider.create_server(spec)
    assert server.status == "ACTIVE"
    assert server.floating_ips  # one was allocated + attached
    assert server.metadata[OWNER_TAG] == "federation"
    # a bootable volume was created + attached
    assert any(v.bootable and v.attached_server_id == server.id for v in provider.list_volumes())

    # lookups by id and by name
    assert provider.get_server(server.id).name == "woodpecker"
    assert provider.get_server("woodpecker").id == server.id
    with pytest.raises(ResourceNotFoundError):
        provider.get_server("nope")

    # owner filter
    assert provider.list_servers(owner="federation")
    assert provider.list_servers(owner="other") == []

    # duplicate name
    with pytest.raises(ResourceAlreadyExistsError):
        provider.create_server(spec)

    # reboot / resize / snapshot
    provider.reboot_server("woodpecker", hard=True)
    provider.resize_server("woodpecker", "a2-ram4-disk40")
    assert provider.get_server("woodpecker").flavor == "a2-ram4-disk40"
    image = provider.snapshot_server("woodpecker", "woodpecker-snap")
    assert image.name == "woodpecker-snap"

    # delete frees the floating IP + detaches the volume
    addr = server.floating_ips[0]
    provider.delete_server("woodpecker")
    assert provider.list_servers() == []
    assert all(
        fip.attached_server_id is None
        for fip in provider.list_floating_ips()
        if fip.address == addr
    )
    assert all(v.attached_server_id is None for v in provider.list_volumes())


def test_fake_volumes_and_floating_ips() -> None:
    provider = FakeProvider()
    provider.create_server(ServerSpec(name="vm", flavor="a2-ram2-disk20", image="Ubuntu 24.04 LTS"))
    provider.create_volume(VolumeSpec(name="data", size_gb=50))
    provider.attach_volume("data", "vm")
    assert provider.get_volume("data").attached_server_id is not None
    provider.detach_volume("data", "vm")
    assert provider.get_volume("data").attached_server_id is None
    snap = provider.snapshot_volume("data", "data-snap")
    assert snap.size_gb == 50
    provider.delete_volume("data")
    with pytest.raises(ResourceNotFoundError):
        provider.get_volume("data")

    fip = provider.create_floating_ip("ext-net1")
    provider.attach_floating_ip(fip.address, "vm")
    assert provider.get_server("vm").floating_ips == (fip.address,)
    provider.detach_floating_ip(fip.address)
    assert provider.get_server("vm").floating_ips == ()
    provider.release_floating_ip(fip.address)
    assert provider.list_floating_ips() == []
    with pytest.raises(ResourceNotFoundError):
        provider.attach_floating_ip("203.0.113.999", "vm")


def test_fake_create_volume_is_idempotent_by_name() -> None:
    """EH-12: a same-name retry must not leak a second billable volume."""
    provider = FakeProvider()
    provider.create_volume(VolumeSpec(name="data", size_gb=50))
    with pytest.raises(ResourceAlreadyExistsError):
        provider.create_volume(VolumeSpec(name="data", size_gb=50))
    assert len(provider.list_volumes()) == 1


def test_fake_object_storage() -> None:
    provider = FakeProvider()
    provider.create_container("backups")
    with pytest.raises(ResourceAlreadyExistsError):
        provider.create_container("backups")
    info = provider.put_object("backups", "a.txt", b"hello", content_type="text/plain")
    assert info.bytes == 5
    assert provider.get_object("backups", "a.txt") == b"hello"
    assert [o.name for o in provider.list_objects("backups")] == ["a.txt"]
    containers = {c.name: c for c in provider.list_containers()}
    assert containers["backups"].object_count == 1
    provider.delete_object("backups", "a.txt")
    with pytest.raises(ResourceNotFoundError):
        provider.get_object("backups", "a.txt")
    provider.delete_container("backups")
    with pytest.raises(ResourceNotFoundError):
        provider.list_objects("backups")
