"""Tests for the unsaved-input guard.

The guard checks the debug window's active tab for dirty form controls before
a disruptive action (navigate, reload). Detection is heuristic and CONSERVATIVE
— false positives block the user's workflow; false negatives only lose typed
text. We prefer the latter.
"""

from __future__ import annotations

import sys
from contextlib import contextmanager
from unittest.mock import MagicMock, patch

import pytest

from a360_mcp.browser.unsaved_guard import (
    DIRTY_THRESHOLD,
    GuardCheckError,
    UnsavedInputBlocked,
    assert_safe_to_disrupt,
    check_unsaved_input,
)
from a360_mcp.browser.window import WindowState

_STATE = WindowState(
    pid=1234,
    port=9222,
    started_at=1000.0,
    control_room_url="https://cr.example.com",
    username="svc_mcp",
    profile_dir="/tmp/profile",
)


def _patch_inspect(
    monkeypatch, *, evaluate_return=None, evaluate_side_effect=None, connect_side_effect=None
):
    pw = MagicMock()
    browser = MagicMock()
    page = MagicMock()
    if evaluate_side_effect is not None:
        page.evaluate.side_effect = evaluate_side_effect
    else:
        page.evaluate.return_value = evaluate_return or {"total_dirty": 0, "sample": []}
    connect_fn = MagicMock(
        side_effect=connect_side_effect if connect_side_effect else lambda s: (pw, browser, page)
    )
    disconnect_fn = MagicMock()
    mod = MagicMock()
    mod._connect = connect_fn
    mod._disconnect = disconnect_fn
    monkeypatch.setitem(sys.modules, "a360_mcp.browser.inspect", mod)
    return connect_fn, disconnect_fn, page


class TestCheckUnsavedInputNoneState:
    def test_none_state_is_safe(self):
        result = check_unsaved_input(None)
        assert result["has_unsaved"] is False
        assert result["dirty_count"] == 0
        assert result["sample"] == []
        assert result["window"] is None

    def test_none_state_custom_threshold(self):
        result = check_unsaved_input(None, threshold=5)
        assert result["threshold"] == 5


class TestCheckUnsavedInputWithState:
    def test_no_dirty_controls(self, monkeypatch):
        _patch_inspect(
            monkeypatch,
            evaluate_return={
                "dirty_inputs": 0,
                "dirty_selects": 0,
                "editable_with_text": 0,
                "total_dirty": 0,
                "sample": [],
            },
        )
        result = check_unsaved_input(_STATE)
        assert result["has_unsaved"] is False
        assert result["dirty_count"] == 0
        assert result["window"] == {"pid": 1234, "port": 9222}

    def test_dirty_controls_above_threshold(self, monkeypatch):
        _patch_inspect(
            monkeypatch,
            evaluate_return={
                "dirty_inputs": 3,
                "dirty_selects": 0,
                "editable_with_text": 0,
                "total_dirty": 3,
                "sample": [{"tag": "input", "name": "username"}],
            },
        )
        result = check_unsaved_input(_STATE)
        assert result["has_unsaved"] is True
        assert result["dirty_count"] == 3

    def test_connect_failure_raises_guard_check_error(self, monkeypatch):
        _patch_inspect(monkeypatch, connect_side_effect=Exception("CDP refused"))
        with pytest.raises(GuardCheckError, match="Could not connect"):
            check_unsaved_input(_STATE)

    def test_evaluate_failure_raises_guard_check_error(self, monkeypatch):
        _patch_inspect(monkeypatch, evaluate_side_effect=Exception("JS error"))
        with pytest.raises(GuardCheckError, match="probe failed"):
            check_unsaved_input(_STATE)

    def test_non_dict_result_treated_as_clean(self, monkeypatch):
        _patch_inspect(monkeypatch, evaluate_return=None)
        result = check_unsaved_input(_STATE)
        assert result["has_unsaved"] is False
        assert result["dirty_count"] == 0

    def test_disconnect_always_called(self, monkeypatch):
        _, disconnect_fn, _ = _patch_inspect(
            monkeypatch, evaluate_return={"total_dirty": 5, "sample": []}
        )
        check_unsaved_input(_STATE)
        disconnect_fn.assert_called_once()

    def test_threshold_override(self, monkeypatch):
        _patch_inspect(monkeypatch, evaluate_return={"total_dirty": 2, "sample": []})
        result = check_unsaved_input(_STATE, threshold=5)
        assert result["has_unsaved"] is False
        assert result["dirty_count"] == 2


class TestAssertSafeToDisrupt:
    def test_none_state_proceeds(self):
        assert_safe_to_disrupt(None, action="test_action")

    def test_dirty_state_blocks(self):
        with patch_check(True, dirty_count=3):
            with pytest.raises(UnsavedInputBlocked, match="3 unsaved form control"):
                assert_safe_to_disrupt(_STATE, action="open_debug_window")

    def test_clean_state_proceeds(self):
        with patch_check(False, dirty_count=0):
            assert_safe_to_disrupt(_STATE, action="reload")

    def test_guard_check_error_fail_open(self):
        with patch_check_error("window dead"):
            assert_safe_to_disrupt(_STATE, action="test")

    def test_blocked_message_includes_action(self):
        with patch_check(True, dirty_count=2):
            with pytest.raises(UnsavedInputBlocked) as exc_info:
                assert_safe_to_disrupt(_STATE, action="my_disruptive_action")
            assert "my_disruptive_action" in str(exc_info.value)


# ---- helper context managers (patch check_unsaved_input at module level) ----


@contextmanager
def patch_check(has_unsaved, dirty_count):
    with patch("a360_mcp.browser.unsaved_guard.check_unsaved_input") as mock_check:
        mock_check.return_value = {
            "has_unsaved": has_unsaved,
            "dirty_count": dirty_count,
            "threshold": DIRTY_THRESHOLD,
            "sample": [],
            "window": {"pid": 1234, "port": 9222},
        }
        yield


@contextmanager
def patch_check_error(msg):
    with patch("a360_mcp.browser.unsaved_guard.check_unsaved_input") as mock_check:
        mock_check.side_effect = GuardCheckError(msg)
        yield


class TestConstants:
    def test_threshold_positive(self):
        assert DIRTY_THRESHOLD >= 1
