"""
Tests for the command pattern implementation in the hosts TUI application.

This module tests the undo/redo functionality, command pattern,
and integration with the HostsManager.
"""

import pytest
from unittest.mock import Mock, patch
from src.hosts.core.commands import (
    Command,
    OperationResult,
    UndoRedoHistory,
    ToggleEntryCommand,
    MoveEntryCommand,
    AddEntryCommand,
    DeleteEntryCommand,
    UpdateEntryCommand,
)
from src.hosts.core.models import HostEntry, HostsFile
from src.hosts.core.manager import HostsManager


class TestOperationResult:
    """Test the OperationResult class."""

    def test_operation_result_success(self):
        """Test successful operation result."""
        result = OperationResult(True, "Operation successful")
        assert result.success is True
        assert result.message == "Operation successful"

    def test_operation_result_failure(self):
        """Test failed operation result."""
        result = OperationResult(False, "Operation failed")
        assert result.success is False
        assert result.message == "Operation failed"

    def test_operation_result_with_data(self):
        """Test operation result with additional data."""
        result = OperationResult(True, "Operation successful", {"key": "value"})
        assert result.success is True
        assert result.message == "Operation successful"
        assert result.data == {"key": "value"}


class TestUndoRedoHistory:
    """Test the UndoRedoHistory class."""

    def test_initial_state(self):
        """Test initial state of undo/redo history."""
        history = UndoRedoHistory()
        assert not history.can_undo()
        assert not history.can_redo()
        assert history.get_undo_description() is None
        assert history.get_redo_description() is None

    def test_execute_command(self):
        """Test executing a command adds it to history."""
        history = UndoRedoHistory()
        hosts_file = HostsFile()
        command = Mock(spec=Command)
        command.execute.return_value = OperationResult(True, "Test executed")
        command.get_description.return_value = "Test command"

        result = history.execute_command(command, hosts_file)

        assert result.success is True
        assert result.message == "Test executed"
        assert history.can_undo()
        assert not history.can_redo()
        assert history.get_undo_description() == "Test command"
        command.execute.assert_called_once_with(hosts_file)

    def test_execute_failed_command(self):
        """Test executing a failed command does not add it to history."""
        history = UndoRedoHistory()
        hosts_file = HostsFile()
        command = Mock(spec=Command)
        command.execute.return_value = OperationResult(False, "Test failed")

        result = history.execute_command(command, hosts_file)

        assert result.success is False
        assert result.message == "Test failed"
        assert not history.can_undo()
        assert not history.can_redo()

    def test_undo_operation(self):
        """Test undoing an operation."""
        history = UndoRedoHistory()
        hosts_file = HostsFile()
        command = Mock(spec=Command)
        command.execute.return_value = OperationResult(True, "Test executed")
        command.undo.return_value = OperationResult(True, "Test undone")
        command.get_description.return_value = "Test command"

        # Execute then undo
        history.execute_command(command, hosts_file)
        result = history.undo(hosts_file)

        assert result.success is True
        assert result.message == "Test undone"
        assert not history.can_undo()
        assert history.can_redo()
        assert history.get_redo_description() == "Test command"
        command.undo.assert_called_once_with(hosts_file)

    def test_redo_operation(self):
        """Test redoing an operation."""
        history = UndoRedoHistory()
        hosts_file = HostsFile()
        command = Mock(spec=Command)
        command.execute.return_value = OperationResult(True, "Test executed")
        command.undo.return_value = OperationResult(True, "Test undone")
        command.get_description.return_value = "Test command"

        # Execute, undo, then redo
        history.execute_command(command, hosts_file)
        history.undo(hosts_file)
        result = history.redo(hosts_file)

        assert result.success is True
        assert result.message == "Test executed"
        assert history.can_undo()
        assert not history.can_redo()
        assert history.get_undo_description() == "Test command"
        assert command.execute.call_count == 2

    def test_undo_with_empty_history(self):
        """Test undo with empty history."""
        history = UndoRedoHistory()
        hosts_file = HostsFile()
        result = history.undo(hosts_file)

        assert result.success is False
        assert "No operations to undo" in result.message

    def test_redo_with_empty_history(self):
        """Test redo with empty history."""
        history = UndoRedoHistory()
        hosts_file = HostsFile()
        result = history.redo(hosts_file)

        assert result.success is False
        assert "No operations to redo" in result.message

    def test_max_history_limit(self):
        """Test that history respects the maximum limit."""
        history = UndoRedoHistory(max_history=2)
        hosts_file = HostsFile()

        # Add 3 commands
        for i in range(3):
            command = Mock(spec=Command)
            command.execute.return_value = OperationResult(True, f"Command {i}")
            command.get_description.return_value = f"Command {i}"
            history.execute_command(command, hosts_file)

        # Should only keep the last 2 commands
        assert len(history.undo_stack) == 2

    def test_clear_history(self):
        """Test clearing the history."""
        history = UndoRedoHistory()
        hosts_file = HostsFile()
        command = Mock(spec=Command)
        command.execute.return_value = OperationResult(True, "Test executed")
        command.get_description.return_value = "Test command"

        history.execute_command(command, hosts_file)
        assert history.can_undo()

        history.clear_history()
        assert not history.can_undo()
        assert not history.can_redo()

    def test_new_command_clears_redo_stack(self):
        """Test that executing a new command clears the redo stack."""
        history = UndoRedoHistory()
        hosts_file = HostsFile()
        
        # Execute and undo a command
        command1 = Mock(spec=Command)
        command1.execute.return_value = OperationResult(True, "Command 1")
        command1.undo.return_value = OperationResult(True, "Undo 1")
        command1.get_description.return_value = "Command 1"
        
        history.execute_command(command1, hosts_file)
        history.undo(hosts_file)
        assert history.can_redo()

        # Execute a new command
        command2 = Mock(spec=Command)
        command2.execute.return_value = OperationResult(True, "Command 2")
        command2.get_description.return_value = "Command 2"
        
        history.execute_command(command2, hosts_file)
        assert not history.can_redo()  # Redo stack should be cleared


