"""
Tests for issue #1202 — OAuth provider cards always show "Not Configured"
when auth is via config.yaml or when a token was consumed by the native CLI.

Root cause: get_providers() unconditionally overwrote has_key=True from
_provider_has_key() with has_key=False from get_auth_status(), discarding
a valid working token in config.yaml.

Fixes:
  1. api/providers.py: elif has_key branch preserves config.yaml token
  2. api/providers.py: except clause no longer forces has_key=False
  3. api/providers.py: auth_error field added to provider dict
  4. static/panels.js: OAuth card shows correct hint + badge per key_source
  5. static/i18n.js: new i18n keys for config_yaml and not_configured hints
"""

import re
import sys
import types
from pathlib import Path
from unittest.mock import MagicMock, patch

REPO_ROOT = Path(__file__).parent.parent.resolve()


# ---------------------------------------------------------------------------
# Helper: build a fake hermes_cli.auth module so tests work without the dep
# ---------------------------------------------------------------------------

def _make_fake_auth(logged_in: bool, error: str | None = None, key_source: str = "oauth"):
    mod = types.ModuleType("hermes_cli.auth")
    def get_auth_status(pid):
        if logged_in:
            return {"logged_in": True, "key_source": key_source}
        result = {"logged_in": False}
        if error:
            result["error"] = error
        return result
    mod.get_auth_status = get_auth_status
    return mod


# ---------------------------------------------------------------------------
# Tests for api/providers.py OAuth block
# ---------------------------------------------------------------------------

class TestGetProvidersOauthBlock:
    """Unit tests for the OAuth override block in get_providers()."""

    def _call_get_providers_for_codex(self, fake_auth_module, has_key_in_config: bool):
        """
        Patch just the OAuth resolution path for openai-codex and return the
        provider dict for that one provider.
        """
        import api.providers as prov_mod

        # Patch _provider_has_key to return our desired value
        with patch.object(prov_mod, "_provider_has_key", return_value=has_key_in_config), \
             patch.object(prov_mod, "_provider_is_oauth", side_effect=lambda pid: pid in ("openai-codex", "nous", "copilot")), \
             patch.dict(sys.modules, {"hermes_cli.auth": fake_auth_module}), \
             patch.object(prov_mod, "get_config", return_value={}):
            result = prov_mod.get_providers()

        providers = {p["id"]: p for p in result["providers"]}
        return providers.get("openai-codex")

    def test_config_yaml_token_shows_configured_when_auth_logged_in(self):
        """When hermes auth says logged_in=True, has_key=True regardless of _provider_has_key."""
        auth = _make_fake_auth(logged_in=True)
        p = self._call_get_providers_for_codex(auth, has_key_in_config=False)
        assert p is not None
        assert p["has_key"] is True
        assert p["key_source"] == "oauth"

    def test_config_yaml_token_shows_configured_when_auth_not_logged_in(self):
        """
        REGRESSION TEST (#1202 Bug 1):
        When _provider_has_key() returns True (token in config.yaml) but
        get_auth_status() returns logged_in=False, has_key must remain True.
        
        Before the fix: has_key was overwritten to False, hiding the working token.
        """
        auth = _make_fake_auth(logged_in=False)
        p = self._call_get_providers_for_codex(auth, has_key_in_config=True)
        assert p is not None
        assert p["has_key"] is True, (
            "REGRESSION: config.yaml token was discarded because get_auth_status() "
            "returned logged_in=False. Bug #1202 has regressed."
        )
        assert p["key_source"] == "config_yaml", (
            f"Expected key_source='config_yaml', got {p['key_source']!r}"
        )

    def test_not_configured_when_no_key_and_not_logged_in(self):
        """When neither config.yaml token nor hermes auth, provider is not configured."""
        auth = _make_fake_auth(logged_in=False)
        p = self._call_get_providers_for_codex(auth, has_key_in_config=False)
        assert p is not None
        assert p["has_key"] is False

    def test_auth_error_preserved_when_not_logged_in_and_no_config_key(self):
        """auth_error from get_auth_status() is returned in the provider dict."""
        err_msg = "Refresh token consumed by Codex CLI. Run hermes auth."
        auth = _make_fake_auth(logged_in=False, error=err_msg)
        p = self._call_get_providers_for_codex(auth, has_key_in_config=False)
        assert p is not None
        assert p["has_key"] is False
        assert p["auth_error"] == err_msg

    def test_auth_error_preserved_when_not_logged_in_but_config_key_present(self):
        """auth_error is still returned even when config.yaml token is present."""
        err_msg = "Refresh token was already consumed."
        auth = _make_fake_auth(logged_in=False, error=err_msg)
        p = self._call_get_providers_for_codex(auth, has_key_in_config=True)
        assert p is not None
        assert p["has_key"] is True
        assert p["auth_error"] == err_msg

    def test_hermes_cli_import_error_does_not_discard_config_yaml_key(self):
        """
        REGRESSION TEST (#1202 Bug 1 - exception path):
        If hermes_cli.auth cannot be imported, has_key from _provider_has_key()
        must be preserved. Before the fix, the except clause forced has_key=False.
        """
        import api.providers as prov_mod

        # Use a module that raises ImportError
        bad_mod = types.ModuleType("hermes_cli.auth")
        def bad_get_auth_status(pid):
            raise ImportError("hermes_cli not installed")
        bad_mod.get_auth_status = bad_get_auth_status

        with patch.object(prov_mod, "_provider_has_key", return_value=True), \
             patch.object(prov_mod, "_provider_is_oauth", side_effect=lambda pid: pid in ("openai-codex", "nous", "copilot")), \
             patch.dict(sys.modules, {"hermes_cli.auth": bad_mod}), \
             patch.object(prov_mod, "get_config", return_value={}):
            result = prov_mod.get_providers()

        providers = {p["id"]: p for p in result["providers"]}
        p = providers.get("openai-codex")
        assert p is not None
        assert p["has_key"] is True, (
            "REGRESSION: hermes_cli import failure discarded config.yaml token. "
            "Exception handler must not override a known-good has_key=True."
        )

    def test_auth_error_field_present_on_all_oauth_providers(self):
        """Every provider dict must include auth_error (may be None)."""
        import api.providers as prov_mod
        auth = _make_fake_auth(logged_in=False)
        with patch.object(prov_mod, "_provider_has_key", return_value=False), \
             patch.dict(sys.modules, {"hermes_cli.auth": auth}), \
             patch.object(prov_mod, "get_config", return_value={}):
            result = prov_mod.get_providers()

        for p in result["providers"]:
            assert "auth_error" in p, (
                f"Provider {p['id']!r} is missing the 'auth_error' field"
            )


