"""
tests/test_cli_providers.py — Tests for ``evaos providers`` CLI command (#683).

Covers:
    - Human-readable output with all keys set
    - Human-readable output with missing keys
    - --json flag produces valid JSON
    - Different provider_priority orders
    - Config path display
    - Embedding provider detection (voyage, google, openai)
"""
from __future__ import annotations

import json
import os
import re
from unittest.mock import patch

import pytest
from click.testing import CliRunner

from cortex.cli import main


@pytest.fixture
def runner():
    return CliRunner()


# ---------------------------------------------------------------------------
# Env var helpers
# ---------------------------------------------------------------------------

_ALL_KEYS = {
    "VENICE_API_KEY": "test-venice-key",
    "OPENAI_API_KEY": "test-openai-key",
    "ANTHROPIC_API_KEY": "test-anthropic-key",
    "VOYAGE_API_KEY": "test-voyage-key",
    "GOOGLE_API_KEY": "test-google-key",
}

_NO_KEYS = {
    "VENICE_API_KEY": "",
    "OPENAI_API_KEY": "",
    "ANTHROPIC_API_KEY": "",
    "VOYAGE_API_KEY": "",
    "GOOGLE_API_KEY": "",
    "GEMINI_API_KEY": "",
}


def _clear_env():
    """Return a dict that blanks all provider env vars (for patch.dict)."""
    return {k: "" for k in _ALL_KEYS}


# ---------------------------------------------------------------------------
# Tests — human-readable output
# ---------------------------------------------------------------------------

class TestProvidersHumanOutput:
    """Tests for the default (non-JSON) output."""

    def test_all_keys_set(self, runner):
        """When all keys are set, all providers show ✓."""
        with patch.dict(os.environ, _ALL_KEYS, clear=False):
            result = runner.invoke(main, ["providers"])
        assert result.exit_code == 0, result.output
        assert "venice" in result.output
        assert "openai" in result.output
        assert "anthropic" in result.output
        # All should show checkmark (either ✓ or ✅ depending on rich)
        assert "missing" not in result.output.lower().replace("google_api_key: missing", "")  # Google might still be set

    def test_missing_keys_show_error(self, runner):
        """When keys are missing, providers show ✗ and 'missing'."""
        with patch.dict(os.environ, _NO_KEYS, clear=False):
            result = runner.invoke(main, ["providers"])
        assert result.exit_code == 0, result.output
        # At least some should show missing
        assert "missing" in result.output.lower()

    def test_mode_displayed(self, runner):
        """Output includes the provider mode."""
        result = runner.invoke(main, ["providers"])
        assert result.exit_code == 0, result.output
        assert "Mode:" in result.output or "mode" in result.output.lower()

    def test_embedding_info(self, runner):
        """Output includes embedding model and dimension."""
        result = runner.invoke(main, ["providers"])
        assert result.exit_code == 0, result.output
        assert "voyage-4-large" in result.output
        assert "1024" in result.output

    def test_extraction_model(self, runner):
        """Output includes extraction model."""
        result = runner.invoke(main, ["providers"])
        assert result.exit_code == 0, result.output
        assert "gemini-2.5-flash" in result.output

    def test_cascade_order(self, runner):
        """Cascade lists providers in configured order."""
        result = runner.invoke(main, ["providers"])
        assert result.exit_code == 0, result.output
        # Default order: google, venice, openai, anthropic
        google_pos = result.output.index("google")
        venice_pos = result.output.index("venice")
        openai_pos = result.output.index("openai")
        anthropic_pos = result.output.index("anthropic")
        assert google_pos < venice_pos < openai_pos < anthropic_pos


# ---------------------------------------------------------------------------
# Tests — JSON output
# ---------------------------------------------------------------------------

