import pytest
import sys
import os
from unittest.mock import MagicMock, patch

# 添加项目根目录到Python路径
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))

from modules.serial_port import SerialPort


class TestSerialPort:
    
    def setup_method(self):
        """每个测试方法执行前的设置"""
        # 创建SerialPort实例
        self.serial_port = SerialPort()
        # 模拟数据处理和状态处理回调
        self.data_callback = MagicMock()
        self.status_callback = MagicMock()
        self.serial_port.set_data_callback(self.data_callback)
        self.serial_port.set_status_callback(self.status_callback)
    
    def teardown_method(self):
        """每个测试方法执行后的清理"""
        # 确保断开连接
        self.serial_port.disconnect()
    
    @patch('modules.serial_port.serial.Serial')
    def test_connect_success(self, mock_serial):
        """测试成功连接串口"""
        # 模拟serial.Serial返回一个有效的串口对象
        mock_serial_instance = MagicMock()
        mock_serial.return_value = mock_serial_instance
        
        # 调用connect方法
        success, message = self.serial_port.connect('COM1', 9600)
        
        # 验证结果
        assert success is True
        assert message == "串口连接成功"
        mock_serial.assert_called_once_with(port='COM1', baudrate=9600, timeout=1)
        # 验证状态回调被调用，且状态为True
        self.status_callback.assert_called_once_with(True)
    
    @patch('modules.serial_port.serial.Serial')
    def test_connect_failure(self, mock_serial):
        """测试连接串口失败"""
        # 模拟serial.Serial抛出异常
        mock_serial.side_effect = Exception("串口未找到")
        
        # 调用connect方法
        success, message = self.serial_port.connect('COM1', 9600)
        
        # 验证结果
        assert success is False
        assert "连接失败" in message
        mock_serial.assert_called_once_with(port='COM1', baudrate=9600, timeout=1)
        # 验证状态回调被调用，且状态为False
        self.status_callback.assert_called_once_with(False)
    
    @patch('modules.serial_port.serial.Serial')
    def test_disconnect(self, mock_serial):
        """测试断开串口连接"""
        # 模拟serial.Serial返回一个有效的串口对象
        mock_serial_instance = MagicMock()
        mock_serial.return_value = mock_serial_instance
        
        # 先连接，再断开
        self.serial_port.connect('COM1', 9600)
        # 重置状态回调的调用记录
        self.status_callback.reset_mock()
        
        # 调用disconnect方法
        self.serial_port.disconnect()
        
        # 验证结果
        mock_serial_instance.close.assert_called_once()
        # 验证状态回调被调用，且状态为False
        self.status_callback.assert_called_once_with(False)
    
    @patch('modules.serial_port.serial.Serial')
    def test_disconnect_not_connected(self, mock_serial):
        """测试断开未连接的串口"""
        # 不连接直接断开
        self.serial_port.disconnect()
        
        # 验证状态回调没有被调用
        self.status_callback.assert_not_called()
    
    @patch('modules.serial_port.serial.Serial')
    def test_send_data_success(self, mock_serial):
        """测试成功发送数据"""
        # 模拟serial.Serial返回一个有效的串口对象
        mock_serial_instance = MagicMock()
        mock_serial.return_value = mock_serial_instance
        
        # 先连接
        self.serial_port.connect('COM1', 9600)
        
        # 调用send_data方法
        test_data = "Hello, Serial!"
        success, message = self.serial_port.send_data(test_data)
        
        # 验证结果
        assert success is True
        assert message == "数据发送成功"
        mock_serial_instance.write.assert_called_once_with(test_data.encode())
    
    @patch('modules.serial_port.serial.Serial')
    def test_send_data_failure(self, mock_serial):
        """测试发送数据失败"""
        # 模拟serial.Serial返回一个有效的串口对象，但write方法抛出异常
        mock_serial_instance = MagicMock()
        mock_serial_instance.write.side_effect = Exception("写入失败")
        mock_serial.return_value = mock_serial_instance
        
        # 先连接
        self.serial_port.connect('COM1', 9600)
        
        # 调用send_data方法
        test_data = "Hello, Serial!"
        success, message = self.serial_port.send_data(test_data)
        
        # 验证结果
        assert success is False
        assert "发送失败" in message
        mock_serial_instance.write.assert_called_once_with(test_data.encode())
    
    @patch('modules.serial_port.serial.Serial')
    def test_send_data_not_connected(self, mock_serial):
        """测试未连接时发送数据"""
        # 不连接直接发送数据
        test_data = "Hello, Serial!"
        success, message = self.serial_port.send_data(test_data)
        
        # 验证结果
        assert success is False
        assert message == "串口未连接"
    
    @patch('modules.serial_port.serial.Serial')
    def test_get_status(self, mock_serial):
        """测试获取串口状态"""
        # 模拟serial.Serial返回一个有效的串口对象
        mock_serial_instance = MagicMock()
        mock_serial.return_value = mock_serial_instance
        
        # 初始状态应该是False
        assert self.serial_port.get_status() is False
        
        # 连接后状态应该是True
        self.serial_port.connect('COM1', 9600)
        assert self.serial_port.get_status() is True
        
        # 断开后状态应该是False
        self.serial_port.disconnect()
        assert self.serial_port.get_status() is False
    
    @patch('modules.serial_port.serial.Serial')
    def test_receive_data(self, mock_serial):
        """测试接收数据"""
        # 模拟serial.Serial返回一个有效的串口对象，并设置readline的返回值
        mock_serial_instance = MagicMock()
        mock_serial_instance.readline.return_value = "Test data\n".encode()
        mock_serial.return_value = mock_serial_instance
        
        # 连接串口
        self.serial_port.connect('COM1', 9600)
        
        # 模拟一个接收到的数据
        # 需要手动触发内部的读取线程来接收数据，但这在单元测试中比较复杂
        # 这里我们直接调用内部的_read_serial方法来模拟
        # 注意：实际代码中可能需要修改SerialPort类，使其_read_serial方法可以从外部调用，
        # 或者添加一个专门用于测试的方法
        
        # 由于_read_serial是一个私有方法，这里我们使用patch来模拟它被调用时的行为
        with patch.object(self.serial_port, '_read_serial') as mock_read_serial:
            # 调用mock_read_serial并模拟其行为
            # 这里我们手动调用数据回调函数来模拟数据接收
            self.data_callback("Test data")
            
            # 验证数据回调被调用
            self.data_callback.assert_called_with("Test data")


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