# ---------------------------------------------------------------------------
# Tests for static/panels.js isOauth detection
# ---------------------------------------------------------------------------

class TestBuildProviderCardJs:
    """Static-analysis tests for the JS card rendering."""

    JS = (REPO_ROOT / "static" / "panels.js").read_text(encoding="utf-8")

    def _get_fn(self):
        idx = self.JS.find("function _buildProviderCard(p){")
        assert idx != -1, "_buildProviderCard not found in panels.js"
        depth = 0
        for i, ch in enumerate(self.JS[idx:], idx):
            if ch == "{": depth += 1
            elif ch == "}":
                depth -= 1
                if depth == 0:
                    return self.JS[idx : i + 1]
        raise AssertionError("Could not extract _buildProviderCard")

    def test_isOauth_uses_is_oauth_backend_field(self):
        """
        REGRESSION TEST (#1202):
        isOauth must use p.is_oauth from the backend, not a hardcoded list of IDs.
        Using p.is_oauth ensures new OAuth providers added to _OAUTH_PROVIDERS in Python
        automatically get the correct card rendering without changing the JS.
        """
        fn = self._get_fn()
        isOauth_line_idx = fn.find("const isOauth=")
        assert isOauth_line_idx != -1, "isOauth not found in _buildProviderCard"
        isOauth_line = fn[isOauth_line_idx: isOauth_line_idx + 150]
        assert "p.is_oauth" in isOauth_line, (
            "REGRESSION: isOauth does not use p.is_oauth from the backend. "
            "OAuth providers may not render the OAuth card correctly."
        )

    def test_oauth_body_has_config_yaml_branch(self):
        """OAuth card body must render a different hint for config_yaml tokens."""
        fn = self._get_fn()
        assert "config_yaml" in fn, (
            "OAuth card body has no config_yaml branch. "
            "Providers with config.yaml tokens will show the generic OAuth hint "
            "instead of explaining how the token was configured."
        )

    def test_oauth_body_surfaces_auth_error(self):
        """OAuth card body must display p.auth_error when present."""
        fn = self._get_fn()
        assert "p.auth_error" in fn, (
            "OAuth card body does not reference p.auth_error. "
            "Actionable error messages (e.g. 'refresh token consumed') will not be shown."
        )

    def test_configured_badge_shown_for_config_yaml(self):
        """The Configured badge must appear when has_key is True regardless of key_source."""
        fn = self._get_fn()
        badge_idx = fn.find("provider-card-badge")
        assert badge_idx != -1, "provider-card-badge not found in _buildProviderCard"
        badge_ctx = fn[max(0, badge_idx - 50) : badge_idx + 80]
        assert "p.has_key" in badge_ctx or "has_key" in badge_ctx, (
            "Configured badge is not conditional on p.has_key"
        )


