# tests/test_configuration.py
"""Tests for configuration validation at startup."""
import os
import pytest
from unittest.mock import patch
from pathlib import Path

try:
    import tomllib
except ModuleNotFoundError:
    import tomli as tomllib


class TestConfigurationValidation:
    """Tests for _validate_configuration() function."""

    def test_rejects_unknown_providers(self):
        """Should raise error for unknown providers in SECOND_OPINION_PROVIDERS."""
        from second_opinion_mcp.server import _validate_configuration

        with patch.dict(os.environ, {"SECOND_OPINION_PROVIDERS": "deepseek,invalid_provider"}):
            with pytest.raises(RuntimeError) as exc_info:
                _validate_configuration()

        assert "Unknown providers" in str(exc_info.value)
        assert "invalid_provider" in str(exc_info.value)

    def test_accepts_valid_providers(self):
        """Should accept valid provider names."""
        from second_opinion_mcp.server import _validate_configuration

        with patch.dict(os.environ, {"SECOND_OPINION_PROVIDERS": "deepseek,moonshot"}, clear=False):
            # Should not raise
            _validate_configuration()

    def test_accepts_kimi_alias(self):
        """Should accept 'kimi' as alias for moonshot."""
        from second_opinion_mcp.server import _validate_configuration

        with patch.dict(os.environ, {"SECOND_OPINION_PROVIDERS": "kimi"}, clear=False):
            # Should not raise
            _validate_configuration()

    def test_warns_on_nonexistent_allowed_root(self, caplog):
        """Should log warning for non-existent allowed roots."""
        from second_opinion_mcp.server import _validate_configuration

        with patch.dict(os.environ, {
            "SECOND_OPINION_ALLOWED_ROOTS": "/nonexistent/path/12345",
            "SECOND_OPINION_PROVIDERS": "",
        }):
            with caplog.at_level("WARNING"):
                _validate_configuration()

        assert "Allowed root does not exist" in caplog.text

    def test_rejects_invalid_moonshot_concurrent_value(self):
        """Should reject MOONSHOT_CONCURRENT outside valid range."""
        from second_opinion_mcp.server import _validate_configuration

        with patch.dict(os.environ, {"SECOND_OPINION_MOONSHOT_CONCURRENT": "100"}):
            with pytest.raises(RuntimeError) as exc_info:
                _validate_configuration()

        assert "MOONSHOT_CONCURRENT" in str(exc_info.value)

    def test_rejects_non_integer_moonshot_concurrent(self):
        """Should reject non-integer MOONSHOT_CONCURRENT."""
        from second_opinion_mcp.server import _validate_configuration

        with patch.dict(os.environ, {"SECOND_OPINION_MOONSHOT_CONCURRENT": "not_a_number"}):
            with pytest.raises(RuntimeError) as exc_info:
                _validate_configuration()

        assert "Invalid SECOND_OPINION_MOONSHOT_CONCURRENT" in str(exc_info.value)

    def test_rejects_invalid_moonshot_rpm_value(self):
        """Should reject MOONSHOT_RPM outside valid range."""
        from second_opinion_mcp.server import _validate_configuration

        with patch.dict(os.environ, {"SECOND_OPINION_MOONSHOT_RPM": "500"}):
            with pytest.raises(RuntimeError) as exc_info:
                _validate_configuration()

        assert "MOONSHOT_RPM" in str(exc_info.value)

    def test_accepts_valid_rate_limits(self):
        """Should accept valid rate limit values."""
        from second_opinion_mcp.server import _validate_configuration

        with patch.dict(os.environ, {
            "SECOND_OPINION_MOONSHOT_CONCURRENT": "5",
            "SECOND_OPINION_MOONSHOT_RPM": "50",
            "SECOND_OPINION_PROVIDERS": "",
        }):
            # Should not raise
            _validate_configuration()


