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

import asyncio
import json
import os
import stat
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

    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()
