"""Tests for ``mgf-fed provision-ci-host`` (Pass 6, WF-11).

Plan-building + cloud-init rendering are pure (stdlib) and always run. The apply
path is exercised by injecting a provider — mgf.cloud's in-memory FakeProvider when
the [cloud] extra is present, else skipped. Graceful degradation (missing extra) is
tested by forcing the import to fail, so it runs even where mgf.cloud is installed.
"""

from __future__ import annotations

import base64
import sys
from pathlib import Path

import pytest

from mgf.fed import _provision
from mgf.fed._provision import (
    SECGROUP_PORTS,
    CloudExtraMissing,
    ProvisionPlan,
    build_plan,
    provision_ci_host,
)

_BUNDLE = Path(__file__).resolve().parents[2] / "deploy" / "woodpecker"


# --------------------------------------------------------------------------
# pure plan-building + cloud-init rendering
# --------------------------------------------------------------------------
def test_build_plan_defaults() -> None:
    plan = build_plan()
    assert isinstance(plan, ProvisionPlan)
    assert plan.server_name == "woodpecker-prod"
    assert plan.secgroup_name == "woodpecker-prod-ci"
    assert plan.secgroup_ports == SECGROUP_PORTS == (22, 80, 443)
    assert plan.volume_size_gb == 20
    assert plan.floating_ip is False  # ext-net1 VMs get a public IP directly — no FIP
    assert plan.owner == "mgf-fed"
    # flavour/image/network resolve to the Infomaniak catalogue
    assert "a1-ram4" in plan.flavor
    assert "Ubuntu 24.04" in plan.image
    assert plan.network == "ext-net1"


def test_build_plan_overrides() -> None:
    plan = build_plan(
        hostname="wp", env="staging", flavor="big", network="net9", volume_size_gb=None
    )
    assert plan.server_name == "wp-staging"
    assert plan.flavor == "big"
    assert plan.network == "net9"
    assert plan.volume_size_gb is None


def test_cloud_init_installs_docker_opens_ports_embeds_bundle_no_secrets() -> None:
    plan = build_plan()
    ci = plan.cloud_init
    assert ci.startswith("#cloud-config")
    assert "get.docker.com" in ci  # Docker install
    for port in SECGROUP_PORTS:
        assert f'"{port}/tcp"' in ci  # ufw opens each port
    assert '"25"' in ci  # egress :25 blocked (DESIGN §6)
    # the compose bundle is base64-embedded under /opt/woodpecker/
    assert "/opt/woodpecker/docker-compose.yml" in ci
    compose = (_BUNDLE / "docker-compose.yml").read_text(encoding="utf-8")
    assert base64.b64encode(compose.encode()).decode() in ci
    # secrets never enter user-data
    assert "WOODPECKER_FORGEJO_SECRET=" not in ci
    # the stack is NOT auto-started by runcmd (it needs .env first) — the only
    # mention of "compose up" is the instructional final_message.
    runcmd_section = ci.split("runcmd:")[1].split("final_message:")[0]
    assert "compose up" not in runcmd_section


def test_build_plan_missing_bundle_raises() -> None:
    with pytest.raises(FileNotFoundError):
        build_plan(bundle_dir=Path("/nonexistent/woodpecker"))


def test_render_is_human_readable() -> None:
    out = build_plan().render()
    assert "provision-ci-host plan:" in out
    assert "woodpecker-prod" in out
    assert "22/80/443" in out


# --------------------------------------------------------------------------
# provision_ci_host — dry-run, confirmation, missing-bundle
# --------------------------------------------------------------------------
def test_dry_run_plans_and_touches_nothing(capsys: pytest.CaptureFixture[str]) -> None:
    rc = provision_ci_host(dry_run=True)
    assert rc == 0
    assert "provision-ci-host plan:" in capsys.readouterr().out


def test_apply_without_yes_refuses(capsys: pytest.CaptureFixture[str]) -> None:
    rc = provision_ci_host(yes=False)
    assert rc == 1
    assert "refusing to provision without --yes" in capsys.readouterr().out


def test_missing_bundle_returns_1(capsys: pytest.CaptureFixture[str]) -> None:
    rc = provision_ci_host(dry_run=True, bundle_dir=Path("/nope/woodpecker"))
    assert rc == 1
    assert "missing" in capsys.readouterr().out


# --------------------------------------------------------------------------
# graceful degradation when the [cloud] extra is absent
# --------------------------------------------------------------------------
def test_missing_cloud_extra_is_actionable(monkeypatch: pytest.MonkeyPatch) -> None:
    # Force `from mgf.cloud import ...` to fail even if mgf.cloud is installed.
    monkeypatch.setitem(sys.modules, "mgf.cloud", None)
    with pytest.raises(CloudExtraMissing) as ei:
        _provision._resolve_provider(None, provider_name="infomaniak", clouds_yaml=None, cloud="x")
    assert "mgf-fed[cloud]" in str(ei.value)


def test_provision_reports_missing_extra(
    monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]
) -> None:
    monkeypatch.setitem(sys.modules, "mgf.cloud", None)
    rc = provision_ci_host(yes=True)
    assert rc == 1
    assert "needs the cloud extra" in capsys.readouterr().out


def test_resolve_provider_returns_injected() -> None:
    sentinel = object()
    got = _provision._resolve_provider(sentinel, provider_name="fake", clouds_yaml=None, cloud="x")
    assert got is sentinel


# --------------------------------------------------------------------------
# apply path — against mgf.cloud's FakeProvider (needs the cloud package)
# --------------------------------------------------------------------------
def test_apply_creates_secgroup_and_server() -> None:
    pytest.importorskip("mgf.cloud")
    from mgf.cloud.providers.fake import FakeProvider

    fake = FakeProvider()
    rc = provision_ci_host(yes=True, provider=fake, key_name="ci-key", floating_ip=True)
    assert rc == 0
    # the security group (22/80/443) was created
    groups = {g.name: g for g in fake.list_security_groups()}
    assert "woodpecker-prod-ci" in groups
    assert {r.port_min for r in groups["woodpecker-prod-ci"].rules} == set(SECGROUP_PORTS)
    # the VM exists, tagged + with a floating IP (opt-in) + the cloud-init as user-data
    servers = fake.list_servers(owner="mgf-fed")
    assert len(servers) == 1
    vm = servers[0]
    assert vm.name == "woodpecker-prod"
    assert vm.floating_ips  # assign_floating_ip honoured when requested


def test_apply_tolerates_existing_secgroup() -> None:
    pytest.importorskip("mgf.cloud")
    from mgf.cloud.providers.fake import FakeProvider

    fake = FakeProvider()
    fake.create_security_group("woodpecker-prod-ci", "pre-existing")
    # second provision must not crash on the duplicate group
    rc = provision_ci_host(yes=True, provider=fake)
    assert rc == 0
    assert any(s.name == "woodpecker-prod" for s in fake.list_servers())