class TestAutoDetectProviders:
    """Tests for auto-detection of providers from credentials."""

    def _run_init(self):
        """Re-run _init_enabled_providers with current mocks."""
        from second_opinion_mcp.server import _init_enabled_providers
        _init_enabled_providers()

    def _save_enabled(self):
        from second_opinion_mcp.server import MODELS
        return {p: MODELS[p]["enabled"] for p in MODELS}

    def _restore_enabled(self, originals):
        from second_opinion_mcp.server import MODELS
        for p, v in originals.items():
            MODELS[p]["enabled"] = v

    def test_auto_enables_providers_with_credentials(self):
        """Should auto-enable providers that have API keys when no env var set."""
        from second_opinion_mcp.server import MODELS
        originals = self._save_enabled()
        try:
            with patch.dict(os.environ, {}, clear=False):
                os.environ.pop("SECOND_OPINION_PROVIDERS", None)
                with patch("second_opinion_mcp.server._get_credential_manager") as mock_mgr:
                    mock_mgr.return_value.get.side_effect = lambda p: {
                        "deepseek": "sk-fake",
                        "moonshot": "fake-key",
                    }.get(p)
                    with patch("second_opinion_mcp.openai_auth.get_openai_token", return_value=None):
                        self._run_init()

                    assert MODELS["deepseek"]["enabled"] is True
                    assert MODELS["moonshot"]["enabled"] is True
                    assert MODELS["openai"]["enabled"] is False
                    assert MODELS["openrouter"]["enabled"] is False
        finally:
            self._restore_enabled(originals)

    def test_auto_enables_openai_with_oauth_token(self):
        """Should auto-enable OpenAI when OAuth token is available."""
        from second_opinion_mcp.server import MODELS
        originals = self._save_enabled()
        try:
            with patch.dict(os.environ, {}, clear=False):
                os.environ.pop("SECOND_OPINION_PROVIDERS", None)
                with patch("second_opinion_mcp.server._get_credential_manager") as mock_mgr:
                    mock_mgr.return_value.get.side_effect = lambda p: {
                        "deepseek": "sk-fake",
                        "moonshot": "fake-key",
                    }.get(p)
                    with patch("second_opinion_mcp.openai_auth.get_openai_token", return_value="eyJ.fake.token"):
                        self._run_init()

                    assert MODELS["openai"]["enabled"] is True
                    assert MODELS["deepseek"]["enabled"] is True
                    assert MODELS["moonshot"]["enabled"] is True
        finally:
            self._restore_enabled(originals)

    def test_auto_enables_openai_with_api_key_fallback(self):
        """Should auto-enable OpenAI when API key exists but no OAuth token."""
        from second_opinion_mcp.server import MODELS
        originals = self._save_enabled()
        try:
            with patch.dict(os.environ, {}, clear=False):
                os.environ.pop("SECOND_OPINION_PROVIDERS", None)
                with patch("second_opinion_mcp.server._get_credential_manager") as mock_mgr:
                    mock_mgr.return_value.get.side_effect = lambda p: {
                        "deepseek": "sk-fake",
                        "moonshot": "fake-key",
                        "openai": "sk-proj-fakekey12345678901234",
                    }.get(p)
                    with patch("second_opinion_mcp.openai_auth.get_openai_token", return_value=None):
                        self._run_init()

                    assert MODELS["openai"]["enabled"] is True
        finally:
            self._restore_enabled(originals)

    def test_env_var_overrides_auto_detect(self):
        """SECOND_OPINION_PROVIDERS env var should override auto-detection."""
        from second_opinion_mcp.server import MODELS
        originals = self._save_enabled()
        try:
            with patch.dict(os.environ, {"SECOND_OPINION_PROVIDERS": "deepseek,moonshot"}, clear=False):
                self._run_init()

                assert MODELS["deepseek"]["enabled"] is True
                assert MODELS["moonshot"]["enabled"] is True
                assert MODELS["openai"]["enabled"] is False
                assert MODELS["openrouter"]["enabled"] is False
        finally:
            self._restore_enabled(originals)

    def test_no_credentials_disables_all(self):
        """Should disable all providers when no credentials exist."""
        from second_opinion_mcp.server import MODELS
        originals = self._save_enabled()
        try:
            with patch.dict(os.environ, {}, clear=False):
                os.environ.pop("SECOND_OPINION_PROVIDERS", None)
                with patch("second_opinion_mcp.server._get_credential_manager") as mock_mgr:
                    mock_mgr.return_value.get.return_value = None
                    with patch("second_opinion_mcp.openai_auth.get_openai_token", return_value=None):
                        self._run_init()

                    for provider in MODELS:
                        assert MODELS[provider]["enabled"] is False
        finally:
            self._restore_enabled(originals)


