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

import logging
import signal
import pytest
from unittest.mock import MagicMock, patch

from sun.configs import TRAY_PID_FILE
from sun.daemon import Notifier, _setup_logger, main


def make_notifier() -> Notifier:
    return Notifier(logger=MagicMock(spec=logging.Logger))


class TestNotifier:

    def test_init_defaults(self):
        notifier = make_notifier()
        assert notifier.count_packages == 0
        assert notifier.message == ''
        assert 'Slackware Updates' in notifier.title
        assert 'SUN' not in notifier.title

    def test_set_notification_message_no_updates(self):
        notifier = make_notifier()
        with patch.object(notifier.fetch, 'updates', return_value=iter([])):
            notifier.set_notification_message()
            assert notifier.count_packages == 0

    def test_set_notification_message_with_updates(self):
        notifier = make_notifier()
        with patch.object(notifier.fetch, 'updates', return_value=iter(['vim', 'curl'])):
            notifier.set_notification_message()
            assert notifier.count_packages == 2
            assert '2' in notifier.message

    def test_icon_path_set(self):
        notifier = make_notifier()
        assert 'sun' in notifier.icon

    def test_daemon_shows_notification_when_updates(self):
        notifier = make_notifier()
        notifier.count_packages = 1
        with patch.object(notifier, 'set_notification_message'):
            with patch.object(notifier.notify, 'show') as mock_show:
                with patch('time.sleep', side_effect=KeyboardInterrupt):
                    try:
                        notifier.run()
                    except KeyboardInterrupt:
                        pass
        mock_show.assert_called_once()

    def test_daemon_no_notification_when_disabled(self):
        notifier = make_notifier()
        notifier.count_packages = 1
        notifier.configs.notifications = False
        with patch.object(notifier, 'set_notification_message'):
            with patch.object(notifier, '_signal_tray'):
                with patch.object(notifier.notify, 'show') as mock_show:
                    with patch('time.sleep', side_effect=KeyboardInterrupt):
                        try:
                            notifier.run()
                        except KeyboardInterrupt:
                            pass
        mock_show.assert_not_called()

    def test_daemon_no_notification_when_no_updates(self):
        notifier = make_notifier()
        notifier.count_packages = 0
        with patch.object(notifier, 'set_notification_message'):
            with patch.object(notifier.notify, 'show') as mock_show:
                with patch('time.sleep', side_effect=KeyboardInterrupt):
                    try:
                        notifier.run()
                    except KeyboardInterrupt:
                        pass
        mock_show.assert_not_called()

    def test_run_logs_update_count(self):
        notifier = make_notifier()
        notifier.count_packages = 3
        with patch.object(notifier, 'set_notification_message'):
            with patch.object(notifier.notify, 'show'):
                with patch('time.sleep', side_effect=KeyboardInterrupt):
                    try:
                        notifier.run()
                    except KeyboardInterrupt:
                        pass
        notifier.logger.info.assert_called()

    def test_run_logs_error_on_exception(self):
        notifier = make_notifier()
        with patch.object(notifier, 'set_notification_message', side_effect=Exception('fail')):
            with patch('time.sleep', side_effect=KeyboardInterrupt):
                try:
                    notifier.run()
                except KeyboardInterrupt:
                    pass
        notifier.logger.exception.assert_called()


class TestSignalTray:

    def test_sends_sigusr1_when_pid_file_exists(self):
        notifier = make_notifier()
        with patch('sun.daemon.TRAY_PID_FILE') as mock_pid_file:
            mock_pid_file.is_file.return_value = True
            mock_pid_file.read_text.return_value = '1234'
            with patch('os.kill') as mock_kill:
                notifier._signal_tray()
                mock_kill.assert_called_once_with(1234, signal.SIGUSR1)

    def test_does_nothing_when_pid_file_missing(self):
        notifier = make_notifier()
        with patch('sun.daemon.TRAY_PID_FILE') as mock_pid_file:
            mock_pid_file.is_file.return_value = False
            with patch('os.kill') as mock_kill:
                notifier._signal_tray()
                mock_kill.assert_not_called()

    def test_handles_process_lookup_error(self):
        notifier = make_notifier()
        with patch('sun.daemon.TRAY_PID_FILE') as mock_pid_file:
            mock_pid_file.is_file.return_value = True
            mock_pid_file.read_text.return_value = '9999'
            with patch('os.kill', side_effect=ProcessLookupError):
                notifier._signal_tray()  # should not raise


class TestMain:

    def test_keyboard_interrupt_exits(self):
        with patch('sun.daemon._setup_logger', return_value=MagicMock(spec=logging.Logger)):
            with patch.object(Notifier, 'run', side_effect=KeyboardInterrupt):
                with patch('pathlib.Path.mkdir'):
                    with patch('pathlib.Path.write_text'):
                        with patch('pathlib.Path.unlink'):
                            with pytest.raises(SystemExit):
                                main()

    def test_pid_file_written_on_start(self):
        with patch('sun.daemon._setup_logger', return_value=MagicMock(spec=logging.Logger)):
            with patch.object(Notifier, 'run', side_effect=KeyboardInterrupt):
                with patch('pathlib.Path.mkdir'):
                    with patch('pathlib.Path.write_text') as mock_write:
                        with patch('pathlib.Path.unlink'):
                            with patch('os.getpid', return_value=9999):
                                with pytest.raises(SystemExit):
                                    main()
                                mock_write.assert_called_once_with('9999')

    def test_pid_file_removed_on_exit(self):
        with patch('sun.daemon._setup_logger', return_value=MagicMock(spec=logging.Logger)):
            with patch.object(Notifier, 'run', side_effect=KeyboardInterrupt):
                with patch('pathlib.Path.mkdir'):
                    with patch('pathlib.Path.write_text'):
                        with patch('pathlib.Path.unlink') as mock_unlink:
                            with pytest.raises(SystemExit):
                                main()
                            mock_unlink.assert_called_once_with(missing_ok=True)
