"""Unit tests for PM3Service.

Tests the PM3Service business logic layer, ensuring:
- Proper session validation
- Command execution flow
- Error handling and error codes
- Response format consistency
"""
import pytest
from unittest.mock import AsyncMock, Mock
from dataclasses import dataclass

from app.backend.services.pm3_service import PM3Service, PM3ServiceResult, PM3ServiceError


@dataclass
class MockPM3Result:
    """Mock PM3Worker command result."""
    success: bool
    output: str = ""
    error: str = None


class TestPM3ServiceCommandExecution:
    """Test command execution with session validation."""

    @pytest.mark.asyncio
    async def test_execute_command_success(self, mock_pm3_worker, mock_session_manager):
        """Test successful command execution."""
        # Arrange
        mock_pm3_worker.execute_command.return_value = MockPM3Result(
            success=True,
            output="Proxmark3 RFID instrument"
        )
        service = PM3Service(mock_pm3_worker, mock_session_manager)

        # Act
        result = await service.execute_command(
            command="hw version",
            session_id="test-session"
        )

        # Assert
        assert result.success is True
        assert result.data is not None
        assert result.data["output"] == "Proxmark3 RFID instrument"
        assert result.data["command"] == "hw version"
        assert result.data["session_id"] == "test-session"
        assert result.error is None

        # Verify session was updated
        mock_session_manager.update_activity.assert_called_once_with("test-session")

    @pytest.mark.asyncio
    async def test_execute_command_session_locked(self, mock_pm3_worker, mock_session_manager):
        """Test command execution when another session is active."""
        # Arrange
        mock_session_manager.can_execute.return_value = False
        service = PM3Service(mock_pm3_worker, mock_session_manager)

        # Act
        result = await service.execute_command(
            command="hw version",
            session_id="test-session"
        )

        # Assert
        assert result.success is False
        assert result.error is not None
        assert result.error.code == "session_locked"
        assert "Another session is active" in result.error.message
        assert result.data is None

        # Verify command was not executed
        mock_pm3_worker.execute_command.assert_not_called()

    @pytest.mark.asyncio
    async def test_execute_command_pm3_not_connected(self, mock_pm3_worker, mock_session_manager):
        """Test command execution when PM3 is not connected."""
        # Arrange
        mock_pm3_worker.is_connected.return_value = False
        service = PM3Service(mock_pm3_worker, mock_session_manager)

        # Act
        result = await service.execute_command(
            command="hw version",
            session_id="test-session"
        )

        # Assert
        assert result.success is False
        assert result.error is not None
        assert result.error.code == "pm3_not_connected"
        assert "not connected" in result.error.message.lower()
        assert result.data is None

        # Verify command was not executed
        mock_pm3_worker.execute_command.assert_not_called()

    @pytest.mark.asyncio
    async def test_execute_command_failure(self, mock_pm3_worker, mock_session_manager):
        """Test command execution when PM3 command fails."""
        # Arrange
        mock_pm3_worker.execute_command.return_value = MockPM3Result(
            success=False,
            error="Invalid command"
        )
        service = PM3Service(mock_pm3_worker, mock_session_manager)

        # Act
        result = await service.execute_command(
            command="invalid",
            session_id="test-session"
        )

        # Assert
        assert result.success is False
        assert result.error is not None
        assert result.error.code == "command_failed"
        assert "Invalid command" in result.error.details

    @pytest.mark.asyncio
    async def test_execute_command_exception_handling(self, mock_pm3_worker, mock_session_manager):
        """Test command execution when an exception occurs."""
        # Arrange
        mock_pm3_worker.execute_command.side_effect = Exception("Connection lost")
        service = PM3Service(mock_pm3_worker, mock_session_manager)

        # Act
        result = await service.execute_command(
            command="hw version",
            session_id="test-session"
        )

        # Assert
        assert result.success is False
        assert result.error is not None
        assert result.error.code == "execution_error"
        assert "Connection lost" in result.error.details