class TestToggleEntryCommand:
    """Test the ToggleEntryCommand class."""

    def test_toggle_active_to_inactive(self):
        """Test toggling an active entry to inactive."""
        hosts_file = HostsFile()
        entry = HostEntry(ip_address="192.168.1.1", hostnames=["test.local"], is_active=True)
        hosts_file.entries.append(entry)

        command = ToggleEntryCommand(0)
        result = command.execute(hosts_file)

        assert result.success is True
        assert not hosts_file.entries[0].is_active
        assert "deactivated" in result.message

    def test_toggle_inactive_to_active(self):
        """Test toggling an inactive entry to active."""
        hosts_file = HostsFile()
        entry = HostEntry(ip_address="192.168.1.1", hostnames=["test.local"], is_active=False)
        hosts_file.entries.append(entry)

        command = ToggleEntryCommand(0)
        result = command.execute(hosts_file)

        assert result.success is True
        assert hosts_file.entries[0].is_active
        assert "activated" in result.message

    def test_toggle_undo(self):
        """Test undoing a toggle operation."""
        hosts_file = HostsFile()
        entry = HostEntry(ip_address="192.168.1.1", hostnames=["test.local"], is_active=True)
        hosts_file.entries.append(entry)

        command = ToggleEntryCommand(0)
        command.execute(hosts_file)  # Toggle to inactive
        result = command.undo(hosts_file)  # Toggle back to active

        assert result.success is True
        assert hosts_file.entries[0].is_active
        assert "Undid toggle" in result.message

    def test_toggle_invalid_index(self):
        """Test toggle with invalid index."""
        hosts_file = HostsFile()
        command = ToggleEntryCommand(0)
        result = command.execute(hosts_file)

        assert result.success is False
        assert "Invalid entry index" in result.message

    def test_toggle_get_description(self):
        """Test getting command description."""
        command = ToggleEntryCommand(0)
        description = command.get_description()
        assert "Toggle entry at index 0" in description


