# This file is part of cloud-init. See LICENSE file for license information.
# pylint: disable=attribute-defined-outside-init

"""test_apt_custom_sources_list
Test templating of custom sources list
"""
import stat

import pytest

from cloudinit import subp, util
from cloudinit.config import cc_apt_configure
from cloudinit.distros.debian import Distro
from cloudinit.subp import SubpResult
from tests.unittests.util import get_cloud

TARGET = "/"

# Input and expected output for the custom template
EXAMPLE_TMPL = """\
## template:jinja
deb {{mirror}} {{codename}} main restricted
deb-src {{mirror}} {{codename}} main restricted
deb {{mirror}} {{codename}}-updates universe restricted
deb {{security}} {{codename}}-security multiverse
"""

EXAMPLE_TMPL_DEB822 = """\
## template:jinja
# Generated by cloud-init
Types: deb deb-src
URIs: {{mirror}}
Suites: {{codename}} {{codename}}-updates
Components: main restricted

# Security section
Types: deb deb-src
URIs: {{security}}
Suites: {{codename}}-security
Components: main restricted
"""

YAML_TEXT_CUSTOM_SL = """
apt:
  primary:
    - arches: [default]
      uri: http://test.ubuntu.com/ubuntu/
  security:
    - arches: [default]
      uri: http://testsec.ubuntu.com/ubuntu/
  sources_list: |

      # Note, this file is written by cloud-init at install time.
      deb $MIRROR $RELEASE main restricted
      deb-src $MIRROR $RELEASE main restricted
      deb $PRIMARY $RELEASE universe restricted
      deb $SECURITY $RELEASE-security multiverse
      # FIND_SOMETHING_SPECIAL
"""

EXPECTED_CONVERTED_CONTENT = """
# Note, this file is written by cloud-init at install time.
deb http://test.ubuntu.com/ubuntu/ fakerel main restricted
deb-src http://test.ubuntu.com/ubuntu/ fakerel main restricted
deb http://test.ubuntu.com/ubuntu/ fakerel universe restricted
deb http://testsec.ubuntu.com/ubuntu/ fakerel-security multiverse
"""

YAML_TEXT_CUSTOM_SL_DEB822 = """
apt:
  primary:
    - arches: [default]
      uri: http://test.ubuntu.com/ubuntu/
  security:
    - arches: [default]
      uri: http://testsec.ubuntu.com/ubuntu/
  sources_list: |

      # custom deb822 sources_list deb822
      Types: deb deb-src
      URIs: $PRIMARY
      Suites: $RELEASE $RELEASE-updates
      Components: main restricted

      Types: deb deb-src
      URIs: $SECURITY
      Suites: $RELEASE-security
      Components: main restricted
"""

EXPECTED_CONVERTED_CONTENT_DEB822 = """
# custom deb822 sources_list deb822
Types: deb deb-src
URIs: http://test.ubuntu.com/ubuntu/
Suites: fakerel fakerel-updates
Components: main restricted

Types: deb deb-src
URIs: http://testsec.ubuntu.com/ubuntu/
Suites: fakerel-security
Components: main restricted
"""

# mocked to be independent to the unittest system

EXPECTED_BASE_CONTENT = """\
deb http://archive.ubuntu.com/ubuntu/ fakerel main restricted
deb-src http://archive.ubuntu.com/ubuntu/ fakerel main restricted
deb http://archive.ubuntu.com/ubuntu/ fakerel-updates universe restricted
deb http://security.ubuntu.com/ubuntu/ fakerel-security multiverse
"""

EXPECTED_MIRROR_CONTENT = """\
deb http://test.ubuntu.com/ubuntu/ fakerel main restricted
deb-src http://test.ubuntu.com/ubuntu/ fakerel main restricted
deb http://test.ubuntu.com/ubuntu/ fakerel-updates main restricted
deb http://test.ubuntu.com/ubuntu/ fakerel-security main restricted
"""

EXPECTED_BASE_CONTENT_DEB822 = """\
Types: deb deb-src
URIs: http://archive.ubuntu.com/ubuntu/
Suites: fakerel fakerel-updates
Components: main restricted

# Security section
Types: deb deb-src
URIs: http://security.ubuntu.com/ubuntu/
Suites: fakerel-security
Components: main restricted
"""

