"""
Unit tests for CPAP MCP Server.
"""

import pytest
import base64
import json
import tempfile
import shutil
import zipfile
import io
from pathlib import Path
from datetime import datetime, date
from unittest.mock import Mock, MagicMock, patch, AsyncMock
from mcp.types import TextContent


# Mark as unit tests
pytestmark = pytest.mark.unit


@pytest.fixture(autouse=True)
def reset_global_state():
    """Reset global state before each test."""
    import cpap_mcp.server as server_module
    server_module.working_dir = None
    server_module.cpap_reader = None
    yield
    # Cleanup after test
    if server_module.working_dir and server_module.working_dir.exists():
        shutil.rmtree(server_module.working_dir, ignore_errors=True)
    server_module.working_dir = None
    server_module.cpap_reader = None


class TestServerInitialization:
    """Test server initialization."""

    def test_server_imports(self):
        """Test that server module can be imported."""
        from cpap_mcp import server
        assert server is not None

    def test_app_exists(self):
        """Test that the MCP server app is created."""
        from cpap_mcp.server import app
        assert app is not None
        assert app.name == "cpap-mcp"


class TestWorkingDirectory:
    """Test working directory management."""

    def test_get_working_dir_creates_directory(self):
        """Test that get_working_dir creates a directory."""
        from cpap_mcp.server import get_working_dir
        
        work_dir = get_working_dir()
        assert work_dir is not None
        assert isinstance(work_dir, Path)
        assert work_dir.exists()

    def test_working_dir_is_persistent(self):
        """Test that working directory persists across calls."""
        from cpap_mcp.server import get_working_dir
        
        dir1 = get_working_dir()
        dir2 = get_working_dir()
        assert dir1 == dir2

    def test_reset_reader(self):
        """Test that reset_reader clears the reader."""
        from cpap_mcp.server import reset_reader
        import cpap_mcp.server as server_module
        
        server_module.cpap_reader = Mock()
        reset_reader()
        assert server_module.cpap_reader is None


class TestHelperFunctions:
    """Test helper functions."""

    def test_serialize_datetime(self):
        """Test datetime serialization."""
        from cpap_mcp.server import serialize_datetime
        from datetime import datetime, date
        
        dt = datetime(2024, 1, 1, 12, 30, 45)
        result = serialize_datetime(dt)
        assert result == "2024-01-01T12:30:45"
        
        d = date(2024, 1, 1)
        result = serialize_datetime(d)
        assert result == "2024-01-01"

    def test_serialize_non_datetime(self):
        """Test that non-datetime objects are returned as-is."""
        from cpap_mcp.server import serialize_datetime
        
        assert serialize_datetime("test") == "test"
        assert serialize_datetime(123) == 123
        assert serialize_datetime(None) is None
        assert serialize_datetime([1, 2, 3]) == [1, 2, 3]
        assert serialize_datetime({"key": "value"}) == {"key": "value"}


@pytest.mark.asyncio
class TestToolListing:
    """Test tool listing functionality."""

    async def test_list_tools_returns_list(self):
        """Test that list_tools returns a list of tools."""
        from cpap_mcp.server import list_tools
        
        tools = await list_tools()
        assert isinstance(tools, list)
        assert len(tools) > 0

    async def test_all_required_tools_exist(self):
        """Test that all required tools are registered."""
        from cpap_mcp.server import list_tools
        
        tools = await list_tools()
        tool_names = [tool.name for tool in tools]
        
        required_tools = [
            "upload_archive",
            "list_uploaded_files",
            "clear_uploaded_files",
            "get_devices",
            "get_sessions",
            "get_session_details",
            "get_session_events",
            "get_session_pressure_data",
            "get_session_flow_data",
            "get_session_spo2_data",
            "get_summary_statistics",
            "get_device_settings",
            "analyze_therapy_effectiveness",
            "get_validation_errors",
            "export_all_data",
            "propose_pressure_adjustment",
            "propose_comfort_improvements",
        ]
        
        for required_tool in required_tools:
            assert required_tool in tool_names

    async def test_tool_schemas_are_valid(self):
        """Test that tool schemas have required properties."""
        from cpap_mcp.server import list_tools
        
        tools = await list_tools()
        for tool in tools:
            assert tool.name
            assert tool.description
            assert tool.inputSchema
            assert "type" in tool.inputSchema
            assert tool.inputSchema["type"] == "object"


