# #region Test.AgentChat.Run [C:3] [TYPE Module] [SEMANTICS test,agent,run,entrypoint]
# @BRIEF Tests for agent/run.py — _find_free_port and _fetch_llm_config.
# @RELATION BINDS_TO -> [AgentChat.Run]

from pathlib import Path
import sys

sys.path.insert(0, str(Path(__file__).parent.parent.parent / "src"))

import socket
from unittest.mock import AsyncMock, MagicMock, patch
import pytest


# #region test_find_free_port [C:2] [TYPE Function]
# @BRIEF Test _find_free_port for port scanning behavior.
class TestFindFreePort:
    def test_returns_free_port(self):
        from src.agent.run import _find_free_port
        with patch("socket.socket") as mock_socket:
            mock_instance = MagicMock()
            mock_socket.return_value.__enter__.return_value = mock_instance
            result = _find_free_port(8000, 10)
            assert result == 8000
            mock_instance.bind.assert_called_once_with(("", 8000))

    def test_skips_busy_ports(self):
        from src.agent.run import _find_free_port
        with patch("socket.socket") as mock_socket:
            mock_instance = MagicMock()
            mock_socket.return_value.__enter__.return_value = mock_instance
            # Ports 8000-8002 busy, 8003 free
            mock_instance.bind.side_effect = [
                OSError("Address in use"),  # 8000
                OSError("Address in use"),  # 8001
                OSError("Address in use"),  # 8002
                None,  # 8003 — success
            ]
            result = _find_free_port(8000, 10)
            assert result == 8003
            assert mock_instance.bind.call_count == 4

    def test_raises_when_all_busy(self):
        from src.agent.run import _find_free_port
        with patch("socket.socket") as mock_socket:
            mock_instance = MagicMock()
            mock_socket.return_value.__enter__.return_value = mock_instance
            mock_instance.bind.side_effect = OSError("Address in use")
            with pytest.raises(OSError, match="No free port found"):
                _find_free_port(8000, 3)
            assert mock_instance.bind.call_count == 3
# #endregion test_find_free_port


# #region test_fetch_llm_config [C:2] [TYPE Function]
# @BRIEF Test _fetch_llm_config with retry and fallback behavior.
class TestFetchLlmConfig:
    def test_returns_config_on_success(self):
        from src.agent.run import _fetch_llm_config
        with patch("src.agent.run.httpx.get") as mock_get:
            mock_response = MagicMock()
            mock_response.json.return_value = {"configured": True, "provider_type": "openai", "default_model": "gpt-4o"}
            mock_get.return_value = mock_response
            result = _fetch_llm_config()
            assert result is not None
            assert result["configured"] is True

    def test_returns_none_when_not_configured(self):
        from src.agent.run import _fetch_llm_config
        with patch("src.agent.run.httpx.get") as mock_get:
            mock_response = MagicMock()
            mock_response.json.return_value = {"configured": False, "reason": "no provider"}
            mock_get.return_value = mock_response
            result = _fetch_llm_config()
            assert result is None

    def test_retries_on_failure(self):
        from src.agent.run import _fetch_llm_config
        import time as time_module
        with patch("src.agent.run.httpx.get") as mock_get, \
             patch.object(time_module, "sleep") as mock_sleep:
            mock_get.side_effect = Exception("Connection refused")
            result = _fetch_llm_config()
            assert result is None
            assert mock_get.call_count == 6

    def test_retries_then_returns_config(self):
        from src.agent.run import _fetch_llm_config
        import time as time_module
        with patch("src.agent.run.httpx.get") as mock_get, \
             patch.object(time_module, "sleep") as mock_sleep:
            mock_get.side_effect = [
                Exception("Timeout"),       # Attempt 1
                Exception("Timeout"),       # Attempt 2
                MagicMock(json=lambda: {"configured": True, "provider_type": "openai"}),  # Attempt 3
            ]
            result = _fetch_llm_config()
            assert result is not None
            assert result["configured"] is True

    def test_returns_none_after_max_retries_with_http_error(self):
        from src.agent.run import _fetch_llm_config
        import time as time_module
        with patch("src.agent.run.httpx.get") as mock_get, \
             patch.object(time_module, "sleep") as mock_sleep:
            mock_response = MagicMock()
            mock_response.raise_for_status.side_effect = Exception("HTTP 500")
            mock_get.return_value = mock_response
            result = _fetch_llm_config()
            assert result is None
            assert mock_get.call_count == 6

    def test_uses_service_token_header(self):
        from src.agent.run import _fetch_llm_config
        with patch("src.agent.run.httpx.get") as mock_get, \
             patch("src.agent.run.os.getenv", return_value="test-token"):
            mock_response = MagicMock()
            mock_response.json.return_value = {"configured": True}
            mock_get.return_value = mock_response
            result = _fetch_llm_config()
            assert result is not None
            # Verify Authorization header was sent
            call_kwargs = mock_get.call_args[1]
            assert call_kwargs["headers"].get("Authorization") == "Bearer test-token"


