"""Tests for the browser session-slot accounting.

The debug window keeps its own Control Room session (it does NOT share the
API's token). This module claims/releases one of the account's five slots on
the shared disk ledger. These tests mock SessionBudget so they exercise the
accounting logic without touching real disk ledgers.
"""

from __future__ import annotations

from unittest.mock import MagicMock, patch

import pytest

from a360_mcp.auth._session_budget import MAX_CONCURRENT_SESSIONS, SessionBudgetExceeded
from a360_mcp.browser.session import (
    BROWSER_OWNER_PREFIX,
    RESERVED_HEADROOM,
    bind_browser_slot,
    browser_owner,
    browser_session_status,
    claim_browser_slot,
    release_browser_slot,
)

_URL = "https://cr.example.com"
_USER = "svc_mcp"


def _mock_budget(summary_dict):
    budget = MagicMock()
    budget.summary.return_value = summary_dict
    return budget


class TestBrowserOwner:
    def test_format(self):
        owner = browser_owner(_USER)
        assert owner == f"{BROWSER_OWNER_PREFIX}:{_USER}"

    def test_prefix_constant(self):
        assert BROWSER_OWNER_PREFIX == "a360-mcp:browser"


class TestClaimBrowserSlot:
    @patch("a360_mcp.browser.session.SessionBudget")
    def test_claims_when_room_available(self, mock_cls):
        mock_cls.return_value = _mock_budget({"used": 1, "limit": 5, "owners": ["api"]})
        claim_browser_slot(control_room_url=_URL, username=_USER)
        budget = mock_cls.return_value
        budget.claim.assert_called_once_with(browser_owner(_USER), now=None)

    @patch("a360_mcp.browser.session.SessionBudget")
    def test_blocks_at_headroom_boundary(self, mock_cls):
        # used + RESERVED_HEADROOM >= limit → block (5-1 = 4 used, limit 5)
        mock_cls.return_value = _mock_budget({"used": 4, "limit": 5, "owners": ["a", "b"]})
        with pytest.raises(SessionBudgetExceeded, match="leaving less than"):
            claim_browser_slot(control_room_url=_URL, username=_USER)

    @patch("a360_mcp.browser.session.SessionBudget")
    def test_blocks_at_limit(self, mock_cls):
        mock_cls.return_value = _mock_budget({"used": 5, "limit": 5, "owners": ["a"]})
        with pytest.raises(SessionBudgetExceeded):
            claim_browser_slot(control_room_url=_URL, username=_USER)

    @patch("a360_mcp.browser.session.SessionBudget")
    def test_claims_just_below_boundary(self, mock_cls):
        # 3 used, limit 5, headroom 1: 3+1=4 < 5 → allowed.
        mock_cls.return_value = _mock_budget({"used": 3, "limit": 5, "owners": []})
        claim_browser_slot(control_room_url=_URL, username=_USER)
        mock_cls.return_value.claim.assert_called_once()

    @patch("a360_mcp.browser.session.SessionBudget")
    def test_error_includes_holders(self, mock_cls):
        mock_cls.return_value = _mock_budget({"used": 5, "limit": 5, "owners": ["alice", "bob"]})
        with pytest.raises(SessionBudgetExceeded) as exc_info:
            claim_browser_slot(control_room_url=_URL, username=_USER)
        assert "alice" in str(exc_info.value)
        assert "bob" in str(exc_info.value)

    @patch("a360_mcp.browser.session.SessionBudget")
    def test_passes_now_through(self, mock_cls):
        mock_cls.return_value = _mock_budget({"used": 0, "limit": 5, "owners": []})
        claim_browser_slot(control_room_url=_URL, username=_USER, now=12345.0)
        mock_cls.return_value.claim.assert_called_once_with(browser_owner(_USER), now=12345.0)


class TestReleaseBrowserSlot:
    @patch("a360_mcp.browser.session.SessionBudget")
    def test_releases(self, mock_cls):
        mock_cls.return_value = MagicMock()
        release_browser_slot(control_room_url=_URL, username=_USER)
        mock_cls.return_value.release.assert_called_once_with(browser_owner(_USER))


class TestBrowserSessionStatus:
    @patch("a360_mcp.browser.session.SessionBudget")
    def test_held_true(self, mock_cls):
        owner = browser_owner(_USER)
        mock_cls.return_value = _mock_budget({"used": 2, "limit": 5, "owners": ["api", owner]})
        status = browser_session_status(control_room_url=_URL, username=_USER)
        assert status["held"] is True
        assert status["owner"] == owner
        assert status["used"] == 2
        assert status["limit"] == 5
        assert owner in status["all_holders"]

    @patch("a360_mcp.browser.session.SessionBudget")
    def test_held_false(self, mock_cls):
        mock_cls.return_value = _mock_budget({"used": 1, "limit": 5, "owners": ["someone_else"]})
        status = browser_session_status(control_room_url=_URL, username=_USER)
        assert status["held"] is False
        assert browser_owner(_USER) not in status["all_holders"]


class TestConstants:
    def test_reserved_headroom_positive(self):
        assert RESERVED_HEADROOM >= 1

    def test_uses_default_max_sessions(self):
        assert MAX_CONCURRENT_SESSIONS == 5


class TestBindBrowserSlot:
    """The ledger slot must name the *Chromium* process, not the MCP server.

    ``claim_browser_slot`` runs before the launch, so it can only stamp the
    server's pid. Chromium is started detached and outlives the server; once
    the server restarts, a slot carrying its dead pid reads as stale and the
    ledger silently frees a session that is still live in the Control Room.
    """

    @patch("a360_mcp.browser.session.SessionBudget")
    def test_rebinds_the_slot_to_the_window_pid(self, mock_cls):
        mock_cls.return_value = _mock_budget({"used": 2, "limit": 5, "owners": ["api"]})
        bind_browser_slot(control_room_url=_URL, username=_USER, pid=424242)
        mock_cls.return_value.claim.assert_called_once_with(
            browser_owner(_USER), now=None, pid=424242
        )

    @patch("a360_mcp.browser.session.SessionBudget")
    def test_a_full_ledger_does_not_undo_a_launched_window(self, mock_cls):
        # Best-effort: the window is already running, so raising here would
        # leak it. The slot stays stamped with the server pid instead.
        budget = _mock_budget({"used": 5, "limit": 5, "owners": ["a"]})
        budget.claim.side_effect = SessionBudgetExceeded("full")
        mock_cls.return_value = budget
        bind_browser_slot(control_room_url=_URL, username=_USER, pid=424242)