@pytest.mark.asyncio
class TestFileOperations:
    """Test file upload and management operations."""

    async def test_upload_archive_success(self):
        """Test successful archive upload."""
        from cpap_mcp.server import upload_archive
        import zipfile
        import io
        
        # Create a ZIP archive in memory
        zip_buffer = io.BytesIO()
        with zipfile.ZipFile(zip_buffer, 'w', zipfile.ZIP_DEFLATED) as zip_file:
            zip_file.writestr('DATALOG/20240101/test.edf', b'Test CPAP data')
            zip_file.writestr('STR.edf', b'Session summary')
        
        zip_content = zip_buffer.getvalue()
        content_b64 = base64.b64encode(zip_content).decode('utf-8')
        
        arguments = {
            "content": content_b64
        }
        
        result = await upload_archive(arguments)
        assert len(result) == 1
        assert isinstance(result[0], TextContent)
        assert "Successfully uploaded and extracted" in result[0].text
        assert "2 files extracted" in result[0].text

    async def test_upload_archive_missing_content(self):
        """Test upload_archive with missing content."""
        from cpap_mcp.server import upload_archive
        
        with pytest.raises(ValueError, match="content is required"):
            await upload_archive({})

    async def test_upload_archive_invalid_base64(self):
        """Test upload_archive with invalid base64."""
        from cpap_mcp.server import upload_archive
        
        arguments = {
            "content": "not-valid-base64!!!"
        }
        
        with pytest.raises(ValueError, match="Failed to decode base64"):
            await upload_archive(arguments)

    async def test_upload_archive_invalid_zip(self):
        """Test upload_archive with invalid ZIP content."""
        from cpap_mcp.server import upload_archive
        
        # Valid base64 but not a ZIP file
        content_b64 = base64.b64encode(b"This is not a ZIP file").decode('utf-8')
        arguments = {
            "content": content_b64
        }
        
        with pytest.raises(ValueError, match="Invalid ZIP file format"):
            await upload_archive(arguments)

    async def test_upload_archive_with_directories(self):
        """Test that directory entries in ZIP are skipped."""
        from cpap_mcp.server import upload_archive
        import zipfile
        import io
        
        # Create a ZIP with directory entries
        zip_buffer = io.BytesIO()
        with zipfile.ZipFile(zip_buffer, 'w', zipfile.ZIP_DEFLATED) as zip_file:
            # Add a directory entry (ends with /)
            zip_file.writestr('DATALOG/', '')
            # Add actual file
            zip_file.writestr('DATALOG/test.edf', b'Test data')
        
        zip_content = zip_buffer.getvalue()
        content_b64 = base64.b64encode(zip_content).decode('utf-8')
        
        result = await upload_archive({"content": content_b64})
        assert "1 files extracted" in result[0].text

    async def test_upload_archive_skips_hidden_files(self):
        """Test that hidden files are skipped."""
        from cpap_mcp.server import upload_archive
        import zipfile
        import io
        
        # Create a ZIP with hidden files
        zip_buffer = io.BytesIO()
        with zipfile.ZipFile(zip_buffer, 'w', zipfile.ZIP_DEFLATED) as zip_file:
            zip_file.writestr('.hidden', b'Hidden file')
            zip_file.writestr('.DS_Store', b'Mac metadata')
            zip_file.writestr('visible.edf', b'Visible file')
        
        zip_content = zip_buffer.getvalue()
        content_b64 = base64.b64encode(zip_content).decode('utf-8')
        
        result = await upload_archive({"content": content_b64})
        assert "1 files extracted" in result[0].text

    async def test_list_uploaded_files_empty(self):
        """Test listing files when none uploaded."""
        from cpap_mcp.server import list_uploaded_files
        
        result = await list_uploaded_files()
        assert len(result) == 1
        assert "No files uploaded" in result[0].text

    async def test_list_uploaded_files_with_files(self):
        """Test listing uploaded files."""
        from cpap_mcp.server import upload_archive, list_uploaded_files
        import zipfile
        import io
        
        # Upload an archive first
        zip_buffer = io.BytesIO()
        with zipfile.ZipFile(zip_buffer, 'w') as zip_file:
            zip_file.writestr('test.edf', b'test data')
        
        content_b64 = base64.b64encode(zip_buffer.getvalue()).decode('utf-8')
        await upload_archive({"content": content_b64})
        
        result = await list_uploaded_files()
        assert len(result) == 1
        assert "test.edf" in result[0].text
        assert "bytes" in result[0].text

    async def test_clear_uploaded_files(self):
        """Test clearing uploaded files."""
        from cpap_mcp.server import upload_archive, clear_uploaded_files, list_uploaded_files
        import zipfile
        import io
        
        # Upload an archive
        zip_buffer = io.BytesIO()
        with zipfile.ZipFile(zip_buffer, 'w') as zip_file:
            zip_file.writestr('test.edf', b'test data')
        
        content_b64 = base64.b64encode(zip_buffer.getvalue()).decode('utf-8')
        await upload_archive({"content": content_b64})
        
        # Clear files
        result = await clear_uploaded_files()
        assert len(result) == 1
        assert "Successfully cleared" in result[0].text
        
        # Verify files are cleared
        result = await list_uploaded_files()
        assert "No files uploaded" in result[0].text


