#!/usr/bin/python3

# Author: Benjamin Drung <bdrung@ubuntu.com>

"""Test debconf configuration."""

import contextlib
import os
import pathlib
import re
import subprocess
import sys
import typing
import unittest

LEGACY_TIMEZONES = (
    "CET",
    "CST6CDT",
    "EET",
    "EST",
    "EST5EDT",
    "HST",
    "MET",
    "MST",
    "MST7MDT",
    "PST8PDT",
    "WET",
)


class TestDebconf(unittest.TestCase):
    """Test debconf configuration."""

    etc_localtime = pathlib.Path("/etc/localtime")
    etc_timezone = pathlib.Path("/etc/timezone")

    def setUp(self) -> None:
        self.orig_timezone = self._get_timezone()
        with contextlib.suppress(FileNotFoundError):
            self.etc_timezone.unlink()

    def tearDown(self) -> None:
        self._set_timezone(self.orig_timezone)

    @staticmethod
    def _call_debconf_set_selections(selections: str) -> None:
        subprocess.run(
            ["debconf-set-selections"], check=True, encoding="utf-8", input=selections
        )

    @staticmethod
    def _call_dpkg_reconfigure() -> None:
        subprocess.run(
            ["dpkg-reconfigure", "--frontend", "noninteractive", "tzdata"], check=True
        )

    @staticmethod
    def _get_debconf_selections():
        debconf_show = subprocess.run(
            ["debconf-show", "tzdata"], capture_output=True, check=True, text=True
        )
        debconf_re = re.compile("^[ *] ([^:]+): *(.*)$", flags=re.MULTILINE)
        return dict(debconf_re.findall(debconf_show.stdout))

    def _get_selection(self) -> str:
        selections = self._get_debconf_selections()
        area = selections["tzdata/Areas"]
        zone = selections[f"tzdata/Zones/{area}"]
        return f"{area}/{zone}"

    def _get_timezone(self) -> str:
        absolute_path = self.etc_localtime.parent / os.readlink(self.etc_localtime)
        timezone = pathlib.Path(os.path.normpath(absolute_path))
        return str(timezone.relative_to("/usr/share/zoneinfo"))

    def _set_timezone(self, timezone: typing.Union[pathlib.Path, str]) -> None:
        with contextlib.suppress(FileNotFoundError):
            self.etc_localtime.unlink()
        if isinstance(timezone, str):
            target = pathlib.Path("/usr/share/zoneinfo") / timezone
        else:
            target = timezone
        self.etc_localtime.symlink_to(target)

    @staticmethod
    def _reset_debconf() -> None:
        subprocess.run(
            ["debconf-communicate", "tzdata"],
            check=True,
            encoding="utf-8",
            env={"DEBIAN_FRONTEND": "noninteractive"},
            input="RESET tzdata/Areas\nRESET tzdata/Zones/Etc\n",
            stdout=subprocess.DEVNULL,
        )

    def test_broken_symlink(self) -> None:
        """Test pointing /etc/localtime to an invalid location."""
        self._set_timezone(pathlib.Path("/bin/sh"))
        self._reset_debconf()
        self._call_dpkg_reconfigure()
        self.assertEqual(self._get_timezone(), "Etc/UTC")
        self.assertEqual(self._get_selection(), "Etc/UTC")

    def test_broken_symlink_but_debconf_preseed(self) -> None:
        """Test broken /etc/localtime but existing debconf answers."""
        self._set_timezone(pathlib.Path("/bin/sh"))
        self._call_debconf_set_selections(
            "tzdata tzdata/Areas select Pacific\n"
            "tzdata tzdata/Zones/Pacific select Yap\n"
        )
        self._call_dpkg_reconfigure()
        self.assertEqual(self._get_timezone(), "Pacific/Yap")
        self.assertEqual(self._get_selection(), "Pacific/Yap")

    def test_etc_localtime_precedes_debconf_preseed(self) -> None:
        """Test dpkg-reconfigure uses /etc/localtime over preseed."""
        self._set_timezone("Asia/Jerusalem")
        self._call_debconf_set_selections(
            "tzdata tzdata/Areas select Australia\n"
            "tzdata tzdata/Zones/Australia select Sydney\n"
        )
        self._call_dpkg_reconfigure()
        self.assertEqual(self._get_timezone(), "Asia/Jerusalem")
        self.assertEqual(self._get_selection(), "Asia/Jerusalem")

    def test_default_to_utc(self) -> None:
        """Test dpkg-reconfigure defaults to Etc/UTC."""
        self.etc_localtime.unlink()
        self._reset_debconf()
        self._call_dpkg_reconfigure()
        self.assertEqual(self._get_timezone(), "Etc/UTC")
        self.assertEqual(self._get_selection(), "Etc/UTC")

    def test_legacy_timezones(self) -> None:
        """Test that legacy timezones can be selected (LP: #2070285)."""
        for timezone in LEGACY_TIMEZONES:
            with self.subTest(timezone):
                self._set_timezone(timezone)
                self._call_dpkg_reconfigure()
                self.assertEqual(self._get_timezone(), timezone)

    def test_reconfigure_creates_etc_timezone(self) -> None:
        """Test dpkg-reconfigure does create /etc/timezone."""
        self._set_timezone("America/New_York")
        self._call_dpkg_reconfigure()
        self.assertEqual(self._get_timezone(), "America/New_York")
        self.assertTrue(self.etc_timezone.exists())
        self.assertEqual(self._get_selection(), "America/New_York")

    def test_reconfigure_updates_etc_timezone(self) -> None:
        """Test dpkg-reconfigure updates existing /etc/timezone."""
        self._set_timezone("Europe/Oslo")
        self.etc_timezone.write_bytes(b"")
        self._call_dpkg_reconfigure()
        self.assertEqual(self._get_timezone(), "Europe/Oslo")
        self.assertEqual(self.etc_timezone.read_text(encoding="utf-8"), "Europe/Oslo\n")
        self.assertEqual(self._get_selection(), "Europe/Oslo")

    def test_reconfigure_fallback_to_etc_timezone(self) -> None:
        """Test dpkg-reconfigure reads /etc/timezone for missing /etc/localtime."""
        self.etc_localtime.unlink()
        self.etc_timezone.write_text("Europe/Kiev\n", encoding="utf-8")
        self._call_dpkg_reconfigure()
        self.assertEqual(self._get_timezone(), "Europe/Kyiv")
        self.assertEqual(self.etc_timezone.read_text(encoding="utf-8"), "Europe/Kyiv\n")
        self.assertEqual(self._get_selection(), "Europe/Kyiv")

    def test_update_obsolete_timezone(self) -> None:
        """Test updating obsolete timezone to current one."""
        self._set_timezone("Mideast/Riyadh88")
        self.etc_timezone.write_text("Mideast/Riyadh88\n", encoding="utf-8")
        self._call_dpkg_reconfigure()
        self.assertEqual(self._get_timezone(), "Asia/Riyadh")
        self.assertEqual(self._get_selection(), "Asia/Riyadh")

    def test_preesed(self) -> None:
        """Test preseeding answers with non-existing /etc/localtime."""
        self.etc_localtime.unlink()
        self._call_debconf_set_selections(
            "tzdata tzdata/Areas select Europe\n"
            "tzdata tzdata/Zones/Europe select Berlin\n"
        )
        self._call_dpkg_reconfigure()
        self.assertEqual(self._get_timezone(), "Europe/Berlin")
        self.assertEqual(self._get_selection(), "Europe/Berlin")


def main() -> None:
    """Run unit tests in verbose mode."""
    argv = sys.argv.copy()
    argv.insert(1, "-v")
    unittest.main(argv=argv)


if __name__ == "__main__":
    main()
