# Copyright (c) 2025 Itential, Inc
# GNU General Public License v3.0+ (see LICENSE or https://www.gnu.org/licenses/gpl-3.0.txt)

import pytest
from unittest.mock import AsyncMock, MagicMock, patch

from fastmcp import Context

from itential_mcp.tools import device_groups
from itential_mcp.models.device_groups import (
    DeviceGroupElement,
    GetDeviceGroupsResponse,
    CreateDeviceGroupResponse,
    AddDevicesToGroupResponse,
    RemoveDevicesFromGroupResponse
)


class TestModule:
    """Test the device_groups tools module"""

    def test_module_tags(self):
        """Test module has correct tags"""
        assert hasattr(device_groups, '__tags__')
        assert device_groups.__tags__ == ("configuration_manager",)

    def test_module_functions_exist(self):
        """Test all expected functions exist in the module"""
        expected_functions = [
            'get_device_groups',
            'create_device_group',
            'add_devices_to_group',
            'remove_devices_from_group'
        ]
        
        for func_name in expected_functions:
            assert hasattr(device_groups, func_name)
            assert callable(getattr(device_groups, func_name))

    def test_functions_are_async(self):
        """Test that all functions are async"""
        import inspect
        
        functions_to_test = [
            device_groups.get_device_groups,
            device_groups.create_device_group,
            device_groups.add_devices_to_group,
            device_groups.remove_devices_from_group
        ]
        
        for func in functions_to_test:
            assert inspect.iscoroutinefunction(func), f"{func.__name__} should be async"


class TestGetDeviceGroups:
    """Test the get_device_groups tool function"""

    @pytest.fixture
    def mock_context(self):
        """Create a mock Context object"""
        context = AsyncMock(spec=Context)
        context.info = AsyncMock()
        
        # Mock client
        mock_client = MagicMock()
        mock_client.get = AsyncMock()
        
        # Mock request context and lifespan context
        context.request_context = MagicMock()
        context.request_context.lifespan_context = MagicMock()
        context.request_context.lifespan_context.get.return_value = mock_client
        
        return context

    @pytest.mark.asyncio
    async def test_get_device_groups_success(self, mock_context):
        """Test get_device_groups with successful response"""
        mock_data = [
            {
                "id": "group-1",
                "name": "Production Routers",
                "devices": ["router1", "router2", "router3"],
                "description": "Production network routers"
            },
            {
                "id": "group-2", 
                "name": "Test Switches",
                "devices": ["switch1", "switch2"],
                "description": "Test environment switches"
            }
        ]
        
        mock_client = mock_context.request_context.lifespan_context.get.return_value
        mock_client.configuration_manager = MagicMock()
        mock_client.configuration_manager.get_device_groups = AsyncMock(return_value=mock_data)
        
        result = await device_groups.get_device_groups(mock_context)
        
        # Verify API call
        mock_client.configuration_manager.get_device_groups.assert_called_once()
        
        # Verify result type and structure
        assert isinstance(result, GetDeviceGroupsResponse)
        assert len(result.root) == 2
        
        # Verify first device group
        first_group = result.root[0]
        assert isinstance(first_group, DeviceGroupElement)
        assert first_group.object_id == "group-1"
        assert first_group.name == "Production Routers"
        assert first_group.devices == ["router1", "router2", "router3"]
        assert first_group.description == "Production network routers"
        
        # Verify second device group
        second_group = result.root[1]
        assert second_group.object_id == "group-2"
        assert second_group.name == "Test Switches"
        assert second_group.devices == ["switch1", "switch2"]

    @pytest.mark.asyncio
    async def test_get_device_groups_empty_response(self, mock_context):
        """Test get_device_groups with empty response"""
        mock_client = mock_context.request_context.lifespan_context.get.return_value
        mock_client.configuration_manager = MagicMock()
        mock_client.configuration_manager.get_device_groups = AsyncMock(return_value=[])
        
        result = await device_groups.get_device_groups(mock_context)
        
        assert isinstance(result, GetDeviceGroupsResponse)
        assert len(result.root) == 0
        assert result.root == []

    @pytest.mark.asyncio
    async def test_get_device_groups_logs_info(self, mock_context):
        """Test get_device_groups logs info message"""
        mock_client = mock_context.request_context.lifespan_context.get.return_value
        mock_client.configuration_manager = MagicMock()
        mock_client.configuration_manager.get_device_groups = AsyncMock(return_value=[])
        
        await device_groups.get_device_groups(mock_context)
        
        mock_context.info.assert_called_once_with("inside get_device_groups(...)")

    @pytest.mark.asyncio
    async def test_get_device_groups_handles_missing_fields(self, mock_context):
        """Test get_device_groups handles missing optional fields gracefully"""
        mock_data = [
            {
                "id": "group-1",
                "name": "Minimal Group",
                "devices": [],
                "description": "A minimal device group"
            }
        ]
        
        mock_client = mock_context.request_context.lifespan_context.get.return_value
        mock_client.configuration_manager = MagicMock()
        mock_client.configuration_manager.get_device_groups = AsyncMock(return_value=mock_data)
        
        result = await device_groups.get_device_groups(mock_context)
        
        assert isinstance(result, GetDeviceGroupsResponse)
        assert len(result.root) == 1
        assert result.root[0].devices == []


