"""Tests for globals.py — gap coverage.

Covers the three functions with zero test coverage:
- get_usernames_with_active_research
- is_research_thread_alive
- update_progress_and_check_active
"""

from unittest.mock import Mock

import pytest

from local_deep_research.web.routes import globals as g


@pytest.fixture(autouse=True)
def _clean_globals():
    """Reset all global state before each test."""
    with g._lock:
        g._active_research.clear()
        g._termination_flags.clear()
    yield
    with g._lock:
        g._active_research.clear()
        g._termination_flags.clear()


# ===================================================================
# get_usernames_with_active_research
# ===================================================================


class TestGetUsernamesWithActiveResearch:
    def test_empty_returns_empty_set(self):
        assert g.get_usernames_with_active_research() == set()

    def test_entry_with_username(self):
        g.set_active_research("r1", {"settings": {"username": "alice"}})
        assert g.get_usernames_with_active_research() == {"alice"}

    def test_entry_with_username_none_filtered(self):
        g.set_active_research("r1", {"settings": {"username": None}})
        assert g.get_usernames_with_active_research() == set()

    def test_entry_with_no_settings_key(self):
        g.set_active_research("r1", {"progress": 0})
        assert g.get_usernames_with_active_research() == set()

    def test_duplicate_usernames_deduplicated(self):
        g.set_active_research("r1", {"settings": {"username": "alice"}})
        g.set_active_research("r2", {"settings": {"username": "alice"}})
        assert g.get_usernames_with_active_research() == {"alice"}

    def test_different_usernames(self):
        g.set_active_research("r1", {"settings": {"username": "alice"}})
        g.set_active_research("r2", {"settings": {"username": "bob"}})
        assert g.get_usernames_with_active_research() == {"alice", "bob"}


# ===================================================================
# is_research_thread_alive
# ===================================================================


class TestIsResearchThreadAlive:
    def test_unknown_research_id(self):
        assert g.is_research_thread_alive("r1") is False

    def test_entry_with_no_thread_key(self):
        g.set_active_research("r1", {"progress": 0})
        assert g.is_research_thread_alive("r1") is False

    def test_entry_with_thread_none(self):
        g.set_active_research("r1", {"thread": None})
        assert g.is_research_thread_alive("r1") is False

    def test_thread_alive(self):
        mock_thread = Mock()
        mock_thread.is_alive.return_value = True
        g.set_active_research("r1", {"thread": mock_thread})
        assert g.is_research_thread_alive("r1") is True

    def test_thread_dead(self):
        mock_thread = Mock()
        mock_thread.is_alive.return_value = False
        g.set_active_research("r1", {"thread": mock_thread})
        assert g.is_research_thread_alive("r1") is False


# ===================================================================
# update_progress_and_check_active
# ===================================================================


class TestUpdateProgressAndCheckActive:
    def test_not_found(self):
        assert g.update_progress_and_check_active("r1", 50) == (None, False)

    def test_new_progress_higher_updates(self):
        g.set_active_research("r1", {"progress": 10})
        result = g.update_progress_and_check_active("r1", 50)
        assert result == (50, True)
        assert g.get_research_field("r1", "progress") == 50

    def test_new_progress_lower_no_update(self):
        g.set_active_research("r1", {"progress": 80})
        result = g.update_progress_and_check_active("r1", 50)
        assert result == (80, True)
        assert g.get_research_field("r1", "progress") == 80

    def test_new_progress_equal_no_update(self):
        g.set_active_research("r1", {"progress": 50})
        result = g.update_progress_and_check_active("r1", 50)
        assert result == (50, True)

    def test_new_progress_none(self):
        g.set_active_research("r1", {"progress": 30})
        result = g.update_progress_and_check_active("r1", None)
        assert result == (30, True)

    def test_no_progress_key_defaults_to_zero(self):
        g.set_active_research("r1", {})
        result = g.update_progress_and_check_active("r1", 10)
        assert result == (10, True)
        assert g.get_research_field("r1", "progress") == 10


# ===================================================================
# check_and_start_research
# ===================================================================


class TestCheckAndStartResearch:
    def _make_data(self, alive=True):
        thread = Mock()
        thread.is_alive.return_value = alive
        return thread, {
            "thread": thread,
            "progress": 0,
            "status": "in_progress",
            "log": [],
            "settings": {},
        }

    def test_registers_when_no_prior_entry(self):
        thread, data = self._make_data()
        result = g.check_and_start_research("r1", data)
        assert result is True
        thread.start.assert_called_once()
        assert g.is_research_thread_alive("r1")

    def test_refuses_when_existing_thread_alive(self):
        existing_thread = Mock()
        existing_thread.is_alive.return_value = True
        g.set_active_research("r1", {"thread": existing_thread})

        new_thread, new_data = self._make_data()
        result = g.check_and_start_research("r1", new_data)

        assert result is False
        new_thread.start.assert_not_called()
        # Existing entry is untouched.
        assert g.get_research_field("r1", "thread") is existing_thread

    def test_overwrites_when_existing_thread_finished(self):
        finished_thread = Mock()
        finished_thread.is_alive.return_value = False
        g.set_active_research("r1", {"thread": finished_thread})

        new_thread, new_data = self._make_data()
        result = g.check_and_start_research("r1", new_data)

        assert result is True
        new_thread.start.assert_called_once()
        assert g.get_research_field("r1", "thread") is new_thread

    def test_overwrites_when_existing_has_no_thread(self):
        g.set_active_research("r1", {"progress": 50})

        new_thread, new_data = self._make_data()
        result = g.check_and_start_research("r1", new_data)

        assert result is True
        new_thread.start.assert_called_once()

    def test_rejects_data_without_thread(self):
        with pytest.raises(ValueError):
            g.check_and_start_research("r1", {"progress": 0})

    def test_rejects_data_with_thread_none(self):
        with pytest.raises(ValueError):
            g.check_and_start_research("r1", {"thread": None})