EXPECTED_PRIMSEC_CONTENT = """\
deb http://test.ubuntu.com/ubuntu/ fakerel main restricted
deb-src http://test.ubuntu.com/ubuntu/ fakerel main restricted
deb http://test.ubuntu.com/ubuntu/ fakerel-updates universe restricted
deb http://testsec.ubuntu.com/ubuntu/ fakerel-security multiverse
"""

EXPECTED_PRIMSEC_CONTENT_DEB822 = """# Generated by cloud-init
Types: deb deb-src
URIs: http://test.ubuntu.com/ubuntu/
Suites: fakerel fakerel-updates
Components: main restricted

# Security section
Types: deb deb-src
URIs: http://testsec.ubuntu.com/ubuntu/
Suites: fakerel-security
Components: main restricted
"""

EXAMPLE_CUSTOM_KEY_TMPL_DEB822 = """\
## template:jinja
# Generated by cloud-init
Types: deb deb-src
URIs: {{mirror}}
Suites: {{codename}} {{codename}}-updates
Components: main restricted
Signed-By: {{
    primary_key
    | default('/usr/share/keyrings/ubuntu-archive-keyring.gpg', true)
}}

# Security section
Types: deb deb-src
URIs: {{security}}
Suites: {{codename}}-security
Components: main restricted
Signed-By: {{
    security_key
    | default(primary_key, true)
    | default('/usr/share/keyrings/ubuntu-archive-keyring.gpg', true)
}}
"""

EXPECTED_PM_BASE_CUSTOM_KEYS = """\
# Generated by cloud-init
Types: deb deb-src
URIs: http://local.ubuntu.com/
Suites: fakerel fakerel-updates
Components: main restricted
"""

EXPECTED_SM_BASE_CUSTOM_KEYS = """\
# Security section
Types: deb deb-src
URIs: http://local.ubuntu.com/
Suites: fakerel-security
Components: main restricted
"""


