"""
Tests for the hosts manager module.

This module tests permission management, edit mode operations,
and safe file modifications with backup and validation.
"""

import pytest
import tempfile
import subprocess
from pathlib import Path
from unittest.mock import Mock, patch
from src.hosts.core.manager import PermissionManager, HostsManager
from src.hosts.core.models import HostEntry, HostsFile


class TestPermissionManager:
    """Test the PermissionManager class."""

    def test_init(self):
        """Test PermissionManager initialization."""
        pm = PermissionManager()
        assert not pm.has_sudo
        assert not pm._sudo_validated

    @patch("subprocess.run")
    def test_request_sudo_already_available(self, mock_run):
        """Test requesting sudo when already available."""
        # Mock successful sudo -n true
        mock_run.return_value = Mock(returncode=0)

        pm = PermissionManager()
        success, message = pm.request_sudo()

        assert success
        assert "already available" in message
        assert pm.has_sudo
        assert pm._sudo_validated

        mock_run.assert_called_once_with(
            ["sudo", "-n", "true"], capture_output=True, text=True, timeout=5
        )

    @patch("subprocess.run")
    def test_request_sudo_prompt_success(self, mock_run):
        """Test requesting sudo with password prompt success."""
        # First call (sudo -n true) fails, second call (sudo -S -v) succeeds
        mock_run.side_effect = [
            Mock(returncode=1),  # sudo -n true fails
            Mock(returncode=0),  # sudo -S -v succeeds
        ]

        pm = PermissionManager()
        success, message = pm.request_sudo("testpassword")

        assert success
        assert "access granted" in message
        assert pm.has_sudo
        assert pm._sudo_validated

        assert mock_run.call_count == 2

    @patch("subprocess.run")
    def test_request_sudo_no_password(self, mock_run):
        """Test requesting sudo when no password is provided."""
        # sudo -n true fails (password needed)
        mock_run.return_value = Mock(returncode=1)

        pm = PermissionManager()
        success, message = pm.request_sudo()

        assert not success
        assert "Password required" in message
        assert not pm.has_sudo
        assert not pm._sudo_validated

    @patch("subprocess.run")
    def test_request_sudo_denied(self, mock_run):
        """Test requesting sudo when access is denied."""
        # Both calls fail
        mock_run.side_effect = [
            Mock(returncode=1),  # sudo -n true fails
            Mock(returncode=1, stderr="access denied"),  # sudo -S -v fails
        ]

        pm = PermissionManager()
        success, message = pm.request_sudo("testpassword")

        assert not success
        assert "denied" in message
        assert not pm.has_sudo
        assert not pm._sudo_validated

    @patch("subprocess.run")
    def test_request_sudo_timeout(self, mock_run):
        """Test requesting sudo with timeout."""
        mock_run.side_effect = subprocess.TimeoutExpired(["sudo", "-n", "true"], 5)

        pm = PermissionManager()
        success, message = pm.request_sudo()

        assert not success
        assert "timed out" in message
        assert not pm.has_sudo

    @patch("subprocess.run")
    def test_request_sudo_exception(self, mock_run):
        """Test requesting sudo with exception."""
        mock_run.side_effect = Exception("Test error")

        pm = PermissionManager()
        success, message = pm.request_sudo()

        assert not success
        assert "Test error" in message
        assert not pm.has_sudo

    @patch("subprocess.run")
    def test_validate_permissions_success(self, mock_run):
        """Test validating permissions successfully."""
        mock_run.return_value = Mock(returncode=0)

        pm = PermissionManager()
        pm.has_sudo = True

        result = pm.validate_permissions("/etc/hosts")

        assert result
        mock_run.assert_called_once_with(
            ["sudo", "-n", "test", "-w", "/etc/hosts"], capture_output=True, timeout=5
        )

    @patch("subprocess.run")
    def test_validate_permissions_no_sudo(self, mock_run):
        """Test validating permissions without sudo."""
        pm = PermissionManager()
        pm.has_sudo = False

        result = pm.validate_permissions("/etc/hosts")

        assert not result
        mock_run.assert_not_called()

    @patch("subprocess.run")
    def test_validate_permissions_failure(self, mock_run):
        """Test validating permissions failure."""
        mock_run.return_value = Mock(returncode=1)

        pm = PermissionManager()
        pm.has_sudo = True

        result = pm.validate_permissions("/etc/hosts")

        assert not result

    @patch("subprocess.run")
    def test_validate_permissions_exception(self, mock_run):
        """Test validating permissions with exception."""
        mock_run.side_effect = Exception("Test error")

        pm = PermissionManager()
        pm.has_sudo = True

        result = pm.validate_permissions("/etc/hosts")

        assert not result

    @patch("subprocess.run")
    def test_release_sudo(self, mock_run):
        """Test releasing sudo permissions."""
        pm = PermissionManager()
        pm.has_sudo = True
        pm._sudo_validated = True

        pm.release_sudo()

        assert not pm.has_sudo
        assert not pm._sudo_validated
        mock_run.assert_called_once_with(["sudo", "-k"], capture_output=True, timeout=5)

    @patch("subprocess.run")
    def test_release_sudo_exception(self, mock_run):
        """Test releasing sudo with exception."""
        mock_run.side_effect = Exception("Test error")

        pm = PermissionManager()
        pm.has_sudo = True
        pm._sudo_validated = True

        pm.release_sudo()

        # Should still reset state even if command fails
        assert not pm.has_sudo
        assert not pm._sudo_validated