class TestMoveEntryCommand:
    """Test the MoveEntryCommand class."""

    def test_move_entry_up(self):
        """Test moving an entry up."""
        hosts_file = HostsFile()
        entry1 = HostEntry(ip_address="192.168.1.1", hostnames=["test1.local"])
        entry2 = HostEntry(ip_address="192.168.1.2", hostnames=["test2.local"])
        hosts_file.entries.extend([entry1, entry2])

        command = MoveEntryCommand(1, 0)  # Move from index 1 to index 0
        result = command.execute(hosts_file)

        assert result.success is True
        assert hosts_file.entries[0] == entry2
        assert hosts_file.entries[1] == entry1
        assert "up" in result.message

    def test_move_entry_down(self):
        """Test moving an entry down."""
        hosts_file = HostsFile()
        entry1 = HostEntry(ip_address="192.168.1.1", hostnames=["test1.local"])
        entry2 = HostEntry(ip_address="192.168.1.2", hostnames=["test2.local"])
        hosts_file.entries.extend([entry1, entry2])

        command = MoveEntryCommand(0, 1)  # Move from index 0 to index 1
        result = command.execute(hosts_file)

        assert result.success is True
        assert hosts_file.entries[0] == entry2
        assert hosts_file.entries[1] == entry1
        assert "down" in result.message

    def test_move_undo(self):
        """Test undoing a move operation."""
        hosts_file = HostsFile()
        entry1 = HostEntry(ip_address="192.168.1.1", hostnames=["test1.local"])
        entry2 = HostEntry(ip_address="192.168.1.2", hostnames=["test2.local"])
        hosts_file.entries.extend([entry1, entry2])

        command = MoveEntryCommand(1, 0)  # Move entry2 up
        command.execute(hosts_file)
        result = command.undo(hosts_file)  # Move back down

        assert result.success is True
        assert hosts_file.entries[0] == entry1
        assert hosts_file.entries[1] == entry2
        assert "Undid move" in result.message

    def test_move_invalid_indices(self):
        """Test move with invalid indices."""
        hosts_file = HostsFile()
        entry = HostEntry(ip_address="192.168.1.1", hostnames=["test.local"])
        hosts_file.entries.append(entry)

        command = MoveEntryCommand(0, 5)  # Invalid target index
        result = command.execute(hosts_file)

        assert result.success is False
        assert "Invalid move" in result.message

    def test_move_same_position(self):
        """Test moving entry to same position."""
        hosts_file = HostsFile()
        entry = HostEntry(ip_address="192.168.1.1", hostnames=["test.local"])
        hosts_file.entries.append(entry)

        command = MoveEntryCommand(0, 0)  # Same position
        result = command.execute(hosts_file)

        assert result.success is True
        assert "No movement needed" in result.message

    def test_move_get_description(self):
        """Test getting command description."""
        command = MoveEntryCommand(1, 0)
        description = command.get_description()
        assert "Move entry up" in description