# #endregion test_fetch_llm_config


# #region test_main_block [C:2] [TYPE Function]
# @BRIEF Test if __name__ == '__main__' block — service JWT, LLM config, port fallback, OSError.
class TestMainBlock:
    """Test the if __name__ == '__main__' entry point block via importlib.util fresh module."""

    def _run_as_main(self, monkeypatch, env_overrides=None, llm_configured=False,
                     port_bind_sequence=None, port_always_fail=False):
        """Execute run.py as __main__ with given mocking configuration."""
        import importlib.util
        from pathlib import Path

        run_path = Path(__file__).parent.parent.parent / "src" / "agent" / "run.py"
        spec = importlib.util.spec_from_file_location("__main__", str(run_path))

        # Apply env overrides
        env_overrides = env_overrides or {}
        for k, v in env_overrides.items():
            monkeypatch.setenv(k, v)

        with patch('httpx.get') as mock_httpx_get, \
             patch('socket.socket') as mock_socket_cls, \
             patch('src.agent.app.create_chat_interface') as mock_create_ci, \
             patch('src.agent.context.set_service_jwt') as mock_set_jwt, \
             patch('src.agent.langgraph_setup.configure_from_api') as mock_configure:
            # httpx for _fetch_llm_config
            mock_resp = MagicMock()
            if llm_configured:
                mock_resp.json.return_value = {
                    "configured": True, "provider_type": "openai",
                    "default_model": "gpt-4o", "api_key": "sk-test",
                }
            else:
                mock_resp.json.return_value = {"configured": False}
            mock_httpx_get.return_value = mock_resp

            # socket for _find_free_port
            mock_sock = MagicMock()
            mock_sock.__enter__.return_value = mock_sock
            mock_socket_cls.return_value = mock_sock
            if port_always_fail:
                mock_sock.bind.side_effect = OSError("all ports busy")
            elif port_bind_sequence is not None:
                mock_sock.bind.side_effect = port_bind_sequence
            else:
                mock_sock.bind.side_effect = [None]  # first bind succeeds

            mock_demo = MagicMock()
            mock_create_ci.return_value = mock_demo

            module = importlib.util.module_from_spec(spec)
            spec.loader.exec_module(module)

        return {
            'set_jwt': mock_set_jwt,
            'configure': mock_configure,
            'demo': mock_demo,
        }

    def test_main_block_basic(self, monkeypatch):
        """Main block with default env, no SERVICE_JWT, no LLM config."""
        # Ensure SERVICE_JWT is NOT set (some tests leak it via os.environ)
        monkeypatch.delenv("SERVICE_JWT", raising=False)
        result = self._run_as_main(monkeypatch,
                                   env_overrides={"GRADIO_SERVER_PORT": "27860"})
        result['set_jwt'].assert_not_called()
        result['configure'].assert_not_called()
        result['demo'].launch.assert_called_once()

    def test_main_block_with_service_jwt(self, monkeypatch):
        """Main block sets service JWT via ContextVar."""
        result = self._run_as_main(monkeypatch, env_overrides={
            "SERVICE_JWT": "test-service-token",
            "GRADIO_SERVER_PORT": "27861",
        })
        result['set_jwt'].assert_called_once_with("test-service-token")

    def test_main_block_with_llm_config(self, monkeypatch):
        """Main block calls configure_from_api when LLM config is active."""
        result = self._run_as_main(monkeypatch,
                                   env_overrides={"GRADIO_SERVER_PORT": "27862"},
                                   llm_configured=True)
        result['configure'].assert_called_once()

    def test_main_block_port_fallback(self, monkeypatch):
        """Port in use triggers fallback warning (logged but continues)."""
        # Ports 27863, 27864 busy → 27865 free
        with patch('src.agent.run.logger') as mock_logger:
            result = self._run_as_main(monkeypatch,
                                       env_overrides={"GRADIO_SERVER_PORT": "27863"},
                                       port_bind_sequence=[OSError("in use"), OSError("in use"), None])
            result['demo'].launch.assert_called_once()

    def test_main_block_port_oserror(self, monkeypatch):
        """OSError during port finding raises in main block."""
        with patch('src.agent.run.logger') as mock_logger:
            with pytest.raises(OSError):
                self._run_as_main(monkeypatch,
                                  env_overrides={"GRADIO_SERVER_PORT": "27866"},
                                  port_always_fail=True)
# #endregion test_main_block
# #endregion Test.AgentChat.Run
