"""Tests for the three pure provisioning renderers.

cloud_init, preseed, and kickstart each produce a text artefact from
a :class:`ProvisioningSpec`. We verify that:

- every spec field surfaces in the expected place in the output,
- optional fields are omitted when unset,
- the cloud-init YAML round-trips through ``yaml.safe_load`` so a
  syntax bug in the generator surfaces as a test failure rather
  than a guest that silently ignores its user-data.

Ported from mgf-vm's ``tests/unit/test_provisioning.py``; the impure
write_files / package_seed_iso / generate tests stay behind in the
legacy adapter (those call atomic_write + cloud-localds).
"""

from __future__ import annotations

import yaml

from mgf.vm.core.provisioning import cloud_init, kickstart, preseed
from mgf.vm.core.provisioning.spec import ProvisioningSpec, SimpleProvisioningInput


def _spec(**prov_kwargs: object) -> SimpleProvisioningInput:
    """Helper: build a provisioning input with a given ProvisioningSpec."""
    return SimpleProvisioningInput(
        name="web-01",
        provisioning=ProvisioningSpec(**prov_kwargs),  # type: ignore[arg-type]
    )


# ---------------------------------------------------------------------------
# cloud-init: build_user_data / build_meta_data
# ---------------------------------------------------------------------------


class TestCloudInitUserData:
    def test_minimal_user_data(self) -> None:
        spec = _spec(backend="cloud_init")
        payload = cloud_init.build_user_data(spec)
        assert payload["hostname"] == "web-01"
        assert payload["manage_etc_hosts"] is True
        assert "users" not in payload
        assert "packages" not in payload
        assert "runcmd" not in payload

    def test_hostname_defaults_to_vm_name(self) -> None:
        spec = SimpleProvisioningInput(name="mybox")
        payload = cloud_init.build_user_data(spec)
        assert payload["hostname"] == "mybox"

    def test_hostname_override(self) -> None:
        spec = _spec(hostname="override-name")
        assert cloud_init.build_user_data(spec)["hostname"] == "override-name"

    def test_user_with_ssh_keys(self) -> None:
        spec = _spec(
            username="alice",
            ssh_authorized_keys=["ssh-ed25519 AAA..."],
        )
        payload = cloud_init.build_user_data(spec)
        assert "users" in payload
        user = payload["users"][0]
        assert user["name"] == "alice"
        assert "sudo" in user
        assert user["ssh_authorized_keys"] == ["ssh-ed25519 AAA..."]
        assert "plain_text_passwd" not in user

    def test_user_with_password_enables_pwauth(self) -> None:
        spec = _spec(username="alice", password="secret")
        payload = cloud_init.build_user_data(spec)
        user = payload["users"][0]
        assert user["plain_text_passwd"] == "secret"
        assert payload["ssh_pwauth"] is True
        assert payload["chpasswd"] == {"expire": False}

    def test_ssh_keys_without_username_goes_top_level(self) -> None:
        """When the spec supplies SSH keys but no username we attach the
        keys to the default cloud-image user instead of creating a
        new account."""
        spec = _spec(ssh_authorized_keys=["ssh-ed25519 AAA..."])
        payload = cloud_init.build_user_data(spec)
        assert "users" not in payload
        assert payload["ssh_authorized_keys"] == ["ssh-ed25519 AAA..."]

    def test_packages(self) -> None:
        spec = _spec(packages=["nginx", "htop"])
        payload = cloud_init.build_user_data(spec)
        assert payload["package_update"] is True
        assert payload["packages"] == ["nginx", "htop"]

    def test_run_commands(self) -> None:
        spec = _spec(run_commands=["systemctl enable nginx", "echo hi"])
        payload = cloud_init.build_user_data(spec)
        assert payload["runcmd"] == ["systemctl enable nginx", "echo hi"]


class TestCloudInitMetaData:
    def test_uses_vm_name_as_instance_and_hostname(self) -> None:
        spec = SimpleProvisioningInput(name="mybox")
        payload = cloud_init.build_meta_data(spec)
        assert payload["instance-id"] == "iid-mybox"
        assert payload["local-hostname"] == "mybox"

    def test_hostname_override_wins(self) -> None:
        spec = _spec(hostname="override")
        payload = cloud_init.build_meta_data(spec)
        assert payload["local-hostname"] == "override"
        # instance-id still derived from the VM name.
        assert payload["instance-id"] == "iid-web-01"


class TestCloudInitRender:
    def test_user_data_has_header(self) -> None:
        text = cloud_init.render_user_data(_spec())
        assert text.startswith("#cloud-config\n")

    def test_user_data_is_valid_yaml(self) -> None:
        spec = _spec(
            username="alice",
            password="secret",
            ssh_authorized_keys=["ssh-ed25519 AAA..."],
            packages=["nginx", "htop"],
            run_commands=["systemctl enable nginx"],
        )
        text = cloud_init.render_user_data(spec)
        # Strip the #cloud-config header and parse what's left.
        _, _, body = text.partition("\n")
        parsed = yaml.safe_load(body)
        assert parsed["users"][0]["name"] == "alice"
        assert parsed["packages"] == ["nginx", "htop"]
        assert parsed["runcmd"] == ["systemctl enable nginx"]

    def test_meta_data_is_valid_yaml(self) -> None:
        text = cloud_init.render_meta_data(_spec())
        parsed = yaml.safe_load(text)
        assert parsed["instance-id"] == "iid-web-01"