class TestAddEntryCommand:
    """Test the AddEntryCommand class."""

    def test_add_entry(self):
        """Test adding an entry."""
        hosts_file = HostsFile()
        entry = HostEntry(ip_address="192.168.1.1", hostnames=["test.local"])

        command = AddEntryCommand(entry)
        result = command.execute(hosts_file)

        assert result.success is True
        assert len(hosts_file.entries) == 1
        assert hosts_file.entries[0] == entry
        assert "Added entry" in result.message

    def test_add_entry_at_specific_index(self):
        """Test adding an entry at a specific index."""
        hosts_file = HostsFile()
        entry1 = HostEntry(ip_address="192.168.1.1", hostnames=["test1.local"])
        entry2 = HostEntry(ip_address="192.168.1.2", hostnames=["test2.local"])
        hosts_file.entries.append(entry1)

        command = AddEntryCommand(entry2, index=0)  # Insert at beginning
        result = command.execute(hosts_file)

        assert result.success is True
        assert len(hosts_file.entries) == 2
        assert hosts_file.entries[0] == entry2
        assert hosts_file.entries[1] == entry1

    def test_add_entry_undo(self):
        """Test undoing an add operation."""
        hosts_file = HostsFile()
        entry = HostEntry(ip_address="192.168.1.1", hostnames=["test.local"])

        command = AddEntryCommand(entry)
        command.execute(hosts_file)
        result = command.undo(hosts_file)

        assert result.success is True
        assert len(hosts_file.entries) == 0
        assert "Undid add" in result.message

    def test_add_entry_invalid_index(self):
        """Test adding entry with invalid index."""
        hosts_file = HostsFile()
        entry = HostEntry(ip_address="192.168.1.1", hostnames=["test.local"])

        command = AddEntryCommand(entry, index=5)  # Invalid index
        result = command.execute(hosts_file)

        assert result.success is False
        assert "Invalid insertion index" in result.message

    def test_add_get_description(self):
        """Test getting command description."""
        entry = HostEntry(ip_address="192.168.1.1", hostnames=["test.local"])
        command = AddEntryCommand(entry)
        description = command.get_description()
        assert "Add entry: 192.168.1.1 test.local" in description


class TestDeleteEntryCommand:
    """Test the DeleteEntryCommand class."""

    def test_delete_entry(self):
        """Test deleting an entry."""
        hosts_file = HostsFile()
        entry = HostEntry(ip_address="192.168.1.1", hostnames=["test.local"])
        hosts_file.entries.append(entry)

        command = DeleteEntryCommand(0)
        result = command.execute(hosts_file)

        assert result.success is True
        assert len(hosts_file.entries) == 0
        assert "Deleted entry" in result.message

    def test_delete_entry_undo(self):
        """Test undoing a delete operation."""
        hosts_file = HostsFile()
        entry = HostEntry(ip_address="192.168.1.1", hostnames=["test.local"])
        hosts_file.entries.append(entry)

        command = DeleteEntryCommand(0)
        command.execute(hosts_file)
        result = command.undo(hosts_file)

        assert result.success is True
        assert len(hosts_file.entries) == 1
        assert hosts_file.entries[0].ip_address == "192.168.1.1"
        assert "Undid delete" in result.message

    def test_delete_invalid_index(self):
        """Test delete with invalid index."""
        hosts_file = HostsFile()
        command = DeleteEntryCommand(0)
        result = command.execute(hosts_file)

        assert result.success is False
        assert "Invalid entry index" in result.message

    def test_delete_get_description(self):
        """Test getting command description."""
        command = DeleteEntryCommand(0)
        description = command.get_description()
        assert "Delete entry at index 0" in description


class TestUpdateEntryCommand:
    """Test the UpdateEntryCommand class."""

    def test_update_entry(self):
        """Test updating an entry."""
        hosts_file = HostsFile()
        old_entry = HostEntry(ip_address="192.168.1.1", hostnames=["test.local"], comment="old comment", is_active=True)
        hosts_file.entries.append(old_entry)

        command = UpdateEntryCommand(0, "192.168.1.2", ["updated.local"], "new comment", False)
        result = command.execute(hosts_file)

        assert result.success is True
        assert hosts_file.entries[0].ip_address == "192.168.1.2"
        assert hosts_file.entries[0].hostnames == ["updated.local"]
        assert hosts_file.entries[0].comment == "new comment"
        assert hosts_file.entries[0].is_active is False
        assert "Updated entry" in result.message

    def test_update_entry_undo(self):
        """Test undoing an update operation."""
        hosts_file = HostsFile()
        old_entry = HostEntry(ip_address="192.168.1.1", hostnames=["test.local"], comment="old comment", is_active=True)
        hosts_file.entries.append(old_entry)

        command = UpdateEntryCommand(0, "192.168.1.2", ["updated.local"], "new comment", False)
        command.execute(hosts_file)
        result = command.undo(hosts_file)

        assert result.success is True
        assert hosts_file.entries[0].ip_address == "192.168.1.1"
        assert hosts_file.entries[0].hostnames == ["test.local"]
        assert hosts_file.entries[0].comment == "old comment"
        assert hosts_file.entries[0].is_active is True
        assert "Undid update" in result.message

    def test_update_invalid_index(self):
        """Test update with invalid index."""
        hosts_file = HostsFile()

        command = UpdateEntryCommand(0, "192.168.1.2", ["updated.local"], None, True)
        result = command.execute(hosts_file)

        assert result.success is False
        assert "Invalid entry index" in result.message

    def test_update_get_description(self):
        """Test getting command description."""
        command = UpdateEntryCommand(0, "192.168.1.2", ["updated.local"], None, True)
        description = command.get_description()
        assert "Update entry at index 0" in description


