"""
Sprint 34 Tests: OAuth provider support in onboarding (issues #303, #304).

Covers:
  1. _provider_oauth_authenticated() returns True for known OAuth providers
     with valid tokens in auth.json
  2. _provider_oauth_authenticated() returns False when auth.json is absent,
     empty, or has no token data
  3. _provider_oauth_authenticated() returns False for unknown/API-key providers
  4. _status_from_runtime() marks copilot/openai-codex as provider_ready when
     credentials exist
  5. _status_from_runtime() gives a helpful "hermes auth" note (not "API key")
     for OAuth providers that have no credentials yet
  6. API route /api/onboarding/status reflects OAuth-ready state
"""

import json
import pathlib
import tempfile
import unittest.mock

import pytest

REPO = pathlib.Path(__file__).parent.parent
from tests._pytest_port import BASE


# ── Helpers ──────────────────────────────────────────────────────────────────

def _make_auth_json(provider_id: str, tokens: dict, tmp_dir: pathlib.Path) -> pathlib.Path:
    """Write an auth.json with the given tokens for provider_id into tmp_dir."""
    store = {"providers": {provider_id: tokens}}
    auth_path = tmp_dir / "auth.json"
    auth_path.write_text(json.dumps(store), encoding="utf-8")
    return auth_path


def _make_auth_json_with_credential_pool(
    provider_id: str, pool_entries: list[dict], tmp_dir: pathlib.Path
) -> pathlib.Path:
    """Write an auth.json with only credential_pool entries for provider_id.

    This reproduces setups where Hermes runtime resolves OAuth credentials from
    credential_pool while providers[provider_id] is absent or stale.
    """
    store = {"providers": {}, "credential_pool": {provider_id: pool_entries}}
    auth_path = tmp_dir / "auth.json"
    auth_path.write_text(json.dumps(store), encoding="utf-8")
    return auth_path


# ── 1–3. _provider_oauth_authenticated unit tests ────────────────────────────

class TestProviderOAuthAuthenticated:
    """Unit tests for the new _provider_oauth_authenticated() helper."""

    def _call(self, provider: str, hermes_home: pathlib.Path) -> bool:
        # Import fresh so we don't get a stale module reference
        from api.onboarding import _provider_oauth_authenticated
        return _provider_oauth_authenticated(provider, hermes_home)

    def test_returns_false_when_auth_json_absent(self, tmp_path):
        """No auth.json -> not authenticated."""
        assert self._call("openai-codex", tmp_path) is False

    def test_openai_codex_with_access_token(self, tmp_path):
        """openai-codex with a valid access_token -> authenticated."""
        _make_auth_json(
            "openai-codex",
            {"access_token": "ey.test.token", "refresh_token": "ref123"},
            tmp_path,
        )
        assert self._call("openai-codex", tmp_path) is True

    def test_openai_codex_with_refresh_token_only(self, tmp_path):
        """openai-codex with only a refresh_token -> still authenticated."""
        _make_auth_json(
            "openai-codex",
            {"access_token": "", "refresh_token": "***"},
            tmp_path,
        )
        assert self._call("openai-codex", tmp_path) is True

    def test_copilot_with_api_key(self, tmp_path):
        """copilot with an api_key (GitHub token) -> authenticated."""
        _make_auth_json("copilot", {"api_key": "ghu_test_token_123"}, tmp_path)
        assert self._call("copilot", tmp_path) is True

    def test_empty_tokens_returns_false(self, tmp_path):
        """All token fields empty -> not authenticated."""
        _make_auth_json(
            "openai-codex",
            {"access_token": "", "refresh_token": "", "api_key": ""},
            tmp_path,
        )
        assert self._call("openai-codex", tmp_path) is False

    def test_missing_provider_key_in_auth_json(self, tmp_path):
        """auth.json present but provider key absent -> not authenticated."""
        store = {"providers": {"some-other-provider": {"access_token": "tok"}}}
        (tmp_path / "auth.json").write_text(json.dumps(store), encoding="utf-8")
        assert self._call("openai-codex", tmp_path) is False

    def test_unknown_provider_not_in_oauth_list(self, tmp_path):
        """A provider that is not a known OAuth provider -> always False."""
        _make_auth_json("some-random-provider", {"access_token": "tok"}, tmp_path)
        assert self._call("some-random-provider", tmp_path) is False

    def test_nous_provider_recognized(self, tmp_path):
        """nous is in the known OAuth set."""
        _make_auth_json("nous", {"access_token": "nous_tok"}, tmp_path)
        assert self._call("nous", tmp_path) is True

    def test_qwen_oauth_provider_recognized(self, tmp_path):
        """qwen-oauth is in the known OAuth set."""
        _make_auth_json("qwen-oauth", {"access_token": "qwen_tok"}, tmp_path)
        assert self._call("qwen-oauth", tmp_path) is True

    def test_empty_provider_string_returns_false(self, tmp_path):
        """Empty provider string -> False, no crash."""
        assert self._call("", tmp_path) is False
        assert self._call("  ", tmp_path) is False