class TestCreateDeviceGroup:
    """Test the create_device_group tool function"""

    @pytest.fixture
    def mock_context(self):
        """Create a mock Context object"""
        context = AsyncMock(spec=Context)
        context.info = AsyncMock()
        
        # Mock client
        mock_client = MagicMock()
        mock_client.get = AsyncMock()
        mock_client.post = AsyncMock()
        
        # Mock request context and lifespan context
        context.request_context = MagicMock()
        context.request_context.lifespan_context = MagicMock()
        context.request_context.lifespan_context.get.return_value = mock_client
        
        return context

    @pytest.mark.asyncio
    async def test_create_device_group_success(self, mock_context):
        """Test create_device_group with successful creation"""
        # Mock get_device_groups to return existing groups
        with patch('itential_mcp.tools.device_groups.get_device_groups') as mock_get:
            mock_get.return_value = GetDeviceGroupsResponse([])  # No existing groups
            
            # Mock create response
            mock_response = MagicMock()
            mock_response_data = {
                "id": "new-group-123",
                "name": "New Production Group",
                "message": "Device group created successfully",
                "status": "active"
            }
            mock_response.json.return_value = mock_response_data
            
            mock_client = mock_context.request_context.lifespan_context.get.return_value
            mock_client.post.return_value = mock_response
            
            result = await device_groups.create_device_group(
                mock_context,
                name="New Production Group",
                description="A new production device group",
                devices=["router1", "router2"]
            )
            
            # Verify get_device_groups was called to check for duplicates
            mock_get.assert_called_once_with(mock_context)
            
            # Verify API call
            expected_body = {
                "groupName": "New Production Group",
                "groupDescription": "A new production device group",
                "deviceNames": "router1,router2"
            }
            mock_client.post.assert_called_once_with(
                "/configuration_manager/devicegroup",
                json=expected_body
            )
            
            # Verify result
            assert isinstance(result, CreateDeviceGroupResponse)
            assert result.object_id == "new-group-123"
            assert result.name == "New Production Group"
            assert result.message == "Device group created successfully"
            assert result.status == "active"

    @pytest.mark.asyncio
    async def test_create_device_group_no_devices(self, mock_context):
        """Test create_device_group with no devices specified"""
        with patch('itential_mcp.tools.device_groups.get_device_groups') as mock_get:
            mock_get.return_value = GetDeviceGroupsResponse([])
            
            mock_response = MagicMock()
            mock_response_data = {
                "id": "empty-group-456",
                "name": "Empty Group",
                "message": "Empty device group created",
                "status": "active"
            }
            mock_response.json.return_value = mock_response_data
            
            mock_client = mock_context.request_context.lifespan_context.get.return_value
            mock_client.post.return_value = mock_response
            
            result = await device_groups.create_device_group(
                mock_context,
                name="Empty Group",
                description="An empty device group"
            )
            
            # Verify empty deviceNames was sent
            expected_body = {
                "groupName": "Empty Group",
                "groupDescription": "An empty device group",
                "deviceNames": ""
            }
            mock_client.post.assert_called_once_with(
                "/configuration_manager/devicegroup",
                json=expected_body
            )
            
            assert result.object_id == "empty-group-456"

    @pytest.mark.asyncio
    async def test_create_device_group_duplicate_name_error(self, mock_context):
        """Test create_device_group raises error for duplicate name"""
        # Mock get_device_groups to return existing group with same name
        existing_group = DeviceGroupElement(
            id="existing-123",
            name="Existing Group",
            devices=["device1"],
            description="Already exists"
        )
        
        with patch('itential_mcp.tools.device_groups.get_device_groups') as mock_get:
            mock_get.return_value = GetDeviceGroupsResponse([existing_group])
            
            with pytest.raises(ValueError, match="device group Existing Group already exists"):
                await device_groups.create_device_group(
                    mock_context,
                    name="Existing Group",
                    description="Duplicate group"
                )

    @pytest.mark.asyncio
    async def test_create_device_group_no_description(self, mock_context):
        """Test create_device_group with no description"""
        with patch('itential_mcp.tools.device_groups.get_device_groups') as mock_get:
            mock_get.return_value = GetDeviceGroupsResponse([])
            
            mock_response = MagicMock()
            mock_response_data = {
                "id": "no-desc-group",
                "name": "No Description Group",
                "message": "Group created without description",
                "status": "active"
            }
            mock_response.json.return_value = mock_response_data
            
            mock_client = mock_context.request_context.lifespan_context.get.return_value
            mock_client.post.return_value = mock_response
            
            await device_groups.create_device_group(
                mock_context,
                name="No Description Group"
            )
            
            # Verify None description was passed
            expected_body = {
                "groupName": "No Description Group",
                "groupDescription": None,
                "deviceNames": ""
            }
            mock_client.post.assert_called_once_with(
                "/configuration_manager/devicegroup",
                json=expected_body
            )

    @pytest.mark.asyncio
    async def test_create_device_group_logs_info(self, mock_context):
        """Test create_device_group logs info message"""
        with patch('itential_mcp.tools.device_groups.get_device_groups') as mock_get:
            mock_get.return_value = GetDeviceGroupsResponse([])
            
            mock_response = MagicMock()
            mock_response.json.return_value = {
                "id": "test", "name": "test", "message": "test", "status": "test"
            }
            
            mock_client = mock_context.request_context.lifespan_context.get.return_value
            mock_client.post.return_value = mock_response
            
            await device_groups.create_device_group(mock_context, name="Test")
            
            mock_context.info.assert_called_once_with("inside create_device_group(...)")