class TestPM3ServiceStatus:
    """Test status query operations."""

    @pytest.mark.asyncio
    async def test_get_status_connected(self, mock_pm3_worker, mock_session_manager):
        """Test status query when PM3 is connected."""
        # Arrange
        mock_pm3_worker.execute_command.return_value = MockPM3Result(
            success=True,
            output="Proxmark3 RFID instrument\nFirmware version: v4.0"
        )
        mock_session_manager.has_active_session.return_value = True
        service = PM3Service(mock_pm3_worker, mock_session_manager)

        # Act
        result = await service.get_status()

        # Assert
        assert result.success is True
        assert result.data["connected"] is True
        assert result.data["device_path"] == "/dev/ttyACM0"
        assert "Proxmark3" in result.data["version"]
        assert result.data["session_active"] is True

    @pytest.mark.asyncio
    async def test_get_status_not_connected(self, mock_pm3_worker, mock_session_manager):
        """Test status query when PM3 is not connected."""
        # Arrange
        mock_pm3_worker.is_connected.return_value = False
        service = PM3Service(mock_pm3_worker, mock_session_manager)

        # Act
        result = await service.get_status()

        # Assert
        assert result.success is True
        assert result.data["connected"] is False
        assert result.data["version"] is None


class TestPM3ServiceConnection:
    """Test connection management."""

    @pytest.mark.asyncio
    async def test_connect_success(self, mock_pm3_worker, mock_session_manager):
        """Test successful PM3 connection."""
        # Arrange
        service = PM3Service(mock_pm3_worker, mock_session_manager)

        # Act
        result = await service.connect()

        # Assert
        assert result.success is True
        assert "Connected" in result.data["message"]
        mock_pm3_worker.connect.assert_called_once()

    @pytest.mark.asyncio
    async def test_connect_failure(self, mock_pm3_worker, mock_session_manager):
        """Test PM3 connection failure."""
        # Arrange
        mock_pm3_worker.connect.side_effect = Exception("Device not found")
        service = PM3Service(mock_pm3_worker, mock_session_manager)

        # Act
        result = await service.connect()

        # Assert
        assert result.success is False
        assert result.error.code == "connection_error"
        assert "Device not found" in result.error.details

    @pytest.mark.asyncio
    async def test_disconnect_success(self, mock_pm3_worker, mock_session_manager):
        """Test successful PM3 disconnection."""
        # Arrange
        service = PM3Service(mock_pm3_worker, mock_session_manager)

        # Act
        result = await service.disconnect()

        # Assert
        assert result.success is True
        assert "Disconnected" in result.data["message"]
        mock_pm3_worker.disconnect.assert_called_once()


class TestPM3ServiceSessionManagement:
    """Test session management operations."""

    def test_create_session_success(self, mock_pm3_worker, mock_session_manager):
        """Test successful session creation."""
        # Arrange
        service = PM3Service(mock_pm3_worker, mock_session_manager)

        # Act
        result = service.create_session(force_takeover=False)

        # Assert
        assert result.success is True
        assert result.data["session_id"] == "test-session-id"
        mock_session_manager.create_session.assert_called_once_with(force_takeover=False)

    def test_create_session_exists(self, mock_pm3_worker, mock_session_manager):
        """Test session creation when session already exists."""
        # Arrange
        mock_session_manager.create_session.return_value = None
        service = PM3Service(mock_pm3_worker, mock_session_manager)

        # Act
        result = service.create_session(force_takeover=False)

        # Assert
        assert result.success is False
        assert result.error.code == "session_exists"
        assert "force_takeover" in result.error.details.lower()

    def test_release_session(self, mock_pm3_worker, mock_session_manager):
        """Test session release."""
        # Arrange
        service = PM3Service(mock_pm3_worker, mock_session_manager)

        # Act
        result = service.release_session("test-session")

        # Assert
        assert result.success is True
        assert "released" in result.data["message"].lower()
        mock_session_manager.release_session.assert_called_once_with("test-session")

    def test_get_session_info_found(self, mock_pm3_worker, mock_session_manager):
        """Test getting session info for existing session."""
        # Arrange
        from datetime import datetime
        mock_session = Mock()
        mock_session.session_id = "test-session"
        mock_session.created_at = datetime(2025, 1, 1, 12, 0, 0)
        mock_session.last_activity = datetime(2025, 1, 1, 12, 5, 0)

        mock_session_manager.get_session.return_value = mock_session
        mock_session_manager.is_session_active.return_value = True

        service = PM3Service(mock_pm3_worker, mock_session_manager)

        # Act
        result = service.get_session_info("test-session")

        # Assert
        assert result.success is True
        assert result.data["session_id"] == "test-session"
        assert result.data["is_active"] is True

    def test_get_session_info_not_found(self, mock_pm3_worker, mock_session_manager):
        """Test getting session info for non-existent session."""
        # Arrange
        mock_session_manager.get_session.return_value = None
        service = PM3Service(mock_pm3_worker, mock_session_manager)

        # Act
        result = service.get_session_info("nonexistent")

        # Assert
        assert result.success is False
        assert result.error.code == "session_not_found"