@pytest.mark.asyncio
class TestCallTool:
    """Test the call_tool dispatcher."""

    async def test_call_tool_unknown_tool(self):
        """Test calling an unknown tool."""
        from cpap_mcp.server import call_tool
        
        result = await call_tool("unknown_tool", {})
        assert len(result) == 1
        assert "Error" in result[0].text
        assert "Unknown tool" in result[0].text

    async def test_call_tool_with_exception(self):
        """Test call_tool error handling."""
        from cpap_mcp.server import call_tool
        
        # Test with unknown tool to trigger error handling
        result = await call_tool("unknown_tool", {})
        assert len(result) == 1
        assert "Error" in result[0].text
        assert "Unknown tool" in result[0].text


@pytest.mark.asyncio
class TestDeviceOperations:
    """Test device-related operations."""

    @patch('cpap_mcp.server.initialize_reader')
    async def test_get_devices(self, mock_init_reader):
        """Test getting devices."""
        from cpap_mcp.server import get_devices
        
        mock_device = Mock()
        mock_device.model_dump.return_value = {
            "serial_number": "12345678",
            "model": "AirSense 10",
            "manufacturer": "ResMed"
        }
        
        mock_reader = Mock()
        mock_reader.get_devices.return_value = [mock_device]
        mock_init_reader.return_value = mock_reader
        
        result = await get_devices()
        assert len(result) == 1
        data = json.loads(result[0].text)
        assert data["count"] == 1
        assert len(data["devices"]) == 1


@pytest.mark.asyncio
class TestSessionOperations:
    """Test session-related operations."""

    @patch('cpap_mcp.server.initialize_reader')
    async def test_get_sessions_no_filters(self, mock_init_reader):
        """Test getting sessions without filters."""
        from cpap_mcp.server import get_sessions
        
        mock_session = Mock()
        mock_session.to_dict.return_value = {"session_id": "test123"}
        
        mock_reader = Mock()
        mock_reader.get_sessions.return_value = [mock_session]
        mock_init_reader.return_value = mock_reader
        
        result = await get_sessions({})
        assert len(result) == 1
        data = json.loads(result[0].text)
        assert data["count"] == 1

    @patch('cpap_mcp.server.initialize_reader')
    async def test_get_sessions_with_filters(self, mock_init_reader):
        """Test getting sessions with filters."""
        from cpap_mcp.server import get_sessions
        
        mock_reader = Mock()
        mock_reader.get_sessions.return_value = []
        mock_init_reader.return_value = mock_reader
        
        arguments = {
            "device_id": "12345678",
            "start_date": "2024-01-01",
            "end_date": "2024-01-31",
            "min_duration_hours": 4.0
        }
        
        result = await get_sessions(arguments)
        assert len(result) == 1
        data = json.loads(result[0].text)
        assert data["filters_applied"]["device_id"] == "12345678"

    @patch('cpap_mcp.server.initialize_reader')
    async def test_get_session_details_not_found(self, mock_init_reader):
        """Test getting details for non-existent session."""
        from cpap_mcp.server import get_session_details
        
        mock_reader = Mock()
        mock_reader.get_sessions.return_value = []
        mock_init_reader.return_value = mock_reader
        
        with pytest.raises(ValueError, match="Session not found"):
            await get_session_details({"session_id": "nonexistent"})

    @patch('cpap_mcp.server.initialize_reader')
    async def test_get_session_details_success(self, mock_init_reader):
        """Test getting session details."""
        from cpap_mcp.server import get_session_details
        
        mock_session = Mock()
        mock_session.session_id = "test123"
        mock_session.to_dict.return_value = {"session_id": "test123", "data": "test"}
        
        mock_reader = Mock()
        mock_reader.get_sessions.return_value = [mock_session]
        mock_init_reader.return_value = mock_reader
        
        result = await get_session_details({"session_id": "test123"})
        assert len(result) == 1
        data = json.loads(result[0].text)
        assert data["session_id"] == "test123"

    async def test_get_session_details_missing_id(self):
        """Test get_session_details without session_id."""
        from cpap_mcp.server import get_session_details
        
        with pytest.raises(ValueError, match="session_id is required"):
            await get_session_details({})