class TestAddDevicesToGroup:
    """Test the add_devices_to_group tool function"""

    @pytest.fixture
    def mock_context(self):
        """Create a mock Context object"""
        context = AsyncMock(spec=Context)
        context.info = AsyncMock()
        
        # Mock client
        mock_client = MagicMock()
        mock_client.put = AsyncMock()
        
        # Mock request context and lifespan context
        context.request_context = MagicMock()
        context.request_context.lifespan_context = MagicMock()
        context.request_context.lifespan_context.get.return_value = mock_client
        
        return context

    @pytest.mark.asyncio
    async def test_add_devices_to_group_success(self, mock_context):
        """Test add_devices_to_group with successful addition"""
        mock_response_data = {
            "status": "success"
        }
        
        mock_client = mock_context.request_context.lifespan_context.get.return_value
        mock_client.configuration_manager = MagicMock()
        mock_client.configuration_manager.add_devices_to_group = AsyncMock(return_value=mock_response_data)
        
        result = await device_groups.add_devices_to_group(
            mock_context,
            name="Test Group",
            devices=["device1", "device2", "device3"]
        )
        
        # Verify service method was called
        mock_client.configuration_manager.add_devices_to_group.assert_called_once_with(
            "Test Group", ["device1", "device2", "device3"]
        )
            
        # Verify result
        assert isinstance(result, AddDevicesToGroupResponse)
        assert result.status == "success"
        assert result.message == "Devices added successfully"

    @pytest.mark.asyncio
    async def test_add_devices_to_group_with_message(self, mock_context):
        """Test add_devices_to_group when API returns a message"""
        mock_response_data = {
            "status": "partial",
            "message": "2 of 3 devices added successfully"
        }
        
        mock_client = mock_context.request_context.lifespan_context.get.return_value
        mock_client.configuration_manager = MagicMock()
        mock_client.configuration_manager.add_devices_to_group = AsyncMock(return_value=mock_response_data)
        
        result = await device_groups.add_devices_to_group(
            mock_context,
            name="Partial Group",
            devices=["device1", "device2", "device3"]
        )
        
        assert result.status == "partial"
        assert result.message == "2 of 3 devices added successfully"

    @pytest.mark.asyncio
    async def test_add_devices_to_group_no_devices(self, mock_context):
        """Test add_devices_to_group with no devices specified"""
        mock_response_data = {"status": "success"}
        
        mock_client = mock_context.request_context.lifespan_context.get.return_value
        mock_client.configuration_manager = MagicMock()
        mock_client.configuration_manager.add_devices_to_group = AsyncMock(return_value=mock_response_data)
        
        await device_groups.add_devices_to_group(
            mock_context,
            name="No Devices Group"
        )
        
        # Verify None devices was handled
        mock_client.configuration_manager.add_devices_to_group.assert_called_once_with(
            "No Devices Group", None
        )

    @pytest.mark.asyncio
    async def test_add_devices_to_group_logs_info(self, mock_context):
        """Test add_devices_to_group logs info message"""
        mock_client = mock_context.request_context.lifespan_context.get.return_value
        mock_client.configuration_manager = MagicMock()
        mock_client.configuration_manager.add_devices_to_group = AsyncMock(return_value={"status": "test"})
        
        await device_groups.add_devices_to_group(mock_context, name="Test")
        
        mock_context.info.assert_called_once_with("inside add_devices_to_group(...)")


