#!/usr/bin/python3
# -*- coding: utf-8 -*-

from pathlib import Path
from unittest.mock import MagicMock, patch

import pytest

from sun.tray_icon import (
    get_theme_colors, is_xserver_ready,
    kill_duplicate_processes, TrayIcon,
    main, sun_tray_autostart
)


class TestGetThemeColors:

    def test_returns_hex_strings(self):
        mock_color = MagicMock()
        mock_color.name.return_value = '#ffffff'
        mock_palette = MagicMock()
        mock_palette.color.return_value = mock_color
        with patch('sun.tray_icon.QtWidgets.QApplication.palette', return_value=mock_palette):
            bg, fg = get_theme_colors()
        assert bg == '#ffffff'
        assert fg == '#ffffff'


@pytest.fixture
def tray():
    """TrayIcon instance with __init__ bypassed."""
    with patch.object(TrayIcon, '__init__', return_value=None):
        t = TrayIcon()
    t.tool = MagicMock()
    t.configs = MagicMock()
    t.configs.max_packages = 10
    t.icon_path = '/usr/share/pixmaps/sun'
    t.msg_box = MagicMock()
    t.data_configs = {'icon_path': Path('/usr/share/pixmaps')}
    t.bgreen = ''
    t.endc = ''
    t._local_log = '/var/lib/slackpkg/ChangeLog.txt'
    t._data_file = ''
    t._log_watcher = MagicMock()
    return t


