#!/usr/bin/python3
"""Unit tests for DistUpgrade.telemetry."""

import json
import os
import stat
import tempfile
import unittest

from mock import MagicMock, mock_open, patch

import DistUpgrade.telemetry as telemetry_module

# Paths that must be mocked (system resources)
PATCH_OS_RELEASE = "DistUpgrade.telemetry.platform.freedesktop_os_release"
PATCH_WHICH = "DistUpgrade.telemetry.shutil.which"
PATCH_ISDIR = "DistUpgrade.telemetry.os.path.isdir"
PATCH_GETPWALL = "DistUpgrade.telemetry.pwd.getpwall"
PATCH_GETPWNAM = "DistUpgrade.telemetry.pwd.getpwnam"
PATCH_SUBPROCESS = "DistUpgrade.telemetry.subprocess.run"


class TelemetryTestBase(unittest.TestCase):
    """Base class with common setup and helper methods."""

    def setUp(self):
        telemetry_module._Telemetry._telemetry = None
        self.tmpdir = tempfile.mkdtemp()

    def tearDown(self):
        telemetry_module._Telemetry._telemetry = None
        # Clean up temp directory
        import shutil

        shutil.rmtree(self.tmpdir, ignore_errors=True)

    def mock_open_for_init(self, uptime="100.0 200.0"):
        """Mock open() for initialization (uptime + media-info)."""

        def open_side_effect(path, *args, **kwargs):
            if path == "/proc/uptime":
                return mock_open(read_data=uptime)()
            elif path == "/var/log/installer/media-info":
                raise FileNotFoundError()
            # Allow real file operations for temp files
            return open(path, *args, **kwargs)

        return open_side_effect

    def create_user_entry(
        self, name="testuser", uid=1000, home_dir="/home/testuser"
    ):
        """Create a mock passwd entry."""
        entry = MagicMock()
        entry.pw_name = name
        entry.pw_uid = uid
        entry.pw_dir = home_dir
        return entry

    def create_telemetry(self):
        """Create a _Telemetry instance with temp file destination."""
        mock_release = MagicMock(return_value={"VERSION_ID": "24.04"})

        with (
            patch(PATCH_OS_RELEASE, mock_release),
            patch("builtins.open", side_effect=self.mock_open_for_init()),
        ):
            t = telemetry_module._Telemetry()

        # Redirect output to temp directory
        t._dest_path = os.path.join(self.tmpdir, "telemetry")
        return t


class TestTelemetryDone(TelemetryTestBase):
    """Tests for the done() method - file writing."""

    def test_done_writes_valid_json(self):
        t = self.create_telemetry()
        t.set_updater_type("gtk")
        t.set_using_third_party_sources(True)

        with patch(PATCH_WHICH, return_value=None):
            t.done()

        with open(t._dest_path) as f:
            data = json.load(f)

        self.assertEqual(data["Type"], "gtk")
        self.assertEqual(data["ThirdPartySources"], True)
        self.assertIn("Stages", data)
        self.assertIn("From", data)

    def test_done_creates_directory(self):
        t = self.create_telemetry()
        t._dest_path = os.path.join(self.tmpdir, "subdir", "telemetry")

        with patch(PATCH_WHICH, return_value=None):
            t.done()

        self.assertTrue(os.path.exists(t._dest_path))

    def test_done_sets_file_permissions(self):
        t = self.create_telemetry()

        with patch(PATCH_WHICH, return_value=None):
            t.done()

        file_stat = os.stat(t._dest_path)
        # Check readable by owner/group/other, writable by owner only
        self.assertTrue(file_stat.st_mode & stat.S_IRUSR)
        self.assertTrue(file_stat.st_mode & stat.S_IWUSR)
        self.assertTrue(file_stat.st_mode & stat.S_IRGRP)
        self.assertTrue(file_stat.st_mode & stat.S_IROTH)
        self.assertFalse(file_stat.st_mode & stat.S_IWGRP)
        self.assertFalse(file_stat.st_mode & stat.S_IWOTH)

    def test_done_skips_insights_when_not_available(self):
        t = self.create_telemetry()

        with (
            patch(PATCH_WHICH, return_value=None),
            patch(PATCH_SUBPROCESS) as mock_sub,
        ):
            t.done()

        # subprocess should never be called if ubuntu-insights not found
        mock_sub.assert_not_called()