class TestHostsManagerIntegration:
    """Test integration of commands with HostsManager."""

    def test_manager_undo_redo_properties(self):
        """Test undo/redo availability properties."""
        manager = HostsManager()
        
        # Initially no undo/redo available
        assert not manager.can_undo()
        assert not manager.can_redo()
        assert manager.get_undo_description() is None
        assert manager.get_redo_description() is None

    def test_manager_undo_without_edit_mode(self):
        """Test undo when not in edit mode."""
        manager = HostsManager()
        hosts_file = HostsFile()
        result = manager.undo_last_operation(hosts_file)
        
        assert result.success is False
        assert "Not in edit mode" in result.message

    def test_manager_redo_without_edit_mode(self):
        """Test redo when not in edit mode."""
        manager = HostsManager()
        hosts_file = HostsFile()
        result = manager.redo_last_operation(hosts_file)
        
        assert result.success is False
        assert "Not in edit mode" in result.message

    @patch('src.hosts.core.manager.HostsManager.enter_edit_mode')
    def test_manager_execute_toggle_command(self, mock_enter_edit):
        """Test executing a toggle command through the manager."""
        mock_enter_edit.return_value = (True, "Edit mode enabled")
        
        manager = HostsManager()
        manager.edit_mode = True  # Simulate edit mode
        
        hosts_file = HostsFile()
        entry = HostEntry(ip_address="192.168.1.1", hostnames=["test.local"], is_active=True)
        hosts_file.entries.append(entry)

        result = manager.execute_toggle_command(hosts_file, 0)
        
        assert result.success is True
        assert not hosts_file.entries[0].is_active

    @patch('src.hosts.core.manager.HostsManager.enter_edit_mode')
    def test_manager_execute_move_command(self, mock_enter_edit):
        """Test executing a move command through the manager."""
        mock_enter_edit.return_value = (True, "Edit mode enabled")
        
        manager = HostsManager()
        manager.edit_mode = True  # Simulate edit mode
        
        hosts_file = HostsFile()
        entry1 = HostEntry(ip_address="192.168.1.1", hostnames=["test1.local"])
        entry2 = HostEntry(ip_address="192.168.1.2", hostnames=["test2.local"])
        hosts_file.entries.extend([entry1, entry2])

        result = manager.execute_move_command(hosts_file, 1, "up")
        
        assert result.success is True
        assert hosts_file.entries[0] == entry2

    @patch('src.hosts.core.manager.HostsManager.enter_edit_mode')
    def test_manager_command_not_in_edit_mode(self, mock_enter_edit):
        """Test executing commands when not in edit mode."""
        manager = HostsManager()
        hosts_file = HostsFile()
        entry = HostEntry(ip_address="192.168.1.1", hostnames=["test.local"])

        result = manager.execute_add_command(hosts_file, entry)
        
        assert result.success is False
        assert "Not in edit mode" in result.message


if __name__ == "__main__":
    pytest.main([__file__])