# ---------------------------------------------------------------------------
# preseed
# ---------------------------------------------------------------------------


class TestPreseed:
    def test_header_and_locale(self) -> None:
        text = preseed.render(_spec())
        assert text.startswith("# Generated by vmanager")
        assert "debian-installer/locale string en_US.UTF-8" in text

    def test_hostname_from_spec(self) -> None:
        text = preseed.render(_spec(hostname="db"))
        assert "netcfg/get_hostname string db" in text

    def test_hostname_defaults_to_vm_name(self) -> None:
        text = preseed.render(SimpleProvisioningInput(name="mybox"))
        assert "netcfg/get_hostname string mybox" in text

    def test_username_and_password_defaults(self) -> None:
        text = preseed.render(_spec())
        # Default username and password are "vmanager"/"vmanager".
        assert "passwd/username string vmanager" in text
        assert "passwd/user-password password vmanager" in text

    def test_custom_username_and_password(self) -> None:
        text = preseed.render(_spec(username="alice", password="s3cret"))
        assert "passwd/username string alice" in text
        assert "passwd/user-password password s3cret" in text
        assert "passwd/user-password-again password s3cret" in text

    def test_packages_go_into_pkgsel(self) -> None:
        text = preseed.render(_spec(packages=["nginx", "htop"]))
        assert "pkgsel/include string nginx htop" in text

    def test_no_packages_line_when_empty(self) -> None:
        text = preseed.render(_spec())
        assert "pkgsel/include" not in text

    def test_ssh_keys_generate_late_command(self) -> None:
        text = preseed.render(_spec(username="alice", ssh_authorized_keys=["ssh-ed25519 AAA..."]))
        assert "preseed/late_command" in text
        assert "/home/alice/.ssh/authorized_keys" in text
        assert "ssh-ed25519 AAA..." in text

    def test_no_late_command_when_no_keys(self) -> None:
        text = preseed.render(_spec())
        assert "preseed/late_command" not in text

    def test_run_commands_emitted(self) -> None:
        text = preseed.render(_spec(run_commands=["systemctl enable ssh", "apt update"]))
        assert "preseed/run string systemctl enable ssh" in text
        assert "preseed/run string apt update" in text

    def test_no_run_commands_section_when_empty(self) -> None:
        text = preseed.render(_spec())
        assert "preseed/run string" not in text

    def test_render_ends_with_newline(self) -> None:
        assert preseed.render(_spec()).endswith("\n")


# ---------------------------------------------------------------------------
# kickstart
# ---------------------------------------------------------------------------


class TestKickstart:
    def test_header_and_core(self) -> None:
        text = kickstart.render(_spec())
        assert text.startswith("# Generated by vmanager")
        assert "lang en_US.UTF-8" in text
        assert "timezone UTC --isUtc" in text
        assert "@core" in text
        assert "openssh-server" in text

    def test_hostname_from_spec(self) -> None:
        text = kickstart.render(_spec(hostname="db"))
        assert "--hostname=db" in text

    def test_hostname_defaults_to_vm_name(self) -> None:
        text = kickstart.render(SimpleProvisioningInput(name="mybox"))
        assert "--hostname=mybox" in text

    def test_user_account(self) -> None:
        text = kickstart.render(_spec(username="alice", password="s3cret"))
        assert "user --name=alice --password=s3cret --plaintext" in text
        assert "--groups=wheel" in text

    def test_user_account_defaults(self) -> None:
        text = kickstart.render(_spec())
        assert "user --name=vmanager --password=vmanager --plaintext" in text

    def test_packages_appended(self) -> None:
        text = kickstart.render(_spec(packages=["nginx", "htop"]))
        # nginx and htop appear in the %packages block after @core.
        assert "nginx" in text
        assert "htop" in text

    def test_ssh_keys_in_post_section(self) -> None:
        text = kickstart.render(
            _spec(
                username="alice",
                ssh_authorized_keys=["ssh-ed25519 KEY1", "ssh-ed25519 KEY2"],
            )
        )
        assert "%post" in text
        assert "/home/alice/.ssh/authorized_keys" in text
        assert "ssh-ed25519 KEY1" in text
        assert "ssh-ed25519 KEY2" in text

    def test_run_commands_in_second_post(self) -> None:
        text = kickstart.render(
            _spec(
                ssh_authorized_keys=["ssh-ed25519 KEY"],
                run_commands=["systemctl enable sshd"],
            )
        )
        # Two %post sections: SSH first, run-commands second.
        assert text.count("%post") == 2
        assert text.count("%end") >= 3  # %packages + 2 %post
        assert "systemctl enable sshd" in text

    def test_no_run_command_post_section_when_unset(self) -> None:
        text = kickstart.render(_spec(ssh_authorized_keys=["ssh-ed25519 KEY"]))
        # Exactly one %post section (SSH keys).
        assert text.count("%post") == 1

    def test_no_post_section_when_no_keys_or_commands(self) -> None:
        text = kickstart.render(_spec())
        assert "%post" not in text

    def test_render_ends_with_newline(self) -> None:
        assert kickstart.render(_spec()).endswith("\n")