class TestTelemetryUbuntuInsights(TelemetryTestBase):
    """Tests for ubuntu-insights integration."""

    def test_calls_insights_for_consented_user(self):
        t = self.create_telemetry()
        user = self.create_user_entry()
        mock_result = MagicMock(returncode=0)

        with (
            patch(PATCH_WHICH, return_value="/usr/bin/ubuntu-insights"),
            patch(PATCH_ISDIR, return_value=True),
            patch(PATCH_GETPWALL, return_value=[user]),
            patch(PATCH_GETPWNAM, return_value=user),
            patch(PATCH_SUBPROCESS, return_value=mock_result) as mock_sub,
        ):
            t.done()

        # Check actual call arguments
        call_cmds = [c[0][0] for c in mock_sub.call_args_list]
        self.assertTrue(any("consent" in cmd for cmd in call_cmds))
        self.assertTrue(any("collect" in cmd for cmd in call_cmds))

    def test_migrates_default_consent_to_source(self):
        t = self.create_telemetry()
        user = self.create_user_entry()
        mock_results = [
            MagicMock(returncode=1),  # No source-specific consent
            MagicMock(
                returncode=0, stdout=b"consent: true"
            ),  # Default consent
            MagicMock(returncode=0),  # Set source consent
            MagicMock(returncode=0),  # Collect
        ]

        with (
            patch(PATCH_WHICH, return_value="/usr/bin/ubuntu-insights"),
            patch(PATCH_ISDIR, return_value=True),
            patch(PATCH_GETPWALL, return_value=[user]),
            patch(PATCH_GETPWNAM, return_value=user),
            patch(PATCH_SUBPROCESS, side_effect=mock_results) as mock_sub,
        ):
            t.done()

        # Verify consent was migrated with -s flag and correct value
        call_cmds = [c[0][0] for c in mock_sub.call_args_list]
        consent_set_calls = [cmd for cmd in call_cmds if "-s" in cmd]
        self.assertListEqual(
            consent_set_calls,
            [["ubuntu-insights", "consent", t._source, "-s", "true"]],
        )

    def test_skips_users_without_insights_config(self):
        t = self.create_telemetry()
        user = self.create_user_entry()

        with (
            patch(PATCH_WHICH, return_value="/usr/bin/ubuntu-insights"),
            patch(PATCH_ISDIR, return_value=False),
            patch(PATCH_GETPWALL, return_value=[user]),
            patch(PATCH_SUBPROCESS) as mock_sub,
        ):
            t.done()

        # No subprocess calls if user doesn't have insights config
        mock_sub.assert_not_called()

    def test_filters_system_users(self):
        t = self.create_telemetry()
        system_user = self.create_user_entry("root", 0, "/root")
        regular_user = self.create_user_entry(
            "testuser", 1000, "/home/testuser"
        )

        with (
            patch(PATCH_WHICH, return_value="/usr/bin/ubuntu-insights"),
            patch(PATCH_ISDIR, return_value=False) as mock_isdir,
            patch(PATCH_GETPWALL, return_value=[system_user, regular_user]),
        ):
            t.done()

        # Should only check config dir for regular user (uid >= 1000)
        mock_isdir.assert_called_once()
        call_arg = mock_isdir.call_args[0][0]
        self.assertIn("testuser", call_arg)
        self.assertNotIn("root", call_arg)

    def test_continues_on_subprocess_error(self):
        t = self.create_telemetry()
        user = self.create_user_entry()

        with (
            patch(PATCH_WHICH, return_value="/usr/bin/ubuntu-insights"),
            patch(PATCH_ISDIR, return_value=True),
            patch(PATCH_GETPWALL, return_value=[user]),
            patch(PATCH_GETPWNAM, return_value=user),
            patch(PATCH_SUBPROCESS, side_effect=Exception("error")),
        ):
            # Should not raise - errors are logged and skipped
            t.done()

        # Verify telemetry file was still written
        self.assertTrue(os.path.exists(t._dest_path))

    def test_consent_parsing_true(self):
        t = self.create_telemetry()
        user = self.create_user_entry()
        mock_results = [
            MagicMock(returncode=1),
            MagicMock(returncode=0, stdout=b"Consent: True"),
            MagicMock(returncode=0),
            MagicMock(returncode=0),
        ]

        with (
            patch(PATCH_WHICH, return_value="/usr/bin/ubuntu-insights"),
            patch(PATCH_ISDIR, return_value=True),
            patch(PATCH_GETPWALL, return_value=[user]),
            patch(PATCH_GETPWNAM, return_value=user),
            patch(PATCH_SUBPROCESS, side_effect=mock_results) as mock_sub,
        ):
            t.done()

        # Find the consent set call and verify 'true' was passed
        call_cmds = [c[0][0] for c in mock_sub.call_args_list]
        consent_set_calls = [cmd for cmd in call_cmds if "-s" in cmd]
        expected = [["ubuntu-insights", "consent", t._source, "-s", "true"]]
        self.assertListEqual(consent_set_calls, expected)

    def test_consent_parsing_false(self):
        t = self.create_telemetry()
        user = self.create_user_entry()
        mock_results = [
            MagicMock(returncode=1),
            MagicMock(returncode=0, stdout=b"Consent: False"),
            MagicMock(returncode=0),
            MagicMock(returncode=0),
        ]

        with (
            patch(PATCH_WHICH, return_value="/usr/bin/ubuntu-insights"),
            patch(PATCH_ISDIR, return_value=True),
            patch(PATCH_GETPWALL, return_value=[user]),
            patch(PATCH_GETPWNAM, return_value=user),
            patch(PATCH_SUBPROCESS, side_effect=mock_results) as mock_sub,
        ):
            t.done()

        # Find the consent set call and verify 'false' was passed
        call_cmds = [c[0][0] for c in mock_sub.call_args_list]
        consent_set_calls = [cmd for cmd in call_cmds if "-s" in cmd]
        self.assertListEqual(
            consent_set_calls,
            [["ubuntu-insights", "consent", t._source, "-s", "false"]],
        )


if __name__ == "__main__":
    unittest.main()