@pytest.mark.asyncio
class TestEventOperations:
    """Test event-related operations."""

    async def test_get_session_events_missing_id(self):
        """Test get_session_events without session_id."""
        from cpap_mcp.server import get_session_events
        
        with pytest.raises(ValueError, match="session_id is required"):
            await get_session_events({})

    @patch('cpap_mcp.server.initialize_reader')
    @patch('cpap_mcp.server.EventType')
    async def test_get_session_events_success(self, mock_event_type, mock_init_reader):
        """Test getting session events."""
        from cpap_mcp.server import get_session_events
        
        mock_event = Mock()
        mock_event.type.value = "OA"
        mock_event.timestamp = datetime(2024, 1, 1, 22, 30)
        mock_event.duration = 12.5
        mock_event.data = {}
        
        mock_session = Mock()
        mock_session.session_id = "test123"
        mock_session.get_events.return_value = [mock_event]
        
        mock_reader = Mock()
        mock_reader.get_sessions.return_value = [mock_session]
        mock_init_reader.return_value = mock_reader
        
        result = await get_session_events({"session_id": "test123"})
        assert len(result) == 1
        data = json.loads(result[0].text)
        assert data["count"] == 1


@pytest.mark.asyncio
class TestWaveformData:
    """Test waveform data operations."""

    async def test_get_pressure_data_missing_id(self):
        """Test get_session_pressure_data without session_id."""
        from cpap_mcp.server import get_session_pressure_data
        
        with pytest.raises(ValueError, match="session_id is required"):
            await get_session_pressure_data({})

    @patch('cpap_mcp.server.initialize_reader')
    async def test_get_pressure_data_no_data(self, mock_init_reader):
        """Test getting pressure data when none available."""
        from cpap_mcp.server import get_session_pressure_data
        
        mock_session = Mock()
        mock_session.session_id = "test123"
        mock_session.get_pressure_data.return_value = None
        
        mock_reader = Mock()
        mock_reader.get_sessions.return_value = [mock_session]
        mock_init_reader.return_value = mock_reader
        
        result = await get_session_pressure_data({"session_id": "test123"})
        assert "No pressure data available" in result[0].text

    async def test_get_flow_data_missing_id(self):
        """Test get_session_flow_data without session_id."""
        from cpap_mcp.server import get_session_flow_data
        
        with pytest.raises(ValueError, match="session_id is required"):
            await get_session_flow_data({})

    async def test_get_spo2_data_missing_id(self):
        """Test get_session_spo2_data without session_id."""
        from cpap_mcp.server import get_session_spo2_data
        
        with pytest.raises(ValueError, match="session_id is required"):
            await get_session_spo2_data({})


@pytest.mark.asyncio
class TestStatisticsAndAnalysis:
    """Test statistics and analysis operations."""

    @patch('cpap_mcp.server.initialize_reader')
    async def test_get_summary_statistics(self, mock_init_reader):
        """Test getting summary statistics."""
        from cpap_mcp.server import get_summary_statistics
        
        mock_reader = Mock()
        mock_reader.get_summary_statistics.return_value = {
            "total_sessions": 30,
            "average_ahi": 3.5
        }
        mock_init_reader.return_value = mock_reader
        
        result = await get_summary_statistics({"days": 30})
        assert len(result) == 1
        data = json.loads(result[0].text)
        assert data["total_sessions"] == 30

    @patch('cpap_mcp.server.initialize_reader')
    async def test_get_device_settings_no_sessions(self, mock_init_reader):
        """Test getting device settings when no sessions exist."""
        from cpap_mcp.server import get_device_settings
        
        mock_reader = Mock()
        mock_reader.get_sessions.return_value = []
        mock_init_reader.return_value = mock_reader
        
        result = await get_device_settings({})
        assert "No sessions found" in result[0].text

    @patch('cpap_mcp.server.initialize_reader')
    async def test_analyze_therapy_effectiveness_no_data(self, mock_init_reader):
        """Test therapy analysis with no data."""
        from cpap_mcp.server import analyze_therapy_effectiveness
        
        mock_reader = Mock()
        mock_reader.get_summary_statistics.return_value = {}
        mock_init_reader.return_value = mock_reader
        
        result = await analyze_therapy_effectiveness({"days": 30})
        assert "No session data available" in result[0].text

    @patch('cpap_mcp.server.initialize_reader')
    async def test_analyze_therapy_effectiveness_with_data(self, mock_init_reader):
        """Test therapy analysis with data."""
        from cpap_mcp.server import analyze_therapy_effectiveness
        
        mock_reader = Mock()
        mock_reader.get_summary_statistics.return_value = {
            "sessions": [
                {"summary": {"duration_hours": 7.5, "ahi": 3.2, "leak_95th": 12}},
                {"summary": {"duration_hours": 6.8, "ahi": 4.1, "leak_95th": 15}},
            ],
            "total_sessions": 2,
            "total_hours": 14.3,
            "average_hours_per_session": 7.15,
            "average_ahi": 3.65,
            "average_leak_95th": 13.5,
        }
        mock_init_reader.return_value = mock_reader
        
        result = await analyze_therapy_effectiveness({"days": 30})
        data = json.loads(result[0].text)
        assert "therapy_effectiveness" in data
        assert "compliance" in data
        assert "recommendations" in data