class TestHostsManager:
    """Test the HostsManager class."""

    def test_init(self):
        """Test HostsManager initialization."""
        with tempfile.NamedTemporaryFile() as temp_file:
            manager = HostsManager(temp_file.name)
            assert not manager.edit_mode
            assert manager._backup_path is None
            assert manager.parser.file_path == Path(temp_file.name)

    @patch("src.hosts.core.manager.HostsManager._create_backup")
    def test_enter_edit_mode_success(self, mock_backup):
        """Test entering edit mode successfully."""
        with tempfile.NamedTemporaryFile() as temp_file:
            manager = HostsManager(temp_file.name)

            # Mock permission manager
            manager.permission_manager.request_sudo = Mock(
                return_value=(True, "Success")
            )
            manager.permission_manager.validate_permissions = Mock(return_value=True)

            success, message = manager.enter_edit_mode()

            assert success
            assert "enabled" in message
            assert manager.edit_mode
            mock_backup.assert_called_once()

    def test_enter_edit_mode_already_in_edit(self):
        """Test entering edit mode when already in edit mode."""
        with tempfile.NamedTemporaryFile() as temp_file:
            manager = HostsManager(temp_file.name)
            manager.edit_mode = True

            success, message = manager.enter_edit_mode()

            assert success
            assert "Already in edit mode" in message

    def test_enter_edit_mode_sudo_failure(self):
        """Test entering edit mode with sudo failure."""
        with tempfile.NamedTemporaryFile() as temp_file:
            manager = HostsManager(temp_file.name)

            # Mock permission manager failure
            manager.permission_manager.request_sudo = Mock(
                return_value=(False, "Denied")
            )

            success, message = manager.enter_edit_mode()

            assert not success
            assert message == "Denied"
            assert not manager.edit_mode

    def test_enter_edit_mode_permission_validation_failure(self):
        """Test entering edit mode with permission validation failure."""
        with tempfile.NamedTemporaryFile() as temp_file:
            manager = HostsManager(temp_file.name)

            # Mock permission manager
            manager.permission_manager.request_sudo = Mock(
                return_value=(True, "Success")
            )
            manager.permission_manager.validate_permissions = Mock(return_value=False)

            success, message = manager.enter_edit_mode()

            assert not success
            assert "Cannot write to hosts file" in message
            assert not manager.edit_mode

    @patch("src.hosts.core.manager.HostsManager._create_backup")
    def test_enter_edit_mode_backup_failure(self, mock_backup):
        """Test entering edit mode with backup failure."""
        mock_backup.side_effect = Exception("Backup failed")

        with tempfile.NamedTemporaryFile() as temp_file:
            manager = HostsManager(temp_file.name)

            # Mock permission manager
            manager.permission_manager.request_sudo = Mock(
                return_value=(True, "Success")
            )
            manager.permission_manager.validate_permissions = Mock(return_value=True)

            success, message = manager.enter_edit_mode()

            assert not success
            assert "Failed to create backup" in message
            assert not manager.edit_mode

    def test_exit_edit_mode_success(self):
        """Test exiting edit mode successfully."""
        with tempfile.NamedTemporaryFile() as temp_file:
            manager = HostsManager(temp_file.name)
            manager.edit_mode = True
            manager._backup_path = Path("/tmp/backup")

            # Mock permission manager
            manager.permission_manager.release_sudo = Mock()

            success, message = manager.exit_edit_mode()

            assert success
            assert "disabled" in message
            assert not manager.edit_mode
            assert manager._backup_path is None
            manager.permission_manager.release_sudo.assert_called_once()

    def test_exit_edit_mode_not_in_edit(self):
        """Test exiting edit mode when not in edit mode."""
        with tempfile.NamedTemporaryFile() as temp_file:
            manager = HostsManager(temp_file.name)
            manager.edit_mode = False

            success, message = manager.exit_edit_mode()

            assert success
            assert "Already in read-only mode" in message

    def test_exit_edit_mode_exception(self):
        """Test exiting edit mode with exception."""
        with tempfile.NamedTemporaryFile() as temp_file:
            manager = HostsManager(temp_file.name)
            manager.edit_mode = True

            # Mock permission manager to raise exception
            manager.permission_manager.release_sudo = Mock(
                side_effect=Exception("Test error")
            )

            success, message = manager.exit_edit_mode()

            assert not success
            assert "Test error" in message

    def test_toggle_entry_success(self):
        """Test toggling entry successfully."""
        with tempfile.NamedTemporaryFile() as temp_file:
            manager = HostsManager(temp_file.name)
            manager.edit_mode = True

            hosts_file = HostsFile()
            entry = HostEntry(
                "192.168.1.1", ["router"], is_active=True
            )  # Non-default entry
            hosts_file.entries.append(entry)

            success, message = manager.toggle_entry(hosts_file, 0)

            assert success
            assert "active to inactive" in message
            assert not hosts_file.entries[0].is_active

    def test_toggle_entry_not_in_edit_mode(self):
        """Test toggling entry when not in edit mode."""
        with tempfile.NamedTemporaryFile() as temp_file:
            manager = HostsManager(temp_file.name)
            manager.edit_mode = False

            hosts_file = HostsFile()

            success, message = manager.toggle_entry(hosts_file, 0)

            assert not success
            assert "Not in edit mode" in message

    def test_toggle_entry_invalid_index(self):
        """Test toggling entry with invalid index."""
        with tempfile.NamedTemporaryFile() as temp_file:
            manager = HostsManager(temp_file.name)
            manager.edit_mode = True

            hosts_file = HostsFile()

            success, message = manager.toggle_entry(hosts_file, 0)

            assert not success
            assert "Invalid entry index" in message

    def test_move_entry_up_success(self):
        """Test moving entry up successfully."""
        with tempfile.NamedTemporaryFile() as temp_file:
            manager = HostsManager(temp_file.name)
            manager.edit_mode = True

            hosts_file = HostsFile()
            entry1 = HostEntry("10.0.0.1", ["test1"])  # Non-default entries
            entry2 = HostEntry("192.168.1.1", ["router"])
            hosts_file.entries.extend([entry1, entry2])

            success, message = manager.move_entry_up(hosts_file, 1)

            assert success
            assert "moved up" in message
            assert hosts_file.entries[0].hostnames[0] == "router"
            assert hosts_file.entries[1].hostnames[0] == "test1"

    def test_move_entry_up_invalid_index(self):
        """Test moving entry up with invalid index."""
        with tempfile.NamedTemporaryFile() as temp_file:
            manager = HostsManager(temp_file.name)
            manager.edit_mode = True

            hosts_file = HostsFile()
            entry = HostEntry("127.0.0.1", ["localhost"])
            hosts_file.entries.append(entry)

            success, message = manager.move_entry_up(hosts_file, 0)

            assert not success
            assert "Cannot move entry up" in message

    def test_move_entry_down_success(self):
        """Test moving entry down successfully."""
        with tempfile.NamedTemporaryFile() as temp_file:
            manager = HostsManager(temp_file.name)
            manager.edit_mode = True

            hosts_file = HostsFile()
            entry1 = HostEntry("10.0.0.1", ["test1"])  # Non-default entries
            entry2 = HostEntry("192.168.1.1", ["router"])
            hosts_file.entries.extend([entry1, entry2])

            success, message = manager.move_entry_down(hosts_file, 0)

            assert success
            assert "moved down" in message
            assert hosts_file.entries[0].hostnames[0] == "router"
            assert hosts_file.entries[1].hostnames[0] == "test1"

    def test_move_entry_down_invalid_index(self):
        """Test moving entry down with invalid index."""
        with tempfile.NamedTemporaryFile() as temp_file:
            manager = HostsManager(temp_file.name)
            manager.edit_mode = True

            hosts_file = HostsFile()
            entry = HostEntry("127.0.0.1", ["localhost"])
            hosts_file.entries.append(entry)

            success, message = manager.move_entry_down(hosts_file, 0)

            assert not success
            assert "Cannot move entry down" in message

    def test_update_entry_success(self):
        """Test updating entry successfully."""
        with tempfile.NamedTemporaryFile() as temp_file:
            manager = HostsManager(temp_file.name)
            manager.edit_mode = True

            hosts_file = HostsFile()
            entry = HostEntry("10.0.0.1", ["test"])  # Non-default entry
            hosts_file.entries.append(entry)

            success, message = manager.update_entry(
                hosts_file, 0, "192.168.1.1", ["newhost"], "New comment"
            )

            assert success
            assert "updated successfully" in message
            assert hosts_file.entries[0].ip_address == "192.168.1.1"
            assert hosts_file.entries[0].hostnames == ["newhost"]
            assert hosts_file.entries[0].comment == "New comment"

    def test_update_entry_invalid_data(self):
        """Test updating entry with invalid data."""
        with tempfile.NamedTemporaryFile() as temp_file:
            manager = HostsManager(temp_file.name)
            manager.edit_mode = True

            hosts_file = HostsFile()
            entry = HostEntry(
                "127.0.0.1", ["localhost"]
            )  # Default entry - cannot be modified
            hosts_file.entries.append(entry)

            success, message = manager.update_entry(
                hosts_file, 0, "invalid-ip", ["newhost"]
            )

            assert not success
            assert "Cannot modify default system entries" in message

    @patch("tempfile.NamedTemporaryFile")
    @patch("subprocess.run")
    @patch("os.unlink")
    def test_save_hosts_file_success(self, mock_unlink, mock_run, mock_temp):
        """Test saving hosts file successfully."""
        # Mock temporary file
        mock_temp_file = Mock()
        mock_temp_file.name = "/tmp/test.hosts"
        mock_temp_file.__enter__ = Mock(return_value=mock_temp_file)
        mock_temp_file.__exit__ = Mock(return_value=None)
        mock_temp.return_value = mock_temp_file

        # Mock subprocess success
        mock_run.return_value = Mock(returncode=0)

        with tempfile.NamedTemporaryFile() as temp_file:
            manager = HostsManager(temp_file.name)
            manager.edit_mode = True
            manager.permission_manager.has_sudo = True

            hosts_file = HostsFile()
            entry = HostEntry("127.0.0.1", ["localhost"])
            hosts_file.entries.append(entry)

            success, message = manager.save_hosts_file(hosts_file)

            assert success
            assert "saved successfully" in message
            mock_run.assert_called_once()
            mock_unlink.assert_called_once_with("/tmp/test.hosts")

    def test_save_hosts_file_not_in_edit_mode(self):
        """Test saving hosts file when not in edit mode."""
        with tempfile.NamedTemporaryFile() as temp_file:
            manager = HostsManager(temp_file.name)
            manager.edit_mode = False

            hosts_file = HostsFile()

            success, message = manager.save_hosts_file(hosts_file)

            assert not success
            assert "Not in edit mode" in message

    def test_save_hosts_file_no_sudo(self):
        """Test saving hosts file without sudo."""
        with tempfile.NamedTemporaryFile() as temp_file:
            manager = HostsManager(temp_file.name)
            manager.edit_mode = True
            manager.permission_manager.has_sudo = False

            hosts_file = HostsFile()

            success, message = manager.save_hosts_file(hosts_file)

            assert not success
            assert "No sudo permissions" in message

    @patch("subprocess.run")
    def test_restore_backup_success(self, mock_run):
        """Test restoring backup successfully."""
        mock_run.return_value = Mock(returncode=0)

        with tempfile.NamedTemporaryFile() as temp_file:
            manager = HostsManager(temp_file.name)
            manager.edit_mode = True

            # Create a mock backup file
            with tempfile.NamedTemporaryFile(delete=False) as backup_file:
                manager._backup_path = Path(backup_file.name)

            try:
                success, message = manager.restore_backup()

                assert success
                assert "restored successfully" in message
                mock_run.assert_called_once()
            finally:
                # Clean up
                manager._backup_path.unlink()

    def test_restore_backup_not_in_edit_mode(self):
        """Test restoring backup when not in edit mode."""
        with tempfile.NamedTemporaryFile() as temp_file:
            manager = HostsManager(temp_file.name)
            manager.edit_mode = False

            success, message = manager.restore_backup()

            assert not success
            assert "Not in edit mode" in message

    def test_restore_backup_no_backup(self):
        """Test restoring backup when no backup exists."""
        with tempfile.NamedTemporaryFile() as temp_file:
            manager = HostsManager(temp_file.name)
            manager.edit_mode = True
            manager._backup_path = None

            success, message = manager.restore_backup()

            assert not success
            assert "No backup available" in message

    @patch("subprocess.run")
    @patch("tempfile.gettempdir")
    @patch("time.time")
    def test_create_backup_success(self, mock_time, mock_tempdir, mock_run):
        """Test creating backup successfully."""
        mock_time.return_value = 1234567890
        mock_tempdir.return_value = "/tmp"
        mock_run.side_effect = [
            Mock(returncode=0),  # cp command
            Mock(returncode=0),  # chmod command
        ]

        # Create a real temporary file for testing
        with tempfile.NamedTemporaryFile(delete=False) as temp_file:
            temp_file.write(b"test content")
            temp_path = temp_file.name

        try:
            manager = HostsManager(temp_path)
            manager._create_backup()

            expected_backup = Path("/tmp/hosts-manager-backups/hosts.backup.1234567890")
            assert manager._backup_path == expected_backup
            assert mock_run.call_count == 2
        finally:
            # Clean up
            Path(temp_path).unlink()

    @patch("subprocess.run")
    def test_create_backup_failure(self, mock_run):
        """Test creating backup with failure."""
        mock_run.return_value = Mock(returncode=1, stderr="Permission denied")

        # Create a real temporary file for testing
        with tempfile.NamedTemporaryFile(delete=False) as temp_file:
            temp_file.write(b"test content")
            temp_path = temp_file.name

        try:
            manager = HostsManager(temp_path)

            with pytest.raises(Exception) as exc_info:
                manager._create_backup()

            assert "Failed to create backup" in str(exc_info.value)
        finally:
            # Clean up
            Path(temp_path).unlink()

    def test_add_entry_success(self):
        """Test successfully adding an entry with immediate save."""
        manager = HostsManager()
        manager.edit_mode = True

        # Mock the save operation to succeed
        with patch.object(manager, "save_hosts_file") as mock_save:
            mock_save.return_value = (True, "File saved successfully")

            hosts_file = HostsFile()
            test_entry = HostEntry(ip_address="192.168.1.100", hostnames=["testhost"])

            success, message = manager.add_entry(hosts_file, test_entry)

            assert success
            assert "Entry added: testhost" in message
            assert len(hosts_file.entries) == 1
            assert hosts_file.entries[0] == test_entry
            mock_save.assert_called_once_with(hosts_file)

    def test_add_entry_save_failure(self):
        """Test adding an entry when save fails."""
        manager = HostsManager()
        manager.edit_mode = True

        # Mock the save operation to fail
        with patch.object(manager, "save_hosts_file") as mock_save:
            mock_save.return_value = (False, "Permission denied")

            hosts_file = HostsFile()
            test_entry = HostEntry(ip_address="192.168.1.100", hostnames=["testhost"])

            success, message = manager.add_entry(hosts_file, test_entry)

            assert not success
            assert "Failed to save after adding entry" in message
            assert (
                len(hosts_file.entries) == 0
            )  # Entry should be removed on save failure
            mock_save.assert_called_once_with(hosts_file)

    def test_add_entry_not_in_edit_mode(self):
        """Test adding an entry when not in edit mode."""
        manager = HostsManager()
        # edit_mode defaults to False

        hosts_file = HostsFile()
        test_entry = HostEntry(ip_address="192.168.1.100", hostnames=["testhost"])

        success, message = manager.add_entry(hosts_file, test_entry)

        assert not success
        assert "Not in edit mode" in message
        assert len(hosts_file.entries) == 0