@pytest.mark.usefixtures("fake_filesystem")
class TestAptSourceConfigSourceList:
    """TestAptSourceConfigSourceList - Class to test sources list rendering"""

    @pytest.fixture(autouse=True)
    def common_mocks(self, mocker):
        self.subp = mocker.patch.object(
            subp,
            "subp",
            return_value=SubpResult("PPID   PID", ""),
        )
        lsb = mocker.patch("cloudinit.util.lsb_release")
        lsb.return_value = {"codename": "fakerel"}
        m_arch = mocker.patch("cloudinit.util.get_dpkg_architecture")
        m_arch.return_value = "amd64"
        mocker.patch("cloudinit.config.cc_apt_configure._ensure_dependencies")
        self.deb822 = mocker.patch.object(
            cc_apt_configure.features, "APT_DEB822_SOURCE_LIST_FILE", True
        )
        mocker.patch.object(
            cc_apt_configure,
            "get_apt_cfg",
            return_value={
                "sourcelist": "/etc/apt/sources.list",
                "sourceparts": "/etc/apt/sources.list.d/",
            },
        )

    @pytest.mark.parametrize(
        "distro,template_present",
        (("ubuntu", True), ("debian", True), ("rhel", False)),
    )
    def test_apt_v3_empty_cfg_source_list_by_distro(
        self, distro, template_present, mocker, tmpdir
    ):
        """Template based on distro, empty config relies on mirror default."""
        template = f"/etc/cloud/templates/sources.list.{distro}.deb822.tmpl"
        if template_present:
            util.write_file(template, EXAMPLE_TMPL_DEB822)

        mycloud = get_cloud(distro)
        mock_shouldcfg = mocker.patch.object(
            cc_apt_configure,
            "_should_configure_on_empty_apt",
            return_value=SubpResult(True, "test"),
        )
        cc_apt_configure.handle("test", {"apt": {}}, mycloud, None)

        sources_file = tmpdir.join(f"/etc/apt/sources.list.d/{distro}.sources")
        if template_present:
            assert (
                "# Generated by cloud-init\n" + EXPECTED_BASE_CONTENT_DEB822
                == sources_file.read()
            )
            assert 0o644 == stat.S_IMODE(sources_file.stat().mode)
        else:
            assert (
                sources_file.exists() is False
            ), f"Unexpected file found: {sources_file}"

        assert 1 == mock_shouldcfg.call_count

    def test_apt_v3_source_list_ubuntu_snappy(self, mocker):
        """test_apt_v3_source_list_ubuntu_snappy - without custom sources or
        parms"""
        cfg = {"apt": {}}
        mycloud = get_cloud()

        mock_writefile = mocker.patch.object(util, "write_file")
        mock_issnappy = mocker.patch.object(util, "system_is_snappy")
        mock_issnappy.return_value = True
        cc_apt_configure.handle("test", cfg, mycloud, None)
        mock_writefile.assert_not_called()
        assert 1 == mock_issnappy.call_count

    @pytest.mark.parametrize(
        "deb822,tmpl_file,tmpl_content,apt_file,expected",
        (
            (
                True,
                "/etc/cloud/templates/sources.list.ubuntu.deb822.tmpl",
                EXAMPLE_TMPL_DEB822,
                "/etc/apt/sources.list.d/ubuntu.sources",
                EXPECTED_PRIMSEC_CONTENT_DEB822,
            ),
            (
                False,
                "/etc/cloud/templates/sources.list.ubuntu.tmpl",
                EXAMPLE_TMPL,
                "/etc/apt/sources.list",
                EXPECTED_PRIMSEC_CONTENT,
            ),
        ),
    )
    def test_apt_v3_source_list_psm_deb822_feature_aware(
        self,
        deb822,
        tmpl_file,
        tmpl_content,
        apt_file,
        expected,
        tmpdir,
        mocker,
    ):
        """test_apt_v3_source_list_psm - Test specifying prim+sec mirrors

        Assert APT_DEB822_SOURCE_LIST_FILE is taken into account when
        determining which sources.list.tmpl source file to read.
        """
        pm = "http://test.ubuntu.com/ubuntu/"
        sm = "http://testsec.ubuntu.com/ubuntu/"
        cfg = {
            "preserve_sources_list": False,
            "primary": [{"arches": ["default"], "uri": pm}],
            "security": [{"arches": ["default"], "uri": sm}],
        }

        self.deb822 = mocker.patch.object(
            cc_apt_configure.features, "APT_DEB822_SOURCE_LIST_FILE", deb822
        )

        util.write_file(tmpl_file, tmpl_content)
        mycloud = get_cloud("ubuntu")
        cc_apt_configure.handle("test", {"apt": cfg}, mycloud, None)

        sources_file = tmpdir.join(apt_file)
        assert expected == sources_file.read()
        assert 0o644 == stat.S_IMODE(sources_file.stat().mode)

    @pytest.mark.parametrize(
        "deb822,cfg,apt_file,expected",
        (
            pytest.param(
                True,
                util.load_yaml(YAML_TEXT_CUSTOM_SL_DEB822),
                "/etc/apt/sources.list.d/debian.sources",
                EXPECTED_CONVERTED_CONTENT_DEB822,
                id="deb822_feature_deb822_sources_list_writes_deb822_source",
            ),
            pytest.param(
                True,
                util.load_yaml(YAML_TEXT_CUSTOM_SL),
                "/etc/apt/sources.list",
                EXPECTED_CONVERTED_CONTENT + "# FIND_SOMETHING_SPECIAL\n",
                id="deb822_feature_nondeb822_sources_list_writes_list_file",
            ),
            pytest.param(
                False,
                util.load_yaml(YAML_TEXT_CUSTOM_SL),
                "/etc/apt/sources.list",
                EXPECTED_CONVERTED_CONTENT + "# FIND_SOMETHING_SPECIAL\n",
                id="nodeb822_feature_nondeb822_sources_list_writes_list_file",
            ),
            pytest.param(
                False,
                util.load_yaml(YAML_TEXT_CUSTOM_SL_DEB822),
                "/etc/apt/sources.list.d/debian.sources",
                EXPECTED_CONVERTED_CONTENT_DEB822,
                id="nodeb822_feature_deb822_sources_list_writes_deb822_source",
            ),
        ),
    )
    def test_apt_v3_srcl_custom_deb822_feature_aware(
        self, deb822, cfg, apt_file, expected, mocker, tmpdir
    ):
        """test_apt_v3_srcl_custom - Test rendering a custom source template

        Also take into account deb822 feature flag to assert writing the
        appropriate deb822 /etc/apt/sources.list.d/*list or *source file based
        on content and deb822 feature flag.
        """
        mycloud = get_cloud("debian")

        self.deb822 = mocker.patch.object(
            cc_apt_configure.features, "APT_DEB822_SOURCE_LIST_FILE", deb822
        )
        mocker.patch.object(Distro, "get_primary_arch", return_value="amd64")
        cc_apt_configure.handle("notimportant", cfg, mycloud, None)
        sources_file = tmpdir.join(apt_file)
        assert expected == sources_file.read()
        assert 0o644 == stat.S_IMODE(sources_file.stat().mode)

    @pytest.mark.parametrize(
        "distro,pm,pmkey,sm,smkey",
        (
            pytest.param(
                "ubuntu",
                "http://local.ubuntu.com/",
                "fakekey 4321",
                "http://local.ubuntu.com/",
                "fakekey 1234",
            ),
            pytest.param(
                "ubuntu",
                "http://local.ubuntu.com/",
                "fakekey 4321",
                None,
                None,
            ),
            pytest.param(
                "ubuntu", "http://local.ubuntu.com/", None, None, None
            ),
            pytest.param(
                "ubuntu",
                "http://local.ubuntu.com/",
                None,
                "http://local.ubuntu.com/",
                "fakekey 1234",
            ),
        ),
    )
    def test_apt_v3_srcl_deb822_custom_psm_keys(
        self,
        distro,
        pm,
        pmkey,
        sm,
        smkey,
        mocker,
        tmpdir,
    ):
        """test_apt_v3_srcl_deb822_custom_psm_keys - Test the ability to
        specify raw GPG keys alongside primary and security mirrors such
        that the keys are both added to the trusted.gpg.d directory
        also the ubuntu.sources template
        """

        self.deb822 = mocker.patch.object(
            cc_apt_configure.features, "APT_DEB822_SOURCE_LIST_FILE", True
        )

        tmpl_file = f"/etc/cloud/templates/sources.list.{distro}.deb822.tmpl"
        tmpl_content = EXAMPLE_CUSTOM_KEY_TMPL_DEB822
        util.write_file(tmpl_file, tmpl_content)

        # Base config
        cfg = {
            "preserve_sources_list": False,
            "primary": [{"arches": ["default"], "uri": pm}],
        }

        # Add defined variables to the config
        if pmkey:
            cfg["primary"][0]["key"] = pmkey
        if sm:
            cfg["security"] = [{"arches": ["default"], "uri": sm}]
            if smkey:
                cfg["security"][0]["key"] = smkey

        mycloud = get_cloud(distro)
        cc_apt_configure.handle("test", {"apt": cfg}, mycloud, None)

        apt_file = f"/etc/apt/sources.list.d/{distro}.sources"
        sources_file = tmpdir.join(apt_file)

        default_keyring = f"/usr/share/keyrings/{distro}-archive-keyring.gpg"
        trusted_keys_dir = "/etc/apt/trusted.gpg.d/"
        primary_keyring = f"{trusted_keys_dir}primary.gpg"
        security_keyring = f"{trusted_keys_dir}security.gpg"

        primary_keyring_path = primary_keyring if pmkey else default_keyring
        primary_signature = f"Signed-By: {primary_keyring_path}"
        expected_pm = f"{EXPECTED_PM_BASE_CUSTOM_KEYS}{primary_signature}"

        fallback_keyring = primary_keyring if pmkey else default_keyring
        security_keyring_path = security_keyring if smkey else fallback_keyring

        security_signature = f"Signed-By: {security_keyring_path}"
        expected_sm = f"{EXPECTED_SM_BASE_CUSTOM_KEYS}{security_signature}"

        expected = f"{expected_pm}\n\n{expected_sm}\n"
        assert expected == sources_file.read()