class TestRemoveDevicesFromGroup:
    """Test the remove_devices_from_group tool function"""

    @pytest.fixture
    def mock_context(self):
        """Create a mock Context object"""
        context = AsyncMock(spec=Context)
        context.info = AsyncMock()
        
        # Mock client with configuration_manager
        mock_client = MagicMock()
        mock_client.put = AsyncMock()
        mock_client.configuration_manager = MagicMock()
        mock_client.configuration_manager.describe_device_group = AsyncMock()
        
        # Mock request context and lifespan context
        context.request_context = MagicMock()
        context.request_context.lifespan_context = MagicMock()
        context.request_context.lifespan_context.get.return_value = mock_client
        
        return context

    @pytest.mark.asyncio
    async def test_remove_devices_from_group_success(self, mock_context):
        """Test remove_devices_from_group with successful removal"""
        mock_response_data = {
            "status": "success"
        }
        
        mock_client = mock_context.request_context.lifespan_context.get.return_value
        mock_client.configuration_manager = MagicMock()
        mock_client.configuration_manager.remove_devices_from_group = AsyncMock(return_value=mock_response_data)
        
        result = await device_groups.remove_devices_from_group(
            mock_context,
            name="Test Group",
            devices=["device2", "device4"]  # Remove 2 devices
        )
        
        # Verify service method was called
        mock_client.configuration_manager.remove_devices_from_group.assert_called_once_with(
            "Test Group", ["device2", "device4"]
        )
        
        # Verify result
        assert isinstance(result, RemoveDevicesFromGroupResponse)
        assert result.status == "success"
        assert result.message == "Devices removed successfully"

    @pytest.mark.asyncio
    async def test_remove_devices_from_group_remove_all(self, mock_context):
        """Test remove_devices_from_group removing all devices"""
        mock_client = mock_context.request_context.lifespan_context.get.return_value
        mock_client.configuration_manager = MagicMock()
        mock_client.configuration_manager.remove_devices_from_group = AsyncMock(return_value={"status": "success"})
        
        result = await device_groups.remove_devices_from_group(
            mock_context,
            name="Empty Group",
            devices=["device1", "device2"]  # Remove all devices
        )
        
        # Verify service method was called
        mock_client.configuration_manager.remove_devices_from_group.assert_called_once_with(
            "Empty Group", ["device1", "device2"]
        )
        
        assert result.status == "success"

    @pytest.mark.asyncio
    async def test_remove_devices_from_group_nonexistent_devices(self, mock_context):
        """Test remove_devices_from_group with devices not in group"""
        mock_client = mock_context.request_context.lifespan_context.get.return_value
        mock_client.configuration_manager = MagicMock()
        mock_client.configuration_manager.remove_devices_from_group = AsyncMock(return_value={"status": "success"})
        
        result = await device_groups.remove_devices_from_group(
            mock_context,
            name="Test Group",
            devices=["device4", "device5"]  # Devices not in group
        )
        
        # Verify service method was called
        mock_client.configuration_manager.remove_devices_from_group.assert_called_once_with(
            "Test Group", ["device4", "device5"]
        )
        
        assert result.status == "success"

    @pytest.mark.asyncio
    async def test_remove_devices_from_group_partial_removal(self, mock_context):
        """Test remove_devices_from_group with partial device removal"""
        mock_client = mock_context.request_context.lifespan_context.get.return_value
        mock_client.configuration_manager = MagicMock()
        mock_client.configuration_manager.remove_devices_from_group = AsyncMock(return_value={"status": "partial"})
        
        result = await device_groups.remove_devices_from_group(
            mock_context,
            name="Mixed Group", 
            devices=["device2", "device5"]  # device2 exists, device5 doesn't
        )
        
        # Verify service method was called
        mock_client.configuration_manager.remove_devices_from_group.assert_called_once_with(
            "Mixed Group", ["device2", "device5"]
        )
        
        assert result.status == "partial"

    @pytest.mark.asyncio
    async def test_remove_devices_from_group_no_devices_specified(self, mock_context):
        """Test remove_devices_from_group with no devices specified"""
        mock_client = mock_context.request_context.lifespan_context.get.return_value
        mock_client.configuration_manager = MagicMock()
        mock_client.configuration_manager.remove_devices_from_group = AsyncMock(return_value={"status": "success"})
        
        result = await device_groups.remove_devices_from_group(
            mock_context,
            name="No Remove Group",
            devices=[]
        )
        
        # Verify service method was called
        mock_client.configuration_manager.remove_devices_from_group.assert_called_once_with(
            "No Remove Group", []
        )
        
        assert result.status == "success"

    @pytest.mark.asyncio
    async def test_remove_devices_from_group_empty_group(self, mock_context):
        """Test remove_devices_from_group with empty group"""
        mock_client = mock_context.request_context.lifespan_context.get.return_value
        mock_client.configuration_manager = MagicMock()
        mock_client.configuration_manager.remove_devices_from_group = AsyncMock(return_value={"status": "warning"})
        
        result = await device_groups.remove_devices_from_group(
            mock_context,
            name="Empty Group",
            devices=["device1"]
        )
        
        # Verify service method was called
        mock_client.configuration_manager.remove_devices_from_group.assert_called_once_with(
            "Empty Group", ["device1"]
        )
        
        assert result.status == "warning"

    @pytest.mark.asyncio
    async def test_remove_devices_from_group_logs_info(self, mock_context):
        """Test remove_devices_from_group logs info message"""
        mock_client = mock_context.request_context.lifespan_context.get.return_value
        mock_client.configuration_manager = MagicMock()
        mock_client.configuration_manager.remove_devices_from_group = AsyncMock(return_value={"status": "test"})
        
        await device_groups.remove_devices_from_group(mock_context, name="Test", devices=[])
        
        mock_context.info.assert_called_once_with("inside remove_devices_from_group(...)")


