"""``ci-host`` — the absorbed Woodpecker CI-host provisioner (Wave-2 P0.4).

The rebuild-truth pins matter most here: the packaged bundle MUST stay
byte-identical to the documented ``examples/`` files (so the verb renders the
CURRENT Postgres topology, never the pre-migration one), and the cloud-init must
carry no secrets. The apply path runs against mgf-cloud's in-memory
FakeProvider — no cloud, no credentials.
"""

from __future__ import annotations

import base64
from pathlib import Path

import pytest

from mgf.cloud_provision import ci_host
from mgf.cloud_provision.__main__ import main
from mgf.cloud_provision.ci_host import (
    BUNDLE_FILES,
    SECGROUP_PORTS,
    agent_service_spec,
    ci_host_service_spec,
    provision_ci_host,
)

_REPO = Path(__file__).resolve().parents[4]
_EXAMPLES = _REPO / "examples"


# --------------------------------------------------------------------------
# packaged bundle ↔ examples/ byte-pins (the "renders the CURRENT topology"
# guarantee — the drift that made mgf-fed's verb render the pre-Postgres
# compose becomes impossible)
# --------------------------------------------------------------------------
@pytest.mark.parametrize(
    ("packaged", "example"),
    [
        ("docker-compose.yml", "woodpecker-compose.yml"),
        ("Caddyfile", "woodpecker-Caddyfile"),
        ("woodpecker-agent.toml", "woodpecker-agent.toml"),
        ("woodpecker-agent-cloud-init.yml", "woodpecker-agent-cloud-init.yml"),
    ],
)
def test_packaged_bundle_is_byte_identical_to_examples(packaged: str, example: str) -> None:
    data = (ci_host._data_dir() / packaged).read_text(encoding="utf-8")
    assert data == (_EXAMPLES / example).read_text(encoding="utf-8")


def test_bundle_is_the_postgres_topology() -> None:
    bundle = ci_host._load_bundle(None)
    assert set(bundle) == set(BUNDLE_FILES)
    # the current (post-2026-06-21) compose, not the SQLite one
    assert "WOODPECKER_DATABASE_DRIVER=postgres" in bundle["docker-compose.yml"]
    assert "woodpecker-postgres" in bundle["docker-compose.yml"]
    # the two-vhost Caddyfile: web/API + the h2c gRPC endpoint remote agents dial
    assert "grpc.{$WOODPECKER_DOMAIN}" in bundle["Caddyfile"]
    assert "h2c://woodpecker-server:9000" in bundle["Caddyfile"]


# --------------------------------------------------------------------------
# spec building + cloud-init rendering (pure)
# --------------------------------------------------------------------------
def test_ci_host_spec_defaults_match_live_estate() -> None:
    spec = ci_host_service_spec()
    assert spec.name == "woodpecker-prod"
    assert spec.security_groups == ("woodpecker-prod-ci",)
    assert spec.flavor == "a4-ram8-disk20-perf1"  # the live woodpecker-prod sizing
    assert "Ubuntu 24.04" in spec.image
    assert spec.network == "ext-net1"
    assert spec.volume_size_gb == 40
    assert spec.assign_floating_ip is False  # ext-net1 is direct-public
    assert spec.owner == "mgf-cloud-provision"


def test_ci_host_spec_overrides() -> None:
    spec = ci_host_service_spec(hostname="wp", env="staging", flavor="big", volume_size_gb=None)
    assert spec.name == "wp-staging"
    assert spec.security_groups == ("wp-staging-ci",)
    assert spec.flavor == "big"
    assert spec.volume_size_gb is None