# ── 4–5. _status_from_runtime integration ────────────────────────────────────

class TestStatusFromRuntimeOAuth:
    """_status_from_runtime should treat OAuth providers with tokens as ready."""

    def _call(self, provider: str, model: str, hermes_home: pathlib.Path) -> dict:
        from api.onboarding import _status_from_runtime
        import api.onboarding as _ob
        orig_home = _ob._get_active_hermes_home
        orig_found = _ob._HERMES_FOUND
        _ob._get_active_hermes_home = lambda: hermes_home
        # Simulate hermes-agent being available so we reach the provider logic
        # (without this, _status_from_runtime short-circuits to agent_unavailable)
        _ob._HERMES_FOUND = True
        try:
            cfg = {"model": {"provider": provider, "default": model}}
            return _status_from_runtime(cfg, True)
        finally:
            _ob._get_active_hermes_home = orig_home
            _ob._HERMES_FOUND = orig_found

    def test_copilot_ready_when_api_key_in_auth_json(self, tmp_path):
        """copilot configured + api_key in auth.json -> provider_ready True."""
        _make_auth_json("copilot", {"api_key": "ghu_abc123"}, tmp_path)
        result = self._call("copilot", "gpt-5.4", tmp_path)
        assert result["provider_configured"] is True
        assert result["provider_ready"] is True
        assert result["setup_state"] == "ready"

    def test_openai_codex_ready_when_token_in_auth_json(self, tmp_path):
        """openai-codex configured + access_token -> provider_ready True."""
        _make_auth_json(
            "openai-codex",
            {"access_token": "***", "refresh_token": "***"},
            tmp_path,
        )
        result = self._call("openai-codex", "codex-mini-latest", tmp_path)
        assert result["provider_configured"] is True
        assert result["provider_ready"] is True
        assert result["setup_state"] == "ready"

    def test_copilot_not_ready_without_credentials(self, tmp_path):
        """copilot configured but no credentials -> provider_ready False.

        We mock hermes_cli.auth to be unavailable so the function falls through
        to the auth.json path.  With no auth.json the result must be False.
        """
        import unittest.mock

        # Prevent the hermes_cli fast path from finding real credentials
        with unittest.mock.patch(
            "api.onboarding._provider_oauth_authenticated",
            return_value=False,
        ):
            result = self._call("copilot", "gpt-5.4", tmp_path)

        assert result["provider_configured"] is True
        assert result["provider_ready"] is False
        assert result["setup_state"] == "provider_incomplete"

    def test_oauth_incomplete_note_mentions_hermes_auth(self, tmp_path):
        """When OAuth provider is incomplete, note should mention hermes auth/model."""
        result = self._call("openai-codex", "codex-mini-latest", tmp_path)
        note = result["provider_note"]
        assert "hermes auth" in note or "hermes model" in note, (
            f"Expected 'hermes auth' or 'hermes model' in note, got: {note!r}"
        )

    def test_oauth_incomplete_note_does_not_say_api_key(self, tmp_path):
        """OAuth provider incomplete note must not say 'API key' — that's misleading."""
        result = self._call("copilot", "gpt-5.4", tmp_path)
        note = result["provider_note"]
        assert "API key" not in note, (
            f"Note misleadingly mentions 'API key' for OAuth provider: {note!r}"
        )

    def test_standard_provider_incomplete_note_still_says_api_key(self, tmp_path):
        """For a standard API-key provider (openrouter), note should still say API key."""
        # openrouter with no .env
        result = self._call("openrouter", "anthropic/claude-sonnet-4.6", tmp_path)
        assert result["provider_ready"] is False
        note = result["provider_note"]
        assert "API key" in note, (
            f"Expected 'API key' in note for openrouter, got: {note!r}"
        )


# ── 6. API endpoint reflects OAuth-ready state ───────────────────────────────