class TestStructuredLogging:
    """Tests for _configure_logging() function."""

    def test_logging_disabled_by_default(self, caplog):
        """Logging should not be enabled unless SECOND_OPINION_LOG_ENABLED is set."""
        import logging
        from second_opinion_mcp.server import logger

        # Logger should have NullHandler by default
        has_null_handler = any(
            isinstance(h, logging.NullHandler)
            for h in logger.handlers
        )
        assert has_null_handler

    def test_logging_enabled_when_env_set(self):
        """Should configure logging when SECOND_OPINION_LOG_ENABLED is set."""
        from second_opinion_mcp.server import _configure_logging, logger
        import logging

        # Clear existing handlers for test
        original_handlers = logger.handlers.copy()
        logger.handlers = [logging.NullHandler()]

        try:
            with patch.dict(os.environ, {
                "SECOND_OPINION_LOG_ENABLED": "1",
                "SECOND_OPINION_LOG_LEVEL": "DEBUG",
            }):
                _configure_logging()

            # Should have added a StreamHandler
            has_stream_handler = any(
                isinstance(h, logging.StreamHandler)
                for h in logger.handlers
            )
            assert has_stream_handler
        finally:
            # Restore original handlers
            logger.handlers = original_handlers

    def test_respects_log_level_setting(self):
        """Should set log level from SECOND_OPINION_LOG_LEVEL."""
        from second_opinion_mcp.server import _configure_logging, logger
        import logging

        original_level = logger.level
        original_handlers = logger.handlers.copy()
        logger.handlers = [logging.NullHandler()]

        try:
            with patch.dict(os.environ, {
                "SECOND_OPINION_LOG_ENABLED": "1",
                "SECOND_OPINION_LOG_LEVEL": "ERROR",
            }):
                _configure_logging()

            assert logger.level == logging.ERROR
        finally:
            logger.level = original_level
            logger.handlers = original_handlers