class TestToolsIntegration:
    """Test integration scenarios between tools functions"""

    @pytest.fixture
    def mock_context(self):
        """Create a mock Context object"""
        context = AsyncMock(spec=Context)
        context.info = AsyncMock()
        
        # Mock client
        mock_client = MagicMock()
        mock_client.get = AsyncMock()
        mock_client.post = AsyncMock()
        mock_client.put = AsyncMock()
        mock_client.configuration_manager = MagicMock()
        mock_client.configuration_manager.describe_device_group = AsyncMock()
        
        # Mock request context and lifespan context
        context.request_context = MagicMock()
        context.request_context.lifespan_context = MagicMock()
        context.request_context.lifespan_context.get.return_value = mock_client
        
        return context

    @pytest.mark.asyncio
    async def test_create_then_add_devices_workflow(self, mock_context):
        """Test workflow: create device group then add devices to it"""
        # Step 1: Create device group
        with patch('itential_mcp.tools.device_groups.get_device_groups') as mock_get:
            mock_get.return_value = GetDeviceGroupsResponse([])  # No existing groups
            
            create_response = MagicMock()
            create_response.json.return_value = {
                "id": "workflow-group",
                "name": "Workflow Test Group",
                "message": "Created successfully",
                "status": "active"
            }
            
            mock_client = mock_context.request_context.lifespan_context.get.return_value
            mock_client.post.return_value = create_response
            
            create_result = await device_groups.create_device_group(
                mock_context,
                name="Workflow Test Group",
                description="Test workflow integration"
            )
            
            assert create_result.object_id == "workflow-group"
            assert create_result.status == "active"
        
        # Step 2: Add devices to the created group
        mock_client.configuration_manager.add_devices_to_group = AsyncMock(return_value={"status": "success"})
        
        add_result = await device_groups.add_devices_to_group(
            mock_context,
            name="Workflow Test Group",
            devices=["device1", "device2", "device3"]
        )
        
        assert add_result.status == "success"
        assert "successfully" in add_result.message

    @pytest.mark.asyncio
    async def test_list_create_list_workflow(self, mock_context):
        """Test workflow: list groups, create new one, list again to verify"""
        mock_client = mock_context.request_context.lifespan_context.get.return_value
        
        # Step 1: List existing groups (empty)
        mock_client.configuration_manager = MagicMock()
        mock_client.configuration_manager.get_device_groups = AsyncMock(return_value=[])
        
        initial_groups = await device_groups.get_device_groups(mock_context)
        assert len(initial_groups.root) == 0
        
        # Step 2: Create new group
        with patch('itential_mcp.tools.device_groups.get_device_groups') as mock_get:
            mock_get.return_value = GetDeviceGroupsResponse([])  # For duplicate check
            
            create_response = MagicMock()
            create_response.json.return_value = {
                "id": "new-workflow-group",
                "name": "New Workflow Group",
                "message": "Created in workflow",
                "status": "active"
            }
            mock_client.post.return_value = create_response
            
            create_result = await device_groups.create_device_group(
                mock_context,
                name="New Workflow Group",
                description="Created in integration test"
            )
            
            assert create_result.object_id == "new-workflow-group"
        
        # Step 3: List groups again (should include new one)
        updated_data = [{
            "id": "new-workflow-group",
            "name": "New Workflow Group",
            "devices": [],
            "description": "Created in integration test"
        }]
        mock_client.configuration_manager.get_device_groups = AsyncMock(return_value=updated_data)
        
        updated_groups = await device_groups.get_device_groups(mock_context)
        assert len(updated_groups.root) == 1
        assert updated_groups.root[0].name == "New Workflow Group"

    @pytest.mark.asyncio
    async def test_error_handling_consistency(self, mock_context):
        """Test that all functions handle errors consistently"""
        mock_client = mock_context.request_context.lifespan_context.get.return_value
        
        # Test get_device_groups with client error
        mock_client.configuration_manager = MagicMock()
        mock_client.configuration_manager.get_device_groups = AsyncMock(side_effect=Exception("API Error"))
        
        with pytest.raises(Exception, match="API Error"):
            await device_groups.get_device_groups(mock_context)
        
        # Test create_device_group with duplicate name detection
        existing_group = DeviceGroupElement(
            id="existing",
            name="Existing Group",
            devices=[],
            description="Exists"
        )
        
        with patch('itential_mcp.tools.device_groups.get_device_groups') as mock_get:
            mock_get.return_value = GetDeviceGroupsResponse([existing_group])
            
            with pytest.raises(ValueError, match="already exists"):
                await device_groups.create_device_group(
                    mock_context,
                    name="Existing Group",
                    description="Should fail"
                )