class TestOnboardingStatusApiOAuth:
    """
    The /api/onboarding/status endpoint should report provider_ready=True
    when an OAuth provider is configured and has valid credentials.
    """

    def test_status_endpoint_returns_200(self):
        import urllib.request
        with urllib.request.urlopen(BASE + "/api/onboarding/status", timeout=10) as r:
            assert r.status == 200
            data = json.loads(r.read())
        assert "system" in data
        assert "provider_ready" in data["system"]

    def test_onboarding_status_has_chat_ready_field(self):
        import urllib.request
        with urllib.request.urlopen(BASE + "/api/onboarding/status", timeout=10) as r:
            data = json.loads(r.read())
        assert "chat_ready" in data["system"]

    def test_status_setup_state_valid_values(self):
        """setup_state must be one of the known string values."""
        import urllib.request
        with urllib.request.urlopen(BASE + "/api/onboarding/status", timeout=10) as r:
            data = json.loads(r.read())
        valid = {"ready", "provider_incomplete", "needs_provider", "agent_unavailable"}
        assert data["system"]["setup_state"] in valid, (
            f"Unexpected setup_state: {data['system']['setup_state']!r}"
        )


# ── Control Center: section reset on close ─────────────────────────────────

def test_control_center_resets_active_section_on_close():
    """Closing the control center must reset _settingsSection to 'conversation'."""
    src = open(pathlib.Path(__file__).parent.parent / 'static' / 'panels.js').read()
    assert '_settingsSection' in src, '_settingsSection state variable missing from panels.js'
    assert "_settingsSection = 'conversation'" in src or "_settingsSection='conversation'" in src, \
        'Control center does not reset section to conversation on close'


def test_control_center_tab_highlight_on_open():
    """The settings left-rail menu must have a CSS rule that highlights the active section."""
    css = open(pathlib.Path(__file__).parent.parent / 'static' / 'style.css').read()
    assert 'side-menu-item' in css, 'side-menu-item CSS class for left-rail nav missing from style.css'
    assert '.side-menu-item.active' in css or 'side-menu-item.active' in css, \
        'No active-state style for .side-menu-item — sidebar section highlight missing'


# ── apply_onboarding_setup: unsupported/OAuth providers complete gracefully ──

class TestApplyOnboardingSetupUnsupportedProvider:
    """PR #323 / Issue #322: apply_onboarding_setup must not raise ValueError for
    providers already configured via CLI (openai-codex, copilot, nous, etc.).
    Instead it marks onboarding complete and returns current status.
    """

    def _call(self, provider: str) -> dict:
        import sys, pathlib, unittest.mock, tempfile, os
        repo = pathlib.Path(__file__).parent.parent
        if str(repo) not in sys.path:
            sys.path.insert(0, str(repo))

        from api.onboarding import apply_onboarding_setup

        with tempfile.TemporaryDirectory() as tmp:
            with unittest.mock.patch("api.onboarding._get_active_hermes_home",
                                     return_value=pathlib.Path(tmp)), \
                 unittest.mock.patch("api.onboarding._get_config_path",
                                     return_value=pathlib.Path(tmp) / "config.yaml"), \
                 unittest.mock.patch("api.onboarding.save_settings") as mock_save, \
                 unittest.mock.patch("api.onboarding.get_onboarding_status",
                                     return_value={"completed": True, "system": {}}):
                result = apply_onboarding_setup({"provider": provider, "model": "", "api_key": ""})
                return result, mock_save

    def test_openai_codex_does_not_raise(self):
        """apply_onboarding_setup with openai-codex must not raise ValueError."""
        result, _ = self._call("openai-codex")
        assert result is not None

    def test_copilot_does_not_raise(self):
        """apply_onboarding_setup with copilot must not raise ValueError."""
        result, _ = self._call("copilot")
        assert result is not None

    def test_nous_does_not_raise(self):
        """apply_onboarding_setup with nous must not raise ValueError."""
        result, _ = self._call("nous")
        assert result is not None

    def test_unsupported_provider_marks_onboarding_complete(self):
        """apply_onboarding_setup with an unsupported provider must save onboarding_completed=True."""
        _, mock_save = self._call("openai-codex")
        calls = [str(c) for c in mock_save.call_args_list]
        assert any("onboarding_completed" in c for c in calls), \
            "save_settings must be called with onboarding_completed=True for unsupported providers"

    def test_unsupported_provider_returns_status_dict(self):
        """apply_onboarding_setup with an unsupported provider must return a status dict (not raise)."""
        result, _ = self._call("openai-codex")
        assert isinstance(result, dict), \
            "apply_onboarding_setup must return a dict for unsupported providers, not raise"