class TestVerifySetup:
    """Tests for the verify_setup() command."""

    def test_shows_configured_providers(self, capsys):
        """Should list providers that have credentials configured."""
        with patch("second_opinion_mcp.cli.check_existing_keys", return_value={
            "deepseek": True,
            "moonshot": True,
            "openrouter": False,
            "openai": False,
        }):
            with patch("second_opinion_mcp.cli.detect_backend", return_value=("file", "Linux file storage")):
                with patch("second_opinion_mcp.cli.CredentialManager") as mock_mgr:
                    mock_mgr.return_value.get.side_effect = lambda p: {
                        "deepseek": "sk-fake",
                        "moonshot": "fake-key",
                    }.get(p)
                    with patch("httpx.get") as mock_get:
                        mock_get.return_value.status_code = 200
                        from second_opinion_mcp.cli import verify_setup
                        verify_setup()

        output = capsys.readouterr().out
        assert "deepseek" in output
        assert "moonshot" in output
        assert "2 providers ready" in output

    def test_handles_no_credentials(self, capsys):
        """Should show helpful message when no providers configured."""
        with patch("second_opinion_mcp.cli.check_existing_keys", return_value={
            "deepseek": False,
            "moonshot": False,
            "openrouter": False,
            "openai": False,
        }):
            with patch("second_opinion_mcp.cli.detect_backend", return_value=("file", "Linux file storage")):
                from second_opinion_mcp.cli import verify_setup
                verify_setup()

        output = capsys.readouterr().out
        assert "none configured" in output
        assert "second-opinion-mcp setup" in output

    def test_handles_api_unreachable(self, capsys):
        """Should report unreachable APIs without crashing."""
        import httpx

        with patch("second_opinion_mcp.cli.check_existing_keys", return_value={
            "deepseek": True,
            "moonshot": False,
            "openrouter": False,
            "openai": False,
        }):
            with patch("second_opinion_mcp.cli.detect_backend", return_value=("file", "Linux file storage")):
                with patch("second_opinion_mcp.cli.CredentialManager") as mock_mgr:
                    mock_mgr.return_value.get.return_value = "sk-fake"
                    with patch("httpx.get", side_effect=httpx.ConnectError("Connection refused")):
                        from second_opinion_mcp.cli import verify_setup
                        verify_setup()

        output = capsys.readouterr().out
        assert "unreachable" in output

    def test_shows_oauth_for_openai(self, capsys):
        """Should show OAuth indicator for OpenAI when using OAuth token."""
        with patch("second_opinion_mcp.cli.check_existing_keys", return_value={
            "deepseek": True,
            "moonshot": True,
            "openrouter": False,
            "openai": True,
        }):
            with patch("second_opinion_mcp.cli.detect_backend", return_value=("file", "Linux file storage")):
                with patch("second_opinion_mcp.cli.CredentialManager") as mock_mgr:
                    mock_mgr.return_value.get.side_effect = lambda p: {
                        "deepseek": "sk-fake",
                        "moonshot": "fake-key",
                    }.get(p)
                    with patch("httpx.get") as mock_get:
                        mock_get.return_value.status_code = 200
                        with patch("second_opinion_mcp.openai_auth.get_openai_token", return_value="eyJ.fake.token"):
                            from second_opinion_mcp.cli import verify_setup
                            verify_setup()

        output = capsys.readouterr().out
        assert "OAuth" in output
        assert "3 providers ready" in output


class TestTTYDetection:
    """Tests for TTY detection in server main()."""

    def _run_server_main(self, is_tty):
        """Helper to run server main with mocked environment."""
        with patch("sys.stdin") as mock_stdin:
            mock_stdin.isatty.return_value = is_tty
            with patch("second_opinion_mcp.server._check_for_updates", return_value=(None, None)):
                with patch("second_opinion_mcp.server._validate_startup_requirements", return_value=(True, None)):
                    with patch("second_opinion_mcp.server._check_provider_keys", return_value=(["deepseek"], [])):
                        with patch("second_opinion_mcp.server.mcp"):
                            from second_opinion_mcp.server import main
                            main()

    def test_tty_shows_banner(self, capsys):
        """Should print friendly message when stdin is a TTY."""
        self._run_server_main(is_tty=True)

        output = capsys.readouterr().err
        assert "MCP server running" in output
        assert "second-opinion-mcp verify" in output
        assert "Ctrl+C" in output

    def test_no_banner_when_piped(self, capsys):
        """Should not print banner when stdin is piped."""
        self._run_server_main(is_tty=False)

        output = capsys.readouterr().err
        assert "MCP server running" not in output


class TestVersionConsistency:
    """Ensure version stays in sync between pyproject.toml and __init__.py."""

    def test_pyproject_matches_init(self):
        from second_opinion_mcp import __version__

        pyproject_path = Path(__file__).parent.parent / "pyproject.toml"
        with open(pyproject_path, "rb") as f:
            pyproject = tomllib.load(f)

        assert pyproject["project"]["version"] == __version__