def test_cloud_init_installs_docker_opens_ports_embeds_bundle_no_secrets() -> None:
    spec = ci_host_service_spec()
    ci = spec.cloud_init or ""
    assert ci.startswith("#cloud-config")
    assert "get.docker.com" in ci
    for port in SECGROUP_PORTS:
        assert f'"{port}/tcp"' in ci
    assert '"25"' in ci  # egress :25 blocked
    # the bundle is base64-embedded under /opt/woodpecker/
    for name in BUNDLE_FILES:
        assert f"/opt/woodpecker/{name}" in ci
    compose = (_EXAMPLES / "woodpecker-compose.yml").read_text(encoding="utf-8")
    assert base64.b64encode(compose.encode()).decode() in ci
    # secrets never enter user-data (the metadata service exposes it)
    assert "WOODPECKER_FORGEJO_SECRET=" not in ci
    # the stack is NOT auto-started (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_agent_spec_is_the_packaged_agent2() -> None:
    spec = agent_service_spec()
    assert spec.name == "woodpecker-agent2"
    assert spec.security_groups == ("woodpecker-prod-ci",)
    assert spec.flavor == "a4-ram8-disk20-perf1"
    assert spec.volume_size_gb == 40
    ci = spec.cloud_init or ""
    assert "WOODPECKER_GRPC_SECURE=true" in ci  # TLS to the server's Caddy h2c proxy
    assert "self-heal.sh" in ci  # the agent self-heal cron ships with the VM


# --------------------------------------------------------------------------
# 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
    out = capsys.readouterr().out
    assert "ci-host plan:" in out
    assert "woodpecker-prod" in out


def test_dry_run_with_agent_plans_both(capsys: pytest.CaptureFixture[str]) -> None:
    rc = provision_ci_host(dry_run=True, with_agent=True)
    assert rc == 0
    out = capsys.readouterr().out
    assert "woodpecker-prod" in out
    assert "woodpecker-agent2" in 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


# --------------------------------------------------------------------------
# apply path — against mgf-cloud's FakeProvider
# --------------------------------------------------------------------------
def test_apply_creates_secgroup_and_server() -> None:
    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
    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)
    servers = fake.list_servers(owner="mgf-cloud-provision")
    assert [s.name for s in servers] == ["woodpecker-prod"]
    assert servers[0].floating_ips  # assign_floating_ip honoured when requested


def test_apply_with_agent_creates_both_vms() -> None:
    from mgf.cloud.providers.fake import FakeProvider

    fake = FakeProvider()
    rc = provision_ci_host(yes=True, provider=fake, with_agent=True)
    assert rc == 0
    names = {s.name for s in fake.list_servers()}
    assert names == {"woodpecker-prod", "woodpecker-agent2"}


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

    fake = FakeProvider()
    fake.create_security_group("woodpecker-prod-ci", "pre-existing")
    rc = provision_ci_host(yes=True, provider=fake)
    assert rc == 0
    assert any(s.name == "woodpecker-prod" for s in fake.list_servers())


def test_second_apply_is_noop(capsys: pytest.CaptureFixture[str]) -> None:
    # Idempotence through the converge engine — a capability the mgf-fed
    # original (create-only) did not have.
    from mgf.cloud.providers.fake import FakeProvider

    fake = FakeProvider()
    assert provision_ci_host(yes=True, provider=fake) == 0
    assert provision_ci_host(yes=True, provider=fake) == 0
    assert "NOOP" in capsys.readouterr().out
    assert len(fake.list_servers()) == 1


# --------------------------------------------------------------------------
# CLI wiring
# --------------------------------------------------------------------------
def test_cli_ci_host_dry_run(capsys: pytest.CaptureFixture[str]) -> None:
    rc = main(["ci-host", "--dry-run"])
    assert rc == 0
    assert "ci-host plan:" in capsys.readouterr().out


def test_cli_ci_host_requires_yes(capsys: pytest.CaptureFixture[str]) -> None:
    rc = main(["ci-host", "--provider", "fake"])
    assert rc == 1
    assert "refusing" in capsys.readouterr().out


def test_cli_ci_host_apply_fake(capsys: pytest.CaptureFixture[str]) -> None:
    rc = main(["ci-host", "--provider", "fake", "--yes", "--with-agent"])
    assert rc == 0
    out = capsys.readouterr().out
    assert "woodpecker-prod: CREATE" in out
    assert "woodpecker-agent2: CREATE" in out
    assert "Next (operator + one-time human steps" in out
