"""Tests for Jupyter Server persistence across runner restart."""

import asyncio
import json
import os
import stat
import sys
import pytest
from pathlib import Path
from unittest.mock import AsyncMock, MagicMock, patch


from skop_runner.jupyter_backend.server_manager import JupyterServerManager


class TestServerPersistence:
    """Tests for server info save/load/reuse."""

    @pytest.fixture
    def manager(self, tmp_path):
        """Create a JupyterServerManager with persistence."""
        return JupyterServerManager(
            persistence_dir=str(tmp_path / "state"),
        )

    def test_save_and_load_server_info(self, tmp_path):
        """Save server info, load it, verify fields match."""
        mgr = JupyterServerManager(persistence_dir=str(tmp_path / "state"))

        # Simulate a running server by setting state directly
        mock_process = MagicMock()
        mock_process.pid = 12345
        mock_process.returncode = None
        mgr._process = mock_process
        mgr.port = 18888
        mgr.token = "test-token-abc"
        mgr.base_url = "http://127.0.0.1:18888"
        mgr._root_dir = "/home/user/project"

        mgr._save_server_info()

        # Verify file exists
        info_file = tmp_path / "state" / "server.json"
        assert info_file.exists()

        # Load and verify
        loaded = mgr._load_server_info()
        assert loaded is not None
        assert loaded["pid"] == 12345
        assert loaded["port"] == 18888
        assert loaded["token"] == "test-token-abc"
        assert loaded["base_url"] == "http://127.0.0.1:18888"
        assert loaded["root_dir"] == "/home/user/project"
        assert loaded["version"] == 1

    @pytest.mark.skipif(sys.platform == "win32", reason="Unix permission bits")
    def test_file_permissions(self, tmp_path):
        """Server info file should have 600 permissions."""
        mgr = JupyterServerManager(persistence_dir=str(tmp_path / "state"))

        mock_process = MagicMock()
        mock_process.pid = 12345
        mgr._process = mock_process
        mgr.port = 18888
        mgr.token = "tok"
        mgr.base_url = "http://127.0.0.1:18888"
        mgr._root_dir = "/tmp/project"

        mgr._save_server_info()

        info_file = tmp_path / "state" / "server.json"
        mode = stat.S_IMODE(os.stat(info_file).st_mode)
        assert mode == 0o600

    def test_load_missing_file(self, tmp_path):
        """Loading when file doesn't exist returns None."""
        mgr = JupyterServerManager(persistence_dir=str(tmp_path / "state"))
        assert mgr._load_server_info() is None

    def test_load_corrupted_file(self, tmp_path):
        """Corrupted JSON should return None."""
        state_dir = tmp_path / "state"
        state_dir.mkdir(parents=True)
        (state_dir / "server.json").write_text("not json {{{")

        mgr = JupyterServerManager(persistence_dir=str(state_dir))
        assert mgr._load_server_info() is None

    def test_load_wrong_version(self, tmp_path):
        """Wrong version should return None."""
        state_dir = tmp_path / "state"
        state_dir.mkdir(parents=True)
        (state_dir / "server.json").write_text(json.dumps({
            "version": 999,
            "pid": 1, "port": 2, "token": "t", "base_url": "u", "root_dir": "r",
        }))

        mgr = JupyterServerManager(persistence_dir=str(state_dir))
        assert mgr._load_server_info() is None

    def test_load_missing_fields(self, tmp_path):
        """Missing required fields should return None."""
        state_dir = tmp_path / "state"
        state_dir.mkdir(parents=True)
        (state_dir / "server.json").write_text(json.dumps({
            "version": 1,
            "pid": 1,
            # Missing port, token, base_url, root_dir
        }))

        mgr = JupyterServerManager(persistence_dir=str(state_dir))
        assert mgr._load_server_info() is None

    def test_delete_server_info(self, tmp_path):
        """Delete should remove the file."""
        mgr = JupyterServerManager(persistence_dir=str(tmp_path / "state"))

        mock_process = MagicMock()
        mock_process.pid = 12345
        mgr._process = mock_process
        mgr.port = 18888
        mgr.token = "tok"
        mgr.base_url = "http://127.0.0.1:18888"
        mgr._root_dir = "/tmp/project"

        mgr._save_server_info()
        info_file = tmp_path / "state" / "server.json"
        assert info_file.exists()

        mgr._delete_server_info()
        assert not info_file.exists()

    def test_no_persistence_without_dir(self):
        """Without persistence_dir, save/load are no-ops."""
        mgr = JupyterServerManager()  # No persistence_dir
        assert mgr._persistence_path is None
        mgr._save_server_info()  # Should not raise
        assert mgr._load_server_info() is None

    @pytest.mark.asyncio
    async def test_reuse_surviving_server(self, tmp_path):
        """When saved server is alive and healthy, reuse it."""
        state_dir = tmp_path / "state"
        state_dir.mkdir(parents=True)
        (state_dir / "server.json").write_text(json.dumps({
            "version": 1,
            "pid": os.getpid(),  # Use our own PID (guaranteed alive)
            "port": 18888,
            "token": "saved-token",
            "base_url": "http://127.0.0.1:18888",
            "root_dir": "/test/root",
        }))

        mgr = JupyterServerManager(persistence_dir=str(state_dir))

        # Mock httpx to simulate healthy server
        mock_response = MagicMock()
        mock_response.status_code = 200

        with patch("httpx.AsyncClient") as MockClient:
            mock_client = AsyncMock()
            mock_client.get = AsyncMock(return_value=mock_response)
            mock_client.__aenter__ = AsyncMock(return_value=mock_client)
            mock_client.__aexit__ = AsyncMock()
            MockClient.return_value = mock_client

            token = await mgr._try_reuse_server("/test/root")

        assert token == "saved-token"
        assert mgr.port == 18888
        assert mgr.base_url == "http://127.0.0.1:18888"
        assert mgr._root_dir == "/test/root"
        assert mgr._reused_pid == os.getpid()
        assert mgr.is_running()

        # Cleanup monitor task
        if mgr._monitor_task:
            mgr._monitor_task.cancel()
            try:
                await mgr._monitor_task
            except asyncio.CancelledError:
                pass

    @pytest.mark.asyncio
    async def test_discard_stale_server(self, tmp_path):
        """When PID is dead, discard saved info."""
        state_dir = tmp_path / "state"
        state_dir.mkdir(parents=True)
        (state_dir / "server.json").write_text(json.dumps({
            "version": 1,
            "pid": 999999999,  # Very unlikely to be a real PID
            "port": 18888,
            "token": "stale-token",
            "base_url": "http://127.0.0.1:18888",
            "root_dir": "/test/root",
        }))

        mgr = JupyterServerManager(persistence_dir=str(state_dir))
        token = await mgr._try_reuse_server("/test/root")

        assert token is None
        # File should be deleted
        assert not (state_dir / "server.json").exists()

    @pytest.mark.asyncio
    async def test_discard_when_root_dir_changed(self, tmp_path):
        """When root_dir doesn't match, don't reuse."""
        state_dir = tmp_path / "state"
        state_dir.mkdir(parents=True)
        (state_dir / "server.json").write_text(json.dumps({
            "version": 1,
            "pid": os.getpid(),
            "port": 18888,
            "token": "saved-token",
            "base_url": "http://127.0.0.1:18888",
            "root_dir": "/old/root",
        }))

        mgr = JupyterServerManager(persistence_dir=str(state_dir))
        token = await mgr._try_reuse_server("/new/root")

        assert token is None

    @pytest.mark.asyncio
    async def test_discard_when_api_fails(self, tmp_path):
        """When API health check fails, discard saved info."""
        state_dir = tmp_path / "state"
        state_dir.mkdir(parents=True)
        (state_dir / "server.json").write_text(json.dumps({
            "version": 1,
            "pid": os.getpid(),
            "port": 18888,
            "token": "saved-token",
            "base_url": "http://127.0.0.1:18888",
            "root_dir": "/test/root",
        }))

        mgr = JupyterServerManager(persistence_dir=str(state_dir))

        # Mock httpx to simulate unhealthy server (wrong token / different process)
        mock_response = MagicMock()
        mock_response.status_code = 403

        with patch("httpx.AsyncClient") as MockClient:
            mock_client = AsyncMock()
            mock_client.get = AsyncMock(return_value=mock_response)
            mock_client.__aenter__ = AsyncMock(return_value=mock_client)
            mock_client.__aexit__ = AsyncMock()
            MockClient.return_value = mock_client

            token = await mgr._try_reuse_server("/test/root")

        assert token is None
        # File should be deleted
        assert not (state_dir / "server.json").exists()

    def test_is_running_with_reused_pid(self, tmp_path):
        """is_running should return True for reused servers with alive PID."""
        mgr = JupyterServerManager(persistence_dir=str(tmp_path / "state"))
        assert not mgr.is_running()

        mgr._reused_pid = os.getpid()  # Our own PID is alive
        assert mgr.is_running()

    def test_is_running_with_dead_reused_pid(self, tmp_path):
        """is_running should return False for reused servers with dead PID."""
        mgr = JupyterServerManager(persistence_dir=str(tmp_path / "state"))
        mgr._reused_pid = 999999999  # Very unlikely to be alive
        assert not mgr.is_running()