class TestProvidersJsonOutput:
    """Tests for --json output."""

    def test_valid_json(self, runner):
        """--json outputs valid JSON."""
        result = runner.invoke(main, ["providers", "--json"])
        assert result.exit_code == 0, result.output
        data = json.loads(result.output)
        assert isinstance(data, dict)

    def test_json_has_required_keys(self, runner):
        """JSON output has all expected top-level keys."""
        result = runner.invoke(main, ["providers", "--json"])
        data = json.loads(result.output)
        assert "config_path" in data
        assert "provider_mode" in data
        assert "llm_cascade" in data
        assert "embedding" in data
        assert "google_embedding" in data
        assert "extraction_model" in data
        assert "reconciliation_model" in data

    def test_json_cascade_structure(self, runner):
        """Each cascade entry has name, model, env_var, key_set, supports_embed."""
        result = runner.invoke(main, ["providers", "--json"])
        data = json.loads(result.output)
        for entry in data["llm_cascade"]:
            assert "name" in entry
            assert "model" in entry
            assert "env_var" in entry
            assert "key_set" in entry
            assert "supports_embed" in entry

    def test_json_all_keys_set(self, runner):
        """With all keys set, all cascade entries show key_set=True."""
        with patch.dict(os.environ, _ALL_KEYS, clear=False):
            result = runner.invoke(main, ["providers", "--json"])
        data = json.loads(result.output)
        for entry in data["llm_cascade"]:
            assert entry["key_set"] is True, f"{entry['name']} should have key_set=True"

    def test_json_missing_keys(self, runner):
        """With missing keys, affected cascade entries show key_set=False."""
        env = {**_NO_KEYS}
        with patch.dict(os.environ, env, clear=False):
            result = runner.invoke(main, ["providers", "--json"])
        data = json.loads(result.output)
        for entry in data["llm_cascade"]:
            if entry["env_var"]:  # Ollama has no env_var
                assert entry["key_set"] is False, f"{entry['name']} should have key_set=False"

    def test_json_embedding_fields(self, runner):
        """Embedding section has model, dim, provider, env_var, key_set."""
        result = runner.invoke(main, ["providers", "--json"])
        data = json.loads(result.output)
        emb = data["embedding"]
        assert "model" in emb
        assert "dim" in emb
        assert "provider" in emb
        assert "env_var" in emb
        assert "key_set" in emb

    def test_json_provider_llm_section(self, runner):
        """Provider LLM section has provider, model, api_key_set."""
        result = runner.invoke(main, ["providers", "--json"])
        data = json.loads(result.output)
        llm = data["provider_llm"]
        assert "provider" in llm
        assert "model" in llm
        assert "api_key_set" in llm

    def test_json_venice_no_embed(self, runner):
        """Venice provider should have supports_embed=False."""
        result = runner.invoke(main, ["providers", "--json"])
        data = json.loads(result.output)
        venice = [e for e in data["llm_cascade"] if e["name"] == "venice"]
        assert len(venice) == 1
        assert venice[0]["supports_embed"] is False

    def test_json_google_no_env_var(self, runner):
        """Google provider should have GOOGLE_API_KEY env_var."""
        result = runner.invoke(main, ["providers", "--json"])
        data = json.loads(result.output)
        google = [e for e in data["llm_cascade"] if e["name"] == "google"]
        assert len(google) == 1


# ---------------------------------------------------------------------------
# Tests — custom config
# ---------------------------------------------------------------------------

class TestProvidersCustomConfig:
    """Tests with custom TOML config files."""

    def test_with_config_path(self, runner, tmp_path):
        """Provider command works with a custom config file."""
        toml_content = """
[cortex]
version = "1.2.0"

[cortex.providers]
mode = "byok"

[cortex.providers.llm]
provider = "anthropic"
model = "claude-sonnet-4-20250514"

[cortex.providers.embedding]
model = "voyage-code-3"
dim = 1024

[cortex.llm]
provider_priority = ["anthropic", "openai"]
"""
        config_file = tmp_path / "cortex.toml"
        config_file.write_text(toml_content)

        result = runner.invoke(main, ["--config", str(config_file), "providers", "--json"])
        assert result.exit_code == 0, result.output
        data = json.loads(result.output)
        assert data["provider_mode"] == "byok"
        assert data["provider_llm"]["provider"] == "anthropic"
        assert data["provider_llm"]["model"] == "claude-sonnet-4-20250514"
        # Cascade should only have anthropic and openai
        cascade_names = [e["name"] for e in data["llm_cascade"]]
        assert cascade_names == ["anthropic", "openai"]

    def test_config_path_displayed(self, runner, tmp_path):
        """Config path is shown in output."""
        toml_content = """
[cortex]
version = "1.2.0"
"""
        config_file = tmp_path / "cortex.toml"
        config_file.write_text(toml_content)

        result = runner.invoke(main, ["--config", str(config_file), "providers"])
        assert result.exit_code == 0, result.output
        # Rich panel wraps long paths across lines — strip box drawing chars
        flat_output = re.sub(r"[│╭╮╰╯─\n]+", " ", result.output)
        assert config_file.name in flat_output


# ---------------------------------------------------------------------------
# Tests — edge cases
# ---------------------------------------------------------------------------

class TestProvidersEdgeCases:
    """Edge case tests."""

    def test_ollama_always_key_set(self, runner):
        """Ollama should always show as key_set=True (no key needed)."""
        with patch.dict(os.environ, _NO_KEYS, clear=False):
            result = runner.invoke(main, ["providers", "--json"])
        data = json.loads(result.output)
        ollama = [e for e in data["llm_cascade"] if e["name"] == "ollama"]
        if ollama:
            assert ollama[0]["key_set"] is True

    def test_google_via_gemini_key(self, runner):
        """Google embedding should detect GEMINI_API_KEY as fallback."""
        env = {**_NO_KEYS, "GEMINI_API_KEY": "test-gemini-key"}
        with patch.dict(os.environ, env, clear=False):
            result = runner.invoke(main, ["providers", "--json"])
        data = json.loads(result.output)
        assert data["google_embedding"]["key_set"] is True

    def test_partial_keys(self, runner):
        """Some keys set, others missing — mixed output."""
        env = {**_NO_KEYS, "VENICE_API_KEY": "test-venice"}
        with patch.dict(os.environ, env, clear=False):
            result = runner.invoke(main, ["providers", "--json"])
        data = json.loads(result.output)
        venice = [e for e in data["llm_cascade"] if e["name"] == "venice"][0]
        openai = [e for e in data["llm_cascade"] if e["name"] == "openai"][0]
        assert venice["key_set"] is True
        assert openai["key_set"] is False