@pytest.mark.asyncio
class TestProposals:
    """Test therapy proposal operations."""

    async def test_propose_pressure_adjustment_missing_args(self):
        """Test pressure adjustment proposal with missing arguments."""
        from cpap_mcp.server import propose_pressure_adjustment
        
        with pytest.raises(ValueError, match="target_pressure and reason are required"):
            await propose_pressure_adjustment({})

    @patch('cpap_mcp.server.initialize_reader')
    @patch('cpap_mcp.server.create_pressure_adjustment_proposal')
    async def test_propose_pressure_adjustment_success(self, mock_create_proposal, mock_init_reader):
        """Test successful pressure adjustment proposal."""
        from cpap_mcp.server import propose_pressure_adjustment
        
        mock_proposal = Mock()
        mock_proposal.to_dict.return_value = {"proposal": "test"}
        mock_create_proposal.return_value = mock_proposal
        
        mock_device = Mock()
        mock_device.serial_number = "12345678"
        
        mock_summary = Mock()
        mock_summary.ahi = 8.5
        
        mock_session = Mock()
        mock_session.session_id = "test123"
        mock_session.start_time = datetime(2024, 1, 1)
        mock_session.settings = Mock()
        mock_session.summary = mock_summary
        
        mock_reader = Mock()
        mock_reader.get_sessions.return_value = [mock_session]
        mock_reader.get_devices.return_value = [mock_device]
        mock_init_reader.return_value = mock_reader
        
        result = await propose_pressure_adjustment({
            "target_pressure": 12.0,
            "reason": "High AHI"
        })
        assert len(result) == 1

    @patch('cpap_mcp.server.initialize_reader')
    @patch('cpap_mcp.server.create_comfort_improvement_proposal')
    async def test_propose_comfort_improvements_success(self, mock_create_proposal, mock_init_reader):
        """Test successful comfort improvement proposal."""
        from cpap_mcp.server import propose_comfort_improvements
        
        mock_proposal = Mock()
        mock_proposal.to_dict.return_value = {"proposal": "test"}
        mock_create_proposal.return_value = mock_proposal
        
        mock_device = Mock()
        mock_device.serial_number = "12345678"
        
        mock_session = Mock()
        mock_session.session_id = "test123"
        mock_session.start_time = datetime(2024, 1, 1)
        mock_session.settings = Mock()
        
        mock_reader = Mock()
        mock_reader.get_sessions.return_value = [mock_session]
        mock_reader.get_devices.return_value = [mock_device]
        mock_init_reader.return_value = mock_reader
        
        result = await propose_comfort_improvements({
            "enable_epr": True,
            "epr_level": 2
        })
        assert len(result) == 1


@pytest.mark.asyncio
class TestValidationAndExport:
    """Test validation and export operations."""

    @patch('cpap_mcp.server.initialize_reader')
    async def test_get_validation_errors(self, mock_init_reader):
        """Test getting validation errors."""
        from cpap_mcp.server import get_validation_errors
        
        mock_error = Mock()
        mock_error.model_dump.return_value = {"message": "test error", "severity": "error"}
        mock_error.severity = "error"
        
        mock_reader = Mock()
        mock_reader.get_validation_errors.return_value = [mock_error]
        mock_init_reader.return_value = mock_reader
        
        result = await get_validation_errors()
        data = json.loads(result[0].text)
        assert data["count"] == 1
        assert data["has_errors"] is True

    @patch('cpap_mcp.server.initialize_reader')
    async def test_export_all_data(self, mock_init_reader):
        """Test exporting all data."""
        from cpap_mcp.server import export_all_data
        
        mock_reader = Mock()
        mock_reader.export_to_dict.return_value = {"exported": "data"}
        mock_init_reader.return_value = mock_reader
        
        result = await export_all_data()
        data = json.loads(result[0].text)
        assert data["exported"] == "data"


class TestMainFunction:
    """Test main entry point functions."""

    def test_run_function_exists(self):
        """Test that run function exists."""
        from cpap_mcp.server import run
        assert callable(run)