# ---------------------------------------------------------------------------
# Tests for i18n.js new keys
# ---------------------------------------------------------------------------

class TestI18nNewKeys:
    """Verify new i18n keys exist in the English locale."""

    I18N = (REPO_ROOT / "static" / "i18n.js").read_text(encoding="utf-8")

    def test_providers_oauth_config_yaml_hint_exists(self):
        assert "providers_oauth_config_yaml_hint" in self.I18N, (
            "Missing i18n key: providers_oauth_config_yaml_hint. "
            "OAuth providers with config.yaml tokens will show undefined as the hint text."
        )

    def test_providers_oauth_not_configured_hint_exists(self):
        assert "providers_oauth_not_configured_hint" in self.I18N, (
            "Missing i18n key: providers_oauth_not_configured_hint."
        )


    def test_is_oauth_field_true_for_oauth_providers(self):
        """
        REGRESSION TEST (#1202 — Opus blocking issue):
        Provider dicts must include is_oauth=True for OAuth providers.
        Without this, the JS filter p.configurable||p.is_oauth would exclude OAuth
        providers from the Settings panel entirely (configurable=False for OAuth).
        """
        import api.providers as prov_mod
        auth = _make_fake_auth(logged_in=False)
        with patch.object(prov_mod, "_provider_has_key", return_value=False), \
             patch.dict(sys.modules, {"hermes_cli.auth": auth}), \
             patch.object(prov_mod, "get_config", return_value={}):
            result = prov_mod.get_providers()

        providers = {p["id"]: p for p in result["providers"]}
        for pid in ("openai-codex", "nous", "copilot"):
            p = providers.get(pid)
            if p is not None:
                assert p["is_oauth"] is True, (
                    f"REGRESSION: {pid} must have is_oauth=True in the provider dict. "
                    "Without it, the JS filter configurable||is_oauth will exclude it "
                    "from Settings → Providers."
                )


class TestProviderListFilter:
    """Test that the JS filter includes OAuth providers."""

    JS = (Path(__file__).parent.parent / "static" / "panels.js").read_text(encoding="utf-8")

    def test_providers_filter_includes_is_oauth(self):
        """
        REGRESSION TEST (#1202 — Opus blocking issue):
        The provider list filter in panels.js must include p.is_oauth so OAuth providers
        appear in the Settings panel even when configurable=False.

        Before the fix: filter(p=>p.configurable) excluded ALL OAuth providers.
        After the fix: filter(p=>p.configurable||p.is_oauth) includes them.
        """
        filter_idx = self.JS.find("filter(p=>p.configurable)")
        assert filter_idx == -1, (
            "REGRESSION: The provider filter 'filter(p=>p.configurable)' is still present. "
            "This excludes ALL OAuth providers (openai-codex, nous, copilot) from the "
            "Settings → Providers panel. The filter must be 'filter(p=>p.configurable||p.is_oauth)'."
        )
        assert "p.is_oauth" in self.JS, (
            "The provider filter must include p.is_oauth in panels.js. "
            "OAuth providers will not appear in Settings → Providers."
        )
        # Current filter also includes is_custom and is_plugin_provider; require
        # the full expression so partial regressions are caught.
        assert (
            "filter(p=>p.configurable||p.is_oauth||p.is_custom||p.is_plugin_provider)"
            in self.JS
        ), (
            "Settings → Providers filter must keep OAuth, custom, and plugin "
            "model-provider entries visible."
        )