class TestTrayIconMethods:

    def test_show_message(self, tray):
        with patch('sun.tray_icon.get_theme_colors', return_value=('#ffffff', '#000000')):
            with patch('sun.tray_icon.QtWidgets'):
                tray.show_message('some data', 'SUN - Check Updates')

    def test_show_ok_button(self, tray):
        with patch('sun.tray_icon.QtWidgets'):
            tray.show_ok_button()

    def test_show_check_updates_no_updates(self, tray):
        tray.tool.check_updates.return_value = ('No news is good news!', [])
        tray.tool.check_pending.return_value = ('System is up to date.', [])
        with patch.object(tray, 'show_message') as mock_show:
            tray.show_check_updates()
            mock_show.assert_called_once_with('No news is good news!', 'SUN - Check Updates')

    def test_show_check_updates_with_updates(self, tray):
        pkgs = ['vim-9.0-x86_64-1.txz:  Upgraded.', 'curl-7.85-x86_64-1.txz:  Rebuilt.']
        tray.tool.check_updates.return_value = ('2 updates\n', pkgs)
        tray.tool.check_pending.return_value = ('System is up to date.', [])
        mock_color = MagicMock()
        mock_color.lightness.return_value = 200
        with patch.object(tray, 'show_message') as mock_show:
            with patch.object(tray, '_make_badge_icon', return_value=MagicMock()):
                with patch('sun.tray_icon.QtWidgets.QApplication.fontMetrics') as mock_fm:
                    with patch('sun.tray_icon.get_theme_colors', return_value=('#ffffff', '#000000')):
                        with patch('sun.tray_icon.QtGui.QColor', return_value=mock_color):
                            mock_fm.return_value.horizontalAdvance.return_value = 100
                            tray.show_check_updates()
                            mock_show.assert_called_once()

    def test_show_check_updates_more_than_10(self, tray):
        pkgs = [f'pkg-{i}.txz:  Upgraded.' for i in range(15)]
        tray.tool.check_updates.return_value = ('15 updates\n', pkgs)
        tray.tool.check_pending.return_value = ('System is up to date.', [])
        mock_color = MagicMock()
        mock_color.lightness.return_value = 200
        with patch.object(tray, 'show_message') as mock_show:
            with patch.object(tray, '_make_badge_icon', return_value=MagicMock()):
                with patch('sun.tray_icon.QtWidgets.QApplication.fontMetrics') as mock_fm:
                    with patch('sun.tray_icon.get_theme_colors', return_value=('#ffffff', '#000000')):
                        with patch('sun.tray_icon.QtGui.QColor', return_value=mock_color):
                            mock_fm.return_value.horizontalAdvance.return_value = 100
                            tray.show_check_updates()
                            call_args = mock_show.call_args[0][0]
                        assert 'and more...' in call_args

    def test_apply_badge_with_count(self, tray):
        tray.icon_path = '/fake/icons'
        with patch.object(tray, '_make_badge_icon', return_value=MagicMock()) as mock_badge:
            with patch.object(tray, 'setIcon') as mock_set:
                with patch.object(tray, 'setToolTip') as mock_tip:
                    tray._apply_badge(5, 0)
                    mock_badge.assert_called_once_with(5, '#f44336')
                    mock_set.assert_called_once()
                    mock_tip.assert_called_once_with('SUN: 5 update(s) available')

    def test_apply_badge_pending_shows_yellow(self, tray):
        tray.icon_path = '/fake/icons'
        with patch.object(tray, '_make_badge_icon', return_value=MagicMock()) as mock_badge:
            with patch.object(tray, 'setIcon'):
                with patch.object(tray, 'setToolTip') as mock_tip:
                    tray._apply_badge(0, 3)
                    mock_badge.assert_called_once_with(3, '#f0c040')
                    mock_tip.assert_called_once_with('SUN: 3 package(s) pending upgrade')

    def test_apply_badge_red_takes_priority_over_yellow(self, tray):
        tray.icon_path = '/fake/icons'
        with patch.object(tray, '_make_badge_icon', return_value=MagicMock()) as mock_badge:
            with patch.object(tray, 'setIcon'):
                with patch.object(tray, 'setToolTip'):
                    tray._apply_badge(2, 3)
                    mock_badge.assert_called_once_with(2, '#f44336')

    def test_apply_badge_zero_resets_icon(self, tray):
        tray.icon_path = '/fake/icons'
        with patch.object(tray, 'setIcon') as mock_set:
            with patch.object(tray, 'setToolTip') as mock_tip:
                tray._apply_badge(0)
                mock_set.assert_called_once()
                mock_tip.assert_called_once_with('SUN (Slackware Update Notifier)')

    def test_update_badge_silent_with_updates(self, tray):
        tray.tool.check_updates.return_value = ('3 updates\n', ['a', 'b', 'c'])
        tray.tool.check_pending.return_value = ('System is up to date.', [])
        tray.tool.daemon_status.return_value = True
        tray._log_watcher = MagicMock()
        tray._log_watcher.files.return_value = []
        tray._log_watcher.directories.return_value = ['/var/log/packages']
        tray.configs.repo_log_path = '/var/lib/slackpkg/'
        tray.configs.repo_log_file = 'ChangeLog.txt'
        with patch('sun.tray_icon.QtCore.QDir.cleanPath', return_value='/var/lib/slackpkg/ChangeLog.txt'):
            with patch.object(tray, '_apply_badge') as mock_apply:
                tray._update_badge_silent()
                mock_apply.assert_called_once_with(3, 0, True)
                tray._log_watcher.addPath.assert_called_once()

    def test_update_badge_silent_no_updates(self, tray):
        tray.tool.check_updates.return_value = ('No news is good news!', [])
        tray.tool.check_pending.return_value = ('System is up to date.', [])
        tray.tool.daemon_status.return_value = True
        tray._log_watcher = MagicMock()
        tray.configs.repo_log_path = '/var/lib/slackpkg/'
        tray.configs.repo_log_file = 'ChangeLog.txt'
        expected = '/var/lib/slackpkg/ChangeLog.txt'
        tray._log_watcher.files.return_value = [expected]
        tray._log_watcher.directories.return_value = ['/var/log/packages']
        with patch('sun.tray_icon.QtCore.QDir.cleanPath', return_value=expected):
            with patch.object(tray, '_apply_badge') as mock_apply:
                tray._update_badge_silent()
                mock_apply.assert_called_once_with(0, 0, True)
                tray._log_watcher.addPath.assert_not_called()

    def test_update_badge_silent_daemon_offline(self, tray):
        tray.tool.check_updates.return_value = ('No news is good news!', [])
        tray.tool.check_pending.return_value = ('System is up to date.', [])
        tray.tool.daemon_status.return_value = False
        tray._log_watcher = MagicMock()
        tray.configs.repo_log_path = '/var/lib/slackpkg/'
        tray.configs.repo_log_file = 'ChangeLog.txt'
        tray._log_watcher.files.return_value = ['/var/lib/slackpkg/ChangeLog.txt']
        with patch('sun.tray_icon.QtCore.QDir.cleanPath', return_value='/var/lib/slackpkg/ChangeLog.txt'):
            with patch.object(tray, '_apply_badge') as mock_apply:
                tray._update_badge_silent()
                mock_apply.assert_called_once_with(0, 0, False)

    def test_update_badge_silent_with_pending(self, tray):
        tray.tool.check_updates.return_value = ('No news is good news!', [])
        tray.tool.check_pending.return_value = ('2 Package(s) Pending Upgrade\n', ['vim-9.1.txz', 'curl-8.txz'])
        tray.tool.daemon_status.return_value = True
        tray._log_watcher = MagicMock()
        tray._log_watcher.files.return_value = []
        tray.configs.repo_log_path = '/var/lib/slpkg/repos/slack/'
        tray.configs.repo_log_file = 'ChangeLog.txt'
        with patch('sun.tray_icon.QtCore.QDir.cleanPath', return_value='/var/lib/slpkg/repos/slack/ChangeLog.txt'):
            with patch.object(tray, '_apply_badge') as mock_apply:
                tray._update_badge_silent()
                mock_apply.assert_called_once_with(0, 2, True)

    def test_show_os_info(self, tray):
        tray.tool.utils.os_info_html.return_value = '<b>OS</b>: Slackware'
        with patch.object(tray, 'show_message') as mock_show:
            tray.show_os_info()
            mock_show.assert_called_once_with('<b>OS</b>: Slackware', 'SUN - OS Info')

    def test_show_config(self, tray):
        tray.tool.utils.config_html.return_value = '<b>INTERVAL</b>  =  720'
        with patch.object(tray, 'show_message') as mock_show:
            tray.show_config()
            mock_show.assert_called_once_with('<b>INTERVAL</b>  =  720', 'SUN - Config')

    def test_show_config_no_color(self, tray):
        tray.configs.colored_output = False
        tray.tool.utils.config_html.return_value = '<span style="color: #00bcd4;"><b>[TIME]</b></span>'
        with patch.object(tray, 'show_message') as mock_show:
            tray.show_config()
            args = mock_show.call_args[0][0]
            assert '<span' not in args
            assert '[TIME]' in args

    def test_daemon_start(self, tray):
        tray.tool.daemon_process.return_value = 'SUN daemon starting...'
        tray.tool.utils.get_daemon_pid.return_value = '1234'
        with patch.object(tray, 'show_message') as mock_show:
            with patch('time.sleep'):
                with patch('sun.tray_icon.QtCore.QTimer.singleShot') as mock_timer:
                    tray.daemon_start()
                    mock_timer.assert_called_once_with(3000, tray._update_badge_silent)
            mock_show.assert_called_once_with('SUN daemon starting... (PID 1234)', 'SUN - Daemon Start')

    def test_daemon_stop_with_pid(self, tray):
        tray.tool.utils.get_daemon_pid.return_value = '1234'
        tray.tool.daemon_process.return_value = 'SUN daemon stopping... (PID 1234)'
        with patch.object(tray, 'show_message') as mock_show:
            with patch('sun.tray_icon.QtCore.QTimer.singleShot') as mock_timer:
                tray.daemon_stop()
                mock_timer.assert_called_once_with(0, tray._update_badge_silent)
            mock_show.assert_called_once()

    def test_daemon_stop_no_pid(self, tray):
        tray.tool.utils.get_daemon_pid.return_value = ''
        tray.tool.daemon_process.return_value = 'SUN is not running'
        with patch.object(tray, 'show_message') as mock_show:
            with patch('sun.tray_icon.QtCore.QTimer.singleShot') as mock_timer:
                tray.daemon_stop()
                mock_timer.assert_not_called()
            mock_show.assert_called_once()

    def test_daemon_restart(self, tray):
        tray.tool.utils.get_daemon_pid.return_value = '1234'
        tray.tool.daemon_process.return_value = 'SUN daemon restarting...'
        with patch.object(tray, 'show_message') as mock_show:
            with patch('time.sleep'):
                with patch('sun.tray_icon.QtCore.QTimer.singleShot') as mock_timer:
                    tray.daemon_restart()
                    mock_timer.assert_called_once_with(3000, tray._update_badge_silent)
            mock_show.assert_called_once()

    def test_daemon_status_running(self, tray):
        tray.tool.daemon_status.return_value = True
        tray.tool.utils.get_daemon_pid.return_value = '1234'
        with patch.object(tray, 'show_message') as mock_show:
            tray.daemon_status()
            call_args = mock_show.call_args[0][0]
            assert 'PID 1234' in call_args

    def test_daemon_status_not_running(self, tray):
        tray.tool.daemon_status.return_value = False
        tray.tool.utils.get_daemon_pid.return_value = ''
        with patch.object(tray, 'show_message') as mock_show:
            tray.daemon_status()
            call_args = mock_show.call_args[0][0]
            assert 'not running' in call_args

    def test_show_pending_upgrades_no_pending(self, tray):
        tray.tool.check_pending.return_value = ('System is up to date.', [])
        with patch.object(tray, 'show_message') as mock_show:
            tray.show_pending_upgrades()
            mock_show.assert_called_once_with('System is up to date.', 'SUN - Pending Upgrades')

    def test_show_pending_upgrades_with_packages(self, tray):
        pkgs = ['vim-9.1-x86_64-1.txz', 'curl-8.0-x86_64-1.txz']
        tray.tool.check_pending.return_value = ('2 Package(s) Pending Upgrade\n', pkgs)
        with patch.object(tray, 'show_message') as mock_show:
            with patch('sun.tray_icon.QtWidgets.QApplication.fontMetrics') as mock_fm:
                mock_fm.return_value.horizontalAdvance.return_value = 100
                tray.show_pending_upgrades()
                mock_show.assert_called_once()
                call_args = mock_show.call_args[0][0]
                assert 'vim-9.1-x86_64-1' in call_args
                assert '.txz' not in call_args

    def test_show_pending_upgrades_more_than_max(self, tray):
        pkgs = [f'pkg-{i}-x86_64-1.txz' for i in range(15)]
        tray.tool.check_pending.return_value = ('15 Package(s) Pending Upgrade\n', pkgs)
        with patch.object(tray, 'show_message') as mock_show:
            with patch('sun.tray_icon.QtWidgets.QApplication.fontMetrics') as mock_fm:
                mock_fm.return_value.horizontalAdvance.return_value = 100
                tray.show_pending_upgrades()
                call_args = mock_show.call_args[0][0]
                assert 'and more...' in call_args

    def _run_changelog(self, tray, content, lightness=200):
        """Helper: run show_changelog and return the text_edit mock."""
        mock_color = MagicMock()
        mock_color.lightness.return_value = lightness
        mock_text_edit = MagicMock()
        mock_widgets = MagicMock()
        mock_widgets.QTextEdit.return_value = mock_text_edit
        with patch.object(tray.tool.utils, 'read_repo_text_file', return_value=content):
            with patch('sun.tray_icon.get_theme_colors', return_value=('#ffffff', '#000000')):
                with patch('sun.tray_icon.QtWidgets', mock_widgets):
                    with patch('sun.tray_icon.QtGui.QColor', return_value=mock_color):
                        tray.show_changelog()
        return mock_text_edit

    def test_show_changelog_date_line_colored(self, tray):
        tray.configs.colored_output = True
        tray.configs.log_font_size = 12
        text_edit = self._run_changelog(tray, 'Mon Jan 01 00:00:00 UTC 2026\npatches/pkg.txz:  Upgraded.')
        html_arg = text_edit.setHtml.call_args[0][0]
        assert '#b8860b' in html_arg  # light-theme date color
        assert 'font-weight:bold' in html_arg
        assert 'font-size: 12pt' in html_arg

    def test_show_changelog_comment_line_colored(self, tray):
        tray.configs.colored_output = True
        text_edit = self._run_changelog(tray, 'Mon Jan 01 00:00:00 UTC 2026\n  Patrick comment.')
        html_arg = text_edit.setHtml.call_args[0][0]
        assert '#5a7080' in html_arg  # light-theme comment color
        assert 'Patrick comment.' in html_arg

    def test_show_changelog_dark_theme_colors(self, tray):
        tray.configs.colored_output = True
        text_edit = self._run_changelog(tray, 'Mon Jan 01 00:00:00 UTC 2026\n  comment', lightness=50)
        html_arg = text_edit.setHtml.call_args[0][0]
        assert '#f0c040' in html_arg   # dark-theme date color
        assert '#88aabb' in html_arg   # dark-theme comment color

    def test_show_changelog_no_color(self, tray):
        tray.configs.colored_output = False
        text_edit = self._run_changelog(tray, 'Mon Jan 01 00:00:00 UTC 2026\n  comment')
        text_edit.setPlainText.assert_called_once()
        text_edit.setHtml.assert_not_called()

    def test_show_changelog_fetch_failure(self, tray):
        tray.configs.colored_output = True
        text_edit = self._run_changelog(tray, '')
        text_edit.setPlainText.assert_called_once_with('SUN: Error: Failed to fetch ChangeLog.')

    def test_show_about(self, tray):
        with patch('sun.tray_icon.get_theme_colors', return_value=('#ffffff', '#000000')):
            with patch('sun.tray_icon.QtWidgets'):
                with patch('sun.tray_icon.QtGui'):
                    with patch('sun.tray_icon.QtCore'):
                        tray.show_about()

    def test_exit_app(self, tray, capsys):
        with patch('sun.tray_icon.QtWidgets'):
            tray.exit_app()
        assert 'Exiting' in capsys.readouterr().out

    def test_on_tray_icon_activated_trigger(self, tray):
        with patch.object(tray, 'show_check_updates') as mock_check:
            from sun.tray_icon import QtWidgets
            tray.on_tray_icon_activated(QtWidgets.QSystemTrayIcon.Trigger)
            mock_check.assert_called_once()

    def test_reload_app(self, tray):
        with patch('sun.tray_icon.QtWidgets'):
            with patch('os.execl') as mock_execl:
                tray.reload_app()
                mock_execl.assert_called_once()


class TestIsXserverReady:

    def test_display_set(self, monkeypatch):
        monkeypatch.setenv('DISPLAY', ':0')
        assert is_xserver_ready() == ':0'

    def test_display_not_set(self, monkeypatch):
        monkeypatch.delenv('DISPLAY', raising=False)
        assert is_xserver_ready() == ''


class TestKillDuplicateProcesses:

    def test_no_duplicates(self):
        with patch('sun.tray_icon.Utilities') as mock_utils_cls:
            mock_utils_cls.return_value.process_pids.return_value = [1234]
            with patch('os.kill') as mock_kill:
                kill_duplicate_processes('sun-tray')
                mock_kill.assert_not_called()

    def test_kills_extra_processes(self):
        with patch('sun.tray_icon.Utilities') as mock_utils_cls:
            mock_utils_cls.return_value.process_pids.return_value = [1234, 5678, 9012]
            with patch('os.kill') as mock_kill:
                kill_duplicate_processes('sun-tray')
                assert mock_kill.call_count == 2

    def test_handles_process_not_found(self, capsys):
        with patch('sun.tray_icon.Utilities') as mock_utils_cls:
            mock_utils_cls.return_value.process_pids.return_value = [1234, 5678]
            with patch('os.kill', side_effect=ProcessLookupError):
                kill_duplicate_processes('sun-tray')
                assert 'not found' in capsys.readouterr().out

    def test_handles_permission_error(self, capsys):
        with patch('sun.tray_icon.Utilities') as mock_utils_cls:
            mock_utils_cls.return_value.process_pids.return_value = [1234, 5678]
            with patch('os.kill', side_effect=PermissionError):
                kill_duplicate_processes('sun-tray')
                assert 'permission' in capsys.readouterr().out.lower()


class TestSunTrayAutostart:

    def test_starts_when_xserver_ready(self, capsys):
        with patch('sun.tray_icon.is_xserver_ready', return_value=':0'):
            with patch('sun.tray_icon.configs_load') as mock_configs:
                mock_configs.delay_load = 0
                with patch('time.sleep'):
                    with patch('sun.tray_icon.kill_duplicate_processes'):
                        with patch('sun.tray_icon.main'):
                            sun_tray_autostart()
        assert 'running' in capsys.readouterr().out


class TestMain:

    def test_main_initializes_qt(self):
        with patch('sun.tray_icon.QtWidgets') as mock_qt:
            with patch('sun.tray_icon.QtCore'):
                with patch('sun.tray_icon.TrayIcon'):
                    with patch('sun.tray_icon.signal'):
                        with patch('sys.exit'):
                            main()
            mock_qt.QApplication.assert_called_once()
